From 86de22fe92560db8d0661a6f4ee9be07a84043a3 Mon Sep 17 00:00:00 2001 From: grunfink Date: Sat, 16 Aug 2025 16:42:50 +0200 Subject: New function collect_replies(). --- activitypub.c | 88 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ main.c | 6 ++++ snac.h | 2 ++ 3 files changed, 96 insertions(+) diff --git a/activitypub.c b/activitypub.c index 634e243..a375584 100644 --- a/activitypub.c +++ b/activitypub.c @@ -936,6 +936,94 @@ xs_str *process_tags(snac *snac, const char *content, xs_list **tag) } +void collect_replies(snac *user, const char *id) +/* collects all replies for a post */ +{ + xs *obj = NULL; + + if (!valid_status(object_get(id, &obj))) { + snac_debug(user, 1, xs_fmt("collect_replies: object '%s' is not here", id)); + return; + } + + const char *next = xs_dict_get_path(obj, "replies.first.next"); + if (!xs_is_string(next)) { + snac_debug(user, 1, xs_fmt("collect_replies: object '%s' does not have a replies.first.next URL", id)); + return; + } + + /* pick the first level replies (may be empty) */ + const xs_list *level0_replies = xs_dict_get_path(obj, "replies.first.items"); + + xs *reply_obj = NULL; + + if (!valid_status(object_get(next, &reply_obj))) { + if (!valid_status(activitypub_request(user, next, &reply_obj))) { + snac_debug(user, 1, xs_fmt("collect_replies: cannot get replies object '%s'", next)); + return; + } + } + + const xs_list *level1_replies = xs_dict_get(reply_obj, "items"); + if (!xs_is_list(level1_replies)) { + snac_debug(user, 1, xs_fmt("collect_replies: cannot get reply items from object '%s'", next)); + return; + } + + xs *items = NULL; + + if (xs_is_list(level0_replies)) + items = xs_list_cat(xs_dup(level0_replies), level1_replies); + else + items = xs_dup(level1_replies); + + const xs_val *v; + + xs_list_foreach(items, v) { + xs *reply = NULL; + + if (xs_is_string(v)) { + /* request the object */ + if (!valid_status(object_get(v, &reply))) { + if (!valid_status(activitypub_request(user, v, &reply))) { + snac_debug(user, 1, xs_fmt("collect_replies: error requesting object '%s'", v)); + continue; + } + } + } + else + if (xs_is_dict(v)) { + /* actually the post object */ + reply = xs_dup(v); + } + + if (!xs_is_dict(reply)) + continue; + + const char *id = xs_dict_get(reply, "id"); + const char *type = xs_dict_get(reply, "type"); + const char *attr_to = get_atto(reply); + + 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("collect_replies: item already in timeline %s", id)); + continue; + } + + enqueue_actor_refresh(user, attr_to, 0); + + timeline_add(user, id, reply); + + snac_log(user, xs_fmt("new '%s' (collect_replies) %s %s", type, attr_to, id)); + } +} + + void notify(snac *snac, const char *type, const char *utype, const char *actor, const xs_dict *msg) /* notifies the user of relevant events */ { diff --git a/main.c b/main.c index c4c298f..1e27302 100644 --- a/main.c +++ b/main.c @@ -730,6 +730,12 @@ int main(int argc, char *argv[]) return 0; } + if (strcmp(cmd, "collect_replies") == 0) { /** **/ + collect_replies(&snac, url); + + return 0; + } + if (strcmp(cmd, "insert") == 0) { /** **/ int status; xs *data = NULL; diff --git a/snac.h b/snac.h index c060363..82c4a98 100644 --- a/snac.h +++ b/snac.h @@ -334,6 +334,8 @@ const char *default_avatar_base64(void); xs_str *process_tags(snac *snac, const char *content, xs_list **tag); +void collect_replies(snac *user, const char *id); + const char *get_atto(const xs_dict *msg); const char *get_in_reply_to(const xs_dict *msg); xs_list *get_attachments(const xs_dict *msg); -- cgit v1.2.3 From 56a19ab87a2e308e2d4f0d123f6522b5cfbb0c22 Mon Sep 17 00:00:00 2001 From: grunfink Date: Mon, 18 Aug 2025 18:26:27 +0200 Subject: Updated documentation. --- doc/snac.1 | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/doc/snac.1 b/doc/snac.1 index fd43338..134ee6e 100644 --- a/doc/snac.1 +++ b/doc/snac.1 @@ -71,6 +71,10 @@ standalone one. 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 Language +A dropdown to select which language your post will be written +in. This control only appears after you configure the set of +languages you expect to use in the User Settings (see below). .It Scheduled post... This dropdown menu allows setting a date and time for the post publication. @@ -188,6 +192,9 @@ can be selected here. .It Time zone The time zone the user is on (default: UTC). Only used for scheduled posts. +.It Languages you usually post in +Fill this string with the ISO 639 Language Codes you expect to +write your posts in, space-separated (example: en fr pt_BR). .It Password Write the same string in these two fields to change your password. Don't write anything if you don't want to do this. -- cgit v1.2.3 From 1b8206fe25cd4737b3b497d9af7e729043a75c76 Mon Sep 17 00:00:00 2001 From: grunfink Date: Mon, 18 Aug 2025 19:29:00 +0200 Subject: Fixed typo. --- main.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/main.c b/main.c index 1e27302..0a78489 100644 --- a/main.c +++ b/main.c @@ -61,7 +61,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" + "export_posts {basedir} {uid} 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" -- cgit v1.2.3 From ff52f779b7d47da0274287c01a4cdd19e952c73e Mon Sep 17 00:00:00 2001 From: grunfink Date: Mon, 18 Aug 2025 19:54:53 +0200 Subject: New function enqueue_collect_replies(). --- activitypub.c | 6 ++++++ data.c | 13 +++++++++++++ main.c | 2 +- snac.h | 1 + 4 files changed, 21 insertions(+), 1 deletion(-) diff --git a/activitypub.c b/activitypub.c index a375584..065fbcd 100644 --- a/activitypub.c +++ b/activitypub.c @@ -2914,6 +2914,12 @@ void process_user_queue_item(snac *user, xs_dict *q_item) } } } + else + if (strcmp(type, "collect_replies") == 0) { + const char *post = xs_dict_get(q_item, "message"); + + collect_replies(user, post); + } else snac_log(user, xs_fmt("unexpected user q_item type '%s'", type)); } diff --git a/data.c b/data.c index 70bd031..ca5e3ed 100644 --- a/data.c +++ b/data.c @@ -3576,6 +3576,19 @@ void enqueue_notify_webhook(snac *user, const xs_dict *noti, int retries) } +void enqueue_collect_replies(snac *user, const char *post) +/* enqueues a collect replies request */ +{ + xs *qmsg = _new_qmsg("collect_replies", post, 0); + 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("enqueue_collect_replies %s", post)); +} + + int was_question_voted(snac *user, const char *id) /* returns true if the user voted in this poll */ { diff --git a/main.c b/main.c index 0a78489..e53bdf8 100644 --- a/main.c +++ b/main.c @@ -731,7 +731,7 @@ int main(int argc, char *argv[]) } if (strcmp(cmd, "collect_replies") == 0) { /** **/ - collect_replies(&snac, url); + enqueue_collect_replies(&snac, url); return 0; } diff --git a/snac.h b/snac.h index 82c4a98..363855f 100644 --- a/snac.h +++ b/snac.h @@ -297,6 +297,7 @@ 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); +void enqueue_collect_replies(snac *user, const char *post); int was_question_voted(snac *user, const char *id); -- cgit v1.2.3 From e963924759225b1f03f2d38e80ce6f97af03c07b Mon Sep 17 00:00:00 2001 From: Xavier Vello Date: Wed, 20 Aug 2025 23:29:38 +0200 Subject: Fix CLI boosts not showing in public timeline --- main.c | 1 + 1 file changed, 1 insertion(+) diff --git a/main.c b/main.c index c4c298f..cb449cb 100644 --- a/main.c +++ b/main.c @@ -494,6 +494,7 @@ int main(int argc, char *argv[]) if (msg != NULL) { enqueue_message(&snac, msg); + timeline_admire(&snac, xs_dict_get(msg, "object"), snac.actor, 0); if (dbglevel) { xs_json_dump(msg, 4, stdout); -- cgit v1.2.3 From 06d063dea50a39b9009a000e94016d9805b3a8a3 Mon Sep 17 00:00:00 2001 From: grunfink Date: Thu, 21 Aug 2025 06:33:49 +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 f0089fa..13a8cec 100644 --- a/RELEASE_NOTES.md +++ b/RELEASE_NOTES.md @@ -12,6 +12,8 @@ Mastodon API: Fixed repeated entries in timelines. Added nodeinfo 2.1 support. +Fixed boosts from the command line not showing in the public timeline (contributed by xvello). + ## 2.81 If the `propagate_local_purge` configuration variable is set to `true` in `server.json`, purged local posts generate a `Delete` activity that is sent everywhere, instead of only deleted from the filesystem. -- cgit v1.2.3 From 3611f42fe202c6ec198c34dbc0a5939b8287583e Mon Sep 17 00:00:00 2001 From: grunfink Date: Fri, 22 Aug 2025 06:48:03 +0200 Subject: Added 'collect_replies' info in the about screen. --- main.c | 1 + 1 file changed, 1 insertion(+) diff --git a/main.c b/main.c index 7df4574..e01b6a2 100644 --- a/main.c +++ b/main.c @@ -39,6 +39,7 @@ int usage(const char *cmd) "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" + "collect_replies {basedir} {uid} {url} Collects all replies from a post\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" -- cgit v1.2.3