From 5deedb3a2e4d19b1adbf94f6e8e8ccdc224219e7 Mon Sep 17 00:00:00 2001 From: Ivan Date: Wed, 21 May 2025 16:42:35 +0000 Subject: Improvements for Russian translation (инстанция -> сервер) --- po/ru.po | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/po/ru.po b/po/ru.po index 4d5d9b4..c2bf193 100644 --- a/po/ru.po +++ b/po/ru.po @@ -679,12 +679,12 @@ msgstr "Ничего не найдено для '%s'" #: html.c:3929 msgid "Showing instance timeline" -msgstr "Показываем ленту инстанции" +msgstr "Показываем ленту сервера" #: html.c:4012 #, c-format msgid "Showing timeline for list '%s'" -msgstr "Показываем ленты инстанции для списка '%s'" +msgstr "Показываем ленту для списка '%s'" #: httpd.c:250 #, c-format @@ -693,7 +693,7 @@ msgstr "Результаты поиска для тега #%s" #: httpd.c:259 msgid "Recent posts by users in this instance" -msgstr "Последние сообщения на этой инстанции" +msgstr "Последние сообщения на этом сервере" #: html.c:1603 msgid "Blocked hashtags..." -- cgit v1.2.3 From 24105f6e9759b74c04522de1de12ddb77cfba568 Mon Sep 17 00:00:00 2001 From: postscriptum Date: Thu, 22 May 2025 03:34:48 +0300 Subject: use utf-8 lowercase function for tags #396 --- activitypub.c | 2 +- data.c | 4 ++-- format.c | 3 ++- 3 files changed, 5 insertions(+), 4 deletions(-) diff --git a/activitypub.c b/activitypub.c index a7e133a..3ff51ad 100644 --- a/activitypub.c +++ b/activitypub.c @@ -903,7 +903,7 @@ xs_str *process_tags(snac *snac, const char *content, xs_list **tag) if (*v == '#') { /* hashtag */ xs *d = xs_dict_new(); - xs *n = xs_tolower_i(xs_dup(v)); + xs *n = xs_utf8_to_lower(xs_dup(v)); xs *h = xs_fmt("%s?t=%s", srv_baseurl, n + 1); xs *l = xs_fmt("%s", h, v); diff --git a/data.c b/data.c index e3fa52d..d5dc171 100644 --- a/data.c +++ b/data.c @@ -2200,7 +2200,7 @@ void tag_index(const char *id, const xs_dict *obj) if (*name == '\0') continue; - name = xs_tolower_i((xs_str *)name); + name = xs_utf8_to_lower((xs_str *)name); xs *md5_tag = xs_md5_hex(name, strlen(name)); xs *tag_dir = xs_fmt("%s/%c%c", g_tag_dir, md5_tag[0], md5_tag[1]); @@ -2230,7 +2230,7 @@ xs_str *tag_fn(const char *tag) if (*tag == '#') tag++; - xs *lw_tag = xs_tolower_i(xs_dup(tag)); + xs *lw_tag = xs_utf8_to_lower(xs_dup(tag)); xs *md5 = xs_md5_hex(lw_tag, strlen(lw_tag)); return xs_fmt("%s/tag/%c%c/%s.idx", srv_basedir, md5[0], md5[1], md5); diff --git a/format.c b/format.c index 2f30a0d..0f844cd 100644 --- a/format.c +++ b/format.c @@ -8,6 +8,7 @@ #include "xs_json.h" #include "xs_time.h" #include "xs_match.h" +#include "xs_unicode.h" #include "snac.h" @@ -443,7 +444,7 @@ xs_str *sanitize(const char *content) if (n & 0x1) { xs *s1 = xs_strip_i(xs_crop_i(xs_dup(v), v[1] == '/' ? 2 : 1, -1)); xs *l1 = xs_split_n(s1, " ", 1); - xs *tag = xs_tolower_i(xs_dup(xs_list_get(l1, 0))); + xs *tag = xs_utf8_to_lower(xs_dup(xs_list_get(l1, 0))); xs *s2 = NULL; int i; -- cgit v1.2.3 From e567736640da21d37848567cd164cb84bbdab507 Mon Sep 17 00:00:00 2001 From: postscriptum Date: Thu, 22 May 2025 04:16:28 +0300 Subject: add missed replacement to the `content_match` function --- data.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/data.c b/data.c index d5dc171..8ca271d 100644 --- a/data.c +++ b/data.c @@ -2817,7 +2817,7 @@ int content_match(const char *file, const xs_dict *msg) /* massage content (strip HTML tags, etc.) */ xs *c = xs_regex_replace(v, "<[^>]+>", " "); c = xs_regex_replace_i(c, " {2,}", " "); - c = xs_tolower_i(c); + c = xs_utf8_to_lower(c); while (!r && !feof(f)) { xs *rx = xs_strip_i(xs_readline(f)); -- cgit v1.2.3 From ec37139f5aac4f54d09f768c54195fb528fd3088 Mon Sep 17 00:00:00 2001 From: postscriptum Date: Thu, 22 May 2025 04:35:52 +0300 Subject: cleanup ending spaces --- mastoapi.c | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/mastoapi.c b/mastoapi.c index 7fa0078..dee73e8 100644 --- a/mastoapi.c +++ b/mastoapi.c @@ -381,7 +381,7 @@ int oauth_post_handler(const xs_dict *req, const char *q_path, } } - /* no code? + /* no code? I'm not sure of the impacts of this right now, but Subway Tooter does not provide a code so one must be generated */ if (xs_is_null(code)){ @@ -1780,7 +1780,7 @@ int mastoapi_get_handler(const xs_dict *req, const char *q_path, } else if (strcmp(opt, "statuses") == 0) { - /* we don't serve statuses of others; return the empty list */ + /* we don't serve statuses of others; return the empty list */ out = xs_list_new(); } else @@ -1999,7 +1999,7 @@ int mastoapi_get_handler(const xs_dict *req, const char *q_path, } else if (strcmp(cmd, "/v2/filters") == 0) { /** **/ - /* snac will never have filters + /* snac will never have filters * but still, without a v2 endpoint a short delay is introduced * in some apps */ *body = xs_dup("[]"); @@ -2459,7 +2459,7 @@ int mastoapi_get_handler(const xs_dict *req, const char *q_path, if (logged_in) { const xs_list *timeline = xs_dict_get(args, "timeline[]"); xs_str *json = NULL; - if (!xs_is_null(timeline)) + if (!xs_is_null(timeline)) json = xs_json_dumps(markers_get(&snac1, timeline), 4); if (!xs_is_null(json)) @@ -3227,7 +3227,7 @@ int mastoapi_post_handler(const xs_dict *req, const char *q_path, if (!xs_is_null(home)) home_marker = xs_dict_get(home, "last_read_id"); } - + const xs_str *notify_marker = xs_dict_get(args, "notifications[last_read_id]"); if (xs_is_null(notify_marker)) { const xs_dict *notify = xs_dict_get(args, "notifications"); -- cgit v1.2.3 From 450e679f5197cfe3101d2b3f032f3fabeadfc848 Mon Sep 17 00:00:00 2001 From: postscriptum Date: Thu, 22 May 2025 04:37:13 +0300 Subject: add missed replacement to the `mastoapi_get_handler` function (`search` case) --- mastoapi.c | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/mastoapi.c b/mastoapi.c index dee73e8..0348ec6 100644 --- a/mastoapi.c +++ b/mastoapi.c @@ -15,6 +15,7 @@ #include "xs_url.h" #include "xs_mime.h" #include "xs_match.h" +#include "xs_unicode.h" #include "snac.h" @@ -1637,7 +1638,7 @@ int mastoapi_get_handler(const xs_dict *req, const char *q_path, const char *aq = xs_dict_get(args, "q"); if (!xs_is_null(aq)) { - xs *q = xs_tolower_i(xs_dup(aq)); + xs *q = xs_utf8_to_lower(xs_dup(aq)); out = xs_list_new(); xs *wing = following_list(&snac1); xs *wers = follower_list(&snac1); -- cgit v1.2.3 From b67ec496123d77b01d798d06ef3c3a7c811d8c63 Mon Sep 17 00:00:00 2001 From: Ivan Date: Thu, 22 May 2025 05:53:36 +0000 Subject: Rename one more instance from "инстанция" to "сервер" --- po/ru.po | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/po/ru.po b/po/ru.po index c2bf193..f95b0d1 100644 --- a/po/ru.po +++ b/po/ru.po @@ -126,7 +126,7 @@ msgstr "люди" #: html.c:936 msgid "instance" -msgstr "экземпляр" +msgstr "сервер" #: html.c:945 msgid "" -- cgit v1.2.3 From 56816b305155fee2154c7991ba9be8c0e7671307 Mon Sep 17 00:00:00 2001 From: grunfink Date: Thu, 22 May 2025 11:18:48 +0200 Subject: Minor memory leak fixes. --- activitypub.c | 2 +- data.c | 8 ++++---- format.c | 2 +- mastoapi.c | 2 +- 4 files changed, 7 insertions(+), 7 deletions(-) diff --git a/activitypub.c b/activitypub.c index c06d8df..0cc7bcb 100644 --- a/activitypub.c +++ b/activitypub.c @@ -903,7 +903,7 @@ xs_str *process_tags(snac *snac, const char *content, xs_list **tag) if (*v == '#') { /* hashtag */ xs *d = xs_dict_new(); - xs *n = xs_utf8_to_lower(xs_dup(v)); + xs *n = xs_utf8_to_lower(v); xs *h = xs_fmt("%s?t=%s", srv_baseurl, n + 1); xs *l = xs_fmt("%s", h, v); diff --git a/data.c b/data.c index 631a68b..5f890d3 100644 --- a/data.c +++ b/data.c @@ -2247,7 +2247,7 @@ xs_str *tag_fn(const char *tag) if (*tag == '#') tag++; - xs *lw_tag = xs_utf8_to_lower(xs_dup(tag)); + xs *lw_tag = xs_utf8_to_lower(tag); xs *md5 = xs_md5_hex(lw_tag, strlen(lw_tag)); return xs_fmt("%s/tag/%c%c/%s.idx", srv_basedir, md5[0], md5[1], md5); @@ -2832,9 +2832,9 @@ int content_match(const char *file, const xs_dict *msg) srv_debug(1, xs_fmt("content_match: loading regexes from %s", fn)); /* massage content (strip HTML tags, etc.) */ - xs *c = xs_regex_replace(v, "<[^>]+>", " "); - c = xs_regex_replace_i(c, " {2,}", " "); - c = xs_utf8_to_lower(c); + xs *c1 = xs_regex_replace(v, "<[^>]+>", " "); + c1 = xs_regex_replace_i(c1, " {2,}", " "); + xs *c = xs_utf8_to_lower(c1); while (!r && !feof(f)) { xs *rx = xs_strip_i(xs_readline(f)); diff --git a/format.c b/format.c index 0f844cd..9bad391 100644 --- a/format.c +++ b/format.c @@ -444,7 +444,7 @@ xs_str *sanitize(const char *content) if (n & 0x1) { xs *s1 = xs_strip_i(xs_crop_i(xs_dup(v), v[1] == '/' ? 2 : 1, -1)); xs *l1 = xs_split_n(s1, " ", 1); - xs *tag = xs_utf8_to_lower(xs_dup(xs_list_get(l1, 0))); + xs *tag = xs_utf8_to_lower(xs_list_get(l1, 0)); xs *s2 = NULL; int i; diff --git a/mastoapi.c b/mastoapi.c index c46a971..9875b12 100644 --- a/mastoapi.c +++ b/mastoapi.c @@ -1638,7 +1638,7 @@ int mastoapi_get_handler(const xs_dict *req, const char *q_path, const char *aq = xs_dict_get(args, "q"); if (!xs_is_null(aq)) { - xs *q = xs_utf8_to_lower(xs_dup(aq)); + xs *q = xs_utf8_to_lower(aq); out = xs_list_new(); xs *wing = following_list(&snac1); xs *wers = follower_list(&snac1); -- cgit v1.2.3 From 11b4a46d54ce55545a8ac4958e5ff292c072d1d3 Mon Sep 17 00:00:00 2001 From: shtrophic Date: Sun, 25 May 2025 11:38:15 +0200 Subject: Increase verbosity for srv_debug() in tag_index() --- data.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/data.c b/data.c index 5f890d3..f45e346 100644 --- a/data.c +++ b/data.c @@ -2235,7 +2235,7 @@ void tag_index(const char *id, const xs_dict *obj) fclose(f); } - srv_debug(0, xs_fmt("tagged %s #%s (#%s)", id, name, md5_tag)); + srv_debug(1, xs_fmt("tagged %s #%s (#%s)", id, name, md5_tag)); } } } -- cgit v1.2.3 From 0d95692d4606d66c1bd194d36001e0c93c940ae7 Mon Sep 17 00:00:00 2001 From: grunfink Date: Mon, 26 May 2025 07:25:38 +0200 Subject: Updated RELEASE_NOTES. --- RELEASE_NOTES.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/RELEASE_NOTES.md b/RELEASE_NOTES.md index 484e425..48ab3d4 100644 --- a/RELEASE_NOTES.md +++ b/RELEASE_NOTES.md @@ -6,6 +6,8 @@ The number of pending follow confirmations is shown next to the "people" link. Faster performance metrics (contributed by dandelions). +Improved lowercasing in tags (contributed by postscriptum). + Mastodon API: added follow confirmation endpoints. ## 2.77 "Ugly Links Everywhere" -- cgit v1.2.3 From 117244e16936840a8abdf4c07d33ec0b50f7d7d2 Mon Sep 17 00:00:00 2001 From: grunfink Date: Mon, 26 May 2025 07:28:00 +0200 Subject: Bumped version. --- snac.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/snac.h b/snac.h index 256731f..841c41e 100644 --- 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.77" +#define VERSION "2.78-dev" #define USER_AGENT "snac/" VERSION -- cgit v1.2.3 From 97bf8a9cbceeddb96abc65e1c61a3022b173b352 Mon Sep 17 00:00:00 2001 From: grunfink Date: Mon, 26 May 2025 07:40:08 +0200 Subject: Fixed search by url for piefed. Piefed returns 200 OK for webfinger queries, even it it doesn't return any valid data. --- html.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/html.c b/html.c index de2fdce..83dac0a 100644 --- a/html.c +++ b/html.c @@ -3748,7 +3748,7 @@ int html_get_handler(const xs_dict *req, const char *q_path, /* may by an actor; try a webfinger */ xs *actor_obj = NULL; - if (valid_status(webfinger_request(q, &actor_obj, &url_acct))) { + 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; } -- cgit v1.2.3 From 36d44ddd9e15e04ede93ef9914376ac4c9349122 Mon Sep 17 00:00:00 2001 From: grunfink Date: Mon, 26 May 2025 07:45:00 +0200 Subject: Updated RELEASE_NOTES. --- RELEASE_NOTES.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/RELEASE_NOTES.md b/RELEASE_NOTES.md index 48ab3d4..620ee7e 100644 --- a/RELEASE_NOTES.md +++ b/RELEASE_NOTES.md @@ -8,6 +8,8 @@ Faster performance metrics (contributed by dandelions). Improved lowercasing in tags (contributed by postscriptum). +A search-by-url tweak for implementations that return 200 for invalid webfinger queries (e.g. piefed). + Mastodon API: added follow confirmation endpoints. ## 2.77 "Ugly Links Everywhere" -- cgit v1.2.3 From 631e44a64a20741b5e4716bf75caf7fa743fef82 Mon Sep 17 00:00:00 2001 From: grunfink Date: Tue, 27 May 2025 21:08:57 +0200 Subject: Renamed timeline_here() to timeline_here_by_md5(), as it always should have been. --- data.c | 4 ++-- html.c | 6 +++--- main.c | 2 +- mastoapi.c | 2 +- snac.h | 2 +- 5 files changed, 8 insertions(+), 8 deletions(-) diff --git a/data.c b/data.c index f45e346..9eada38 100644 --- a/data.c +++ b/data.c @@ -1391,7 +1391,7 @@ xs_str *timeline_fn_by_md5(snac *snac, const char *md5) } -int timeline_here(snac *snac, const char *md5) +int timeline_here_by_md5(snac *snac, const char *md5) /* checks if an object is in the user cache */ { xs *fn = timeline_fn_by_md5(snac, md5); @@ -1515,7 +1515,7 @@ xs_list *timeline_top_level(snac *snac, const xs_list *list) break; /* well, there is a parent... but is it here? */ - if (!timeline_here(snac, line2)) + if (!timeline_here_by_md5(snac, line2)) break; /* it's here! try again with its own parent */ diff --git a/html.c b/html.c index 83dac0a..4aa0fa4 100644 --- a/html.c +++ b/html.c @@ -2117,7 +2117,7 @@ xs_html *html_entry(snac *user, xs_dict *msg, int read_only, if (!xs_is_null(parent) && *parent) { xs *md5 = xs_md5_hex(parent, strlen(parent)); - if (!timeline_here(user, md5)) { + if (!timeline_here_by_md5(user, md5)) { xs_html_add(post_header, xs_html_tag("div", xs_html_attr("class", "snac-origin"), @@ -3775,7 +3775,7 @@ int html_get_handler(const xs_dict *req, const char *q_path, /* add the post to the timeline */ xs *md5 = xs_md5_hex(q, strlen(q)); - if (!timeline_here(&snac, md5)) + if (!timeline_here_by_md5(&snac, md5)) timeline_add(&snac, q, object); } } @@ -3917,7 +3917,7 @@ int html_get_handler(const xs_dict *req, const char *q_path, xs *l = xs_split(p_path, "/"); const char *md5 = xs_list_get(l, -1); - if (md5 && *md5 && timeline_here(&snac, md5)) { + if (md5 && *md5 && timeline_here_by_md5(&snac, md5)) { xs *list0 = xs_list_append(xs_list_new(), md5); xs *list = timeline_top_level(&snac, list0); diff --git a/main.c b/main.c index 80df2d0..9cfbb0d 100644 --- a/main.c +++ b/main.c @@ -695,7 +695,7 @@ int main(int argc, char *argv[]) xs_json_dump(data, 4, stdout); enqueue_actor_refresh(&snac, xs_dict_get(data, "attributedTo"), 0); - if (!timeline_here(&snac, url)) + if (!timeline_here_by_md5(&snac, url)) timeline_add(&snac, url, data); else printf("Post %s already here\n", url); diff --git a/mastoapi.c b/mastoapi.c index 9875b12..c88fd1b 100644 --- a/mastoapi.c +++ b/mastoapi.c @@ -2510,7 +2510,7 @@ int mastoapi_get_handler(const xs_dict *req, const char *q_path, if (xs_startswith(q, "https://")) { xs *md5 = xs_md5_hex(q, strlen(q)); - if (!timeline_here(&snac1, md5)) { + if (!timeline_here_by_md5(&snac1, md5)) { xs *object = NULL; int status; diff --git a/snac.h b/snac.h index 841c41e..621ab8a 100644 --- a/snac.h +++ b/snac.h @@ -164,7 +164,7 @@ int pending_count(snac *user); double timeline_mtime(snac *snac); int timeline_touch(snac *snac); -int timeline_here(snac *snac, const char *md5); +int timeline_here_by_md5(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); -- cgit v1.2.3 From 5bc451159420d5d51a507fda82a623069cfae92b Mon Sep 17 00:00:00 2001 From: grunfink Date: Tue, 27 May 2025 21:14:23 +0200 Subject: New function timeline_here(). --- data.c | 8 ++++++++ html.c | 8 ++------ main.c | 2 +- mastoapi.c | 4 +--- snac.h | 1 + 5 files changed, 13 insertions(+), 10 deletions(-) diff --git a/data.c b/data.c index 9eada38..224976b 100644 --- a/data.c +++ b/data.c @@ -1400,6 +1400,14 @@ int timeline_here_by_md5(snac *snac, const char *md5) } +int timeline_here(snac *user, const char *id) +{ + xs *md5 = xs_md5_hex(id, strlen(id)); + + return timeline_here_by_md5(user, md5); +} + + int timeline_get_by_md5(snac *snac, const char *md5, xs_dict **msg) /* gets a message from the timeline */ { diff --git a/html.c b/html.c index 4aa0fa4..4c30355 100644 --- a/html.c +++ b/html.c @@ -2115,9 +2115,7 @@ xs_html *html_entry(snac *user, xs_dict *msg, int read_only, const char *parent = get_in_reply_to(msg); if (!xs_is_null(parent) && *parent) { - xs *md5 = xs_md5_hex(parent, strlen(parent)); - - if (!timeline_here_by_md5(user, md5)) { + if (!timeline_here(user, parent)) { xs_html_add(post_header, xs_html_tag("div", xs_html_attr("class", "snac-origin"), @@ -3773,9 +3771,7 @@ int html_get_handler(const xs_dict *req, const char *q_path, q = url_acct; /* add the post to the timeline */ - xs *md5 = xs_md5_hex(q, strlen(q)); - - if (!timeline_here_by_md5(&snac, md5)) + if (!timeline_here(&snac, q)) timeline_add(&snac, q, object); } } diff --git a/main.c b/main.c index 9cfbb0d..80df2d0 100644 --- a/main.c +++ b/main.c @@ -695,7 +695,7 @@ int main(int argc, char *argv[]) xs_json_dump(data, 4, stdout); enqueue_actor_refresh(&snac, xs_dict_get(data, "attributedTo"), 0); - if (!timeline_here_by_md5(&snac, url)) + if (!timeline_here(&snac, url)) timeline_add(&snac, url, data); else printf("Post %s already here\n", url); diff --git a/mastoapi.c b/mastoapi.c index c88fd1b..df713a8 100644 --- a/mastoapi.c +++ b/mastoapi.c @@ -2508,9 +2508,7 @@ int mastoapi_get_handler(const xs_dict *req, const char *q_path, /* reply something only for offset 0; otherwise, apps like Tusky keep asking again and again */ if (xs_startswith(q, "https://")) { - xs *md5 = xs_md5_hex(q, strlen(q)); - - if (!timeline_here_by_md5(&snac1, md5)) { + if (!timeline_here(&snac1, q)) { xs *object = NULL; int status; diff --git a/snac.h b/snac.h index 621ab8a..f32a90d 100644 --- a/snac.h +++ b/snac.h @@ -165,6 +165,7 @@ int pending_count(snac *user); double timeline_mtime(snac *snac); int timeline_touch(snac *snac); int timeline_here_by_md5(snac *snac, const char *md5); +int timeline_here(snac *snac, const char *id); 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); -- cgit v1.2.3 From 859de583d9b575a830bab92a6379f77c70910a93 Mon Sep 17 00:00:00 2001 From: grunfink Date: Wed, 28 May 2025 07:44:34 +0200 Subject: Renamed timeline_to_rss() to rss_from_timeline(). --- html.c | 4 ++-- httpd.c | 2 +- snac.h | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/html.c b/html.c index 4c30355..d3d5075 100644 --- a/html.c +++ b/html.c @@ -4123,7 +4123,7 @@ int html_get_handler(const xs_dict *req, const char *q_path, xs_dict_get(srv_config, "host")); xs *rss_link = xs_fmt("%s.rss", snac.actor); - *body = timeline_to_rss(&snac, elems, rss_title, rss_link, bio); + *body = rss_from_timeline(&snac, elems, rss_title, rss_link, bio); *b_size = strlen(*body); *ctype = "application/rss+xml; charset=utf-8"; status = HTTP_STATUS_OK; @@ -5022,7 +5022,7 @@ int html_post_handler(const xs_dict *req, const char *q_path, } -xs_str *timeline_to_rss(snac *user, const xs_list *timeline, +xs_str *rss_from_timeline(snac *user, const xs_list *timeline, const char *title, const char *link, const char *desc) /* converts a timeline to rss */ { diff --git a/httpd.c b/httpd.c index 6f7d69b..41b2515 100644 --- a/httpd.c +++ b/httpd.c @@ -244,7 +244,7 @@ int server_get_handler(xs_dict *req, const char *q_path, if (!xs_is_null(accept) && strcmp(accept, "application/rss+xml") == 0) { xs *link = xs_fmt("%s/?t=%s", srv_baseurl, t); - *body = timeline_to_rss(NULL, tl, link, link, link); + *body = rss_from_timeline(NULL, tl, link, link, link); *ctype = "application/rss+xml; charset=utf-8"; } else { diff --git a/snac.h b/snac.h index f32a90d..9a5aca0 100644 --- a/snac.h +++ b/snac.h @@ -395,7 +395,7 @@ int html_get_handler(const xs_dict *req, const char *q_path, int html_post_handler(const xs_dict *req, const char *q_path, char *payload, int p_size, char **body, int *b_size, char **ctype); -xs_str *timeline_to_rss(snac *user, const xs_list *timeline, +xs_str *rss_from_timeline(snac *user, const xs_list *timeline, const char *title, const char *link, const char *desc); int write_default_css(void); -- cgit v1.2.3 From 97ff66116b7633066375bbc6cc88be5b8587453b Mon Sep 17 00:00:00 2001 From: grunfink Date: Wed, 28 May 2025 07:48:47 +0200 Subject: New file rss.c. --- Makefile | 9 ++--- Makefile.NetBSD | 9 ++--- html.c | 97 --------------------------------------------------- rss.c | 105 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++ snac.h | 5 +-- 5 files changed, 118 insertions(+), 107 deletions(-) create mode 100644 rss.c diff --git a/Makefile b/Makefile index 46fdb09..bb4029c 100644 --- a/Makefile +++ b/Makefile @@ -5,7 +5,7 @@ CFLAGS?=-g -Wall -Wextra -pedantic all: snac snac: snac.o main.o sandbox.o data.o http.o httpd.o webfinger.o \ - activitypub.o html.o utils.o format.o upgrade.o mastoapi.o + activitypub.o html.o utils.o format.o upgrade.o mastoapi.o rss.o $(CC) $(CFLAGS) -L$(PREFIX)/lib *.o -lcurl -lcrypto $(LDFLAGS) -pthread -o $@ test: tests/smtp @@ -48,12 +48,12 @@ update-po: activitypub.o: activitypub.c xs.h xs_json.h xs_curl.h xs_mime.h \ xs_openssl.h xs_regex.h xs_time.h xs_set.h xs_match.h xs_unicode.h \ - snac.h http_codes.h + xs_webmention.h snac.h http_codes.h data.o: data.c xs.h xs_hex.h xs_io.h xs_json.h xs_openssl.h xs_glob.h \ xs_set.h xs_time.h xs_regex.h xs_match.h xs_unicode.h xs_random.h \ xs_po.h snac.h http_codes.h format.o: format.c xs.h xs_regex.h xs_mime.h xs_html.h xs_json.h \ - xs_time.h xs_match.h snac.h http_codes.h + xs_time.h xs_match.h xs_unicode.h snac.h http_codes.h html.o: html.c xs.h xs_io.h xs_json.h xs_regex.h xs_set.h xs_openssl.h \ xs_time.h xs_mime.h xs_match.h xs_html.h xs_curl.h xs_unicode.h xs_url.h \ xs_random.h snac.h http_codes.h @@ -66,7 +66,8 @@ main.o: main.c xs.h xs_io.h xs_json.h xs_time.h xs_openssl.h xs_match.h \ snac.h http_codes.h mastoapi.o: mastoapi.c xs.h xs_hex.h xs_openssl.h xs_json.h xs_io.h \ xs_time.h xs_glob.h xs_set.h xs_random.h xs_url.h xs_mime.h xs_match.h \ - snac.h http_codes.h + xs_unicode.h snac.h http_codes.h +rss.o: rss.c xs.h xs_html.h xs_regex.h xs_time.h snac.h http_codes.h sandbox.o: sandbox.c xs.h snac.h http_codes.h snac.o: snac.c xs.h xs_hex.h xs_io.h xs_unicode_tbl.h xs_unicode.h \ xs_json.h xs_curl.h xs_openssl.h xs_socket.h xs_unix_socket.h xs_url.h \ diff --git a/Makefile.NetBSD b/Makefile.NetBSD index ecf8205..a2bf83c 100644 --- a/Makefile.NetBSD +++ b/Makefile.NetBSD @@ -6,7 +6,7 @@ LDFLAGS=-lrt all: snac snac: snac.o main.o sandbox.o data.o http.o httpd.o webfinger.o \ - activitypub.o html.o utils.o format.o upgrade.o mastoapi.o + activitypub.o html.o utils.o format.o upgrade.o mastoapi.o rss.o $(CC) $(CFLAGS) -L/usr/pkg/lib *.o -lcurl -lcrypto -pthread $(LDFLAGS) -Wl,-rpath,/usr/lib -Wl,-rpath,/usr/pkg/lib -o $@ @@ -37,12 +37,12 @@ uninstall: activitypub.o: activitypub.c xs.h xs_json.h xs_curl.h xs_mime.h \ xs_openssl.h xs_regex.h xs_time.h xs_set.h xs_match.h xs_unicode.h \ - snac.h http_codes.h + xs_webmention.h snac.h http_codes.h data.o: data.c xs.h xs_hex.h xs_io.h xs_json.h xs_openssl.h xs_glob.h \ xs_set.h xs_time.h xs_regex.h xs_match.h xs_unicode.h xs_random.h \ xs_po.h snac.h http_codes.h format.o: format.c xs.h xs_regex.h xs_mime.h xs_html.h xs_json.h \ - xs_time.h xs_match.h snac.h http_codes.h + xs_time.h xs_match.h xs_unicode.h snac.h http_codes.h html.o: html.c xs.h xs_io.h xs_json.h xs_regex.h xs_set.h xs_openssl.h \ xs_time.h xs_mime.h xs_match.h xs_html.h xs_curl.h xs_unicode.h xs_url.h \ xs_random.h snac.h http_codes.h @@ -55,7 +55,8 @@ main.o: main.c xs.h xs_io.h xs_json.h xs_time.h xs_openssl.h xs_match.h \ snac.h http_codes.h mastoapi.o: mastoapi.c xs.h xs_hex.h xs_openssl.h xs_json.h xs_io.h \ xs_time.h xs_glob.h xs_set.h xs_random.h xs_url.h xs_mime.h xs_match.h \ - snac.h http_codes.h + xs_unicode.h snac.h http_codes.h +rss.o: rss.c xs.h xs_html.h xs_regex.h xs_time.h snac.h http_codes.h sandbox.o: sandbox.c xs.h snac.h http_codes.h snac.o: snac.c xs.h xs_hex.h xs_io.h xs_unicode_tbl.h xs_unicode.h \ xs_json.h xs_curl.h xs_openssl.h xs_socket.h xs_unix_socket.h xs_url.h \ diff --git a/html.c b/html.c index d3d5075..fadcfb0 100644 --- a/html.c +++ b/html.c @@ -5020,100 +5020,3 @@ int html_post_handler(const xs_dict *req, const char *q_path, return status; } - - -xs_str *rss_from_timeline(snac *user, const xs_list *timeline, - const char *title, const char *link, const char *desc) -/* converts a timeline to rss */ -{ - xs_html *rss = xs_html_tag("rss", - xs_html_attr("xmlns:content", "http:/" "/purl.org/rss/1.0/modules/content/"), - xs_html_attr("version", "2.0"), - xs_html_attr("xmlns:atom", "http:/" "/www.w3.org/2005/Atom")); - - xs_html *channel = xs_html_tag("channel", - xs_html_tag("title", - xs_html_text(title)), - xs_html_tag("language", - xs_html_text("en")), - xs_html_tag("link", - xs_html_text(link)), - xs_html_sctag("atom:link", - xs_html_attr("href", link), - xs_html_attr("rel", "self"), - xs_html_attr("type", "application/rss+xml")), - xs_html_tag("generator", - xs_html_text(USER_AGENT)), - xs_html_tag("description", - xs_html_text(desc))); - - xs_html_add(rss, channel); - - int cnt = 0; - const char *v; - - xs_list_foreach(timeline, v) { - xs *msg = NULL; - - if (user) { - if (!valid_status(timeline_get_by_md5(user, v, &msg))) - continue; - } - else { - if (!valid_status(object_get_by_md5(v, &msg))) - continue; - } - - const char *id = xs_dict_get(msg, "id"); - const char *content = xs_dict_get(msg, "content"); - const char *published = xs_dict_get(msg, "published"); - - if (user && !xs_startswith(id, user->actor)) - continue; - - if (!id || !content || !published) - continue; - - /* create a title with the first line of the content */ - xs *title = xs_replace(content, "
", "\n"); - title = xs_regex_replace_i(title, "<[^>]+>", " "); - title = xs_regex_replace_i(title, "&[^;]+;", " "); - int i; - - for (i = 0; title[i] && title[i] != '\n' && i < 50; i++); - - if (title[i] != '\0') { - title[i] = '\0'; - title = xs_str_cat(title, "..."); - } - - title = xs_strip_i(title); - - /* convert the date */ - time_t t = xs_parse_iso_date(published, 0); - xs *rss_date = xs_str_utctime(t, "%a, %d %b %Y %T +0000"); - - /* if it's the first one, add it to the header */ - if (cnt == 0) - xs_html_add(channel, - xs_html_tag("lastBuildDate", - xs_html_text(rss_date))); - - xs_html_add(channel, - xs_html_tag("item", - xs_html_tag("title", - xs_html_text(title)), - xs_html_tag("link", - xs_html_text(id)), - xs_html_tag("guid", - xs_html_text(id)), - xs_html_tag("pubDate", - xs_html_text(rss_date)), - xs_html_tag("description", - xs_html_text(content)))); - - cnt++; - } - - return xs_html_render_s(rss, "\n"); -} diff --git a/rss.c b/rss.c new file mode 100644 index 0000000..61620fa --- /dev/null +++ b/rss.c @@ -0,0 +1,105 @@ +/* snac - A simple, minimalistic ActivityPub instance */ +/* copyright (c) 2025 grunfink et al. / MIT license */ + +#include "xs.h" +#include "xs_html.h" +#include "xs_regex.h" +#include "xs_time.h" + +#include "snac.h" + +xs_str *rss_from_timeline(snac *user, const xs_list *timeline, + const char *title, const char *link, const char *desc) +/* converts a timeline to rss */ +{ + xs_html *rss = xs_html_tag("rss", + xs_html_attr("xmlns:content", "http:/" "/purl.org/rss/1.0/modules/content/"), + xs_html_attr("version", "2.0"), + xs_html_attr("xmlns:atom", "http:/" "/www.w3.org/2005/Atom")); + + xs_html *channel = xs_html_tag("channel", + xs_html_tag("title", + xs_html_text(title)), + xs_html_tag("language", + xs_html_text("en")), + xs_html_tag("link", + xs_html_text(link)), + xs_html_sctag("atom:link", + xs_html_attr("href", link), + xs_html_attr("rel", "self"), + xs_html_attr("type", "application/rss+xml")), + xs_html_tag("generator", + xs_html_text(USER_AGENT)), + xs_html_tag("description", + xs_html_text(desc))); + + xs_html_add(rss, channel); + + int cnt = 0; + const char *v; + + xs_list_foreach(timeline, v) { + xs *msg = NULL; + + if (user) { + if (!valid_status(timeline_get_by_md5(user, v, &msg))) + continue; + } + else { + if (!valid_status(object_get_by_md5(v, &msg))) + continue; + } + + const char *id = xs_dict_get(msg, "id"); + const char *content = xs_dict_get(msg, "content"); + const char *published = xs_dict_get(msg, "published"); + + if (user && !xs_startswith(id, user->actor)) + continue; + + if (!id || !content || !published) + continue; + + /* create a title with the first line of the content */ + xs *title = xs_replace(content, "
", "\n"); + title = xs_regex_replace_i(title, "<[^>]+>", " "); + title = xs_regex_replace_i(title, "&[^;]+;", " "); + int i; + + for (i = 0; title[i] && title[i] != '\n' && i < 50; i++); + + if (title[i] != '\0') { + title[i] = '\0'; + title = xs_str_cat(title, "..."); + } + + title = xs_strip_i(title); + + /* convert the date */ + time_t t = xs_parse_iso_date(published, 0); + xs *rss_date = xs_str_utctime(t, "%a, %d %b %Y %T +0000"); + + /* if it's the first one, add it to the header */ + if (cnt == 0) + xs_html_add(channel, + xs_html_tag("lastBuildDate", + xs_html_text(rss_date))); + + xs_html_add(channel, + xs_html_tag("item", + xs_html_tag("title", + xs_html_text(title)), + xs_html_tag("link", + xs_html_text(id)), + xs_html_tag("guid", + xs_html_text(id)), + xs_html_tag("pubDate", + xs_html_text(rss_date)), + xs_html_tag("description", + xs_html_text(content)))); + + cnt++; + } + + return xs_html_render_s(rss, "\n"); +} diff --git a/snac.h b/snac.h index 9a5aca0..f95ba4d 100644 --- a/snac.h +++ b/snac.h @@ -395,8 +395,6 @@ int html_get_handler(const xs_dict *req, const char *q_path, int html_post_handler(const xs_dict *req, const char *q_path, char *payload, int p_size, char **body, int *b_size, char **ctype); -xs_str *rss_from_timeline(snac *user, const xs_list *timeline, - const char *title, const char *link, const char *desc); int write_default_css(void); int snac_init(const char *_basedir); @@ -462,3 +460,6 @@ int badlogin_check(const char *user, const char *addr); void badlogin_inc(const char *user, const char *addr); const char *lang_str(const char *str, const snac *user); + +xs_str *rss_from_timeline(snac *user, const xs_list *timeline, + const char *title, const char *link, const char *desc); -- cgit v1.2.3 From b783f287c8b3e77bbd1eb94892ea645ba05e8770 Mon Sep 17 00:00:00 2001 From: grunfink Date: Wed, 28 May 2025 07:56:44 +0200 Subject: New function rss_to_timeline(). --- rss.c | 124 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ snac.h | 2 ++ 2 files changed, 126 insertions(+) diff --git a/rss.c b/rss.c index 61620fa..a3a092e 100644 --- a/rss.c +++ b/rss.c @@ -5,6 +5,8 @@ #include "xs_html.h" #include "xs_regex.h" #include "xs_time.h" +#include "xs_match.h" +#include "xs_curl.h" #include "snac.h" @@ -103,3 +105,125 @@ xs_str *rss_from_timeline(snac *user, const xs_list *timeline, return xs_html_render_s(rss, "\n"); } + + +void rss_to_timeline(snac *user, const char *url) +/* reads an RSS and inserts all ActivityPub posts into the user's timeline */ +{ + xs *hdrs = xs_dict_new(); + hdrs = xs_dict_set(hdrs, "accept", "application/rss+xml"); + hdrs = xs_dict_set(hdrs, "user-agent", USER_AGENT); + + xs *payload = NULL; + int status; + int p_size; + + xs *rsp = xs_http_request("GET", url, hdrs, NULL, 0, &status, &payload, &p_size, 0); + + if (!valid_status(status) || !xs_is_string(payload)) + return; + + /* not an RSS? done */ + const char *ctype = xs_dict_get(rsp, "content-type"); + if (!xs_is_string(ctype) || xs_str_in(ctype, "application/rss+xml") == -1) + return; + + snac_log(user, xs_fmt("parsing RSS %s", url)); + + /* yes, parsing is done with regexes (now I have two problems blah blah blah) */ + xs *links = xs_regex_select(payload, "[^<]+"); + const char *link; + + xs_list_foreach(links, link) { + xs *l = xs_replace(link, "", ""); + char *p = strchr(l, '<'); + + if (p == NULL) + continue; + *p = '\0'; + + /* skip this same URL */ + if (strcmp(l, url) == 0) + continue; + + snac_debug(user, 1, xs_fmt("RSS link: %s", l)); + + if (timeline_here(user, l)) { + snac_debug(user, 1, xs_fmt("RSS entry already in timeline %s", l)); + continue; + } + + /* special trick for Mastodon: convert from the alternate format */ + if (strchr(l, '@') != NULL) { + xs *l2 = xs_split(l, "/"); + + if (xs_list_len(l2) == 5) { + const char *uid = xs_list_get(l2, 3); + if (*uid == '@') { + xs *guessed_id = xs_fmt("https:/" "/%s/users/%s/statuses/%s", + xs_list_get(l2, 2), uid + 1, xs_list_get(l2, -1)); + + if (timeline_here(user, guessed_id)) { + snac_debug(user, 1, xs_fmt("RSS entry already in timeline (alt) %s", guessed_id)); + continue; + } + } + } + } + + xs *obj = NULL; + + if (!valid_status(object_get(l, &obj))) { + /* object is not here: bring it */ + if (!valid_status(activitypub_request(user, l, &obj))) + continue; + } + + if (xs_is_dict(obj)) { + const char *id = xs_dict_get(obj, "id"); + const char *type = xs_dict_get(obj, "type"); + const char *attr_to = get_atto(obj); + + if (!xs_is_string(id) || !xs_is_string(type) || !xs_is_string(attr_to)) + continue; + + if (!xs_match(type, POSTLIKE_OBJECT_TYPE)) + continue; + + if (timeline_here(user, id)) { + snac_debug(user, 1, xs_fmt("RSS entry already in timeline (id) %s", id)); + continue; + } + + if (!valid_status(actor_request(user, attr_to, NULL))) + continue; + + timeline_add(user, id, obj); + } + } +} + + +void rss_process(void) +/* parses all RSS from all users */ +{ + xs *list = user_list(); + const char *uid; + + xs_list_foreach(list, uid) { + snac user; + + if (user_open(&user, uid)) { + const xs_list *rss = xs_dict_get(user.config, "rss"); + + if (xs_is_list(rss)) { + const char *url; + + xs_list_foreach(rss, url) + rss_to_timeline(&user, url); + } + + user_free(&user); + } + } +} diff --git a/snac.h b/snac.h index f95ba4d..db51a1c 100644 --- a/snac.h +++ b/snac.h @@ -463,3 +463,5 @@ const char *lang_str(const char *str, const snac *user); xs_str *rss_from_timeline(snac *user, const xs_list *timeline, const char *title, const char *link, const char *desc); +void rss_to_timeline(snac *user, const char *url); +void rss_process(void); -- cgit v1.2.3 From c10f6f0e6c3b82c02cbd47707b399330173cae38 Mon Sep 17 00:00:00 2001 From: grunfink Date: Wed, 28 May 2025 07:59:50 +0200 Subject: Updated dependencies. --- Makefile | 3 ++- Makefile.NetBSD | 3 ++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/Makefile b/Makefile index bb4029c..4a24899 100644 --- a/Makefile +++ b/Makefile @@ -67,7 +67,8 @@ main.o: main.c xs.h xs_io.h xs_json.h xs_time.h xs_openssl.h xs_match.h \ mastoapi.o: mastoapi.c xs.h xs_hex.h xs_openssl.h xs_json.h xs_io.h \ xs_time.h xs_glob.h xs_set.h xs_random.h xs_url.h xs_mime.h xs_match.h \ xs_unicode.h snac.h http_codes.h -rss.o: rss.c xs.h xs_html.h xs_regex.h xs_time.h snac.h http_codes.h +rss.o: rss.c xs.h xs_html.h xs_regex.h xs_time.h xs_match.h xs_curl.h \ + snac.h http_codes.h sandbox.o: sandbox.c xs.h snac.h http_codes.h snac.o: snac.c xs.h xs_hex.h xs_io.h xs_unicode_tbl.h xs_unicode.h \ xs_json.h xs_curl.h xs_openssl.h xs_socket.h xs_unix_socket.h xs_url.h \ diff --git a/Makefile.NetBSD b/Makefile.NetBSD index a2bf83c..9dc534e 100644 --- a/Makefile.NetBSD +++ b/Makefile.NetBSD @@ -56,7 +56,8 @@ main.o: main.c xs.h xs_io.h xs_json.h xs_time.h xs_openssl.h xs_match.h \ mastoapi.o: mastoapi.c xs.h xs_hex.h xs_openssl.h xs_json.h xs_io.h \ xs_time.h xs_glob.h xs_set.h xs_random.h xs_url.h xs_mime.h xs_match.h \ xs_unicode.h snac.h http_codes.h -rss.o: rss.c xs.h xs_html.h xs_regex.h xs_time.h snac.h http_codes.h +rss.o: rss.c xs.h xs_html.h xs_regex.h xs_time.h xs_match.h xs_curl.h \ + snac.h http_codes.h sandbox.o: sandbox.c xs.h snac.h http_codes.h snac.o: snac.c xs.h xs_hex.h xs_io.h xs_unicode_tbl.h xs_unicode.h \ xs_json.h xs_curl.h xs_openssl.h xs_socket.h xs_unix_socket.h xs_url.h \ -- cgit v1.2.3 From 9f8cd38ee72d93dec626b1e926f969cb75fd3094 Mon Sep 17 00:00:00 2001 From: grunfink Date: Wed, 28 May 2025 08:00:47 +0200 Subject: Disabled rss_process() by now. --- rss.c | 2 ++ 1 file changed, 2 insertions(+) diff --git a/rss.c b/rss.c index a3a092e..b31ffd3 100644 --- a/rss.c +++ b/rss.c @@ -207,6 +207,7 @@ void rss_to_timeline(snac *user, const char *url) void rss_process(void) /* parses all RSS from all users */ { +#if 0 xs *list = user_list(); const char *uid; @@ -226,4 +227,5 @@ void rss_process(void) user_free(&user); } } +#endif } -- cgit v1.2.3 From e2bb8078641f810b019635dd41f3e7ea191c5b52 Mon Sep 17 00:00:00 2001 From: grunfink Date: Wed, 28 May 2025 08:45:44 +0200 Subject: Don't prepend a # before a hashtag that is really an URL. --- html.c | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/html.c b/html.c index fadcfb0..7fcc266 100644 --- a/html.c +++ b/html.c @@ -4956,9 +4956,16 @@ int html_post_handler(const xs_dict *req, const char *q_path, if (*s1 == '\0') continue; - xs *s2 = xs_utf8_to_lower(s1); - if (*s2 != '#') - s2 = xs_str_prepend_i(s2, "#"); + xs *s2 = NULL; + + if (xs_startswith(s1, "https:/")) + s2 = xs_dup(s1); + else { + s2 = xs_utf8_to_lower(s1); + + if (*s2 != '#') + s2 = xs_str_prepend_i(s2, "#"); + } new_hashtags = xs_list_append(new_hashtags, s2); } -- cgit v1.2.3 From a1369b39c1bd3d2036af12368997648454ca5564 Mon Sep 17 00:00:00 2001 From: grunfink Date: Wed, 28 May 2025 09:07:19 +0200 Subject: Activated hashtag RSS polling. --- activitypub.c | 4 ++++ html.c | 3 ++- httpd.c | 42 +++++++++++++++++++++++++++++++----------- rss.c | 9 +++++---- snac.h | 2 +- 5 files changed, 43 insertions(+), 17 deletions(-) diff --git a/activitypub.c b/activitypub.c index 0cc7bcb..2cffeac 100644 --- a/activitypub.c +++ b/activitypub.c @@ -3049,6 +3049,10 @@ void process_queue_item(xs_dict *q_item) } } } + else + if (strcmp(type, "rss_poll") == 0) { + rss_poll_hashtags(); + } else srv_log(xs_fmt("unexpected q_item type '%s'", type)); } diff --git a/html.c b/html.c index 7fcc266..3d213a2 100644 --- a/html.c +++ b/html.c @@ -1601,7 +1601,8 @@ xs_html *html_top_controls(snac *user) xs_html_attr("name", "followed_hashtags"), xs_html_attr("cols", "40"), xs_html_attr("rows", "4"), - xs_html_attr("placeholder", "#cats\n#windowfriday\n#classicalmusic"), + xs_html_attr("placeholder", "#cats\n#windowfriday\n#classicalmusic\nhttps:/" + "/mastodon.social/tags/dogs"), xs_html_text(followed_hashtags)), xs_html_tag("br", NULL), diff --git a/httpd.c b/httpd.c index 41b2515..c94a542 100644 --- a/httpd.c +++ b/httpd.c @@ -705,34 +705,36 @@ static pthread_cond_t sleep_cond; static void *background_thread(void *arg) /* background thread (queue management and other things) */ { - time_t purge_time; + time_t t, purge_time, rss_time; (void)arg; + t = time(NULL); + /* first purge time */ - purge_time = time(NULL) + 10 * 60; + purge_time = t + 10 * 60; + + /* first RSS polling time */ + rss_time = t + 15 * 60; srv_log(xs_fmt("background thread started")); while (p_state->srv_running) { - time_t t; int cnt = 0; p_state->th_state[0] = THST_QUEUE; { xs *list = user_list(); - char *p; const char *uid; /* process queues for all users */ - p = list; - while (xs_list_iter(&p, &uid)) { - snac snac; + xs_list_foreach(list, uid) { + snac user; - if (user_open(&snac, uid)) { - cnt += process_user_queue(&snac); - user_free(&snac); + if (user_open(&user, uid)) { + cnt += process_user_queue(&user); + user_free(&user); } } } @@ -740,8 +742,10 @@ static void *background_thread(void *arg) /* global queue */ cnt += process_queue(); + t = time(NULL); + /* time to purge? */ - if ((t = time(NULL)) > purge_time) { + if (t > purge_time) { /* next purge time is tomorrow */ purge_time = t + 24 * 60 * 60; @@ -750,6 +754,22 @@ static void *background_thread(void *arg) job_post(q_item, 0); } + /* time to poll the RSS? */ + if (t > rss_time) { + /* next RSS poll time */ + int hours = xs_number_get(xs_dict_get_def(srv_config, "rss_poll_hours", "4")); + + /* don't hammer servers too much */ + if (hours < 1) + hours = 1; + + rss_time = t + 60 * 60 * hours; + + xs *q_item = xs_dict_new(); + q_item = xs_dict_append(q_item, "type", "rss_poll"); + job_post(q_item, 0); + } + if (cnt == 0) { /* sleep 3 seconds */ diff --git a/rss.c b/rss.c index b31ffd3..ce3e184 100644 --- a/rss.c +++ b/rss.c @@ -110,6 +110,9 @@ xs_str *rss_from_timeline(snac *user, const xs_list *timeline, void rss_to_timeline(snac *user, const char *url) /* reads an RSS and inserts all ActivityPub posts into the user's timeline */ { + if (!xs_startswith(url, "https:/") && !xs_startswith(url, "http:/")) + return; + xs *hdrs = xs_dict_new(); hdrs = xs_dict_set(hdrs, "accept", "application/rss+xml"); hdrs = xs_dict_set(hdrs, "user-agent", USER_AGENT); @@ -204,10 +207,9 @@ void rss_to_timeline(snac *user, const char *url) } -void rss_process(void) +void rss_poll_hashtags(void) /* parses all RSS from all users */ { -#if 0 xs *list = user_list(); const char *uid; @@ -215,7 +217,7 @@ void rss_process(void) snac user; if (user_open(&user, uid)) { - const xs_list *rss = xs_dict_get(user.config, "rss"); + const xs_list *rss = xs_dict_get(user.config, "followed_hashtags"); if (xs_is_list(rss)) { const char *url; @@ -227,5 +229,4 @@ void rss_process(void) user_free(&user); } } -#endif } diff --git a/snac.h b/snac.h index db51a1c..06a36f1 100644 --- a/snac.h +++ b/snac.h @@ -464,4 +464,4 @@ const char *lang_str(const char *str, const snac *user); xs_str *rss_from_timeline(snac *user, const xs_list *timeline, const char *title, const char *link, const char *desc); void rss_to_timeline(snac *user, const char *url); -void rss_process(void); +void rss_poll_hashtags(void); -- cgit v1.2.3 From 30b47a716fb02221cbffa4f91d1859ed9c450881 Mon Sep 17 00:00:00 2001 From: grunfink Date: Wed, 28 May 2025 09:15:18 +0200 Subject: Updated documentation. --- doc/snac.8 | 3 +++ 1 file changed, 3 insertions(+) diff --git a/doc/snac.8 b/doc/snac.8 index 1537f89..729848d 100644 --- a/doc/snac.8 +++ b/doc/snac.8 @@ -277,6 +277,9 @@ the usual one, like in smtp://mail.example.com:587. .It Ic smtp_user .It Ic smtp_password To be filled if the SMTP server defined by the previous directive needs credentials. +.It Ic rss_hashtag_poll_hours +The periodic number of hours hashtag RSS are polled (default: 4). It has a minimum +value of 1 to avoid hammering servers. .El .Pp You must restart the server to make effective these changes. -- cgit v1.2.3 From 0594197af7a4a11795150a85fafa7fa5bc6fc4f5 Mon Sep 17 00:00:00 2001 From: grunfink Date: Wed, 28 May 2025 09:16:23 +0200 Subject: Renamed server knob to 'rss_hashtag_poll_hours'. --- activitypub.c | 2 +- httpd.c | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/activitypub.c b/activitypub.c index 2cffeac..8877a27 100644 --- a/activitypub.c +++ b/activitypub.c @@ -3050,7 +3050,7 @@ void process_queue_item(xs_dict *q_item) } } else - if (strcmp(type, "rss_poll") == 0) { + if (strcmp(type, "rss_hashtag_poll") == 0) { rss_poll_hashtags(); } else diff --git a/httpd.c b/httpd.c index c94a542..15634d1 100644 --- a/httpd.c +++ b/httpd.c @@ -757,7 +757,7 @@ static void *background_thread(void *arg) /* time to poll the RSS? */ if (t > rss_time) { /* next RSS poll time */ - int hours = xs_number_get(xs_dict_get_def(srv_config, "rss_poll_hours", "4")); + int hours = xs_number_get(xs_dict_get_def(srv_config, "rss_hashtag_poll_hours", "4")); /* don't hammer servers too much */ if (hours < 1) @@ -766,7 +766,7 @@ static void *background_thread(void *arg) rss_time = t + 60 * 60 * hours; xs *q_item = xs_dict_new(); - q_item = xs_dict_append(q_item, "type", "rss_poll"); + q_item = xs_dict_append(q_item, "type", "rss_hashtag_poll"); job_post(q_item, 0); } -- cgit v1.2.3 From fc8e688e1b2e1685e6e32adfcde1639c008c5b28 Mon Sep 17 00:00:00 2001 From: grunfink Date: Wed, 28 May 2025 09:21:48 +0200 Subject: Updated documentation. --- doc/snac.1 | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/doc/snac.1 b/doc/snac.1 index 92e30b7..a1139db 100644 --- a/doc/snac.1 +++ b/doc/snac.1 @@ -85,7 +85,9 @@ liked. This option opens the user setup dialog. .It Followed hashtags... Enter here the list of hashtags you want to follow, one -per line, with or without the # symbol. +per line, with or without the # symbol. Since version 2.78, +URLs to RSS feeds of ActivityPub objects are also allowed +(like e.g. https://mastodon.social/tags/bloomscrolling). .It Blocked hashtags... Enter here the list of hashtags you want to block, one per line, with or without the # symbol. -- cgit v1.2.3 From d10ec94f384634fa96f1bd552463bb01dbec3b02 Mon Sep 17 00:00:00 2001 From: grunfink Date: Wed, 28 May 2025 09:27:58 +0200 Subject: Updated RELEASE_NOTES. --- RELEASE_NOTES.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/RELEASE_NOTES.md b/RELEASE_NOTES.md index 620ee7e..2d53aca 100644 --- a/RELEASE_NOTES.md +++ b/RELEASE_NOTES.md @@ -2,6 +2,8 @@ ## UNRELEASED +Hashtag following also allow URLs to RSS feeds of ActivityPub objects (like e.g. https://mastodon.social/tags/ThankYouTuesday). + The number of pending follow confirmations is shown next to the "people" link. Faster performance metrics (contributed by dandelions). -- cgit v1.2.3 From 77b9cbe6c3064c1656fa5375f7d2d498e34a34e6 Mon Sep 17 00:00:00 2001 From: grunfink Date: Wed, 28 May 2025 09:39:07 +0200 Subject: Added more checks. --- rss.c | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/rss.c b/rss.c index ce3e184..8798cac 100644 --- a/rss.c +++ b/rss.c @@ -149,6 +149,10 @@ void rss_to_timeline(snac *user, const char *url) if (strcmp(l, url) == 0) continue; + /* skip crap */ + if (!xs_startswith(l, "https:/") && !xs_startswith(l, "http:/")) + return; + snac_debug(user, 1, xs_fmt("RSS link: %s", l)); if (timeline_here(user, l)) { -- cgit v1.2.3 From 34d85b3e1bbda2e89191026b3815dd9b6f23afcb Mon Sep 17 00:00:00 2001 From: grunfink Date: Thu, 29 May 2025 13:12:43 +0200 Subject: mastoapi: fixed uploaded images name collision. --- html.c | 2 +- mastoapi.c | 6 ++++-- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/html.c b/html.c index 3d213a2..56dc2cc 100644 --- a/html.c +++ b/html.c @@ -4362,7 +4362,7 @@ int html_post_handler(const xs_dict *req, const char *q_path, xs_rnd_buf(rnd, sizeof(rnd)); const char *ext = strrchr(fn, '.'); - xs *hash = xs_md5_hex(rnd, strlen(rnd)); + xs *hash = xs_md5_hex(rnd, sizeof(rnd)); xs *id = xs_fmt("post-%s%s", hash, ext ? ext : ""); xs *url = xs_fmt("%s/s/%s", snac.actor, id); int fo = xs_number_get(xs_list_get(attach_file, 1)); diff --git a/mastoapi.c b/mastoapi.c index df713a8..27c476e 100644 --- a/mastoapi.c +++ b/mastoapi.c @@ -2978,8 +2978,10 @@ int mastoapi_post_handler(const xs_dict *req, const char *q_path, if (*fn != '\0') { char *ext = strrchr(fn, '.'); - xs *hash = xs_md5_hex(fn, strlen(fn)); - xs *id = xs_fmt("%s%s", hash, ext); + char rnd[32]; + xs_rnd_buf(rnd, sizeof(rnd)); + xs *hash = xs_md5_hex(rnd, sizeof(rnd)); + xs *id = xs_fmt("post-%s%s", hash, ext ? ext : ""); xs *url = xs_fmt("%s/s/%s", snac.actor, id); int fo = xs_number_get(xs_list_get(file, 1)); int fs = xs_number_get(xs_list_get(file, 2)); -- cgit v1.2.3 From f77ee3df67e07a286319f64952b926cb427cd2ae Mon Sep 17 00:00:00 2001 From: grunfink Date: Thu, 29 May 2025 13:31:21 +0200 Subject: Updated RELEASE_NOTES. --- RELEASE_NOTES.md | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/RELEASE_NOTES.md b/RELEASE_NOTES.md index 2d53aca..2e1b37e 100644 --- a/RELEASE_NOTES.md +++ b/RELEASE_NOTES.md @@ -8,11 +8,13 @@ The number of pending follow confirmations is shown next to the "people" link. Faster performance metrics (contributed by dandelions). -Improved lowercasing in tags (contributed by postscriptum). +Improved lowercasing in hashtags (contributed by postscriptum). A search-by-url tweak for implementations that return 200 for invalid webfinger queries (e.g. piefed). -Mastodon API: added follow confirmation endpoints. +Mastodon API: added follow confirmation endpoints, fixed collisions in attachment file names. + +Fixed potential crashes in attachment uploads. ## 2.77 "Ugly Links Everywhere" -- cgit v1.2.3 From b3067987354ca8512979eef87d3de89adb5f741d Mon Sep 17 00:00:00 2001 From: grunfink Date: Thu, 29 May 2025 17:33:41 +0200 Subject: Fixed typo in rss_to_timeline(). --- rss.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/rss.c b/rss.c index 8798cac..ed0bf05 100644 --- a/rss.c +++ b/rss.c @@ -151,7 +151,7 @@ void rss_to_timeline(snac *user, const char *url) /* skip crap */ if (!xs_startswith(l, "https:/") && !xs_startswith(l, "http:/")) - return; + continue; snac_debug(user, 1, xs_fmt("RSS link: %s", l)); -- cgit v1.2.3 From e030fe6c5054c0a9b76a55adc80bb81d7a73fcc3 Mon Sep 17 00:00:00 2001 From: grunfink Date: Thu, 29 May 2025 18:03:23 +0200 Subject: Use HTTP caching (etag / if-none-match) in RSS downloads. --- Makefile | 2 +- Makefile.NetBSD | 2 +- rss.c | 40 ++++++++++++++++++++++++++++++++++++++-- 3 files changed, 40 insertions(+), 4 deletions(-) diff --git a/Makefile b/Makefile index 4a24899..6059fef 100644 --- a/Makefile +++ b/Makefile @@ -68,7 +68,7 @@ mastoapi.o: mastoapi.c xs.h xs_hex.h xs_openssl.h xs_json.h xs_io.h \ xs_time.h xs_glob.h xs_set.h xs_random.h xs_url.h xs_mime.h xs_match.h \ xs_unicode.h snac.h http_codes.h rss.o: rss.c xs.h xs_html.h xs_regex.h xs_time.h xs_match.h xs_curl.h \ - snac.h http_codes.h + xs_openssl.h xs_json.h snac.h http_codes.h sandbox.o: sandbox.c xs.h snac.h http_codes.h snac.o: snac.c xs.h xs_hex.h xs_io.h xs_unicode_tbl.h xs_unicode.h \ xs_json.h xs_curl.h xs_openssl.h xs_socket.h xs_unix_socket.h xs_url.h \ diff --git a/Makefile.NetBSD b/Makefile.NetBSD index 9dc534e..d19ab2d 100644 --- a/Makefile.NetBSD +++ b/Makefile.NetBSD @@ -57,7 +57,7 @@ mastoapi.o: mastoapi.c xs.h xs_hex.h xs_openssl.h xs_json.h xs_io.h \ xs_time.h xs_glob.h xs_set.h xs_random.h xs_url.h xs_mime.h xs_match.h \ xs_unicode.h snac.h http_codes.h rss.o: rss.c xs.h xs_html.h xs_regex.h xs_time.h xs_match.h xs_curl.h \ - snac.h http_codes.h + xs_openssl.h xs_json.h snac.h http_codes.h sandbox.o: sandbox.c xs.h snac.h http_codes.h snac.o: snac.c xs.h xs_hex.h xs_io.h xs_unicode_tbl.h xs_unicode.h \ xs_json.h xs_curl.h xs_openssl.h xs_socket.h xs_unix_socket.h xs_url.h \ diff --git a/rss.c b/rss.c index ed0bf05..9d582f7 100644 --- a/rss.c +++ b/rss.c @@ -7,6 +7,8 @@ #include "xs_time.h" #include "xs_match.h" #include "xs_curl.h" +#include "xs_openssl.h" +#include "xs_json.h" #include "snac.h" @@ -117,12 +119,37 @@ void rss_to_timeline(snac *user, const char *url) hdrs = xs_dict_set(hdrs, "accept", "application/rss+xml"); hdrs = xs_dict_set(hdrs, "user-agent", USER_AGENT); + /* get the RSS metadata */ + xs *md5 = xs_md5_hex(url, strlen(url)); + xs *rss_md_fn = xs_fmt("%s/rss", user->basedir); + mkdirx(rss_md_fn); + rss_md_fn = xs_str_cat(rss_md_fn, "/", md5, ".json"); + + xs *rss_md = NULL; + const char *etag = NULL; + + FILE *f; + if ((f = fopen(rss_md_fn, "r")) != NULL) { + rss_md = xs_json_load(f); + fclose(f); + + etag = xs_dict_get(rss_md, "etag"); + + if (xs_is_string(etag)) + hdrs = xs_dict_set(hdrs, "if-none-match", etag); + } + + if (rss_md == NULL) + rss_md = xs_dict_new(); + xs *payload = NULL; int status; int p_size; xs *rsp = xs_http_request("GET", url, hdrs, NULL, 0, &status, &payload, &p_size, 0); + snac_log(user, xs_fmt("parsing RSS %s %d", url, status)); + if (!valid_status(status) || !xs_is_string(payload)) return; @@ -131,8 +158,6 @@ void rss_to_timeline(snac *user, const char *url) if (!xs_is_string(ctype) || xs_str_in(ctype, "application/rss+xml") == -1) return; - snac_log(user, xs_fmt("parsing RSS %s", url)); - /* yes, parsing is done with regexes (now I have two problems blah blah blah) */ xs *links = xs_regex_select(payload, "[^<]+"); const char *link; @@ -208,6 +233,17 @@ void rss_to_timeline(snac *user, const char *url) timeline_add(user, id, obj); } } + + /* update the RSS metadata */ + etag = xs_dict_get(rsp, "etag"); + + if (xs_is_string(etag)) { + rss_md = xs_dict_set(rss_md, "etag", etag); + if ((f = fopen(rss_md_fn, "w")) != NULL) { + xs_json_dump(rss_md, 4, f); + fclose(f); + } + } } -- cgit v1.2.3 From 5ec55544e4d83503776f517d62262ae176ca15c6 Mon Sep 17 00:00:00 2001 From: grunfink Date: Thu, 29 May 2025 22:52:20 +0200 Subject: New command-line option 'poll_hashtag_rss'. --- main.c | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/main.c b/main.c index 80df2d0..c948b57 100644 --- a/main.c +++ b/main.c @@ -143,6 +143,11 @@ int main(int argc, char *argv[]) return 0; } + if (strcmp(cmd, "poll_hashtag_rss") == 0) { /** **/ + rss_poll_hashtags(); + return 0; + } + if (strcmp(cmd, "state") == 0) { /** **/ xs *shm_name = NULL; srv_state *p_state = srv_state_op(&shm_name, 1); -- cgit v1.2.3 From 714ff17e3ccc55a9e70e03ee7b91cd1dd75e134b Mon Sep 17 00:00:00 2001 From: grunfink Date: Thu, 29 May 2025 22:52:39 +0200 Subject: Also store the url in the RSS metadata. --- rss.c | 1 + 1 file changed, 1 insertion(+) diff --git a/rss.c b/rss.c index 9d582f7..8d55370 100644 --- a/rss.c +++ b/rss.c @@ -239,6 +239,7 @@ void rss_to_timeline(snac *user, const char *url) if (xs_is_string(etag)) { rss_md = xs_dict_set(rss_md, "etag", etag); + rss_md = xs_dict_set(rss_md, "url", url); if ((f = fopen(rss_md_fn, "w")) != NULL) { xs_json_dump(rss_md, 4, f); fclose(f); -- cgit v1.2.3 From cd51d5310c8fa08a27018b40e6afdd1eb3d6e12e Mon Sep 17 00:00:00 2001 From: grunfink Date: Fri, 30 May 2025 09:47:23 +0200 Subject: Call enqueue_actor_fresh() from rss_to_timeline(). --- rss.c | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/rss.c b/rss.c index 8d55370..f72dac4 100644 --- a/rss.c +++ b/rss.c @@ -227,10 +227,11 @@ void rss_to_timeline(snac *user, const char *url) continue; } - if (!valid_status(actor_request(user, attr_to, NULL))) - continue; + enqueue_actor_refresh(user, attr_to, 0); timeline_add(user, id, obj); + + snac_log(user, xs_fmt("new '%s' (RSS) %s %s", type, attr_to, id)); } } -- cgit v1.2.3 From c8f6e1865dca9477d0cdfb95168bdca4a62852c3 Mon Sep 17 00:00:00 2001 From: grunfink Date: Fri, 30 May 2025 11:30:37 +0200 Subject: New function enqueue_notify_webhook(). --- data.c | 19 +++++++++++++++++++ snac.h | 2 ++ 2 files changed, 21 insertions(+) diff --git a/data.c b/data.c index 224976b..ddfb443 100644 --- a/data.c +++ b/data.c @@ -3166,6 +3166,8 @@ void notify_add(snac *snac, const char *type, const char *utype, pthread_mutex_unlock(&data_mutex); } + + enqueue_notify_webhook(snac, noti, 0); } @@ -3521,6 +3523,23 @@ void enqueue_webmention(const xs_dict *msg) } +void enqueue_notify_webhook(snac *user, const xs_dict *noti, int retries) +/* enqueues a notification webhook */ +{ + const char *webhook = xs_dict_get(user->config, "notify_webhook"); + + if (xs_is_string(webhook)) { + xs *qmsg = _new_qmsg("notify_webhook", noti, retries); + const char *ntid = xs_dict_get(qmsg, "ntid"); + xs *fn = xs_fmt("%s/queue/%s.json", user->basedir, ntid); + + qmsg = _enqueue_put(fn, qmsg); + + snac_debug(user, 1, xs_fmt("notify_webhook")); + } +} + + int was_question_voted(snac *user, const char *id) /* returns true if the user voted in this poll */ { diff --git a/snac.h b/snac.h index 06a36f1..b13a44a 100644 --- a/snac.h +++ b/snac.h @@ -294,6 +294,8 @@ void enqueue_object_request(snac *user, const char *id, int forward_secs); void enqueue_verify_links(snac *user); void enqueue_actor_refresh(snac *user, const char *actor, int forward_secs); void enqueue_webmention(const xs_dict *msg); +void enqueue_notify_webhook(snac *user, const xs_dict *noti, int retries); + int was_question_voted(snac *user, const char *id); xs_list *user_queue(snac *snac); -- cgit v1.2.3 From 5a2aef8666a82a30dff329992bd41baa53d4e123 Mon Sep 17 00:00:00 2001 From: grunfink Date: Fri, 30 May 2025 12:12:19 +0200 Subject: More notify_webhook work. --- activitypub.c | 33 +++++++++++++++++++++++++++++++++ data.c | 11 ++++++++++- 2 files changed, 43 insertions(+), 1 deletion(-) diff --git a/activitypub.c b/activitypub.c index 8877a27..fb0c42c 100644 --- a/activitypub.c +++ b/activitypub.c @@ -2765,6 +2765,39 @@ void process_user_queue_item(snac *user, xs_dict *q_item) snac_log(user, xs_fmt("actor_refresh %s %d", actor, status)); } } + else + if (strcmp(type, "notify_webhook") == 0) { + const char *webhook = xs_dict_get(user->config, "notify_webhook"); + + if (xs_is_string(webhook)) { + const xs_dict *msg = xs_dict_get(q_item, "message"); + int retries = xs_number_get(xs_dict_get(q_item, "retries")); + + xs *hdrs = xs_dict_new(); + + hdrs = xs_dict_set(hdrs, "content-type", "application/json"); + hdrs = xs_dict_set(hdrs, "user-agent", USER_AGENT); + + xs *body = xs_json_dumps(msg, 4); + + int status; + xs *rsp = xs_http_request("POST", webhook, hdrs, body, strlen(body), &status, NULL, NULL, 0); + + snac_debug(user, 0, xs_fmt("webhook post %s %d", webhook, status)); + + if (!valid_status(status)) { + retries++; + + if (retries > queue_retry_max) + snac_debug(user, 0, xs_fmt("webhook post giving up %s", webhook)); + else { + snac_debug(user, 0, xs_fmt("webhook post requeue %s %d", webhook, retries)); + + enqueue_notify_webhook(user, msg, retries); + } + } + } + } else snac_log(user, xs_fmt("unexpected user q_item type '%s'", type)); } diff --git a/data.c b/data.c index ddfb443..10084f9 100644 --- a/data.c +++ b/data.c @@ -3529,7 +3529,16 @@ void enqueue_notify_webhook(snac *user, const xs_dict *noti, int retries) const char *webhook = xs_dict_get(user->config, "notify_webhook"); if (xs_is_string(webhook)) { - xs *qmsg = _new_qmsg("notify_webhook", noti, retries); + xs *msg = xs_dup(noti); + + /* add more data */ + msg = xs_dict_set(msg, "target", user->actor); + xs *actor_obj = NULL; + + if (valid_status(object_get(xs_dict_get(noti, "actor"), &actor_obj)) && actor_obj) + msg = xs_dict_set(msg, "account", actor_obj); + + xs *qmsg = _new_qmsg("notify_webhook", msg, retries); const char *ntid = xs_dict_get(qmsg, "ntid"); xs *fn = xs_fmt("%s/queue/%s.json", user->basedir, ntid); -- cgit v1.2.3 From 017e59e98c33ec538c04eb7452f7c335676a67c4 Mon Sep 17 00:00:00 2001 From: grunfink Date: Fri, 30 May 2025 18:03:18 +0200 Subject: Updated RELEASE_NOTES. --- RELEASE_NOTES.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/RELEASE_NOTES.md b/RELEASE_NOTES.md index 2e1b37e..bde5218 100644 --- a/RELEASE_NOTES.md +++ b/RELEASE_NOTES.md @@ -4,6 +4,8 @@ Hashtag following also allow URLs to RSS feeds of ActivityPub objects (like e.g. https://mastodon.social/tags/ThankYouTuesday). +Users can now configure a webhook to receive an HTTP POST for every notification. This can be useful for implementing bots that react to activities (see `snac(1)` for more information). + The number of pending follow confirmations is shown next to the "people" link. Faster performance metrics (contributed by dandelions). -- cgit v1.2.3 From e1b6ea0cbee222839185cdb608bc729c970dd1ec Mon Sep 17 00:00:00 2001 From: grunfink Date: Fri, 30 May 2025 19:03:49 +0200 Subject: auto_follower_webhook.py new example program. --- examples/auto_follower_webhook.py | 55 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 55 insertions(+) create mode 100755 examples/auto_follower_webhook.py diff --git a/examples/auto_follower_webhook.py b/examples/auto_follower_webhook.py new file mode 100755 index 0000000..e1192c1 --- /dev/null +++ b/examples/auto_follower_webhook.py @@ -0,0 +1,55 @@ +#!/usr/bin/env python3 + +# This is an example of a snac webhook that automatically follows all new followers. + +# To use it, set the variables snac_basedir and snac_user to something reasonable, +# configure the user webhook to be http://localhost:12345, and run this program. + +# copyright (C) 2025 grunfink et al. / MIT license + +from http.server import BaseHTTPRequestHandler, HTTPServer +import time +import json +import os + +host_name = "localhost" +server_port = 12345 +snac_basedir = "/var/snac/example_instance" +snac_user = "example_user" + +class SnacAutoResponderServer(BaseHTTPRequestHandler): + + def do_POST(self): + self.send_response(200) + self.end_headers() + + content_type = self.headers["content-type"] + content_length = int(self.headers["content-length"]) + payload = self.rfile.read(content_length).decode("utf-8") + + if content_type == "application/json": + try: + noti = json.loads(payload) + + type = noti["type"] + + if type == "Follow": + actor = noti["actor"] + cmd = "snac follow %s %s %s" % (snac_basedir, snac_user, actor) + + os.system(cmd) + + except: + print("Error parsing notification") + +if __name__ == "__main__": + webServer = HTTPServer((host_name, server_port), SnacAutoResponderServer) + print("Webhook started http://%s:%s" % (host_name, server_port)) + + try: + webServer.serve_forever() + except KeyboardInterrupt: + pass + + webServer.server_close() + print("Webhook stopped.") -- cgit v1.2.3 From ffc7e52c6679d6cfeaa0567941cf803351031dcd Mon Sep 17 00:00:00 2001 From: grunfink Date: Fri, 30 May 2025 19:13:40 +0200 Subject: Added web UI to set the notify webhook. --- html.c | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/html.c b/html.c index 56dc2cc..a6e5f98 100644 --- a/html.c +++ b/html.c @@ -1290,6 +1290,7 @@ xs_html *html_top_controls(snac *user) const xs_val *show_foll = xs_dict_get(user->config, "show_contact_metrics"); const char *latitude = xs_dict_get_def(user->config, "latitude", ""); const char *longitude = xs_dict_get_def(user->config, "longitude", ""); + const char *webhook = xs_dict_get_def(user->config, "notify_webhook", ""); xs *metadata = NULL; const xs_dict *md = xs_dict_get(user->config, "metadata"); @@ -1451,6 +1452,14 @@ xs_html *html_top_controls(snac *user) xs_html_attr("name", "ntfy_token"), xs_html_attr("value", ntfy_token), xs_html_attr("placeholder", L("ntfy token - if needed")))), + xs_html_tag("p", + xs_html_text(L("Notify webhook:")), + xs_html_sctag("br", NULL), + xs_html_sctag("input", + xs_html_attr("type", "url"), + xs_html_attr("name", "notify_webhook"), + xs_html_attr("value", webhook), + xs_html_attr("placeholder", L("http://example.com/webhook")))), xs_html_tag("p", xs_html_text(L("Maximum days to keep posts (0: server settings):")), xs_html_sctag("br", NULL), @@ -4822,6 +4831,8 @@ int html_post_handler(const xs_dict *req, const char *q_path, 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", "")); + snac.config = xs_dict_set(snac.config, "notify_webhook", xs_dict_get_def(p_vars, "notify_webhook", "")); + if ((v = xs_dict_get(p_vars, "metadata")) != NULL) snac.config = xs_dict_set(snac.config, "metadata", v); -- cgit v1.2.3 From e41e5974c7943f1eb02497051ce5e8d5c5b40aed Mon Sep 17 00:00:00 2001 From: grunfink Date: Fri, 30 May 2025 19:22:43 +0200 Subject: Updated documentation. --- doc/snac.1 | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/doc/snac.1 b/doc/snac.1 index a1139db..d9eea5d 100644 --- a/doc/snac.1 +++ b/doc/snac.1 @@ -410,6 +410,14 @@ You can obtain an API Token by connecting to the following URL: https://$SNAC_HOST/oauth/x-snac-get-token .Ed .Pp +.Ss Webhook for notifications +Since version 2.78, users can set the URL to a webhook that will receive +an HTTP POST with every notification (in JSON format). This can be used to +implement some automation whenever a new activity happens. The +.Pa examples/ +subdirectory contains a tiny Python program that implements an auto-follower +for every new follow. +.Pp .Sh ENVIRONMENT .Bl -tag -width Ds .It SNAC_BASEDIR -- cgit v1.2.3 From f055c8b694a398868d14fd70df61d02429846dae Mon Sep 17 00:00:00 2001 From: grunfink Date: Fri, 30 May 2025 19:34:11 +0200 Subject: Added a new server knob disable_notify_webhook. --- activitypub.c | 2 +- data.c | 5 +++-- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/activitypub.c b/activitypub.c index fb0c42c..ab63abe 100644 --- a/activitypub.c +++ b/activitypub.c @@ -2769,7 +2769,7 @@ void process_user_queue_item(snac *user, xs_dict *q_item) if (strcmp(type, "notify_webhook") == 0) { const char *webhook = xs_dict_get(user->config, "notify_webhook"); - if (xs_is_string(webhook)) { + if (xs_is_string(webhook) && *webhook) { const xs_dict *msg = xs_dict_get(q_item, "message"); int retries = xs_number_get(xs_dict_get(q_item, "retries")); diff --git a/data.c b/data.c index 10084f9..74bc112 100644 --- a/data.c +++ b/data.c @@ -3167,7 +3167,8 @@ void notify_add(snac *snac, const char *type, const char *utype, pthread_mutex_unlock(&data_mutex); } - enqueue_notify_webhook(snac, noti, 0); + if (!xs_is_true(xs_dict_get(srv_config, "disable_notify_webhook"))) + enqueue_notify_webhook(snac, noti, 0); } @@ -3528,7 +3529,7 @@ void enqueue_notify_webhook(snac *user, const xs_dict *noti, int retries) { const char *webhook = xs_dict_get(user->config, "notify_webhook"); - if (xs_is_string(webhook)) { + if (xs_is_string(webhook) && *webhook) { xs *msg = xs_dup(noti); /* add more data */ -- cgit v1.2.3 From 0621742a3e7d5e56bf2f155a1f829fa2508c34a0 Mon Sep 17 00:00:00 2001 From: grunfink Date: Fri, 30 May 2025 19:37:32 +0200 Subject: Updated documentation. --- doc/snac.8 | 3 +++ 1 file changed, 3 insertions(+) diff --git a/doc/snac.8 b/doc/snac.8 index 729848d..85d08dd 100644 --- a/doc/snac.8 +++ b/doc/snac.8 @@ -280,6 +280,9 @@ To be filled if the SMTP server defined by the previous directive needs credenti .It Ic rss_hashtag_poll_hours The periodic number of hours hashtag RSS are polled (default: 4). It has a minimum value of 1 to avoid hammering servers. +.It Ic disable_notify_webhook +Since version 2.78, users can set a webhook URL to receive notifications. Set this +to true if you don't want your users to have this privilege. .El .Pp You must restart the server to make effective these changes. -- cgit v1.2.3 From 706816b4321337d56ff3121186567363b882d01d Mon Sep 17 00:00:00 2001 From: grunfink Date: Fri, 30 May 2025 19:43:55 +0200 Subject: Added uid, basedir and baseurl to the notify webhook message. --- data.c | 4 ++++ examples/auto_follower_webhook.py | 10 +++++----- 2 files changed, 9 insertions(+), 5 deletions(-) diff --git a/data.c b/data.c index 74bc112..28f7cd9 100644 --- a/data.c +++ b/data.c @@ -3534,6 +3534,10 @@ void enqueue_notify_webhook(snac *user, const xs_dict *noti, int retries) /* add more data */ msg = xs_dict_set(msg, "target", user->actor); + msg = xs_dict_set(msg, "uid", user->uid); + msg = xs_dict_set(msg, "basedir", srv_basedir); + msg = xs_dict_set(msg, "baseurl", srv_baseurl); + xs *actor_obj = NULL; if (valid_status(object_get(xs_dict_get(noti, "actor"), &actor_obj)) && actor_obj) diff --git a/examples/auto_follower_webhook.py b/examples/auto_follower_webhook.py index e1192c1..eb632a3 100755 --- a/examples/auto_follower_webhook.py +++ b/examples/auto_follower_webhook.py @@ -2,8 +2,7 @@ # This is an example of a snac webhook that automatically follows all new followers. -# To use it, set the variables snac_basedir and snac_user to something reasonable, -# configure the user webhook to be http://localhost:12345, and run this program. +# To use it, configure the user webhook to be http://localhost:12345, and run this program. # copyright (C) 2025 grunfink et al. / MIT license @@ -14,8 +13,6 @@ import os host_name = "localhost" server_port = 12345 -snac_basedir = "/var/snac/example_instance" -snac_user = "example_user" class SnacAutoResponderServer(BaseHTTPRequestHandler): @@ -35,7 +32,10 @@ class SnacAutoResponderServer(BaseHTTPRequestHandler): if type == "Follow": actor = noti["actor"] - cmd = "snac follow %s %s %s" % (snac_basedir, snac_user, actor) + uid = noti["uid"] + basedir = noti["basedir"] + + cmd = "snac follow %s %s %s" % (basedir, uid, actor) os.system(cmd) -- cgit v1.2.3 From 6a233d5ba051f8018e819c0dcf5217aba66558a2 Mon Sep 17 00:00:00 2001 From: grunfink Date: Fri, 30 May 2025 19:55:36 +0200 Subject: Updated documentation. --- doc/snac.1 | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/doc/snac.1 b/doc/snac.1 index d9eea5d..51673d4 100644 --- a/doc/snac.1 +++ b/doc/snac.1 @@ -127,6 +127,10 @@ standard ntfy.sh server), fill the two provided fields (ntfy server/topic and, if protected, the token). You need to refer to the https://ntfy.sh web site for more information on this process. +.It Notify webhook +If this is set to an URL, an HTTP POST will be sent to it +whenever a new notification happens (see the 'Webhook for +notifications' section below for more information). .It Maximum days to keep posts This numeric value specifies the number of days to pass before posts (yours and others') will be purged. This value overrides -- cgit v1.2.3 From c8848f6e9f8e9fd9d17290b1ef301d3bf7beccb4 Mon Sep 17 00:00:00 2001 From: grunfink Date: Fri, 30 May 2025 19:59:40 +0200 Subject: More webhook checks. --- activitypub.c | 2 +- data.c | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/activitypub.c b/activitypub.c index ab63abe..100db67 100644 --- a/activitypub.c +++ b/activitypub.c @@ -2769,7 +2769,7 @@ void process_user_queue_item(snac *user, xs_dict *q_item) if (strcmp(type, "notify_webhook") == 0) { const char *webhook = xs_dict_get(user->config, "notify_webhook"); - if (xs_is_string(webhook) && *webhook) { + if (xs_is_string(webhook) && xs_match(webhook, "https://*|http://*")) { /** **/ const xs_dict *msg = xs_dict_get(q_item, "message"); int retries = xs_number_get(xs_dict_get(q_item, "retries")); diff --git a/data.c b/data.c index 28f7cd9..6c38631 100644 --- a/data.c +++ b/data.c @@ -3529,7 +3529,7 @@ void enqueue_notify_webhook(snac *user, const xs_dict *noti, int retries) { const char *webhook = xs_dict_get(user->config, "notify_webhook"); - if (xs_is_string(webhook) && *webhook) { + if (xs_is_string(webhook) && xs_match(webhook, "https://*|http://*")) { /** **/ xs *msg = xs_dup(noti); /* add more data */ -- cgit v1.2.3 From 955b591a74399d48d54d52ac8899e3e70d369939 Mon Sep 17 00:00:00 2001 From: grunfink Date: Fri, 30 May 2025 20:52:15 +0200 Subject: Updated documentation. --- doc/snac.1 | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/doc/snac.1 b/doc/snac.1 index 51673d4..5fcb0c6 100644 --- a/doc/snac.1 +++ b/doc/snac.1 @@ -417,7 +417,8 @@ https://$SNAC_HOST/oauth/x-snac-get-token .Ss Webhook for notifications Since version 2.78, users can set the URL to a webhook that will receive an HTTP POST with every notification (in JSON format). This can be used to -implement some automation whenever a new activity happens. The +implement some automation whenever a new activity happens, like autorepliers +or chatbots or whatever. The .Pa examples/ subdirectory contains a tiny Python program that implements an auto-follower for every new follow. -- cgit v1.2.3 From a136f7533d223865adec2a0794b9c290be631290 Mon Sep 17 00:00:00 2001 From: grunfink Date: Sun, 1 Jun 2025 17:07:23 +0200 Subject: Updated documentation. --- doc/snac.1 | 19 ++++++++++++++++--- 1 file changed, 16 insertions(+), 3 deletions(-) diff --git a/doc/snac.1 b/doc/snac.1 index 5fcb0c6..d4e9af7 100644 --- a/doc/snac.1 +++ b/doc/snac.1 @@ -417,11 +417,24 @@ https://$SNAC_HOST/oauth/x-snac-get-token .Ss Webhook for notifications Since version 2.78, users can set the URL to a webhook that will receive an HTTP POST with every notification (in JSON format). This can be used to -implement some automation whenever a new activity happens, like autorepliers -or chatbots or whatever. The +implement some automation whenever a new activity happens, like autorepliers, +chatbots, interactive textual games or whatever. The .Pa examples/ subdirectory contains a tiny Python program that implements an auto-follower -for every new follow. +for every new follow. The JSON notification object includes the following data: +.Bd -literal -offset indent +id: a unique notification identifier +actor: the origin actor id +target: the target actor id +date: the notification date +message: the full ActivityPub action JSON object +objid: the object identifier (extracted from message, may be null) +type: the action type (extracted from message) +utype: the action subtype (extracted from message, may be null) +uid: the user identifier (account name) +basedir: the server base directory +baseurl: the server base URL +.Ed .Pp .Sh ENVIRONMENT .Bl -tag -width Ds -- cgit v1.2.3 From 4c7cc83708a0e335b383071df7594e13c5f05798 Mon Sep 17 00:00:00 2001 From: grunfink Date: Sun, 1 Jun 2025 17:19:59 +0200 Subject: Updated documentation. --- doc/snac.1 | 37 ++++++++++++++++++++++++------------- 1 file changed, 24 insertions(+), 13 deletions(-) diff --git a/doc/snac.1 b/doc/snac.1 index d4e9af7..583fe2f 100644 --- a/doc/snac.1 +++ b/doc/snac.1 @@ -422,19 +422,30 @@ chatbots, interactive textual games or whatever. The .Pa examples/ subdirectory contains a tiny Python program that implements an auto-follower for every new follow. The JSON notification object includes the following data: -.Bd -literal -offset indent -id: a unique notification identifier -actor: the origin actor id -target: the target actor id -date: the notification date -message: the full ActivityPub action JSON object -objid: the object identifier (extracted from message, may be null) -type: the action type (extracted from message) -utype: the action subtype (extracted from message, may be null) -uid: the user identifier (account name) -basedir: the server base directory -baseurl: the server base URL -.Ed +.Bl -tag -offset indent +.It id +a unique notification identifier +.It actor +the origin actor id +.It target +the target actor id +.It date +the notification date +.It message +the full ActivityPub action JSON object +.It objid +the object identifier (extracted from message, may be null) +.It type +the action type (extracted from message) +.It utype +the action subtype (extracted from message, may be null) +.It uid +the user identifier (account name) +.It basedir +the server base directory +.It baseurl +the server base URL +.El .Pp .Sh ENVIRONMENT .Bl -tag -width Ds -- cgit v1.2.3 From a723bf67ad22f4baead900efbf569c397d8063ed Mon Sep 17 00:00:00 2001 From: grunfink Date: Mon, 2 Jun 2025 09:58:02 +0200 Subject: Updated RELEASE_NOTES. --- RELEASE_NOTES.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/RELEASE_NOTES.md b/RELEASE_NOTES.md index bde5218..2132ce4 100644 --- a/RELEASE_NOTES.md +++ b/RELEASE_NOTES.md @@ -4,7 +4,7 @@ Hashtag following also allow URLs to RSS feeds of ActivityPub objects (like e.g. https://mastodon.social/tags/ThankYouTuesday). -Users can now configure a webhook to receive an HTTP POST for every notification. This can be useful for implementing bots that react to activities (see `snac(1)` for more information). +Users can now configure a webhook to receive an HTTP POST for every notification. This can be useful for implementing bots that react to activities, like autorepliers, chatbots or interactive textual games (see `snac(1)` for more information). The number of pending follow confirmations is shown next to the "people" link. -- cgit v1.2.3 From 59987f8970fcc17eb789c7d027e68819a8db5600 Mon Sep 17 00:00:00 2001 From: grunfink Date: Mon, 2 Jun 2025 12:34:43 +0200 Subject: Version 2.78 RELEASED. --- snac.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/snac.h b/snac.h index b13a44a..f3cb40d 100644 --- 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.78-dev" +#define VERSION "2.78" #define USER_AGENT "snac/" VERSION -- cgit v1.2.3 From 27caa511503b55af87483702aacc6051cc412f82 Mon Sep 17 00:00:00 2001 From: grunfink Date: Mon, 2 Jun 2025 12:40:10 +0200 Subject: Updated RELEASE_NOTES. --- RELEASE_NOTES.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/RELEASE_NOTES.md b/RELEASE_NOTES.md index 2132ce4..0960356 100644 --- a/RELEASE_NOTES.md +++ b/RELEASE_NOTES.md @@ -1,6 +1,6 @@ # Release Notes -## UNRELEASED +## 2.78 Hashtag following also allow URLs to RSS feeds of ActivityPub objects (like e.g. https://mastodon.social/tags/ThankYouTuesday). -- cgit v1.2.3 From 89c1a4a94b1032aed3af293c0f04d7a8f058ccda Mon Sep 17 00:00:00 2001 From: grunfink Date: Tue, 3 Jun 2025 03:31:31 +0200 Subject: Fixed regression while sending email via pipe on OpenBSD. --- sandbox.c | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/sandbox.c b/sandbox.c index 1ea9c1c..9e06155 100644 --- a/sandbox.c +++ b/sandbox.c @@ -13,6 +13,8 @@ void sbox_enter(const char *basedir) return; } + int smail = !xs_is_true(xs_dict_get(srv_config, "disable_email_notifications")); + srv_debug(1, xs_fmt("Calling unveil()")); unveil(basedir, "rwc"); unveil("/tmp", "rwc"); @@ -25,6 +27,9 @@ void sbox_enter(const char *basedir) if (*address == '/') unveil(address, "rwc"); + if (smail) + unveil("/usr/sbin/sendmail", "x"); + unveil(NULL, NULL); srv_debug(1, xs_fmt("Calling pledge()")); @@ -34,6 +39,9 @@ void sbox_enter(const char *basedir) if (*address == '/') p = xs_str_cat(p, " unix"); + if (smail) + p = xs_str_cat(p, " exec"); + pledge(p, NULL); } -- cgit v1.2.3 From ee35e7d78931759989e3329962dc1899e6b05f76 Mon Sep 17 00:00:00 2001 From: grunfink Date: Tue, 3 Jun 2025 03:32:06 +0200 Subject: Updated RELEASE_NOTES. --- RELEASE_NOTES.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/RELEASE_NOTES.md b/RELEASE_NOTES.md index 0960356..df8c6f3 100644 --- a/RELEASE_NOTES.md +++ b/RELEASE_NOTES.md @@ -1,5 +1,9 @@ # Release Notes +## UNRELEASED + +Fixed regression while sending email via pipe on OpenBSD. + ## 2.78 Hashtag following also allow URLs to RSS feeds of ActivityPub objects (like e.g. https://mastodon.social/tags/ThankYouTuesday). -- cgit v1.2.3 From c2cac572e9e51fc91296e0aa86ed3e165a75dd2d Mon Sep 17 00:00:00 2001 From: grunfink Date: Wed, 4 Jun 2025 06:35:39 +0200 Subject: Don't enable unveil() and pledge() for sendmail spawn if "smtp_url" is set. --- sandbox.c | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/sandbox.c b/sandbox.c index 9e06155..a172f46 100644 --- a/sandbox.c +++ b/sandbox.c @@ -15,6 +15,11 @@ void sbox_enter(const char *basedir) int smail = !xs_is_true(xs_dict_get(srv_config, "disable_email_notifications")); + const char *url = xs_dict_get(srv_config, "smtp_url"); + + if (xs_is_string(url) && *url) + smail = 0; + srv_debug(1, xs_fmt("Calling unveil()")); unveil(basedir, "rwc"); unveil("/tmp", "rwc"); -- cgit v1.2.3 From f0509f8b2b8df704226a94e9af8c06c3065234d4 Mon Sep 17 00:00:00 2001 From: grunfink Date: Wed, 4 Jun 2025 06:38:44 +0200 Subject: Minor tweak. --- sandbox.c | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/sandbox.c b/sandbox.c index a172f46..c6cfdcb 100644 --- a/sandbox.c +++ b/sandbox.c @@ -13,12 +13,13 @@ void sbox_enter(const char *basedir) return; } - int smail = !xs_is_true(xs_dict_get(srv_config, "disable_email_notifications")); - + int smail; const char *url = xs_dict_get(srv_config, "smtp_url"); if (xs_is_string(url) && *url) smail = 0; + else + smail = !xs_is_true(xs_dict_get(srv_config, "disable_email_notifications")); srv_debug(1, xs_fmt("Calling unveil()")); unveil(basedir, "rwc"); -- cgit v1.2.3 From 22f326ed66a5371fddbac739e0a7ce6f7e5cb658 Mon Sep 17 00:00:00 2001 From: grunfink Date: Wed, 4 Jun 2025 06:57:13 +0200 Subject: Fixed markdown links with parenthesis. --- format.c | 29 ++++++++++++++++------------- 1 file changed, 16 insertions(+), 13 deletions(-) diff --git a/format.c b/format.c index 9bad391..7b18909 100644 --- a/format.c +++ b/format.c @@ -96,8 +96,8 @@ static xs_str *format_line(const char *line, xs_list **attach) "~~[^~]+~~" "|" "\\*\\*?\\*?[^\\*]+\\*?\\*?\\*" "|" "__[^_]+__" "|" //anzu - "!\\[[^]]+\\]\\([^\\)]+\\)" "|" - "\\[[^]]+\\]\\([^\\)]+\\)" "|" + "!\\[[^]]+\\]\\([^\\)]+\\)\\)?" "|" + "\\[[^]]+\\]\\([^\\)]+\\)\\)?" "|" "[a-z]+:/" "/" NOSPACE "|" "(mailto|xmpp):[^@[:space:]]+@" NOSPACE ")"); @@ -149,14 +149,15 @@ static xs_str *format_line(const char *line, xs_list **attach) else if (*v == '[') { /* markdown-like links [label](url) */ - xs *w = xs_strip_chars_i( - xs_replace_i(xs_replace(v, "#", "#"), "@", "@"), - "![)"); + xs *w = xs_replace_i(xs_replace(v, "#", "#"), "@", "@"); xs *l = xs_split_n(w, "](", 1); if (xs_list_len(l) == 2) { - const char *name = xs_list_get(l, 0); - const char *url = xs_list_get(l, 1); + xs *name = xs_dup(xs_list_get(l, 0)); + xs *url = xs_dup(xs_list_get(l, 1)); + + name = xs_crop_i(name, 1, 0); + url = xs_crop_i(url, 0, -1); xs *link = xs_fmt("%s", url, name); @@ -168,15 +169,17 @@ static xs_str *format_line(const char *line, xs_list **attach) else if (*v == '!') { /* markdown-like images ![alt text](url to image) */ - xs *w = xs_strip_chars_i( - xs_replace_i(xs_replace(v, "#", "#"), "@", "@"), - "![)"); + xs *w = xs_replace_i(xs_replace(v, "#", "#"), "@", "@"); xs *l = xs_split_n(w, "](", 1); if (xs_list_len(l) == 2) { - const char *alt_text = xs_list_get(l, 0); - const char *img_url = xs_list_get(l, 1); - const char *mime = xs_mime_by_ext(img_url); + xs *alt_text = xs_dup(xs_list_get(l, 0)); + xs *img_url = xs_dup(xs_list_get(l, 1)); + + alt_text = xs_crop_i(alt_text, 2, 0); + img_url = xs_crop_i(img_url, 0, -1); + + const char *mime = xs_mime_by_ext(img_url); if (attach != NULL && xs_startswith(mime, "image/")) { const xs_dict *ad; -- cgit v1.2.3 From 292fa229ecd34af70b79126cef26fb224db5de16 Mon Sep 17 00:00:00 2001 From: grunfink Date: Wed, 4 Jun 2025 07:04:31 +0200 Subject: Updated RELEASE_NOTES. --- RELEASE_NOTES.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/RELEASE_NOTES.md b/RELEASE_NOTES.md index df8c6f3..607e32b 100644 --- a/RELEASE_NOTES.md +++ b/RELEASE_NOTES.md @@ -4,6 +4,8 @@ Fixed regression while sending email via pipe on OpenBSD. +Fixed Markdown parsing when the URL has parenthesis. + ## 2.78 Hashtag following also allow URLs to RSS feeds of ActivityPub objects (like e.g. https://mastodon.social/tags/ThankYouTuesday). -- cgit v1.2.3 From cbf71ca0cabdf46a1eb595db01601858c8823acc Mon Sep 17 00:00:00 2001 From: grunfink Date: Fri, 6 Jun 2025 21:02:28 +0200 Subject: Skip incomplete attachments. --- html.c | 3 +++ 1 file changed, 3 insertions(+) diff --git a/html.c b/html.c index a6e5f98..d513d10 100644 --- a/html.c +++ b/html.c @@ -2427,6 +2427,9 @@ xs_html *html_entry(snac *user, xs_dict *msg, int read_only, const char *o_href = xs_dict_get(a, "href"); const char *name = xs_dict_get(a, "name"); + if (!xs_is_string(type) || !xs_is_string(o_href)) + continue; + /* if this URL is already in the post content, skip */ if (content && xs_str_in(content, o_href) != -1) continue; -- cgit v1.2.3 From c9c1323e40080acbd8bffe904e8ba8fc3ff8bbd7 Mon Sep 17 00:00:00 2001 From: grunfink Date: Sat, 7 Jun 2025 06:43:58 +0200 Subject: Always show the 'pending follow confirmations' if there are any. Even if the toggle is off. --- html.c | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/html.c b/html.c index d513d10..892475c 100644 --- a/html.c +++ b/html.c @@ -3295,12 +3295,12 @@ xs_str *html_people(snac *user) xs *wing = following_list(user); xs *wers = follower_list(user); + xs *pending = pending_list(user); xs_html *lists = xs_html_tag("div", xs_html_attr("class", "snac-posts")); - if (xs_is_true(xs_dict_get(user->config, "approve_followers"))) { - xs *pending = pending_list(user); + if (xs_list_len(pending) || xs_is_true(xs_dict_get(user->config, "approve_followers"))) { xs_html_add(lists, html_people_list(user, pending, L("Pending follow confirmations"), "p", proxy)); } -- cgit v1.2.3 From d5dbdad930d44d9345bec03d0f2ce4076cb028a8 Mon Sep 17 00:00:00 2001 From: grunfink Date: Sat, 7 Jun 2025 07:53:00 +0200 Subject: If a metadata value is an account handle, it's also verified using webfinger. --- html.c | 13 ++++++++++--- utils.c | 16 ++++++++++++++++ 2 files changed, 26 insertions(+), 3 deletions(-) diff --git a/html.c b/html.c index 892475c..f5d9aea 100644 --- a/html.c +++ b/html.c @@ -1077,10 +1077,17 @@ static xs_html *html_user_body(snac *user, int read_only) while (xs_dict_next(metadata, &k, &v, &c)) { xs_html *value; - if (xs_startswith(v, "https:/") || xs_startswith(v, "http:/")) { + if (xs_startswith(v, "https:/") || xs_startswith(v, "http:/") || *v == '@') { /* is this link validated? */ xs *verified_link = NULL; const xs_number *val_time = xs_dict_get(val_links, v); + const char *url = NULL; + + if (xs_is_string(val_time)) { + /* resolve again, as it may be an account handle */ + url = val_time; + val_time = xs_dict_get(val_links, val_time); + } if (xs_type(val_time) == XSTYPE_NUMBER) { time_t t = xs_number_get(val_time); @@ -1098,13 +1105,13 @@ static xs_html *html_user_body(snac *user, int read_only) xs_html_tag("a", xs_html_attr("rel", "me"), xs_html_attr("target", "_blank"), - xs_html_attr("href", v), + xs_html_attr("href", url ? url : v), xs_html_text(v))); } else { value = xs_html_tag("a", xs_html_attr("rel", "me"), - xs_html_attr("href", v), + xs_html_attr("href", url ? url : v), xs_html_text(v)); } } diff --git a/utils.c b/utils.c index d50707a..d8c55dc 100644 --- a/utils.c +++ b/utils.c @@ -488,6 +488,18 @@ void verify_links(snac *user) int c = 0; while (metadata && xs_dict_next(metadata, &k, &v, &c)) { + xs *wfinger = NULL; + const char *ov = NULL; + + /* is it an account handle? */ + if (*v == '@' && strchr(v + 1, '@')) { + /* resolve it via webfinger */ + if (valid_status(webfinger_request(v, &wfinger, NULL)) && xs_is_string(wfinger)) { + ov = v; + v = wfinger; + } + } + /* not an https link? skip */ if (!xs_startswith(v, "https:/" "/")) continue; @@ -563,6 +575,10 @@ void verify_links(snac *user) user->links = xs_dict_set(user->links, v, verified_time); + /* also add the original value if it was 'resolved' */ + if (xs_is_string(ov)) + user->links = xs_dict_set(user->links, ov, v); + vfied = 1; } else -- cgit v1.2.3 From 6e26dc7e2a045168d13711b646257471cc830b17 Mon Sep 17 00:00:00 2001 From: grunfink Date: Sat, 7 Jun 2025 07:56:21 +0200 Subject: Updated documentation. --- doc/snac.1 | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/doc/snac.1 b/doc/snac.1 index 583fe2f..7a1fec3 100644 --- a/doc/snac.1 +++ b/doc/snac.1 @@ -291,9 +291,9 @@ blocked without further inspection. .It Cm unblock Ar basedir Ar instance_url Unblocks a previously blocked instance. .It Cm verify_links Ar basedir Ar uid -Verifies all links stored as metadata for the given user. This verification -is done by downloading the link content and searching for a link back to -the +Verifies all links or account handles stored as metadata for the given user. +This verification is done by downloading the link content and searching for +a link back to the .Nm user url that also contains a rel="me" attribute. These links are specially marked as verified in the user's public timeline and also via the Mastodon API. -- cgit v1.2.3 From dfd246a0e9df31f315f1fd650c9b916195b2c9e4 Mon Sep 17 00:00:00 2001 From: grunfink Date: Sat, 7 Jun 2025 08:19:40 +0200 Subject: Always store resolved account handles as metadata, even if they weren't verified. --- utils.c | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/utils.c b/utils.c index d8c55dc..7adbce2 100644 --- a/utils.c +++ b/utils.c @@ -497,6 +497,14 @@ void verify_links(snac *user) if (valid_status(webfinger_request(v, &wfinger, NULL)) && xs_is_string(wfinger)) { ov = v; v = wfinger; + + /* store the alias */ + if (user->links == NULL) + user->links = xs_dict_new(); + + user->links = xs_dict_set(user->links, ov, v); + + changed++; } } @@ -575,10 +583,6 @@ void verify_links(snac *user) user->links = xs_dict_set(user->links, v, verified_time); - /* also add the original value if it was 'resolved' */ - if (xs_is_string(ov)) - user->links = xs_dict_set(user->links, ov, v); - vfied = 1; } else -- cgit v1.2.3 From 13ffca1d029f47cc3f9fd92824d75aa1010a64db Mon Sep 17 00:00:00 2001 From: grunfink Date: Sat, 7 Jun 2025 10:05:23 +0200 Subject: Another search by URL tweak (for Pixelfed). --- html.c | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/html.c b/html.c index f5d9aea..b4588f7 100644 --- a/html.c +++ b/html.c @@ -3795,6 +3795,14 @@ int html_get_handler(const xs_dict *req, const char *q_path, timeline_add(&snac, q, object); } } + 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; + } } } -- cgit v1.2.3 From 6b39635fc3ec8d9dd2c05756b59fcf8bcb84600f Mon Sep 17 00:00:00 2001 From: grunfink Date: Sun, 8 Jun 2025 08:08:59 +0200 Subject: Updated TODO. --- TODO.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/TODO.md b/TODO.md index 8b87d37..73615ac 100644 --- a/TODO.md +++ b/TODO.md @@ -22,6 +22,8 @@ The instance timeline should also show boosts from users. Mastoapi: implement /v1/conversations. +Track "BadgeFed - ActivityPub Badges" https://github.com/tryvocalcat/badgefed + 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 -- cgit v1.2.3 From 1147430d78e6c35fecaf02a3ed8d461bb18de376 Mon Sep 17 00:00:00 2001 From: grunfink Date: Sun, 8 Jun 2025 09:18:06 +0200 Subject: Updated RELEASE_NOTES. --- RELEASE_NOTES.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/RELEASE_NOTES.md b/RELEASE_NOTES.md index 607e32b..c564003 100644 --- a/RELEASE_NOTES.md +++ b/RELEASE_NOTES.md @@ -6,6 +6,12 @@ Fixed regression while sending email via pipe on OpenBSD. Fixed Markdown parsing when the URL has parenthesis. +Always show the 'pending follow confirmations' section if there are any (even if the toggle is off). + +If a metadata value is an account handler, it's also tried to be validated (rel="me" links). + +Another search by URL tweak (this time for Pixelfed links). + ## 2.78 Hashtag following also allow URLs to RSS feeds of ActivityPub objects (like e.g. https://mastodon.social/tags/ThankYouTuesday). -- cgit v1.2.3 From 2e8bc4b5da56b7c2c6a6fe8930b0dc6c16896934 Mon Sep 17 00:00:00 2001 From: grunfink Date: Tue, 10 Jun 2025 17:51:04 +0200 Subject: Updated TODO. --- TODO.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/TODO.md b/TODO.md index 73615ac..f9e73d0 100644 --- a/TODO.md +++ b/TODO.md @@ -12,6 +12,8 @@ Unfollowing guppe groups seems to work (http status of 200), but messages contin Important: deleting a follower should do more that just delete the object, see https://codeberg.org/grunfink/snac2/issues/43#issuecomment-956721 +Verbatim text in posts should not allow other markup. + ## Wishlist Add account reporting. -- cgit v1.2.3 From 408d7fcd0a0e4368120c02850b620abc4d10a9d0 Mon Sep 17 00:00:00 2001 From: grunfink Date: Wed, 11 Jun 2025 04:15:20 +0200 Subject: Updated TODO. --- TODO.md | 2 -- 1 file changed, 2 deletions(-) diff --git a/TODO.md b/TODO.md index f9e73d0..73615ac 100644 --- a/TODO.md +++ b/TODO.md @@ -12,8 +12,6 @@ Unfollowing guppe groups seems to work (http status of 200), but messages contin Important: deleting a follower should do more that just delete the object, see https://codeberg.org/grunfink/snac2/issues/43#issuecomment-956721 -Verbatim text in posts should not allow other markup. - ## Wishlist Add account reporting. -- cgit v1.2.3 From cc0e90fbf7c22991bf72af38c5d47d8b3a5ab552 Mon Sep 17 00:00:00 2001 From: grunfink Date: Wed, 11 Jun 2025 04:40:47 +0200 Subject: mastoapi: fixed reblog identifiers. This fixes weird behaviour in apps like Tusky, where boosts' content disappear after clicking on them. --- mastoapi.c | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/mastoapi.c b/mastoapi.c index 27c476e..2c86ff9 100644 --- a/mastoapi.c +++ b/mastoapi.c @@ -1134,9 +1134,15 @@ xs_dict *mastoapi_status(snac *snac, const xs_dict *msg) bst = xs_dict_set(bst, "content", ""); bst = xs_dict_set(bst, "reblog", st); + /* reblogs must have their own id */ + xs *b_id = xs_fmt("%s%s", xs_dict_get(st, "id"), boosted_by_md5); + bst = xs_dict_set(bst, "id", b_id); + xs_free(st); st = bst; } + else + xs_free(bst); } return st; -- cgit v1.2.3 From 9e2cf4bc0b48ee316ad756b4f9ec60c37890cd54 Mon Sep 17 00:00:00 2001 From: grunfink Date: Wed, 11 Jun 2025 04:43:10 +0200 Subject: Updated RELEASE_NOTES. --- RELEASE_NOTES.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/RELEASE_NOTES.md b/RELEASE_NOTES.md index c564003..d602c67 100644 --- a/RELEASE_NOTES.md +++ b/RELEASE_NOTES.md @@ -12,6 +12,8 @@ If a metadata value is an account handler, it's also tried to be validated (rel= Another search by URL tweak (this time for Pixelfed links). +Mastodon API: fixed a bug that made some boosts disappear after being shown in apps like Tusky. + ## 2.78 Hashtag following also allow URLs to RSS feeds of ActivityPub objects (like e.g. https://mastodon.social/tags/ThankYouTuesday). -- cgit v1.2.3 From f9151c2ffc9626a8464bfa7038fe84120734735f Mon Sep 17 00:00:00 2001 From: grunfink Date: Wed, 11 Jun 2025 05:17:20 +0200 Subject: Bumped version. --- snac.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/snac.h b/snac.h index f3cb40d..7f109e0 100644 --- 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.78" +#define VERSION "2.79-dev" #define USER_AGENT "snac/" VERSION -- cgit v1.2.3 From e4ecaea5df064189eda16859251d5b8727107179 Mon Sep 17 00:00:00 2001 From: grunfink Date: Wed, 11 Jun 2025 06:23:35 +0200 Subject: Updated documentation. --- doc/snac.1 | 22 ++++++++++++++++++---- 1 file changed, 18 insertions(+), 4 deletions(-) diff --git a/doc/snac.1 b/doc/snac.1 index 7a1fec3..f68a29f 100644 --- a/doc/snac.1 +++ b/doc/snac.1 @@ -24,9 +24,9 @@ For file and data formats, see .Ss Web Interface The web interface provided by .Nm -is split in two data streams: the public timeline and the -private timeline. There are no other feeds like the server-scoped -or the federated firehoses provided by other similar ActivityPub +is split in three data streams: the public timeline, the private +timeline and the instance timeline. There are no other feeds like +the federated firehoses provided by other similar ActivityPub implementations like Mastodon or Pleroma. .Pp The public timeline, also called the local timeline, is what an @@ -67,9 +67,23 @@ sent to those people you mention in the post body. If you fill this optional text field with the URL of another one's post, your text will be considered as a reply to it, not a standalone one. +.It Draft +If you set this checkbox, your text will not be sent when you +push the Post button, but stored for later modification in +the "Drafts" section. +.It Scheduled post... +This dropdown menu allows setting a date and time for the +post publication. +.It Attachments... +This dropdown menu allows uploading media attachments (images, +audio, video, etc.) to your post. +.It Poll... +this dropdown menu gives access to the voting options, that +will make your post a poll. You can set the options to be +voted, if it's a multiple choice poll and the due date. .El .Pp -More options are hidden under a toggle control. They are the +More options are hidden under dropdown menus. They are the following: .Bl -tag -offset indent .It Follow (by URL or user@host) -- cgit v1.2.3 From 699d22a86438a93a12e802e4c39c44416b4ab4a0 Mon Sep 17 00:00:00 2001 From: grunfink Date: Wed, 11 Jun 2025 19:54:07 +0200 Subject: mastoapi: reverted reblog id, as it was breaking things. --- mastoapi.c | 4 ---- 1 file changed, 4 deletions(-) diff --git a/mastoapi.c b/mastoapi.c index 2c86ff9..19aa610 100644 --- a/mastoapi.c +++ b/mastoapi.c @@ -1134,10 +1134,6 @@ xs_dict *mastoapi_status(snac *snac, const xs_dict *msg) bst = xs_dict_set(bst, "content", ""); bst = xs_dict_set(bst, "reblog", st); - /* reblogs must have their own id */ - xs *b_id = xs_fmt("%s%s", xs_dict_get(st, "id"), boosted_by_md5); - bst = xs_dict_set(bst, "id", b_id); - xs_free(st); st = bst; } -- cgit v1.2.3 From 2391d2a53b42e1189151d44b5d4f7f06ec50dcf4 Mon Sep 17 00:00:00 2001 From: grunfink Date: Wed, 11 Jun 2025 19:55:10 +0200 Subject: Updated RELEASE_NOTES. --- RELEASE_NOTES.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/RELEASE_NOTES.md b/RELEASE_NOTES.md index d602c67..572d0fe 100644 --- a/RELEASE_NOTES.md +++ b/RELEASE_NOTES.md @@ -12,7 +12,7 @@ If a metadata value is an account handler, it's also tried to be validated (rel= Another search by URL tweak (this time for Pixelfed links). -Mastodon API: fixed a bug that made some boosts disappear after being shown in apps like Tusky. +~~Mastodon API: fixed a bug that made some boosts disappear after being shown in apps like Tusky.~~ ## 2.78 -- cgit v1.2.3 From 55213f660d0c2a14ee5b556c4422b3b9199c9355 Mon Sep 17 00:00:00 2001 From: grunfink Date: Thu, 12 Jun 2025 10:59:26 +0200 Subject: mastoapi: another try to fix collapsing boosted posts in some apps. --- mastoapi.c | 11 ++++++++--- xs.h | 15 +++++++++++++++ 2 files changed, 23 insertions(+), 3 deletions(-) diff --git a/mastoapi.c b/mastoapi.c index 19aa610..ca44751 100644 --- a/mastoapi.c +++ b/mastoapi.c @@ -1134,6 +1134,9 @@ xs_dict *mastoapi_status(snac *snac, const xs_dict *msg) bst = xs_dict_set(bst, "content", ""); bst = xs_dict_set(bst, "reblog", st); + xs *b_id = xs_toupper_i(xs_dup(xs_dict_get(st, "id"))); + bst = xs_dict_set(bst, "id", b_id); + xs_free(st); st = bst; } @@ -2338,15 +2341,17 @@ int mastoapi_get_handler(const xs_dict *req, const char *q_path, /* information about a status */ if (logged_in) { xs *l = xs_split(cmd, "/"); - const char *id = xs_list_get(l, 3); + const char *oid = xs_list_get(l, 3); const char *op = xs_list_get(l, 4); - if (!xs_is_null(id)) { + if (!xs_is_null(oid)) { xs *msg = NULL; xs *out = NULL; /* skip the 'fake' part of the id */ - id = MID_TO_MD5(id); + oid = MID_TO_MD5(oid); + + xs *id = xs_tolower_i(xs_dup(oid)); if (valid_status(object_get_by_md5(id, &msg))) { if (op == NULL) { diff --git a/xs.h b/xs.h index ab5a264..54b913e 100644 --- a/xs.h +++ b/xs.h @@ -90,6 +90,7 @@ xs_str *xs_rstrip_chars_i(xs_str *str, const char *chars); xs_str *xs_strip_chars_i(xs_str *str, const char *chars); #define xs_strip_i(str) xs_strip_chars_i(str, " \r\n\t\v\f") xs_str *xs_tolower_i(xs_str *str); +xs_str *xs_toupper_i(xs_str *str); xs_list *xs_list_new(void); xs_list *xs_list_append_m(xs_list *list, const char *mem, int dsz); @@ -692,6 +693,20 @@ xs_str *xs_tolower_i(xs_str *str) } +xs_str *xs_toupper_i(xs_str *str) +/* convert to lowercase */ +{ + XS_ASSERT_TYPE(str, XSTYPE_STRING); + + int n; + + for (n = 0; str[n]; n++) + str[n] = toupper(str[n]); + + return str; +} + + /** lists **/ xs_list *xs_list_new(void) -- cgit v1.2.3 From 1ca5b346a556f2418c110b480ce1859ce7d5b36c Mon Sep 17 00:00:00 2001 From: grunfink Date: Thu, 12 Jun 2025 12:11:34 +0200 Subject: The usage "screen" shows info only about the entered command. If there is no command or no command starts with that string, the full list is shown as before. --- main.c | 128 +++++++++++++++++++++++++++++++++++------------------------ xs_version.h | 2 +- 2 files changed, 77 insertions(+), 53 deletions(-) diff --git a/main.c b/main.c index c948b57..2995e3f 100644 --- a/main.c +++ b/main.c @@ -13,58 +13,82 @@ #include #include -int usage(void) +int usage(const char *cmd) { printf("snac " VERSION " - A simple, minimalistic ActivityPub instance\n"); printf("Copyright (c) 2022 - 2025 grunfink et al. / MIT license\n"); printf("\n"); - printf("Commands:\n"); - printf("\n"); - printf("init [{basedir}] Initializes the data storage\n"); - printf("upgrade {basedir} Upgrade to a new version\n"); - printf("adduser {basedir} [{uid}] Adds a new user\n"); - printf("deluser {basedir} {uid} Deletes a user\n"); - printf("httpd {basedir} Starts the HTTPD daemon\n"); - printf("purge {basedir} Purges old data\n"); - printf("state {basedir} Prints server state\n"); - printf("webfinger {basedir} {account} Queries about an account (@user@host or actor url)\n"); - printf("queue {basedir} {uid} Processes a user queue\n"); - printf("follow {basedir} {uid} {actor} Follows an actor\n"); - printf("unfollow {basedir} {uid} {actor} Unfollows an actor\n"); - printf("request {basedir} {uid} {url} Requests an object\n"); - printf("insert {basedir} {uid} {url} Requests an object and inserts it into the timeline\n"); - printf("actor {basedir} [{uid}] {url} Requests an actor\n"); - printf("note {basedir} {uid} {text} [files...] Sends a note with optional attachments\n"); - printf("note_unlisted {basedir} {uid} {text} [files...] Sends an unlisted note with optional attachments\n"); - printf("note_mention {basedir} {uid} {text} [files...] Sends a note only to mentioned accounts\n"); - printf("boost|announce {basedir} {uid} {url} Boosts (announces) a post\n"); - printf("unboost {basedir} {uid} {url} Unboosts a post\n"); - printf("resetpwd {basedir} {uid} Resets the password of a user\n"); - printf("ping {basedir} {uid} {actor} Pings an actor\n"); - printf("webfinger_s {basedir} {uid} {account} Queries about an account (@user@host or actor url)\n"); - printf("pin {basedir} {uid} {msg_url} Pins a message\n"); - printf("unpin {basedir} {uid} {msg_url} Unpins a message\n"); - printf("bookmark {basedir} {uid} {msg_url} Bookmarks a message\n"); - printf("unbookmark {basedir} {uid} {msg_url} Unbookmarks a message\n"); - printf("block {basedir} {instance_url} Blocks a full instance\n"); - 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"); - printf("alias {basedir} {uid} {account} Sets account (@user@host or actor url) as an alias\n"); - printf("migrate {basedir} {uid} Migrates to the account defined as the alias\n"); - printf("import_csv {basedir} {uid} Imports data from CSV files\n"); - printf("import_list {basedir} {uid} {file} Imports a Mastodon CSV list file\n"); - printf("import_block_list {basedir} {uid} {file} Imports a Mastodon CSV block list file\n"); - printf("lists {basedir} {uid} Returns the names of the lists created by the user\n"); - printf("list_members {basedir} {uid} {name} Returns the list of accounts inside a list\n"); - printf("create_list {basedir} {uid} {name} Creates a new list\n"); - printf("delete_list {basedir} {uid} {name} Deletes an existing list\n"); - printf("list_add {basedir} {uid} {name} {acct} Adds an account (@user@host or actor url) to a list\n"); - printf("list_del {basedir} {uid} {name} {actor} Deletes an actor URL from a list\n"); + + if (cmd == NULL) { + printf("Commands:\n"); + printf("\n"); + } + + const char *cmds = + "init [{basedir}] Initializes the data storage\n" + "upgrade {basedir} Upgrade to a new version\n" + "adduser {basedir} [{uid}] Adds a new user\n" + "deluser {basedir} {uid} Deletes a user\n" + "httpd {basedir} Starts the HTTPD daemon\n" + "purge {basedir} Purges old data\n" + "state {basedir} Prints server state\n" + "webfinger {basedir} {account} Queries about an account (@user@host or actor url)\n" + "queue {basedir} {uid} Processes a user queue\n" + "follow {basedir} {uid} {actor} Follows an actor\n" + "unfollow {basedir} {uid} {actor} Unfollows an actor\n" + "request {basedir} {uid} {url} Requests an object\n" + "insert {basedir} {uid} {url} Requests an object and inserts it into the timeline\n" + "actor {basedir} [{uid}] {url} Requests an actor\n" + "note {basedir} {uid} {text} [files...] Sends a note with optional attachments\n" + "note_unlisted {basedir} {uid} {text} [files...] Sends an unlisted note with optional attachments\n" + "note_mention {basedir} {uid} {text} [files...] Sends a note only to mentioned accounts\n" + "boost|announce {basedir} {uid} {url} Boosts (announces) a post\n" + "unboost {basedir} {uid} {url} Unboosts a post\n" + "resetpwd {basedir} {uid} Resets the password of a user\n" + "ping {basedir} {uid} {actor} Pings an actor\n" + "webfinger_s {basedir} {uid} {account} Queries about an account (@user@host or actor url)\n" + "pin {basedir} {uid} {msg_url} Pins a message\n" + "unpin {basedir} {uid} {msg_url} Unpins a message\n" + "bookmark {basedir} {uid} {msg_url} Bookmarks a message\n" + "unbookmark {basedir} {uid} {msg_url} Unbookmarks a message\n" + "block {basedir} {instance_url} Blocks a full instance\n" + "unblock {basedir} {instance_url} Unblocks a full instance\n" + "limit {basedir} {uid} {actor} Limits an actor (drops their announces)\n" + "unlimit {basedir} {uid} {actor} Unlimits an actor\n" + "unmute {basedir} {uid} {actor} Unmutes a previously muted actor\n" + "verify_links {basedir} {uid} Verifies a user's links (in the metadata)\n" + "search {basedir} {uid} {regex} Searches posts by content\n" + "export_csv {basedir} {uid} Exports data as CSV files\n" + "alias {basedir} {uid} {account} Sets account (@user@host or actor url) as an alias\n" + "migrate {basedir} {uid} Migrates to the account defined as the alias\n" + "import_csv {basedir} {uid} Imports data from CSV files\n" + "import_list {basedir} {uid} {file} Imports a Mastodon CSV list file\n" + "import_block_list {basedir} {uid} {file} Imports a Mastodon CSV block list file\n" + "lists {basedir} {uid} Returns the names of the lists created by the user\n" + "list_members {basedir} {uid} {name} Returns the list of accounts inside a list\n" + "create_list {basedir} {uid} {name} Creates a new list\n" + "delete_list {basedir} {uid} {name} Deletes an existing list\n" + "list_add {basedir} {uid} {name} {acct} Adds an account (@user@host or actor url) to a list\n" + "list_del {basedir} {uid} {name} {actor} Deletes an actor URL from a list\n"; + + if (cmd == NULL) + printf("%s", cmds); + else { + /* only show help for the entered command */ + xs *l = xs_split(cmds, "\n"); + const char *v; + int cnt = 0; + + xs_list_foreach(l, v) { + if (xs_startswith(v, cmd)) { + printf("%s\n", v); + cnt++; + } + } + + if (cnt == 0) + printf("%s", cmds); + } return 1; } @@ -94,7 +118,7 @@ int main(int argc, char *argv[]) umask(0007); if ((cmd = GET_ARGV()) == NULL) - return usage(); + return usage(cmd); if (strcmp(cmd, "init") == 0) { /** **/ /* initialize the data storage */ @@ -106,7 +130,7 @@ int main(int argc, char *argv[]) if ((basedir = getenv("SNAC_BASEDIR")) == NULL) { if ((basedir = GET_ARGV()) == NULL) - return usage(); + return usage(cmd); } if (strcmp(cmd, "upgrade") == 0) { /** **/ @@ -172,7 +196,7 @@ int main(int argc, char *argv[]) } if ((user = GET_ARGV()) == NULL) - return usage(); + return usage(cmd); if (strcmp(cmd, "block") == 0) { /** **/ int ret = instance_block(user); @@ -305,7 +329,7 @@ int main(int argc, char *argv[]) } if ((url = GET_ARGV()) == NULL) - return usage(); + return usage(cmd); if (strcmp(cmd, "list_members") == 0) { /** **/ xs *lid = list_maint(&snac, url, 4); diff --git a/xs_version.h b/xs_version.h index c7789a7..c9418a7 100644 --- a/xs_version.h +++ b/xs_version.h @@ -1 +1 @@ -/* 871d420cef893b6efe32869407294baf084ce3ab 2025-05-04T11:01:01+02:00 */ +/* 963b6129fca4fb5c009533db0ed602ab8ed5424d 2025-06-12T11:33:02+02:00 */ -- cgit v1.2.3 From 7eecd4bb95116caef35d98f97e6e56f5dc910bd5 Mon Sep 17 00:00:00 2001 From: grunfink Date: Thu, 12 Jun 2025 12:14:18 +0200 Subject: Improved usage screen. --- main.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/main.c b/main.c index 2995e3f..85576b6 100644 --- a/main.c +++ b/main.c @@ -80,7 +80,7 @@ int usage(const char *cmd) int cnt = 0; xs_list_foreach(l, v) { - if (xs_startswith(v, cmd)) { + if (xs_str_in(v, cmd) != -1) { printf("%s\n", v); cnt++; } -- cgit v1.2.3 From 2cba505dba666eac983045336f9c2a53b1948168 Mon Sep 17 00:00:00 2001 From: grunfink Date: Sun, 15 Jun 2025 08:28:29 +0200 Subject: Updated RELEASE_NOTES. --- RELEASE_NOTES.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/RELEASE_NOTES.md b/RELEASE_NOTES.md index 572d0fe..d602c67 100644 --- a/RELEASE_NOTES.md +++ b/RELEASE_NOTES.md @@ -12,7 +12,7 @@ If a metadata value is an account handler, it's also tried to be validated (rel= Another search by URL tweak (this time for Pixelfed links). -~~Mastodon API: fixed a bug that made some boosts disappear after being shown in apps like Tusky.~~ +Mastodon API: fixed a bug that made some boosts disappear after being shown in apps like Tusky. ## 2.78 -- cgit v1.2.3 From e1fb414ffd4c46f51903f48ade7b399c0f5de9bb Mon Sep 17 00:00:00 2001 From: grunfink Date: Sun, 15 Jun 2025 09:43:42 +0200 Subject: Renamed some list command line options. create_list to list_create delete_list to list_delete. --- main.c | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/main.c b/main.c index 85576b6..758e692 100644 --- a/main.c +++ b/main.c @@ -66,8 +66,8 @@ int usage(const char *cmd) "import_block_list {basedir} {uid} {file} Imports a Mastodon CSV block list file\n" "lists {basedir} {uid} Returns the names of the lists created by the user\n" "list_members {basedir} {uid} {name} Returns the list of accounts inside a list\n" - "create_list {basedir} {uid} {name} Creates a new list\n" - "delete_list {basedir} {uid} {name} Deletes an existing list\n" + "list_create {basedir} {uid} {name} Creates a new list\n" + "list_delete {basedir} {uid} {name} Deletes an existing list\n" "list_add {basedir} {uid} {name} {acct} Adds an account (@user@host or actor url) to a list\n" "list_del {basedir} {uid} {name} {actor} Deletes an actor URL from a list\n"; @@ -352,7 +352,7 @@ int main(int argc, char *argv[]) return 0; } - if (strcmp(cmd, "create_list") == 0) { /** **/ + if (strcmp(cmd, "list_create") == 0) { /** **/ xs *lid = list_maint(&snac, url, 4); if (lid == NULL) { @@ -365,7 +365,7 @@ int main(int argc, char *argv[]) return 0; } - if (strcmp(cmd, "delete_list") == 0) { /** **/ + if (strcmp(cmd, "list_delete") == 0) { /** **/ xs *lid = list_maint(&snac, url, 4); if (lid != NULL) { -- cgit v1.2.3 From deb90ed0ac6dd5ccea4516b3988e16d95f9687b2 Mon Sep 17 00:00:00 2001 From: grunfink Date: Sun, 15 Jun 2025 09:44:56 +0200 Subject: Renamed cmd list_delete to list_remove. --- main.c | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/main.c b/main.c index 758e692..5af3695 100644 --- a/main.c +++ b/main.c @@ -67,7 +67,7 @@ int usage(const char *cmd) "lists {basedir} {uid} Returns the names of the lists created by the user\n" "list_members {basedir} {uid} {name} Returns the list of accounts inside a list\n" "list_create {basedir} {uid} {name} Creates a new list\n" - "list_delete {basedir} {uid} {name} Deletes an existing list\n" + "list_remove {basedir} {uid} {name} Removes an existing list\n" "list_add {basedir} {uid} {name} {acct} Adds an account (@user@host or actor url) to a list\n" "list_del {basedir} {uid} {name} {actor} Deletes an actor URL from a list\n"; @@ -365,7 +365,7 @@ int main(int argc, char *argv[]) return 0; } - if (strcmp(cmd, "list_delete") == 0) { /** **/ + if (strcmp(cmd, "list_remove") == 0) { /** **/ xs *lid = list_maint(&snac, url, 4); if (lid != NULL) { -- cgit v1.2.3 From 74084f3d9b6423bf29766b9648320fa7c8b62d16 Mon Sep 17 00:00:00 2001 From: grunfink Date: Sun, 15 Jun 2025 09:46:15 +0200 Subject: Updated documentation. --- doc/snac.1 | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/doc/snac.1 b/doc/snac.1 index f68a29f..5e020a4 100644 --- a/doc/snac.1 +++ b/doc/snac.1 @@ -374,10 +374,10 @@ subdirectory of a user's directory inside the server base directory. Prints the name of the user created lists. .It Cm list_members Ar basedir Ar uid Ar name Prints the list of actors in the named list. -.It Cm create_list Ar basedir Ar uid Ar name +.It Cm list_create Ar basedir Ar uid Ar name Creates a new list. -.It Cm delete_list Ar basedir Ar uid Ar name -Deletes an existing list. +.It Cm list_remove Ar basedir Ar uid Ar name +Removes an existing list. .It Cm list_add Ar basedir Ar uid Ar name Ar account Adds an account (by its @name@host handle or actor URL) to a list. .It Cm list_del Ar basedir Ar uid Ar name Ar actor_url -- cgit v1.2.3 From d74164763e8d620c8513b251f8ef2e0f2811719d Mon Sep 17 00:00:00 2001 From: grunfink Date: Sun, 15 Jun 2025 09:49:11 +0200 Subject: Updated RELEASE_NOTES. --- RELEASE_NOTES.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/RELEASE_NOTES.md b/RELEASE_NOTES.md index d602c67..1360303 100644 --- a/RELEASE_NOTES.md +++ b/RELEASE_NOTES.md @@ -14,6 +14,8 @@ Another search by URL tweak (this time for Pixelfed links). Mastodon API: fixed a bug that made some boosts disappear after being shown in apps like Tusky. +Renamed command-line actions `create_list` to `list_create` and `delete_list` to `list_remove`. + ## 2.78 Hashtag following also allow URLs to RSS feeds of ActivityPub objects (like e.g. https://mastodon.social/tags/ThankYouTuesday). -- cgit v1.2.3 From f8055ccfb52c5fdf9c08b608060409625982cd05 Mon Sep 17 00:00:00 2001 From: grunfink Date: Mon, 16 Jun 2025 05:11:33 +0200 Subject: mastoapi: added entrypoint /v1/followed_tags. --- mastoapi.c | 34 ++++++++++++++++++++++++++++++++++ 1 file changed, 34 insertions(+) diff --git a/mastoapi.c b/mastoapi.c index ca44751..9437f27 100644 --- a/mastoapi.c +++ b/mastoapi.c @@ -2483,6 +2483,40 @@ int mastoapi_get_handler(const xs_dict *req, const char *q_path, } else if (strcmp(cmd, "/v1/followed_tags") == 0) { /** **/ + if (logged_in) { + xs *r = xs_list_new(); + const xs_list *followed_hashtags = xs_dict_get_def(snac1.config, + "followed_hashtags", xs_stock(XSTYPE_LIST)); + const char *hashtag; + + xs_list_foreach(followed_hashtags, hashtag) { + if (*hashtag == '#') { + xs *d = xs_dict_new(); + xs *s = xs_fmt("%s?t=%s", srv_baseurl, hashtag + 1); + + d = xs_dict_set(d, "name", hashtag + 1); + d = xs_dict_set(d, "url", s); + d = xs_dict_set(d, "history", xs_stock(XSTYPE_LIST)); + + r = xs_list_append(r, d); + } + } + + *body = xs_json_dumps(r, 4); + *ctype = "application/json"; + status = HTTP_STATUS_OK; + } + else + status = HTTP_STATUS_UNAUTHORIZED; + } + else + if (strcmp(cmd, "/v1/blocks") == 0) { /** **/ + *body = xs_dup("[]"); + *ctype = "application/json"; + status = HTTP_STATUS_OK; + } + else + if (strcmp(cmd, "/v1/mutes") == 0) { /** **/ *body = xs_dup("[]"); *ctype = "application/json"; status = HTTP_STATUS_OK; -- cgit v1.2.3 From 5a0f23c76a989a389fa3cc1573bc73ef3031964f Mon Sep 17 00:00:00 2001 From: grunfink Date: Mon, 16 Jun 2025 05:44:23 +0200 Subject: mastoapi: Added followed hashtag maintenance. --- mastoapi.c | 48 ++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 48 insertions(+) diff --git a/mastoapi.c b/mastoapi.c index 9437f27..2e8063b 100644 --- a/mastoapi.c +++ b/mastoapi.c @@ -3337,6 +3337,54 @@ int mastoapi_post_handler(const xs_dict *req, const char *q_path, } } } + else + if (xs_startswith(cmd, "/v1/tags/")) { /** **/ + if (logged_in) { + xs *l = xs_split(cmd, "/"); + const char *i_tag = xs_list_get(l, 3); + const char *cmd = xs_list_get(l, 4); + + status = HTTP_STATUS_UNPROCESSABLE_CONTENT; + + if (xs_is_string(i_tag) && xs_is_string(cmd)) { + int ok = 0; + + xs *tag = xs_fmt("#%s", i_tag); + xs *followed_hashtags = xs_dup(xs_dict_get_def(snac.config, + "followed_hashtags", xs_stock(XSTYPE_LIST))); + + if (strcmp(cmd, "follow") == 0) { + followed_hashtags = xs_list_append(followed_hashtags, tag); + ok = 1; + } + else + if (strcmp(cmd, "unfollow") == 0) { + int off = xs_list_in(followed_hashtags, tag); + + if (off != -1) + followed_hashtags = xs_list_del(followed_hashtags, off); + + ok = 1; + } + + if (ok) { + /* update */ + xs_dict_set(snac.config, "followed_hashtags", followed_hashtags); + user_persist(&snac, 0); + + xs *d = xs_dict_new(); + xs *s = xs_fmt("%s?t=%s", srv_baseurl, i_tag); + d = xs_dict_set(d, "name", i_tag); + d = xs_dict_set(d, "url", s); + d = xs_dict_set(d, "history", xs_stock(XSTYPE_LIST)); + + *body = xs_json_dumps(d, 4); + *ctype = "application/json"; + status = HTTP_STATUS_OK; + } + } + } + } else status = HTTP_STATUS_UNPROCESSABLE_CONTENT; -- cgit v1.2.3 From 7a75f1ea021fff963e2c9aa96b84316a56aa8238 Mon Sep 17 00:00:00 2001 From: grunfink Date: Mon, 16 Jun 2025 05:49:01 +0200 Subject: Updated RELEASE_NOTES. --- RELEASE_NOTES.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/RELEASE_NOTES.md b/RELEASE_NOTES.md index 1360303..64df737 100644 --- a/RELEASE_NOTES.md +++ b/RELEASE_NOTES.md @@ -12,7 +12,7 @@ If a metadata value is an account handler, it's also tried to be validated (rel= Another search by URL tweak (this time for Pixelfed links). -Mastodon API: fixed a bug that made some boosts disappear after being shown in apps like Tusky. +Mastodon API: fixed a bug that made some boosts disappear after being shown in apps like Tusky, added followed hashtags maintenance. Renamed command-line actions `create_list` to `list_create` and `delete_list` to `list_remove`. -- cgit v1.2.3 From d87d294b7ddd93b16d0c89c324612aef5c6d648e Mon Sep 17 00:00:00 2001 From: grunfink Date: Mon, 16 Jun 2025 18:16:45 +0200 Subject: In enqueue_notify_webhook(), also add the inReplyTo object if there is one. --- data.c | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/data.c b/data.c index 6c38631..32b47d8 100644 --- a/data.c +++ b/data.c @@ -3543,6 +3543,16 @@ void enqueue_notify_webhook(snac *user, const xs_dict *noti, int retries) if (valid_status(object_get(xs_dict_get(noti, "actor"), &actor_obj)) && actor_obj) msg = xs_dict_set(msg, "account", actor_obj); + /* if this post is a reply, also add the inReplyTo object */ + const char *in_reply_to = xs_dict_get_path(msg, "msg.object.inReplyTo"); + + if (xs_is_string(in_reply_to)) { + xs *irt_obj = NULL; + + if (valid_status(object_get(in_reply_to, &irt_obj))) + msg = xs_dict_set(msg, "inReplyTo", irt_obj); + } + xs *qmsg = _new_qmsg("notify_webhook", msg, retries); const char *ntid = xs_dict_get(qmsg, "ntid"); xs *fn = xs_fmt("%s/queue/%s.json", user->basedir, ntid); -- cgit v1.2.3 From 10701e56deb3594505efc5deb63a4967b6d037d6 Mon Sep 17 00:00:00 2001 From: grunfink Date: Mon, 16 Jun 2025 18:20:32 +0200 Subject: Updated documentation. --- doc/snac.1 | 2 ++ 1 file changed, 2 insertions(+) diff --git a/doc/snac.1 b/doc/snac.1 index 5e020a4..5dc419b 100644 --- a/doc/snac.1 +++ b/doc/snac.1 @@ -459,6 +459,8 @@ the user identifier (account name) the server base directory .It baseurl the server base URL +.It reply +the activity this post is a reply to (may not exist) .El .Pp .Sh ENVIRONMENT -- cgit v1.2.3 From f580014c5031f78489257cbf152d38098a6f38ff Mon Sep 17 00:00:00 2001 From: grunfink Date: Mon, 16 Jun 2025 18:24:47 +0200 Subject: Renamed webhook 'inReplyTo' to 'reply'. --- data.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/data.c b/data.c index 32b47d8..e1cab52 100644 --- a/data.c +++ b/data.c @@ -3550,7 +3550,7 @@ void enqueue_notify_webhook(snac *user, const xs_dict *noti, int retries) xs *irt_obj = NULL; if (valid_status(object_get(in_reply_to, &irt_obj))) - msg = xs_dict_set(msg, "inReplyTo", irt_obj); + msg = xs_dict_set(msg, "reply", irt_obj); } xs *qmsg = _new_qmsg("notify_webhook", msg, retries); -- cgit v1.2.3 From 8985a115bab24d0b3c1fa1dd7a58d8d566098c1b Mon Sep 17 00:00:00 2001 From: grunfink Date: Mon, 16 Jun 2025 18:25:10 +0200 Subject: Updated documentation. --- doc/snac.1 | 2 ++ 1 file changed, 2 insertions(+) diff --git a/doc/snac.1 b/doc/snac.1 index 5dc419b..778a46a 100644 --- a/doc/snac.1 +++ b/doc/snac.1 @@ -459,6 +459,8 @@ the user identifier (account name) the server base directory .It baseurl the server base URL +.It account +the origin actor object .It reply the activity this post is a reply to (may not exist) .El -- cgit v1.2.3 From ce773d17d6f7f53543715acc1e5ea21b4125e19c Mon Sep 17 00:00:00 2001 From: grunfink Date: Mon, 16 Jun 2025 18:27:54 +0200 Subject: Updated documentation. --- doc/snac.1 | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/snac.1 b/doc/snac.1 index 778a46a..40f07af 100644 --- a/doc/snac.1 +++ b/doc/snac.1 @@ -445,7 +445,7 @@ the origin actor id the target actor id .It date the notification date -.It message +.It msg the full ActivityPub action JSON object .It objid the object identifier (extracted from message, may be null) -- cgit v1.2.3 From d362a91c4c59c06228005a9645e58a67ec038a9f Mon Sep 17 00:00:00 2001 From: grunfink Date: Mon, 16 Jun 2025 18:30:08 +0200 Subject: Updated documentation. --- doc/snac.1 | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/doc/snac.1 b/doc/snac.1 index 40f07af..d0a37a4 100644 --- a/doc/snac.1 +++ b/doc/snac.1 @@ -448,11 +448,11 @@ the notification date .It msg the full ActivityPub action JSON object .It objid -the object identifier (extracted from message, may be null) +the object identifier (extracted from msg, may be null) .It type -the action type (extracted from message) +the action type (extracted from msg) .It utype -the action subtype (extracted from message, may be null) +the action subtype (extracted from msg, may be null) .It uid the user identifier (account name) .It basedir -- cgit v1.2.3 From 0155c794087875ad8da73b194a2d719b379ac0d9 Mon Sep 17 00:00:00 2001 From: grunfink Date: Tue, 17 Jun 2025 08:32:38 +0200 Subject: Updated documentation. --- doc/snac.1 | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/doc/snac.1 b/doc/snac.1 index d0a37a4..0ab46d1 100644 --- a/doc/snac.1 +++ b/doc/snac.1 @@ -479,9 +479,9 @@ Overrides the debugging level from the server 'dbglevel' configuration variable. Set it to an integer value. The higher, the deeper in meaningless verbiage you'll find yourself into. .It Ev EDITOR -The user-preferred interactive text editor to prepare messages. +The user-preferred interactive text editor to prepare notes. .It Ev LANG -The language of the post when sending messages. +The language of the post when sending notes from the command line. .El .Sh SEE ALSO .Xr snac 5 , -- cgit v1.2.3 From 9b134e9076569b31eb5ec77ebda60eb9001485a8 Mon Sep 17 00:00:00 2001 From: grunfink Date: Wed, 18 Jun 2025 09:00:09 +0200 Subject: Added 'alias' and 'alias_raw' as forced update in user_persist(). --- data.c | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/data.c b/data.c index e1cab52..f26da49 100644 --- a/data.c +++ b/data.c @@ -333,6 +333,7 @@ int user_open_by_md5(snac *snac, const char *md5) return 0; } + int user_persist(snac *snac, int publish) /* store user */ { @@ -348,7 +349,7 @@ int user_persist(snac *snac, int publish) if (old != NULL) { int nw = 0; - const char *fields[] = { "header", "avatar", "name", "bio", + const char *fields[] = { "header", "avatar", "name", "bio", "alias", "alias_raw", "metadata", "latitude", "longitude", NULL }; for (int n = 0; fields[n]; n++) { -- cgit v1.2.3 From 0e30411f5890bfbe578a196a54ad46b72e3e49d7 Mon Sep 17 00:00:00 2001 From: grunfink Date: Wed, 18 Jun 2025 09:40:10 +0200 Subject: New field 'favicon_url' in server.json. --- html.c | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/html.c b/html.c index b4588f7..c472e3b 100644 --- a/html.c +++ b/html.c @@ -636,13 +636,18 @@ static xs_html *html_base_head(void) xs_html_attr("content", USER_AGENT))); /* add server CSS and favicon */ - xs *f; - f = xs_fmt("%s/favicon.ico", srv_baseurl); + xs *f = NULL; + const char *favicon = xs_dict_get(srv_config, "favicon_url"); + + if (xs_is_string(favicon)) + f = xs_dup(favicon); + else + f = xs_fmt("%s/favicon.ico", srv_baseurl); + const xs_list *p = xs_dict_get(srv_config, "cssurls"); const char *v; - int c = 0; - while (xs_list_next(p, &v, &c)) { + xs_list_foreach(p, v) { xs_html_add(head, xs_html_sctag("link", xs_html_attr("rel", "stylesheet"), -- cgit v1.2.3 From c0a2625672a3d6181a3ce66f1c4d09101cc1a835 Mon Sep 17 00:00:00 2001 From: grunfink Date: Wed, 18 Jun 2025 09:42:35 +0200 Subject: Updated documentation. --- doc/snac.8 | 2 ++ 1 file changed, 2 insertions(+) diff --git a/doc/snac.8 b/doc/snac.8 index 85d08dd..39081f2 100644 --- a/doc/snac.8 +++ b/doc/snac.8 @@ -283,6 +283,8 @@ value of 1 to avoid hammering servers. .It Ic disable_notify_webhook Since version 2.78, users can set a webhook URL to receive notifications. Set this to true if you don't want your users to have this privilege. +.It Ic favicon_url +The URL to a favicon. If it's not set, the default one is used instead. .El .Pp You must restart the server to make effective these changes. -- cgit v1.2.3 From dbcc977a8a661dfdb12128ed73563498b7e000c3 Mon Sep 17 00:00:00 2001 From: grunfink Date: Wed, 18 Jun 2025 09:54:29 +0200 Subject: Updated RELEASE_NOTES. --- RELEASE_NOTES.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/RELEASE_NOTES.md b/RELEASE_NOTES.md index 64df737..009ba68 100644 --- a/RELEASE_NOTES.md +++ b/RELEASE_NOTES.md @@ -16,6 +16,8 @@ Mastodon API: fixed a bug that made some boosts disappear after being shown in a Renamed command-line actions `create_list` to `list_create` and `delete_list` to `list_remove`. +The default favicon URL can be changed from the server configuration. + ## 2.78 Hashtag following also allow URLs to RSS feeds of ActivityPub objects (like e.g. https://mastodon.social/tags/ThankYouTuesday). -- cgit v1.2.3 From 8c9eb18099370f34a577859d7fddea1d5f412612 Mon Sep 17 00:00:00 2001 From: grunfink Date: Thu, 19 Jun 2025 18:41:59 +0200 Subject: Minor tweak to usage screen. --- main.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/main.c b/main.c index 5af3695..ddb02a8 100644 --- a/main.c +++ b/main.c @@ -58,7 +58,7 @@ int usage(const char *cmd) "unmute {basedir} {uid} {actor} Unmutes a previously muted actor\n" "verify_links {basedir} {uid} Verifies a user's links (in the metadata)\n" "search {basedir} {uid} {regex} Searches posts by content\n" - "export_csv {basedir} {uid} Exports data as CSV files\n" + "export_csv {basedir} {uid} Exports followers, lists, MUTEd and bookmarks to CSV\n" "alias {basedir} {uid} {account} Sets account (@user@host or actor url) as an alias\n" "migrate {basedir} {uid} Migrates to the account defined as the alias\n" "import_csv {basedir} {uid} Imports data from CSV files\n" -- cgit v1.2.3 From a07458f408c304ae9d037c95bee77dbf4522f3f0 Mon Sep 17 00:00:00 2001 From: grunfink Date: Thu, 19 Jun 2025 19:19:08 +0200 Subject: New command-line option export_posts. --- main.c | 6 ++++++ snac.h | 2 ++ utils.c | 52 ++++++++++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 60 insertions(+) diff --git a/main.c b/main.c index ddb02a8..f271abc 100644 --- a/main.c +++ b/main.c @@ -59,6 +59,7 @@ int usage(const char *cmd) "verify_links {basedir} {uid} Verifies a user's links (in the metadata)\n" "search {basedir} {uid} {regex} Searches posts by content\n" "export_csv {basedir} {uid} Exports followers, lists, MUTEd and bookmarks to CSV\n" + "export_posts {basedir} {iod} Exports all posts to outbox.json\n" "alias {basedir} {uid} {account} Sets account (@user@host or actor url) as an alias\n" "migrate {basedir} {uid} Migrates to the account defined as the alias\n" "import_csv {basedir} {uid} Imports data from CSV files\n" @@ -308,6 +309,11 @@ int main(int argc, char *argv[]) return 0; } + if (strcmp(cmd, "export_posts") == 0) { /** **/ + export_posts(&snac); + return 0; + } + if (strcmp(cmd, "import_csv") == 0) { /** **/ import_csv(&snac); return 0; diff --git a/snac.h b/snac.h index 7f109e0..42fce87 100644 --- a/snac.h +++ b/snac.h @@ -434,6 +434,8 @@ void mastoapi_purge(void); void verify_links(snac *user); void export_csv(snac *user); +void export_posts(snac *user); + int migrate_account(snac *user); void import_blocked_accounts_csv(snac *user, const char *fn); diff --git a/utils.c b/utils.c index 7adbce2..5367f22 100644 --- a/utils.c +++ b/utils.c @@ -725,6 +725,58 @@ void export_csv(snac *user) } +void export_posts(snac *user) +/* exports all posts to an OrderedCollection */ +{ + xs *ifn = xs_fmt("%s/public.idx", user->basedir); + xs *index = index_list(ifn, XS_ALL); + xs *ofn = xs_fmt("%s/export/outbox.json", user->basedir); + FILE *f; + + if ((f = fopen(ofn, "w")) == NULL) { + snac_log(user, xs_fmt("Cannot create file %s", ofn)); + return; + } + + int cnt = 0; + + /* raw output */ + fprintf(f, "{\"@context\": \"https:/" "/www.w3.org/ns/activitystreams\","); + fprintf(f, "\"id\": \"outbox.json\","); + fprintf(f, "\"type\": \"OrderedCollection\","); + fprintf(f, "\"orderedItems\": ["); + + const char *md5; + + snac_log(user, xs_fmt("Creating %s...", ofn)); + + xs_list_foreach(index, md5) { + xs *obj = NULL; + + if (!valid_status(object_get_by_md5(md5, &obj))) + continue; + + const char *type = xs_dict_get(obj, "type"); + + if (!xs_is_string(type) || strcmp(type, "Note")) + continue; + + const char *atto = get_atto(obj); + + if (!xs_is_string(atto) || strcmp(atto, user->actor)) + continue; + + xs *c_msg = msg_create(user, obj); + xs_json_dump(c_msg, 0, f); + cnt++; + } + + fprintf(f, "], \"totalItems\": %d}", cnt); + + fclose(f); +} + + void import_blocked_accounts_csv(snac *user, const char *ifn) /* imports a Mastodon CSV file of blocked accounts */ { -- cgit v1.2.3 From 8fdfb5b70d638b0f3c426c8a3ea7d2c2c0937a05 Mon Sep 17 00:00:00 2001 From: grunfink Date: Thu, 19 Jun 2025 19:31:42 +0200 Subject: Fixed bug in export_posts(). --- utils.c | 3 +++ 1 file changed, 3 insertions(+) diff --git a/utils.c b/utils.c index 5367f22..f0307cc 100644 --- a/utils.c +++ b/utils.c @@ -766,6 +766,9 @@ void export_posts(snac *user) if (!xs_is_string(atto) || strcmp(atto, user->actor)) continue; + if (cnt) + fprintf(f, ","); + xs *c_msg = msg_create(user, obj); xs_json_dump(c_msg, 0, f); cnt++; -- cgit v1.2.3 From 883c4b7e765a4114c00cc6a411f923a347e9e5f3 Mon Sep 17 00:00:00 2001 From: grunfink Date: Thu, 19 Jun 2025 22:40:57 +0200 Subject: Updated documentation. --- doc/snac.1 | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/doc/snac.1 b/doc/snac.1 index 0ab46d1..30bf1d4 100644 --- a/doc/snac.1 +++ b/doc/snac.1 @@ -320,6 +320,13 @@ subdirectory inside the user directory: .Pa blocked_accounts.csv , .Pa lists.csv , and .Pa following_accounts.csv . +.It Cm export_posts Ar basedir Ar uid +Exports all posts written by the user to the file +.Pa outbox.json +inside the +.Pa export/ +subdirectory inside the user directory. The format is compatible with the +one generated by the equivalent option in Mastodon. .It Cm alias Ar basedir Ar uid Ar "@account@remotehost" Sets an account as an alias of this one. This is a necessary step to migrate an account to a remote Mastodon instance (see -- cgit v1.2.3 From 508726cc04768bd2cdad4c774b6fa1c411c41ab3 Mon Sep 17 00:00:00 2001 From: grunfink Date: Thu, 19 Jun 2025 22:42:23 +0200 Subject: Updated RELEASE_NOTES. --- RELEASE_NOTES.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/RELEASE_NOTES.md b/RELEASE_NOTES.md index 009ba68..ded17c9 100644 --- a/RELEASE_NOTES.md +++ b/RELEASE_NOTES.md @@ -18,6 +18,8 @@ Renamed command-line actions `create_list` to `list_create` and `delete_list` to The default favicon URL can be changed from the server configuration. +New command-line option `export_posts`, to export all posts by a user in a JSON format compatible with the one generated by Mastodon. + ## 2.78 Hashtag following also allow URLs to RSS feeds of ActivityPub objects (like e.g. https://mastodon.social/tags/ThankYouTuesday). -- cgit v1.2.3 From fa986ad91e15bdea6a9764d04fdb90e404d39e55 Mon Sep 17 00:00:00 2001 From: grunfink Date: Sat, 21 Jun 2025 19:08:38 +0200 Subject: Added support for /api/v1/instance/extended_description (with a hardcoded text). --- mastoapi.c | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/mastoapi.c b/mastoapi.c index 2e8063b..0829a84 100644 --- a/mastoapi.c +++ b/mastoapi.c @@ -2337,6 +2337,21 @@ int mastoapi_get_handler(const xs_dict *req, const char *q_path, status = HTTP_STATUS_OK; } else + if (strcmp(cmd, "/v1/instance/extended_description") == 0) { /** **/ + xs *d = xs_dict_new(); + xs *greeting = xs_fmt("%s/greeting.html", srv_basedir); + time_t t = mtime(greeting); + xs *updated_at = xs_str_iso_date(t); + xs *content = xs_replace(snac_blurb, "%host%", xs_dict_get(srv_config, "host")); + + d = xs_dict_set(d, "updated_at", updated_at); + d = xs_dict_set(d, "content", content); + + *body = xs_json_dumps(d, 4); + *ctype = "application/json"; + status = HTTP_STATUS_OK; + } + else if (xs_startswith(cmd, "/v1/statuses/")) { /** **/ /* information about a status */ if (logged_in) { -- cgit v1.2.3 From 6fe632ec769f6a03825e55840093fada928a0a8a Mon Sep 17 00:00:00 2001 From: grunfink Date: Sat, 21 Jun 2025 19:20:18 +0200 Subject: Backport from xs. --- xs_json.h | 124 ++++++++++++++++++++++++++++++++++------------------------- xs_version.h | 2 +- 2 files changed, 73 insertions(+), 53 deletions(-) diff --git a/xs_json.h b/xs_json.h index 8b449a9..436fa72 100644 --- a/xs_json.h +++ b/xs_json.h @@ -19,8 +19,8 @@ xs_val *xs_json_loads_full(const xs_str *json, int maxdepth); xstype xs_json_load_type(FILE *f); int xs_json_load_array_iter(FILE *f, xs_val **value, xstype *pt, int *c); int xs_json_load_object_iter(FILE *f, xs_str **key, xs_val **value, xstype *pt, int *c); -xs_list *xs_json_load_array(FILE *f, int maxdepth); -xs_dict *xs_json_load_object(FILE *f, int maxdepth); +int xs_json_load_array(FILE *f, int maxdepth, xs_list **l); +int xs_json_load_object(FILE *f, int maxdepth, xs_dict **d); #ifdef XS_IMPLEMENTATION @@ -377,43 +377,51 @@ int xs_json_load_array_iter(FILE *f, xs_val **value, xstype *pt, int *c) } -xs_list *xs_json_load_array(FILE *f, int maxdepth) +int xs_json_load_array(FILE *f, int maxdepth, xs_list **l) /* loads a full JSON array (after the initial OBRACK) */ +/* l can be NULL for the content to be dropped */ { xstype t; - xs_list *l = xs_list_new(); + int r = 0; int c = 0; for (;;) { xs *v = NULL; - int r = xs_json_load_array_iter(f, &v, &t, &c); - - if (r == -1) - l = xs_free(l); + r = xs_json_load_array_iter(f, &v, &t, &c); if (r == 1) { /* partial load? */ if (v == NULL && maxdepth != 0) { - if (t == XSTYPE_LIST) - v = xs_json_load_array(f, maxdepth - 1); + if (t == XSTYPE_LIST) { + if (l) + v = xs_list_new(); + + r = xs_json_load_array(f, maxdepth - 1, &v); + } else - if (t == XSTYPE_DICT) - v = xs_json_load_object(f, maxdepth - 1); + if (t == XSTYPE_DICT) { + if (l) + v = xs_dict_new(); + + r = xs_json_load_object(f, maxdepth - 1, &v); + } } - /* still null? fail */ - if (v == NULL) { - l = xs_free(l); + /* error? */ + if (r < 0) break; - } - l = xs_list_append(l, v); + if (l) + *l = xs_list_append(*l, v); } else break; } - return l; + if (r < 0 && l) + *l = xs_free(*l); + + return r; } @@ -465,59 +473,52 @@ int xs_json_load_object_iter(FILE *f, xs_str **key, xs_val **value, xstype *pt, } -xs_dict *xs_json_load_object(FILE *f, int maxdepth) +int xs_json_load_object(FILE *f, int maxdepth, xs_dict **d) /* loads a full JSON object (after the initial OCURLY) */ +/* d can be NULL for the content to be dropped */ { xstype t; - xs_dict *d = xs_dict_new(); + int r = 0; int c = 0; for (;;) { xs *k = NULL; xs *v = NULL; - int r = xs_json_load_object_iter(f, &k, &v, &t, &c); - - if (r == -1) - d = xs_free(d); + r = xs_json_load_object_iter(f, &k, &v, &t, &c); if (r == 1) { /* partial load? */ if (v == NULL && maxdepth != 0) { - if (t == XSTYPE_LIST) - v = xs_json_load_array(f, maxdepth - 1); + if (t == XSTYPE_LIST) { + if (d) + v = xs_list_new(); + + r = xs_json_load_array(f, maxdepth - 1, &v); + } else - if (t == XSTYPE_DICT) - v = xs_json_load_object(f, maxdepth - 1); + if (t == XSTYPE_DICT) { + if (d) + v = xs_dict_new(); + + r = xs_json_load_object(f, maxdepth - 1, &v); + } } - /* still null? fail */ - if (v == NULL) { - d = xs_free(d); + /* error? */ + if (r < 0) break; - } - d = xs_dict_append(d, k, v); + if (d) + *d = xs_dict_append(*d, k, v); } else break; } - return d; -} - - -xs_val *xs_json_loads_full(const xs_str *json, int maxdepth) -/* loads a string in JSON format and converts to a multiple data */ -{ - FILE *f; - xs_val *v = NULL; - - if ((f = fmemopen((char *)json, strlen(json), "r")) != NULL) { - v = xs_json_load_full(f, maxdepth); - fclose(f); - } + if (r < 0 && d) + *d = xs_free(*d); - return v; + return r; } @@ -545,11 +546,30 @@ xs_val *xs_json_load_full(FILE *f, int maxdepth) xs_val *v = NULL; xstype t = xs_json_load_type(f); - if (t == XSTYPE_LIST) - v = xs_json_load_array(f, maxdepth); + if (t == XSTYPE_LIST) { + v = xs_list_new(); + xs_json_load_array(f, maxdepth, &v); + } else - if (t == XSTYPE_DICT) - v = xs_json_load_object(f, maxdepth); + if (t == XSTYPE_DICT) { + v = xs_dict_new(); + xs_json_load_object(f, maxdepth, &v); + } + + return v; +} + + +xs_val *xs_json_loads_full(const xs_str *json, int maxdepth) +/* loads a string in JSON format and converts to a multiple data */ +{ + FILE *f; + xs_val *v = NULL; + + if ((f = fmemopen((char *)json, strlen(json), "r")) != NULL) { + v = xs_json_load_full(f, maxdepth); + fclose(f); + } return v; } diff --git a/xs_version.h b/xs_version.h index c9418a7..74f453f 100644 --- a/xs_version.h +++ b/xs_version.h @@ -1 +1 @@ -/* 963b6129fca4fb5c009533db0ed602ab8ed5424d 2025-06-12T11:33:02+02:00 */ +/* 851c21892c8df1d8f05ae95eaa4a5913e852653b 2025-06-21T17:58:11+02:00 */ -- cgit v1.2.3 From f7d398ef07314d4547c48bdfe6f889535e6a6bcc Mon Sep 17 00:00:00 2001 From: grunfink Date: Sat, 21 Jun 2025 19:23:00 +0200 Subject: Updated RELEASE_NOTES. --- RELEASE_NOTES.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/RELEASE_NOTES.md b/RELEASE_NOTES.md index ded17c9..664da11 100644 --- a/RELEASE_NOTES.md +++ b/RELEASE_NOTES.md @@ -12,7 +12,7 @@ If a metadata value is an account handler, it's also tried to be validated (rel= Another search by URL tweak (this time for Pixelfed links). -Mastodon API: fixed a bug that made some boosts disappear after being shown in apps like Tusky, added followed hashtags maintenance. +Mastodon API: fixed a bug that made some boosts disappear after being shown in apps like Tusky, added followed hashtags maintenance, other minor changes. Renamed command-line actions `create_list` to `list_create` and `delete_list` to `list_remove`. -- cgit v1.2.3 From 238c5c5f3918f4c64df2364b7c25eb82fd8f16d2 Mon Sep 17 00:00:00 2001 From: grunfink Date: Tue, 24 Jun 2025 12:59:02 +0200 Subject: Some JSON code tweaks. --- xs_json.h | 39 ++++++++++++++++++++++----------------- xs_version.h | 2 +- 2 files changed, 23 insertions(+), 18 deletions(-) diff --git a/xs_json.h b/xs_json.h index 436fa72..07800fa 100644 --- a/xs_json.h +++ b/xs_json.h @@ -8,6 +8,7 @@ #define MAX_JSON_DEPTH 32 #endif +void xs_json_dump_value(const xs_val *data, int level, int indent, FILE *f); int xs_json_dump(const xs_val *data, int indent, FILE *f); xs_str *xs_json_dumps(const xs_val *data, int indent); @@ -77,7 +78,7 @@ static void _xs_json_indent(int level, int indent, FILE *f) } -static void _xs_json_dump(const xs_val *data, int level, int indent, FILE *f) +void xs_json_dump_value(const xs_val *data, int level, int indent, FILE *f) /* dumps partial data as JSON */ { int c = 0; @@ -108,7 +109,7 @@ static void _xs_json_dump(const xs_val *data, int level, int indent, FILE *f) fputc(',', f); _xs_json_indent(level + 1, indent, f); - _xs_json_dump(v, level + 1, indent, f); + xs_json_dump_value(v, level + 1, indent, f); c++; } @@ -135,7 +136,7 @@ static void _xs_json_dump(const xs_val *data, int level, int indent, FILE *f) if (indent) fputc(' ', f); - _xs_json_dump(v, level + 1, indent, f); + xs_json_dump_value(v, level + 1, indent, f); c++; } @@ -154,6 +155,20 @@ static void _xs_json_dump(const xs_val *data, int level, int indent, FILE *f) } +int xs_json_dump(const xs_val *data, int indent, FILE *f) +/* dumps data into a file as JSON */ +{ + xstype t = xs_type(data); + + if (t == XSTYPE_LIST || t == XSTYPE_DICT) { + xs_json_dump_value(data, 0, indent, f); + return 1; + } + + return 0; +} + + xs_str *xs_json_dumps(const xs_val *data, int indent) /* dumps data as a JSON string */ { @@ -173,20 +188,6 @@ xs_str *xs_json_dumps(const xs_val *data, int indent) } -int xs_json_dump(const xs_val *data, int indent, FILE *f) -/* dumps data into a file as JSON */ -{ - xstype t = xs_type(data); - - if (t == XSTYPE_LIST || t == XSTYPE_DICT) { - _xs_json_dump(data, 0, indent, f); - return 1; - } - - return 0; -} - - /** JSON loads **/ typedef enum { @@ -370,6 +371,8 @@ int xs_json_load_array_iter(FILE *f, xs_val **value, xstype *pt, int *c) else return -1; } + else + *pt = xs_type(*value); *c = *c + 1; @@ -466,6 +469,8 @@ int xs_json_load_object_iter(FILE *f, xs_str **key, xs_val **value, xstype *pt, else return -1; } + else + *pt = xs_type(*value); *c = *c + 1; diff --git a/xs_version.h b/xs_version.h index 74f453f..09b1bdc 100644 --- a/xs_version.h +++ b/xs_version.h @@ -1 +1 @@ -/* 851c21892c8df1d8f05ae95eaa4a5913e852653b 2025-06-21T17:58:11+02:00 */ +/* a32c0d513ae24ad28ffc5c6c2c1cde75bb758e09 2025-06-23T17:43:10+02:00 */ -- cgit v1.2.3 From f88bdd796045dd206b38e07adb13b5af4489b4c5 Mon Sep 17 00:00:00 2001 From: grunfink Date: Tue, 24 Jun 2025 14:06:41 +0200 Subject: Added a webmention hook. --- html.c | 20 ++++++++++++++----- httpd.c | 62 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++ xs_webmention.h | 42 ++++++++++++++++++++++++++++++++++++++ 3 files changed, 119 insertions(+), 5 deletions(-) diff --git a/html.c b/html.c index c472e3b..672ea44 100644 --- a/html.c +++ b/html.c @@ -868,6 +868,14 @@ xs_html *html_user_head(snac *user, const char *desc, const char *url) xs_html_attr("type", "application/activity+json"), xs_html_attr("href", url ? url : user->actor))); + /* webmention hook */ + xs *wbh = xs_fmt("%s/webmention-hook", srv_baseurl); + + xs_html_add(head, + xs_html_sctag("link", + xs_html_attr("rel", "webmention"), + xs_html_attr("href", wbh))); + return head; } @@ -3407,13 +3415,15 @@ xs_str *html_notifications(snac *user, int skip, int show) const char *actor_id = xs_dict_get(noti, "actor"); xs *actor = NULL; + xs *a_name = NULL; - if (!valid_status(actor_get(actor_id, &actor))) - continue; + if (valid_status(actor_get(actor_id, &actor))) + a_name = actor_name(actor, proxy); + else + a_name = xs_dup(actor_id); - xs *a_name = actor_name(actor, proxy); - xs *label_sanatized = sanitize(type); - const char *label = label_sanatized; + xs *label_sanitized = sanitize(type); + const char *label = label_sanitized; if (strcmp(type, "Create") == 0) label = L("Mention"); diff --git a/httpd.c b/httpd.c index 15634d1..af78e09 100644 --- a/httpd.c +++ b/httpd.c @@ -12,6 +12,7 @@ #include "xs_openssl.h" #include "xs_fcgi.h" #include "xs_html.h" +#include "xs_webmention.h" #include "snac.h" @@ -373,6 +374,63 @@ int server_get_handler(xs_dict *req, const char *q_path, } +int server_post_handler(const xs_dict *req, const char *q_path, + char *payload, int p_size, + char **body, int *b_size, char **ctype) +{ + int status = 0; + + if (strcmp(q_path, "/webmention-hook") == 0) { + status = HTTP_STATUS_BAD_REQUEST; + + const xs_dict *p_vars = xs_dict_get(req, "p_vars"); + + if (!xs_is_dict(p_vars)) + return status; + + const char *source = xs_dict_get(p_vars, "source"); + const char *target = xs_dict_get(p_vars, "target"); + + if (!xs_is_string(source) || !xs_is_string(target)) { + srv_debug(1, xs_fmt("webmention-hook bad source or target")); + return status; + } + + if (!xs_startswith(target, srv_baseurl)) { + srv_debug(1, xs_fmt("webmention-hook unknown target %s", target)); + return status; + } + + if (!object_here(target)) { + srv_debug(0, xs_fmt("webmention-hook target %s not / no longer here", target)); + return status; + } + + /* get the user */ + xs *s1 = xs_replace(target, srv_baseurl, ""); + + xs *l1 = xs_split(s1, "/"); + const char *uid = xs_list_get(l1, 1); + snac user; + + if (!xs_is_string(uid) || !user_open(&user, uid)) + return status; + + int r = xs_webmention_hook(source, target, USER_AGENT); + + if (r > 0) + notify_add(&user, "Webmention", NULL, source, target, xs_stock(XSTYPE_DICT)); + + srv_log(xs_fmt("webmention-hook source=%s target=%s %d", source, target, r)); + + user_free(&user); + status = HTTP_STATUS_OK; + } + + return status; +} + + void httpd_connection(FILE *f) /* the connection processor */ { @@ -444,6 +502,10 @@ void httpd_connection(FILE *f) else if (strcmp(method, "POST") == 0) { + if (status == 0) + status = server_post_handler(req, q_path, + payload, p_size, &body, &b_size, &ctype); + #ifndef NO_MASTODON_API if (status == 0) status = oauth_post_handler(req, q_path, diff --git a/xs_webmention.h b/xs_webmention.h index 8415629..e177573 100644 --- a/xs_webmention.h +++ b/xs_webmention.h @@ -5,6 +5,7 @@ #define _XS_WEBMENTION_H int xs_webmention_send(const char *source, const char *target, const char *user_agent); +int xs_webmention_hook(const char *source, const char *target, const char *user_agent); #ifdef XS_IMPLEMENTATION @@ -118,6 +119,47 @@ int xs_webmention_send(const char *source, const char *target, const char *user_ } +int xs_webmention_hook(const char *source, const char *target, const char *user_agent) +/* a Webmention has been received for a target that is ours; check if the source + really contains a link to our target */ +{ + int status = 0; + + xs *ua = xs_fmt("%s (Webmention)", user_agent ? user_agent : "xs_webmention"); + xs *headers = xs_dict_new(); + headers = xs_dict_set(headers, "accept", "text/html"); + headers = xs_dict_set(headers, "user-agent", ua); + + xs *g_req = NULL; + xs *payload = NULL; + int p_size = 0; + + g_req = xs_http_request("GET", source, headers, NULL, 0, &status, &payload, &p_size, 0); + + if (status < 200 || status > 299) + return -1; + + if (!xs_is_string(payload)) + return -2; + + /* note: a "rogue" webmention can include a link to our target in commented-out HTML code */ + + xs *links = xs_regex_select(payload, "<(a +|link +)[^>]+>"); + const char *link; + + status = 0; + xs_list_foreach(links, link) { + /* if the link contains our target, it's valid */ + if (xs_str_in(link, target) != -1) { + status = 1; + break; + } + } + + return status; +} + + #endif /* XS_IMPLEMENTATION */ #endif /* _XS_WEBMENTION_H */ -- cgit v1.2.3 From 1aa8fe8573bfc93f569bf266770e9f39a1c2da14 Mon Sep 17 00:00:00 2001 From: grunfink Date: Tue, 24 Jun 2025 14:08:59 +0200 Subject: "voided" unused arguments in server_post_handler(). --- httpd.c | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/httpd.c b/httpd.c index af78e09..9216bdc 100644 --- a/httpd.c +++ b/httpd.c @@ -380,6 +380,12 @@ int server_post_handler(const xs_dict *req, const char *q_path, { int status = 0; + (void)payload; + (void)p_size; + (void)body; + (void)b_size; + (void)ctype; + if (strcmp(q_path, "/webmention-hook") == 0) { status = HTTP_STATUS_BAD_REQUEST; -- cgit v1.2.3 From 9403fb41effc6da7a997898b07b6c6b46f8e2ba1 Mon Sep 17 00:00:00 2001 From: grunfink Date: Tue, 24 Jun 2025 14:34:55 +0200 Subject: More webmention hook tweaks. --- html.c | 7 +++++++ httpd.c | 13 ++++++------- 2 files changed, 13 insertions(+), 7 deletions(-) diff --git a/html.c b/html.c index 672ea44..aeecdf5 100644 --- a/html.c +++ b/html.c @@ -3531,6 +3531,13 @@ xs_str *html_notifications(snac *user, int skip, int show) xs_html_text(L("Context")))), h); } + else + xs_html_add(entry, + xs_html_tag("p", + xs_html_text(L("Location: ")), + xs_html_tag("a", + xs_html_attr("href", id), + xs_html_text(id)))); } if (strcmp(v, n_time) > 0) { diff --git a/httpd.c b/httpd.c index 9216bdc..8f2ef4d 100644 --- a/httpd.c +++ b/httpd.c @@ -407,11 +407,6 @@ int server_post_handler(const xs_dict *req, const char *q_path, return status; } - if (!object_here(target)) { - srv_debug(0, xs_fmt("webmention-hook target %s not / no longer here", target)); - return status; - } - /* get the user */ xs *s1 = xs_replace(target, srv_baseurl, ""); @@ -419,13 +414,17 @@ int server_post_handler(const xs_dict *req, const char *q_path, const char *uid = xs_list_get(l1, 1); snac user; - if (!xs_is_string(uid) || !user_open(&user, uid)) + if (!xs_is_string(uid) || !user_open(&user, uid)) { + srv_debug(1, xs_fmt("webmention-hook cannot find user for %s", target)); return status; + } int r = xs_webmention_hook(source, target, USER_AGENT); - if (r > 0) + if (r > 0) { notify_add(&user, "Webmention", NULL, source, target, xs_stock(XSTYPE_DICT)); + timeline_touch(&user); + } srv_log(xs_fmt("webmention-hook source=%s target=%s %d", source, target, r)); -- cgit v1.2.3 From bed5789dc67e1a52d2d9d0b5d8afd926d9d60e0f Mon Sep 17 00:00:00 2001 From: grunfink Date: Tue, 24 Jun 2025 14:49:01 +0200 Subject: Updated RELEASE_NOTES. --- RELEASE_NOTES.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/RELEASE_NOTES.md b/RELEASE_NOTES.md index 664da11..729b9f7 100644 --- a/RELEASE_NOTES.md +++ b/RELEASE_NOTES.md @@ -2,6 +2,8 @@ ## UNRELEASED +Added a server-wide Webmention hook; this way, if somebody out there (that supports Webmention) links to a user or post in this instance, a notification is sent (this is the complementary of what was implemented in version 2.76). + Fixed regression while sending email via pipe on OpenBSD. Fixed Markdown parsing when the URL has parenthesis. -- cgit v1.2.3 From 5d680a5ba227c5275624fbeefe0cce05cfb60ee6 Mon Sep 17 00:00:00 2001 From: grunfink Date: Wed, 25 Jun 2025 12:47:07 +0200 Subject: Fixed crash. --- html.c | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/html.c b/html.c index aeecdf5..b482d10 100644 --- a/html.c +++ b/html.c @@ -3495,10 +3495,11 @@ xs_str *html_notifications(snac *user, int skip, int show) 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, NULL))); + if (actor) + 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, NULL))); } else if (strcmp(type, "Move") == 0) { -- cgit v1.2.3 From c976b9a334872e14aadc1f454680599468652231 Mon Sep 17 00:00:00 2001 From: grunfink Date: Thu, 26 Jun 2025 10:39:47 +0200 Subject: Sending notes from the cmdline allows setting an inReplyTo field. --- Makefile | 6 +++--- Makefile.NetBSD | 6 +++--- main.c | 23 +++++++++++++++++++---- 3 files changed, 25 insertions(+), 10 deletions(-) diff --git a/Makefile b/Makefile index 6059fef..5de2882 100644 --- a/Makefile +++ b/Makefile @@ -60,10 +60,10 @@ html.o: html.c xs.h xs_io.h xs_json.h xs_regex.h xs_set.h xs_openssl.h \ http.o: http.c xs.h xs_io.h xs_openssl.h xs_curl.h xs_time.h xs_json.h \ snac.h http_codes.h httpd.o: httpd.c xs.h xs_io.h xs_json.h xs_socket.h xs_unix_socket.h \ - xs_httpd.h xs_mime.h xs_time.h xs_openssl.h xs_fcgi.h xs_html.h snac.h \ - http_codes.h + xs_httpd.h xs_mime.h xs_time.h xs_openssl.h xs_fcgi.h xs_html.h \ + xs_webmention.h snac.h http_codes.h main.o: main.c xs.h xs_io.h xs_json.h xs_time.h xs_openssl.h xs_match.h \ - snac.h http_codes.h + xs_random.h snac.h http_codes.h mastoapi.o: mastoapi.c xs.h xs_hex.h xs_openssl.h xs_json.h xs_io.h \ xs_time.h xs_glob.h xs_set.h xs_random.h xs_url.h xs_mime.h xs_match.h \ xs_unicode.h snac.h http_codes.h diff --git a/Makefile.NetBSD b/Makefile.NetBSD index d19ab2d..ac5b052 100644 --- a/Makefile.NetBSD +++ b/Makefile.NetBSD @@ -49,10 +49,10 @@ html.o: html.c xs.h xs_io.h xs_json.h xs_regex.h xs_set.h xs_openssl.h \ http.o: http.c xs.h xs_io.h xs_openssl.h xs_curl.h xs_time.h xs_json.h \ snac.h http_codes.h httpd.o: httpd.c xs.h xs_io.h xs_json.h xs_socket.h xs_unix_socket.h \ - xs_httpd.h xs_mime.h xs_time.h xs_openssl.h xs_fcgi.h xs_html.h snac.h \ - http_codes.h + xs_httpd.h xs_mime.h xs_time.h xs_openssl.h xs_fcgi.h xs_html.h \ + xs_webmention.h snac.h http_codes.h main.o: main.c xs.h xs_io.h xs_json.h xs_time.h xs_openssl.h xs_match.h \ - snac.h http_codes.h + xs_random.h snac.h http_codes.h mastoapi.o: mastoapi.c xs.h xs_hex.h xs_openssl.h xs_json.h xs_io.h \ xs_time.h xs_glob.h xs_set.h xs_random.h xs_url.h xs_mime.h xs_match.h \ xs_unicode.h snac.h http_codes.h diff --git a/main.c b/main.c index f271abc..ce4e7ce 100644 --- a/main.c +++ b/main.c @@ -7,6 +7,7 @@ #include "xs_time.h" #include "xs_openssl.h" #include "xs_match.h" +#include "xs_random.h" #include "snac.h" @@ -779,12 +780,24 @@ int main(int argc, char *argv[]) xs *msg = NULL; xs *c_msg = NULL; xs *attl = xs_list_new(); - char *fn = NULL; + const char *fn = NULL; + const char *in_reply_to = NULL; + const char **next = NULL; /* iterate possible attachments */ while ((fn = GET_ARGV())) { FILE *f; + if (next) { + *next = fn; + next = NULL; + } + else + if (strcmp(fn, "-r") == 0) { + /* next argument is an inReplyTo */ + next = &in_reply_to; + } + else if ((f = fopen(fn, "rb")) != NULL) { /* get the file size and content */ fseek(f, 0, SEEK_END); @@ -794,8 +807,10 @@ int main(int argc, char *argv[]) fclose(f); char *ext = strrchr(fn, '.'); - xs *hash = xs_md5_hex(fn, strlen(fn)); - xs *id = xs_fmt("%s%s", hash, ext); + char rnd[32]; + xs_rnd_buf(rnd, sizeof(rnd)); + xs *hash = xs_md5_hex(rnd, sizeof(rnd)); + xs *id = xs_fmt("post-%s%s", hash, ext ? ext : ""); xs *url = xs_fmt("%s/s/%s", snac.actor, id); /* store */ @@ -856,7 +871,7 @@ int main(int argc, char *argv[]) if (strcmp(cmd, "note_unlisted") == 0) scope = 2; - msg = msg_note(&snac, content, NULL, NULL, attl, scope, getenv("LANG"), NULL); + msg = msg_note(&snac, content, NULL, in_reply_to, attl, scope, getenv("LANG"), NULL); c_msg = msg_create(&snac, msg); -- cgit v1.2.3 From cbe18516b17112bfb7c8fee178f2c2ce422ec677 Mon Sep 17 00:00:00 2001 From: grunfink Date: Thu, 26 Jun 2025 10:44:58 +0200 Subject: Updated documentation. --- doc/snac.1 | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/doc/snac.1 b/doc/snac.1 index 30bf1d4..674ef82 100644 --- a/doc/snac.1 +++ b/doc/snac.1 @@ -284,7 +284,7 @@ Requests an object and dumps it to stdout. This is a very low level command that is not very useful to you. .It Cm announce Ar basedir Ar uid Ar url Announces (boosts) a post via its URL. -.It Cm note Ar basedir Ar uid Ar text Op file file ... +.It Cm note Ar basedir Ar uid Ar text Op file file ... Op -r inReplyTo Enqueues a Create + Note message to all followers. If the .Ar text argument is -e, the external editor defined by the EDITOR @@ -292,10 +292,11 @@ environment variable will be invoked to prepare a message; if it's - (a lonely hyphen), the post content will be read from stdin. The rest of command line arguments are treated as media files to be attached to the post. The LANG environment variable (if defined) is used -as the post language. -.It Cm note_unlisted Ar basedir Ar uid Ar text Op file file ... +as the post language. An optional URL to a Fediverse post can be specified +for this note to be a reply to. +.It Cm note_unlisted Ar basedir Ar uid Ar text Op file file ... Op -r inReplyTo 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 ... +.It Cm note_mention Ar basedir Ar uid Ar text Op file file ... Op -r inReplyTo Like the previous one, but creates a post only for accounts mentioned in the post body. .It Cm block Ar basedir Ar instance_url -- cgit v1.2.3 From 077e2ba462be7a88e5ad03a43124529ecb7bf401 Mon Sep 17 00:00:00 2001 From: grunfink Date: Thu, 26 Jun 2025 10:46:48 +0200 Subject: Updated RELEASE_NOTES. --- RELEASE_NOTES.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/RELEASE_NOTES.md b/RELEASE_NOTES.md index 729b9f7..3a35a8d 100644 --- a/RELEASE_NOTES.md +++ b/RELEASE_NOTES.md @@ -22,6 +22,8 @@ The default favicon URL can be changed from the server configuration. New command-line option `export_posts`, to export all posts by a user in a JSON format compatible with the one generated by Mastodon. +The command-line options to send notes also allow an optional `-r` argument, to set the URL of a Fediverse post this note is a reply to. + ## 2.78 Hashtag following also allow URLs to RSS feeds of ActivityPub objects (like e.g. https://mastodon.social/tags/ThankYouTuesday). -- cgit v1.2.3 From f33fabe303ea1bfe12e61b8057f8978c0f6bafbf Mon Sep 17 00:00:00 2001 From: grunfink Date: Fri, 27 Jun 2025 06:39:41 +0200 Subject: Updated documentation. --- doc/snac.1 | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/doc/snac.1 b/doc/snac.1 index 674ef82..e905610 100644 --- a/doc/snac.1 +++ b/doc/snac.1 @@ -292,8 +292,8 @@ environment variable will be invoked to prepare a message; if it's - (a lonely hyphen), the post content will be read from stdin. The rest of command line arguments are treated as media files to be attached to the post. The LANG environment variable (if defined) is used -as the post language. An optional URL to a Fediverse post can be specified -for this note to be a reply to. +as the post language. An optional URL to a Fediverse post, prefixed by -r, +can be specified for this note to be a reply to. .It Cm note_unlisted Ar basedir Ar uid Ar text Op file file ... Op -r inReplyTo 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 ... Op -r inReplyTo -- cgit v1.2.3 From a5ce94c824b0705da068420ca58e81bda7f8bef3 Mon Sep 17 00:00:00 2001 From: grunfink Date: Fri, 27 Jun 2025 15:24:27 +0200 Subject: Added an actor refresh in html_entry(). --- html.c | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/html.c b/html.c index b482d10..7d748f9 100644 --- a/html.c +++ b/html.c @@ -2008,8 +2008,13 @@ xs_html *html_entry(snac *user, xs_dict *msg, int read_only, } if ((user == NULL || strcmp(actor, user->actor) != 0) - && !valid_status(actor_get(actor, NULL))) + && !valid_status(actor_get(actor, NULL))) { + + if (user) + enqueue_actor_refresh(user, actor, 0); + return NULL; + } /** html_entry top tag **/ xs_html *entry_top = xs_html_tag("div", NULL); -- cgit v1.2.3 From 23157768d1b94b0a4147b16e5a4272bbf2de1ad5 Mon Sep 17 00:00:00 2001 From: grunfink Date: Sat, 28 Jun 2025 22:46:38 +0200 Subject: Minor CSS tweak. --- doc/style.css | 1 + utils.c | 1 + 2 files changed, 2 insertions(+) diff --git a/doc/style.css b/doc/style.css index 027fc43..5289332 100644 --- a/doc/style.css +++ b/doc/style.css @@ -1,5 +1,6 @@ body { max-width: 48em; margin: auto; line-height: 1.5; padding: 0.8em; word-wrap: break-word; } pre { overflow-x: scroll; } +blockquote { font-style: italic; } .snac-embedded-video, img { max-width: 100% } .snac-origin { font-size: 85% } .snac-score { float: right; font-size: 85% } diff --git a/utils.c b/utils.c index f0307cc..8db20bd 100644 --- a/utils.c +++ b/utils.c @@ -45,6 +45,7 @@ static const char *default_srv_config = "{" static const char *default_css = "body { max-width: 48em; margin: auto; line-height: 1.5; padding: 0.8em; word-wrap: break-word; }\n" "pre { overflow-x: scroll; }\n" + "blockquote { font-style: italic; }\n" ".snac-embedded-video, img { max-width: 100% }\n" ".snac-origin { font-size: 85% }\n" ".snac-score { float: right; font-size: 85% }\n" -- cgit v1.2.3 From 446173af00edf66f7c3056b20b84b1aa87953d79 Mon Sep 17 00:00:00 2001 From: grunfink Date: Mon, 30 Jun 2025 09:52:32 +0200 Subject: Updated RELEASE_NOTES. --- RELEASE_NOTES.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/RELEASE_NOTES.md b/RELEASE_NOTES.md index 3a35a8d..b65991e 100644 --- a/RELEASE_NOTES.md +++ b/RELEASE_NOTES.md @@ -1,6 +1,6 @@ # Release Notes -## UNRELEASED +## 2.79 Added a server-wide Webmention hook; this way, if somebody out there (that supports Webmention) links to a user or post in this instance, a notification is sent (this is the complementary of what was implemented in version 2.76). -- cgit v1.2.3 From 9ecca1f145be9d99301ba857da9a20ac2d62a27f Mon Sep 17 00:00:00 2001 From: grunfink Date: Mon, 30 Jun 2025 09:53:02 +0200 Subject: Version 2.79 RELEASED. --- snac.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/snac.h b/snac.h index 42fce87..ae7b4b3 100644 --- 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.79-dev" +#define VERSION "2.79" #define USER_AGENT "snac/" VERSION -- cgit v1.2.3 From 42895f7be2c77b4dd391beecae0652de719d090c Mon Sep 17 00:00:00 2001 From: grunfink Date: Sat, 5 Jul 2025 19:11:05 +0200 Subject: mastoapi: fixed a bug in mastoapi_timeline(). --- mastoapi.c | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/mastoapi.c b/mastoapi.c index 0829a84..d4e4e08 100644 --- a/mastoapi.c +++ b/mastoapi.c @@ -1344,9 +1344,9 @@ xs_list *mastoapi_timeline(snac *user, const xs_dict *args, const char *index_fn if ((f = fopen(index_fn, "r")) == NULL) return out; - const char *max_id = xs_dict_get(args, "max_id"); - const char *since_id = xs_dict_get(args, "since_id"); - const char *min_id = xs_dict_get(args, "min_id"); /* unsupported old-to-new navigation */ + const char *o_max_id = xs_dict_get(args, "max_id"); + const char *o_since_id = xs_dict_get(args, "since_id"); + const char *o_min_id = xs_dict_get(args, "min_id"); /* unsupported old-to-new navigation */ const char *limit_s = xs_dict_get(args, "limit"); int (*iterator)(FILE *, char *); int initial_status = 0; @@ -1354,6 +1354,10 @@ xs_list *mastoapi_timeline(snac *user, const xs_dict *args, const char *index_fn int limit = 0; int cnt = 0; + xs *max_id = o_max_id ? xs_tolower_i(xs_dup(o_max_id)) : NULL; + xs *since_id = o_since_id ? xs_tolower_i(xs_dup(o_since_id)) : NULL; + xs *min_id = o_min_id ? xs_tolower_i(xs_dup(o_min_id)) : NULL; + if (!xs_is_null(limit_s)) limit = atoi(limit_s); -- cgit v1.2.3 From b204cf400e6b5ccdf0e87da0c9d18bca49ac7693 Mon Sep 17 00:00:00 2001 From: grunfink Date: Sat, 5 Jul 2025 19:14:14 +0200 Subject: Updated RELEASE_NOTES. --- RELEASE_NOTES.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/RELEASE_NOTES.md b/RELEASE_NOTES.md index b65991e..c44f9f3 100644 --- a/RELEASE_NOTES.md +++ b/RELEASE_NOTES.md @@ -1,5 +1,9 @@ # Release Notes +## UNRELEASED + +Mastodon API: fixed a bug (introduced in previous version in the "boosts disappear" fix) that made timelines shorter than it should. + ## 2.79 Added a server-wide Webmention hook; this way, if somebody out there (that supports Webmention) links to a user or post in this instance, a notification is sent (this is the complementary of what was implemented in version 2.76). -- cgit v1.2.3 From 8ff1c54c9ea40452b899cfde2d96252ad5fc7cf0 Mon Sep 17 00:00:00 2001 From: grunfink Date: Sat, 5 Jul 2025 19:16:59 +0200 Subject: Bumped version. --- snac.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/snac.h b/snac.h index ae7b4b3..ea92e4d 100644 --- 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.79" +#define VERSION "2.80-dev" #define USER_AGENT "snac/" VERSION -- cgit v1.2.3 From daeb5a1cc576d35ea54aaeaec0e65d11d278acef Mon Sep 17 00:00:00 2001 From: grunfink Date: Sat, 5 Jul 2025 19:21:31 +0200 Subject: mastoapi: fixed the fix. --- mastoapi.c | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/mastoapi.c b/mastoapi.c index d4e4e08..7fb995a 100644 --- a/mastoapi.c +++ b/mastoapi.c @@ -1381,7 +1381,7 @@ xs_list *mastoapi_timeline(snac *user, const xs_dict *args, const char *index_fn /* only return entries older that max_id */ if (max_id) { if (strcmp(md5, MID_TO_MD5(max_id)) == 0) { - max_id = NULL; + max_id = xs_free(max_id); if (ascending) break; } @@ -1394,7 +1394,7 @@ xs_list *mastoapi_timeline(snac *user, const xs_dict *args, const char *index_fn if (strcmp(md5, MID_TO_MD5(since_id)) == 0) { if (!ascending) break; - since_id = NULL; + since_id = xs_free(since_id); } if (ascending) continue; -- cgit v1.2.3 From 18f19e88bb1eda37c537af8b9b134b421101276c Mon Sep 17 00:00:00 2001 From: grunfink Date: Sat, 5 Jul 2025 19:27:09 +0200 Subject: Updated TODO. --- TODO.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/TODO.md b/TODO.md index 73615ac..4ed3692 100644 --- a/TODO.md +++ b/TODO.md @@ -14,6 +14,8 @@ Important: deleting a follower should do more that just delete the object, see h ## Wishlist +The local purge should generate `Delete` activities for local posts. + Add account reporting. Add a list option to hide member posts from the main timeline, see https://codeberg.org/grunfink/snac2/issues/383 -- cgit v1.2.3 From baab7a063084918e8960ce470b7add19224b550c Mon Sep 17 00:00:00 2001 From: grunfink Date: Sun, 6 Jul 2025 05:59:51 +0200 Subject: Updated RELEASE_NOTES. --- RELEASE_NOTES.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/RELEASE_NOTES.md b/RELEASE_NOTES.md index c44f9f3..ece81ca 100644 --- a/RELEASE_NOTES.md +++ b/RELEASE_NOTES.md @@ -2,7 +2,7 @@ ## UNRELEASED -Mastodon API: fixed a bug (introduced in previous version in the "boosts disappear" fix) that made timelines shorter than it should. +Mastodon API: fixed a regression (introduced in previous version in the "boosts disappear in Tusky" fix) that interrupted timelines. ## 2.79 -- cgit v1.2.3 From 278a4593f23443af708373b2b3305f7ad0946bef Mon Sep 17 00:00:00 2001 From: grunfink Date: Mon, 7 Jul 2025 09:42:27 +0200 Subject: Updated RELEASE_NOTES. --- RELEASE_NOTES.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/RELEASE_NOTES.md b/RELEASE_NOTES.md index ece81ca..80dfa29 100644 --- a/RELEASE_NOTES.md +++ b/RELEASE_NOTES.md @@ -1,6 +1,6 @@ # Release Notes -## UNRELEASED +## 2.80 Mastodon API: fixed a regression (introduced in previous version in the "boosts disappear in Tusky" fix) that interrupted timelines. -- cgit v1.2.3 From ed3428d9533a6076553d57d5a119cb9212641d46 Mon Sep 17 00:00:00 2001 From: grunfink Date: Mon, 7 Jul 2025 09:42:51 +0200 Subject: Version 2.80 RELEASED. --- snac.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/snac.h b/snac.h index ea92e4d..cc66686 100644 --- 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.80-dev" +#define VERSION "2.80" #define USER_AGENT "snac/" VERSION -- cgit v1.2.3 From 1417ffd9dc6aab00898d1ce346742ec4250e657a Mon Sep 17 00:00:00 2001 From: grunfink Date: Tue, 8 Jul 2025 17:20:51 +0200 Subject: Minor webfinger tweak. Some implementations (at least, Activitypub for Wordpress) return valid webfinger queries also for posts; this confused poor simple snac. This change only accepts as valid actor requests those queries that have a 'subject' field that start with 'acct:'. If this has a side effect, remains to be seen. --- webfinger.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/webfinger.c b/webfinger.c index 5db9a97..dc0855a 100644 --- a/webfinger.c +++ b/webfinger.c @@ -93,7 +93,7 @@ int webfinger_request_signed(snac *snac, const char *qs, xs_str **actor, xs_str if (user != NULL) { const char *subject = xs_dict_get(obj, "subject"); - if (subject) + if (subject && xs_startswith(subject, "acct:")) *user = xs_replace_n(subject, "acct:", "", 1); } -- cgit v1.2.3 From 08f03cde6b469428494d3870ac54f740fd4641c4 Mon Sep 17 00:00:00 2001 From: grunfink Date: Tue, 8 Jul 2025 17:30:45 +0200 Subject: Added some const here and there. --- snac.h | 2 +- webfinger.c | 2 +- xs_fcgi.h | 4 ++-- xs_httpd.h | 12 ++++++------ xs_version.h | 2 +- 5 files changed, 11 insertions(+), 11 deletions(-) diff --git a/snac.h b/snac.h index cc66686..b50f345 100644 --- a/snac.h +++ b/snac.h @@ -325,7 +325,7 @@ void httpd(void); int webfinger_request_signed(snac *snac, const char *qs, xs_str **actor, xs_str **user); int webfinger_request(const char *qs, xs_str **actor, xs_str **user); int webfinger_request_fake(const char *qs, xs_str **actor, xs_str **user); -int webfinger_get_handler(xs_dict *req, const char *q_path, +int webfinger_get_handler(const xs_dict *req, const char *q_path, xs_val **body, int *b_size, char **ctype); const char *default_avatar_base64(void); diff --git a/webfinger.c b/webfinger.c index dc0855a..46b7edb 100644 --- a/webfinger.c +++ b/webfinger.c @@ -152,7 +152,7 @@ int webfinger_request_fake(const char *qs, xs_str **actor, xs_str **user) } -int webfinger_get_handler(xs_dict *req, const char *q_path, +int webfinger_get_handler(const xs_dict *req, const char *q_path, xs_val **body, int *b_size, char **ctype) /* serves webfinger queries */ { diff --git a/xs_fcgi.h b/xs_fcgi.h index 0b53dac..b3cb892 100644 --- a/xs_fcgi.h +++ b/xs_fcgi.h @@ -13,7 +13,7 @@ #define _XS_FCGI_H xs_dict *xs_fcgi_request(FILE *f, xs_str **payload, int *p_size, int *id); - void xs_fcgi_response(FILE *f, int status, xs_dict *headers, xs_str *body, int b_size, int id); + void xs_fcgi_response(FILE *f, int status, const xs_dict *headers, const xs_str *body, int b_size, int id); #ifdef XS_IMPLEMENTATION @@ -290,7 +290,7 @@ end: } -void xs_fcgi_response(FILE *f, int status, xs_dict *headers, xs_str *body, int b_size, int fcgi_id) +void xs_fcgi_response(FILE *f, int status, const xs_dict *headers, const xs_str *body, int b_size, int fcgi_id) /* writes an FCGI response */ { struct fcgi_record_header hdr = {0}; diff --git a/xs_httpd.h b/xs_httpd.h index 4cc8263..57759c4 100644 --- a/xs_httpd.h +++ b/xs_httpd.h @@ -5,7 +5,8 @@ #define _XS_HTTPD_H xs_dict *xs_httpd_request(FILE *f, xs_str **payload, int *p_size); -void xs_httpd_response(FILE *f, int status, const char *status_text, xs_dict *headers, xs_str *body, int b_size); +void xs_httpd_response(FILE *f, int status, const char *status_text, + const xs_dict *headers, const xs_val *body, int b_size); #ifdef XS_IMPLEMENTATION @@ -109,16 +110,15 @@ xs_dict *xs_httpd_request(FILE *f, xs_str **payload, int *p_size) } -void xs_httpd_response(FILE *f, int status, const char *status_text, xs_dict *headers, xs_str *body, int b_size) +void xs_httpd_response(FILE *f, int status, const char *status_text, + const xs_dict *headers, const xs_val *body, int b_size) /* sends an httpd response */ { - xs *proto; + fprintf(f, "HTTP/1.1 %d %s\r\n", status, status_text ? status_text : ""); + const xs_str *k; const xs_val *v; - proto = xs_fmt("HTTP/1.1 %d %s", status, status_text); - fprintf(f, "%s\r\n", proto); - xs_dict_foreach(headers, k, v) { fprintf(f, "%s: %s\r\n", k, v); } diff --git a/xs_version.h b/xs_version.h index 09b1bdc..466535b 100644 --- a/xs_version.h +++ b/xs_version.h @@ -1 +1 @@ -/* a32c0d513ae24ad28ffc5c6c2c1cde75bb758e09 2025-06-23T17:43:10+02:00 */ +/* 401d229ffbec89b4a5cf97793926b7afb84a4f26 2025-07-08T15:44:54+02:00 */ -- cgit v1.2.3 From 11e9a128594f93f3e05d53de543733d04feb5555 Mon Sep 17 00:00:00 2001 From: grunfink Date: Tue, 8 Jul 2025 17:49:48 +0200 Subject: Bumped version. --- snac.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/snac.h b/snac.h index b50f345..e4e190d 100644 --- 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.80" +#define VERSION "2.81-dev" #define USER_AGENT "snac/" VERSION -- cgit v1.2.3 From 7365a1aa4dbe87b3f3cf620d88448edf5fc750f2 Mon Sep 17 00:00:00 2001 From: grunfink Date: Thu, 10 Jul 2025 05:37:02 +0200 Subject: Updated documentation. --- doc/snac.8 | 2 ++ 1 file changed, 2 insertions(+) diff --git a/doc/snac.8 b/doc/snac.8 index 39081f2..89c00b4 100644 --- a/doc/snac.8 +++ b/doc/snac.8 @@ -207,6 +207,8 @@ fields are set (see below), they are also shown. The email address of the instance administrator (optional). .It Ic admin_account The user name of the instance administrator (optional). +.It Ic title +The name of the instance (optional). .It Ic short_description A textual short description about the instance (optional). .It Ic short_description_raw -- cgit v1.2.3 From 26502fa62ee05465e524987ab5b86078f2e64e6f Mon Sep 17 00:00:00 2001 From: grunfink Date: Thu, 10 Jul 2025 05:45:22 +0200 Subject: Added metadata to nodeinfo 2.0. --- httpd.c | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/httpd.c b/httpd.c index 8f2ef4d..d01eb9c 100644 --- a/httpd.c +++ b/httpd.c @@ -66,7 +66,9 @@ const char *nodeinfo_2_0_template = "" "\"services\":{\"outbound\":[],\"inbound\":[]}," "\"usage\":{\"users\":{\"total\":%d,\"activeMonth\":%d,\"activeHalfyear\":%d}," "\"localPosts\":%d}," - "\"openRegistrations\":false,\"metadata\":{}}"; + "\"openRegistrations\":false,\"metadata\":{" + "\"nodeDescription\":\"%s\",\"nodeName\":\"%s\"" + "}}"; xs_str *nodeinfo_2_0(void) /* builds a nodeinfo json object */ @@ -99,7 +101,10 @@ xs_str *nodeinfo_2_0(void) n_posts += index_len(pidxfn); } - return xs_fmt(nodeinfo_2_0_template, n_utotal, n_umonth, n_uhyear, n_posts); + const char *name = xs_dict_get_def(srv_config, "title", ""); + const char *desc = xs_dict_get_def(srv_config, "short_description", ""); + + return xs_fmt(nodeinfo_2_0_template, n_utotal, n_umonth, n_uhyear, n_posts, desc, name); } -- cgit v1.2.3 From 08556e2451ff82d49756c38a381c6d750d6ec5fe Mon Sep 17 00:00:00 2001 From: Wincent Balin Date: Mon, 14 Jul 2025 04:19:27 +0200 Subject: Added Ukrainian translation --- po/uk.po | 768 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 768 insertions(+) create mode 100644 po/uk.po diff --git a/po/uk.po b/po/uk.po new file mode 100644 index 0000000..fd59678 --- /dev/null +++ b/po/uk.po @@ -0,0 +1,768 @@ +# snac message translation file +# +msgid "" +msgstr "" +"Project-Id-Version: snac\n" +"Last-Translator: grunfink\n" +"Language: uk\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Plural-Forms: nplurals=3; plural=(n%10==1 && n%100!=11 ? 0 : n%10>=2 && " +"n%10<=4 && (n%100<12 || n%100>14) ? 1 : 2);\n" +"POT-Creation-Date: \n" +"PO-Revision-Date: \n" +"Language-Team: \n" +"MIME-Version: 1.0\n" +"Content-Transfer-Encoding: 8bit\n" +"X-Generator: Poedit 3.0\n" + +#: html.c:384 +msgid "Sensitive content: " +msgstr "Делікатний вміст: " + +#: html.c:392 +msgid "Sensitive content description" +msgstr "Опис делікатного вмісту" + +#: html.c:405 +msgid "Only for mentioned people: " +msgstr "Тільки для зазначених осіб: " + +#: html.c:428 +msgid "Reply to (URL): " +msgstr "Відповісти до (URL): " + +#: html.c:437 +msgid "Don't send, but store as a draft" +msgstr "Не надсилати, але зберігти як чернетку" + +#: html.c:438 +msgid "Draft:" +msgstr "Чернетка:" + +#: html.c:494 +msgid "Attachments..." +msgstr "Вкладення..." + +#: html.c:517 +msgid "File:" +msgstr "Файл:" + +#: html.c:521 +msgid "Clear this field to delete the attachment" +msgstr "Очистіть це поле, щоб видалити вкладення" + +#: html.c:530 html.c:555 +msgid "Attachment description" +msgstr "Опис вкладення" + +#: html.c:566 +msgid "Poll..." +msgstr "Опитування..." + +#: html.c:568 +msgid "Poll options (one per line, up to 8):" +msgstr "Варіанти відповідей (по одному в рядку, до 8):" + +#: html.c:580 +msgid "One choice" +msgstr "Єдиний вибір" + +#: html.c:583 +msgid "Multiple choices" +msgstr "Декілька варіантів вибору" + +#: html.c:589 +msgid "End in 5 minutes" +msgstr "Закінчення через 5 хвилин" + +#: html.c:593 +msgid "End in 1 hour" +msgstr "Закінчення через 1 годину" + +#: html.c:596 +msgid "End in 1 day" +msgstr "Закінчення через 1 день" + +#: html.c:604 +msgid "Post" +msgstr "Надіслати" + +#: html.c:701 html.c:708 +msgid "Site description" +msgstr "Опис сайту" + +#: html.c:719 +msgid "Admin email" +msgstr "Пошта админа" + +#: html.c:732 +msgid "Admin account" +msgstr "Обліковий запис адміна" + +#: html.c:800 html.c:1136 +#, c-format +msgid "%d following, %d followers" +msgstr "%d підписок, %d підписників" + +#: html.c:890 +msgid "RSS" +msgstr "RSS" + +#: html.c:895 html.c:923 +msgid "private" +msgstr "особисте" + +#: html.c:919 +msgid "public" +msgstr "публічне" + +#: html.c:927 +msgid "notifications" +msgstr "повідомлення" + +#: html.c:932 +msgid "people" +msgstr "люди" + +#: html.c:936 +msgid "instance" +msgstr "сервер" + +#: html.c:945 +msgid "" +"Search posts by URL or content (regular expression), @user@host accounts, or " +"#tag" +msgstr "" +"Шукати дописи за URL або вмістом (регулярний вираз), акаунтами @user@host " +"або #тегом" + +#: html.c:946 +msgid "Content search" +msgstr "Пошук за вмістом" + +#: html.c:1068 +msgid "verified link" +msgstr "перевірене посилання" + +#: html.c:1125 html.c:2540 html.c:2553 html.c:2562 +msgid "Location: " +msgstr "Місце знаходження: " + +#: html.c:1161 +msgid "New Post..." +msgstr "Новий допис..." + +#: html.c:1163 +msgid "What's on your mind?" +msgstr "Що у вас на думці?" + +#: html.c:1172 +msgid "Operations..." +msgstr "Дії..." + +#: html.c:1187 html.c:1788 html.c:3193 html.c:4578 +msgid "Follow" +msgstr "Підписатися" + +#: html.c:1189 +msgid "(by URL or user@host)" +msgstr "(за URL або user@host)" + +#: html.c:1204 html.c:1764 html.c:4527 +msgid "Boost" +msgstr "Просувати" + +#: html.c:1206 html.c:1223 +msgid "(by URL)" +msgstr "(за URL)" + +#: html.c:1221 html.c:1743 html.c:4518 +msgid "Like" +msgstr "Вподобайка" + +#: html.c:1347 +msgid "User Settings..." +msgstr "Налаштування користувача..." + +#: html.c:1356 +msgid "Display name:" +msgstr "Видиме ім'я:" + +#: html.c:1362 +msgid "Your name" +msgstr "Ваше ім'я" + +#: html.c:1364 +msgid "Avatar: " +msgstr "Аватар: " + +#: html.c:1372 +msgid "Delete current avatar" +msgstr "Видалити поточний аватар" + +#: html.c:1374 +msgid "Header image (banner): " +msgstr "Зображення заголовка (банер): " + +#: html.c:1382 +msgid "Delete current header image" +msgstr "Видалити поточне зображення заголовка" + +#: html.c:1384 +msgid "Bio:" +msgstr "Про себе:" + +#: html.c:1390 +msgid "Write about yourself here..." +msgstr "Напишіть про себе тут..." + +#: html.c:1399 +msgid "Always show sensitive content" +msgstr "Завжди показувати делікатний вміст" + +#: html.c:1401 +msgid "Email address for notifications:" +msgstr "Поштова адреса для повідомлень:" + +#: html.c:1409 +msgid "Telegram notifications (bot key and chat id):" +msgstr "Повідомлення в Telegram (ключ бота та id чату):" + +#: html.c:1423 +msgid "ntfy notifications (ntfy server and token):" +msgstr "Повідомлення в ntfy (сервер та токен ntfy):" + +#: html.c:1437 +msgid "Maximum days to keep posts (0: server settings):" +msgstr "Максимальний час для зберігання дописів (0: налаштування сервера):" + +#: html.c:1451 +msgid "Drop direct messages from people you don't follow" +msgstr "Відхиляти особисті повідомлення від незнайомців" + +#: html.c:1460 +msgid "This account is a bot" +msgstr "Цей акаунт є ботом" + +#: html.c:1469 +msgid "Auto-boost all mentions to this account" +msgstr "Автоматично просувати всі згадки цього акаунта" + +#: html.c:1478 +msgid "This account is private (posts are not shown through the web)" +msgstr "Це закритий акаунт (дописи не показуються в мережі)" + +#: html.c:1488 +msgid "Collapse top threads by default" +msgstr "Згорнути потоки за замовчуванням" + +#: html.c:1497 +msgid "Follow requests must be approved" +msgstr "Запити на підписку мають бути схвалені" + +#: html.c:1506 +msgid "Publish follower and following metrics" +msgstr "Публікувати метрики підписок та підписників" + +#: html.c:1508 +msgid "Current location:" +msgstr "Поточне місцезнаходження:" + +#: html.c:1522 +msgid "Profile metadata (key=value pairs in each line):" +msgstr "Метадані профілю (пари ключ=значення в кожному рядку):" + +#: html.c:1533 +msgid "Web interface language:" +msgstr "Мова інтерфейсу:" + +#: html.c:1543 +msgid "New password:" +msgstr "Новий пароль:" + +#: html.c:1550 +msgid "Repeat new password:" +msgstr "Повторити новий пароль:" + +#: html.c:1560 +msgid "Update user info" +msgstr "Оновлення даних користувача" + +#: html.c:1571 +msgid "Followed hashtags..." +msgstr "Підписані хештеги..." + +#: html.c:1573 html.c:1605 +msgid "One hashtag per line" +msgstr "Один хештег на рядок" + +#: html.c:1594 html.c:1626 +msgid "Update hashtags" +msgstr "Оновити хештеги" + +#: html.c:1743 +msgid "Say you like this post" +msgstr "Позначте допис як вподобаний" + +#: html.c:1748 html.c:4536 +msgid "Unlike" +msgstr "Більше не подобається" + +#: html.c:1748 +msgid "Nah don't like it that much" +msgstr "Мені це не дуже подобається" + +#: html.c:1754 html.c:4673 +msgid "Unpin" +msgstr "Відкріпити" + +#: html.c:1754 +msgid "Unpin this post from your timeline" +msgstr "Відкріпити цей допис зі власної стрічки" + +#: html.c:1757 html.c:4668 +msgid "Pin" +msgstr "Закріпити" + +#: html.c:1757 +msgid "Pin this post to the top of your timeline" +msgstr "Закріпити цей допис до власної стрічки" + +#: html.c:1764 +msgid "Announce this post to your followers" +msgstr "Поділитися цим дописом зі своїми підписниками" + +#: html.c:1769 html.c:4544 +msgid "Unboost" +msgstr "Скасувати просування" + +#: html.c:1769 +msgid "I regret I boosted this" +msgstr "Я шкодую, що просував це" + +#: html.c:1775 html.c:4683 +msgid "Unbookmark" +msgstr "Видалити з закладок" + +#: html.c:1775 +msgid "Delete this post from your bookmarks" +msgstr "Видалити цей допис з закладок" + +#: html.c:1778 html.c:4678 +msgid "Bookmark" +msgstr "Додати в закладки" + +#: html.c:1778 +msgid "Add this post to your bookmarks" +msgstr "Додайте цей допис в закладки" + +#: html.c:1784 html.c:3179 html.c:3367 html.c:4591 +msgid "Unfollow" +msgstr "Відписатися" + +#: html.c:1784 html.c:3180 +msgid "Stop following this user's activity" +msgstr "Відписатися від цього користувача" + +#: html.c:1788 html.c:3194 +msgid "Start following this user's activity" +msgstr "Підписатися до цього користувача" + +#: html.c:1794 html.c:4621 +msgid "Unfollow Group" +msgstr "Відписатися від групи" + +#: html.c:1795 +msgid "Stop following this group or channel" +msgstr "Відписатися від групи чи канала" + +#: html.c:1799 html.c:4608 +msgid "Follow Group" +msgstr "Підписатися на групу" + +#: html.c:1800 +msgid "Start following this group or channel" +msgstr "Підписатися на групу чи канал" + +#: html.c:1805 html.c:3216 html.c:4552 +msgid "MUTE" +msgstr "Заглушити" + +#: html.c:1806 +msgid "Block any activity from this user forever" +msgstr "Назавжди заблокувати активність цього користувача" + +#: html.c:1811 html.c:3198 html.c:4638 +msgid "Delete" +msgstr "Видалити" + +#: html.c:1811 +msgid "Delete this post" +msgstr "Видалити цей допис" + +#: html.c:1814 html.c:4560 +msgid "Hide" +msgstr "Приховати" + +#: html.c:1814 +msgid "Hide this post and its children" +msgstr "Приховати цей допис разом з обговоренням" + +#: html.c:1845 +msgid "Edit..." +msgstr "Редагувати..." + +#: html.c:1865 +msgid "Reply..." +msgstr "Відповісти..." + +#: html.c:1916 +msgid "Truncated (too deep)" +msgstr "Обрізано (занадто багато)" + +#: html.c:1925 +msgid "follows you" +msgstr "підписан на вас" + +#: html.c:1988 +msgid "Pinned" +msgstr "Закріплено" + +#: html.c:1996 +msgid "Bookmarked" +msgstr "Додано до закладок" + +#: html.c:2004 +msgid "Poll" +msgstr "Опитування" + +#: html.c:2011 +msgid "Voted" +msgstr "Проголосовано" + +#: html.c:2020 +msgid "Event" +msgstr "Подія" + +#: html.c:2052 html.c:2081 +msgid "boosted" +msgstr "просунуто" + +#: html.c:2097 +msgid "in reply to" +msgstr "у відповідь на" + +#: html.c:2148 +msgid " [SENSITIVE CONTENT]" +msgstr " [ДЕЛІКАТНИЙ ВМІСТ]" + +#: html.c:2325 +msgid "Vote" +msgstr "Голос" + +#: html.c:2335 +msgid "Closed" +msgstr "Завершено" + +#: html.c:2360 +msgid "Closes in" +msgstr "Завершується через" + +#: html.c:2441 +msgid "Video" +msgstr "Відео" + +#: html.c:2456 +msgid "Audio" +msgstr "Аудіо" + +#: html.c:2484 +msgid "Attachment" +msgstr "Вкладення" + +#: html.c:2498 +msgid "Alt..." +msgstr "Опис..." + +#: html.c:2511 +msgid "Source channel or community" +msgstr "Вихідний канал або спільнота" + +#: html.c:2605 +msgid "Time: " +msgstr "Час: " + +#: html.c:2686 +msgid "Older..." +msgstr "Раніше..." + +#: html.c:2788 +msgid "about this site" +msgstr "про цей сайт" + +#: html.c:2790 +msgid "powered by " +msgstr "на базі " + +#: html.c:2855 +msgid "Dismiss" +msgstr "Відхилити" + +#: html.c:2872 +#, c-format +msgid "Timeline for list '%s'" +msgstr "Стрічки для списку '%s'" + +#: html.c:2891 html.c:3944 +msgid "Pinned posts" +msgstr "Закріплені дописи" + +#: html.c:2903 html.c:3959 +msgid "Bookmarked posts" +msgstr "Дописи у закладках" + +#: html.c:2915 html.c:3974 +msgid "Post drafts" +msgstr "Чернетки дописів" + +#: html.c:2986 +msgid "No more unseen posts" +msgstr "Усе переглянуто" + +#: html.c:2990 html.c:3090 +msgid "Back to top" +msgstr "Повернутися до початку" + +#: html.c:3043 +msgid "History" +msgstr "Історія" + +#: html.c:3095 html.c:3515 +msgid "More..." +msgstr "Більше..." + +#: html.c:3184 html.c:4574 +msgid "Unlimit" +msgstr "Без обмеження" + +#: html.c:3185 +msgid "Allow announces (boosts) from this user" +msgstr "Дозволити просування від цього користувача" + +#: html.c:3188 html.c:4570 +msgid "Limit" +msgstr "Обмежити" + +#: html.c:3189 +msgid "Block announces (boosts) from this user" +msgstr "Заборонити просування від цього користувача" + +#: html.c:3198 +msgid "Delete this user" +msgstr "Видалити користувача" + +#: html.c:3203 html.c:4688 +msgid "Approve" +msgstr "Підтвердити" + +#: html.c:3204 +msgid "Approve this follow request" +msgstr "Підтвердити цей запит на підписку" + +#: html.c:3207 html.c:4712 +msgid "Discard" +msgstr "Відхилити" + +#: html.c:3207 +msgid "Discard this follow request" +msgstr "Відхилити цей запит на підписку" + +#: html.c:3212 html.c:4556 +msgid "Unmute" +msgstr "Скасувати глушіння" + +#: html.c:3213 +msgid "Stop blocking activities from this user" +msgstr "Припинити глушіння дій цього користувача" + +#: html.c:3217 +msgid "Block any activity from this user" +msgstr "Заглушити всі дії цього користувача" + +#: html.c:3225 +msgid "Direct Message..." +msgstr "Особисте повідомлення..." + +#: html.c:3260 +msgid "Pending follow confirmations" +msgstr "Запити на підписку очікують на розгляд" + +#: html.c:3264 +msgid "People you follow" +msgstr "Ваші підписки" + +#: html.c:3265 +msgid "People that follow you" +msgstr "Ваші підписники" + +#: html.c:3304 +msgid "Clear all" +msgstr "Очистити все" + +#: html.c:3361 +msgid "Mention" +msgstr "Згадка" + +#: html.c:3364 +msgid "Finished poll" +msgstr "Завершене опитування" + +#: html.c:3379 +msgid "Follow Request" +msgstr "Запит на підписку" + +#: html.c:3462 +msgid "Context" +msgstr "Контекст" + +#: html.c:3473 +msgid "New" +msgstr "Нове" + +#: html.c:3488 +msgid "Already seen" +msgstr "Вже переглянуто" + +#: html.c:3503 +msgid "None" +msgstr "Нема" + +#: html.c:3769 +#, c-format +msgid "Search results for account %s" +msgstr "Результати пошуку для облікового запису %s" + +#: html.c:3776 +#, c-format +msgid "Account %s not found" +msgstr "Обліковий запис %s не знайдений" + +#: html.c:3807 +#, c-format +msgid "Search results for tag %s" +msgstr "Результати пошуку тега %s" + +#: html.c:3807 +#, c-format +msgid "Nothing found for tag %s" +msgstr "Нічого не знайдено за тегом %s" + +#: html.c:3823 +#, c-format +msgid "Search results for '%s' (may be more)" +msgstr "Результати пошуку для '%s' (можливо є більше)" + +#: html.c:3826 +#, c-format +msgid "Search results for '%s'" +msgstr "Результати пошуку для '%s'" + +#: html.c:3829 +#, c-format +msgid "No more matches for '%s'" +msgstr "Немає більше збігів для '%s'" + +#: html.c:3831 +#, c-format +msgid "Nothing found for '%s'" +msgstr "Нічого не знайдено для '%s'" + +#: html.c:3929 +msgid "Showing instance timeline" +msgstr "Показуємо стрічку сервера" + +#: html.c:4012 +#, c-format +msgid "Showing timeline for list '%s'" +msgstr "Показуємо стрічку для списку '%s'" + +#: httpd.c:250 +#, c-format +msgid "Search results for tag #%s" +msgstr "Результати пошуку для тега #%s" + +#: httpd.c:259 +msgid "Recent posts by users in this instance" +msgstr "Останні повідомлення на цьому сервері" + +#: html.c:1603 +msgid "Blocked hashtags..." +msgstr "Заблоковані теги..." + +#: html.c:432 +msgid "Optional URL to reply to" +msgstr "Необов'язковий URL для відповіді" + +#: html.c:575 +msgid "" +"Option 1...\n" +"Option 2...\n" +"Option 3...\n" +"..." +msgstr "" +"Варіант 1...\n" +"Варіант 2...\n" +"Варіант 3...\n" +"..." + +#: html.c:1415 +msgid "Bot API key" +msgstr "Ключ API для бота" + +#: html.c:1421 +msgid "Chat id" +msgstr "Id чату" + +#: html.c:1429 +msgid "ntfy server - full URL (example: https://ntfy.sh/YourTopic)" +msgstr "повна URL сервера ntfy (наприклад https://ntfy.sh/YourTopic)" + +#: html.c:1435 +msgid "ntfy token - if needed" +msgstr "токен ntfy - якщо потрібно" + +#: html.c:2892 +msgid "pinned" +msgstr "закріплено" + +#: html.c:2904 +msgid "bookmarks" +msgstr "закладки" + +#: html.c:2916 +msgid "drafts" +msgstr "чернетки" + +#: html.c:464 +msgid "Scheduled post..." +msgstr "Запланувати..." + +msgid "Post date and time:" +msgstr "Час допису:" + +#: html.c:2927 html.c:3989 +msgid "Scheduled posts" +msgstr "Заплановані дописи" + +#: html.c:2928 +msgid "scheduled posts" +msgstr "заплановані дописи" + +#: html.c:458 +#, c-format +msgid "Post date and time (timezone: %s):" +msgstr "Дата та час допису (часовий пояс: %s):" + +#: html.c:1538 +msgid "Time zone:" +msgstr "Часовий пояс:" -- cgit v1.2.3 From a2518f5d70b19f9cafd860bd6cde22399c452b60 Mon Sep 17 00:00:00 2001 From: grunfink Date: Mon, 14 Jul 2025 15:34:08 +0200 Subject: Updated RELEASE_NOTES. --- RELEASE_NOTES.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/RELEASE_NOTES.md b/RELEASE_NOTES.md index 80dfa29..b932cc2 100644 --- a/RELEASE_NOTES.md +++ b/RELEASE_NOTES.md @@ -1,5 +1,11 @@ # Release Notes +## UNRELEASED + +Included a small tweak to avoid being confused by implementations that return valid webfinger queries for non-account URLs (like i.e. the Wordpress ActivityPub plugin in some configurations). + +Added Ukrainian translation (contributed by wincentbalin). + ## 2.80 Mastodon API: fixed a regression (introduced in previous version in the "boosts disappear in Tusky" fix) that interrupted timelines. -- cgit v1.2.3