From a4a1990dbb16e3e4599500299c32c980c9cbd5bf Mon Sep 17 00:00:00 2001 From: byte Date: Sat, 25 Oct 2025 16:21:42 +0200 Subject: rendering visibility conditionally, with lesser reach if needed --- html.c | 72 +++++++++++++++++++++++++++++------------------------------------- 1 file changed, 32 insertions(+), 40 deletions(-) (limited to 'html.c') diff --git a/html.c b/html.c index 584059b..7c39237 100644 --- a/html.c +++ b/html.c @@ -398,6 +398,37 @@ xs_html *html_msg_icon(snac *user, const char *actor_id, const xs_dict *msg, return actor_icon; } +void html_note_render_visibility(snac* user, xs_html *form, const int scope) +{ + // scopes aren't sorted by value unfortunately, so simple math won't work to limit them. using map-like thing here + static const int public_scopes[5] = {SCOPE_PUBLIC, SCOPE_UNLISTED, SCOPE_FOLLOWERS, SCOPE_MENTIONED, -1}; + static const int unlisted_scopes[4] = {SCOPE_UNLISTED, SCOPE_FOLLOWERS, SCOPE_MENTIONED, -1}; + static const int followers_scopes[3] = {SCOPE_FOLLOWERS, SCOPE_MENTIONED, -1}; + static const int mentioned_scopes[2] = {SCOPE_MENTIONED, -1}; + static const int * const scopes[4] = { public_scopes, mentioned_scopes, unlisted_scopes, followers_scopes}; + static const char * const scopes_tags[4] = { "public", "mentioned", "unlisted", "followers"}; + static const char * const scopes_names[4] = { "Public", "Direct Message", "Unlisted", "Followers-only"}; + + xs_html *paragraph = xs_html_tag("p", xs_html_text(L("Visibility: "))); + const int* to_render = scopes[scope]; + for( int i = 0; to_render[i] != -1; i++ ){ + const int scope_i = to_render[i]; + const char* value = scopes_tags[scope_i]; + const char* name = scopes_names[scope_i]; + xs_html_add(paragraph, + xs_html_tag("label", + xs_html_sctag("input", + xs_html_attr("type", "radio"), + xs_html_attr("name", "visibility"), + xs_html_attr("value", value), + xs_html_attr(scope == scope_i ? "checked" : "", NULL)), + xs_html_text(" "), + xs_html_text(L(name)), + xs_html_text(" ")) + ); + } + xs_html_add(form, paragraph); +} xs_html *html_note(snac *user, const char *summary, const char *div_id, const char *form_id, @@ -455,46 +486,7 @@ xs_html *html_note(snac *user, const char *summary, xs_html_attr("type", "hidden"), xs_html_attr("name", "to"), xs_html_attr("value", actor_id))); - else { - xs_html_add(form, - xs_html_tag("p", - xs_html_text(L("Visibility: ")), - xs_html_tag("label", - xs_html_sctag("input", - xs_html_attr("type", "radio"), - xs_html_attr("name", "visibility"), - xs_html_attr("value", "public"), - xs_html_attr(scope == SCOPE_PUBLIC ? "checked" : "", NULL)), - xs_html_text(" "), - xs_html_text(L("Public"))), - xs_html_text(" "), - xs_html_tag("label", - xs_html_sctag("input", - xs_html_attr("type", "radio"), - xs_html_attr("name", "visibility"), - xs_html_attr("value", "unlisted"), - xs_html_attr(scope == SCOPE_UNLISTED ? "checked" : "", NULL)), - xs_html_text(" "), - xs_html_text(L("Unlisted"))), - xs_html_text(" "), - xs_html_tag("label", - xs_html_sctag("input", - xs_html_attr("type", "radio"), - xs_html_attr("name", "visibility"), - xs_html_attr("value", "followers"), - xs_html_attr(scope == SCOPE_FOLLOWERS ? "checked" : "", NULL)), - xs_html_text(" "), - xs_html_text(L("Followers-only"))), - xs_html_text(" "), - xs_html_tag("label", - xs_html_sctag("input", - xs_html_attr("type", "radio"), - xs_html_attr("name", "visibility"), - xs_html_attr("value", "mentioned"), - xs_html_attr(scope == SCOPE_MENTIONED ? "checked" : "", NULL)), - xs_html_text(" "), - xs_html_text(L("Direct Message"))))); - } + html_note_render_visibility(user, form, scope); if (redir) xs_html_add(form, -- cgit From daee7581dbf5b458f28e486fd89804b91209a9bb Mon Sep 17 00:00:00 2001 From: byte Date: Sat, 1 Nov 2025 01:18:28 +0100 Subject: no visibility for editing --- html.c | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) (limited to 'html.c') diff --git a/html.c b/html.c index a2dfb79..84636e9 100644 --- a/html.c +++ b/html.c @@ -486,7 +486,8 @@ xs_html *html_note(snac *user, const char *summary, xs_html_attr("type", "hidden"), xs_html_attr("name", "to"), xs_html_attr("value", actor_id))); - html_note_render_visibility(user, form, scope); + if (edit_id == NULL) + html_note_render_visibility(user, form, scope); if (redir) xs_html_add(form, -- cgit From fcae55fcb1542f6fa0a330c45e3b806f902a84de Mon Sep 17 00:00:00 2001 From: grunfink Date: Wed, 26 Nov 2025 17:21:16 +0100 Subject: Some attachments are of type 'Video' instead of a MIME type (bsky). --- html.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'html.c') diff --git a/html.c b/html.c index df5b508..12d269a 100644 --- a/html.c +++ b/html.c @@ -2634,7 +2634,7 @@ xs_html *html_entry(snac *user, xs_dict *msg, int read_only, xs_html_attr("title", name)))); } else - if (xs_startswith(type, "video/")) { + if (xs_startswith(type, "video/") || strcmp(type, "Video") == 0) { xs_html_add(content_attachments, xs_html_tag("video", xs_html_attr("preload", "none"), -- cgit From a45c1ce152011e8fe25eb1d25594ac5705f65404 Mon Sep 17 00:00:00 2001 From: rako Date: Fri, 28 Nov 2025 10:37:49 +0100 Subject: Fix user matching In order to be a proper prefix, the actor url must end with a '/' otherwise it can match another user that starts with the same prefix: for example 'testuser' will match anything made by 'testuser2' --- activitypub.c | 14 +++++++------- data.c | 8 ++++---- html.c | 6 +++--- mastoapi.c | 4 ++-- rss.c | 2 +- upgrade.c | 2 +- 6 files changed, 18 insertions(+), 18 deletions(-) (limited to 'html.c') diff --git a/activitypub.c b/activitypub.c index 2c0fa2e..90230d8 100644 --- a/activitypub.c +++ b/activitypub.c @@ -779,7 +779,7 @@ int is_msg_for_me(snac *snac, const xs_dict *c_msg) object_get(object, &obj); /* if it's about one of our posts, accept it */ - if (xs_startswith(object, snac->actor)) + if (is_msg_mine(snac, object)) return 2; /* blocked by hashtag? */ @@ -1242,7 +1242,7 @@ void notify(snac *snac, const char *type, const char *utype, const char *actor, if (xs_match(type, "Like|Announce|EmojiReact")) { /* if it's not an admiration about something by us, done */ - if (xs_is_null(objid) || !xs_startswith(objid, snac->actor)) + if (xs_is_null(objid) || !is_msg_mine(snac, objid)) return; /* if it's an announce by our own relay, done */ @@ -1267,7 +1267,7 @@ void notify(snac *snac, const char *type, const char *utype, const char *actor, return; /* if it's not ours and we didn't vote, discard */ - if (!xs_startswith(poll_id, snac->actor) && !was_question_voted(snac, poll_id)) + if (!is_msg_mine(snac, poll_id) && !was_question_voted(snac, poll_id)) return; } @@ -2792,10 +2792,10 @@ int process_input_message(snac *snac, const xs_dict *msg, const xs_dict *req) if (xs_is_null(object)) snac_log(snac, xs_fmt("malformed message: no 'id' field")); else - if (is_muted(snac, actor) && !xs_startswith(object, snac->actor)) + if (is_muted(snac, actor) && !is_msg_mine(snac, object)) snac_log(snac, xs_fmt("dropped 'Announce' from muted actor %s", actor)); else - if (is_limited(snac, actor) && !xs_startswith(object, snac->actor)) + if (is_limited(snac, actor) && !is_msg_mine(snac, object)) snac_log(snac, xs_fmt("dropped 'Announce' from limited actor %s", actor)); else { xs *a_msg = NULL; @@ -2903,7 +2903,7 @@ int process_input_message(snac *snac, const xs_dict *msg, const xs_dict *req) snac_log(snac, xs_fmt("malformed message: no 'id' field")); else if (object_here(object)) { - if (xs_startswith(object, srv_baseurl) && !xs_startswith(object, actor)) + if (xs_startswith(object, srv_baseurl) && !is_msg_mine(snac, object)) snac_log(snac, xs_fmt("ignored incorrect 'Delete' %s %s", actor, object)); else { timeline_del(snac, object); @@ -3716,7 +3716,7 @@ int activitypub_get_handler(const xs_dict *req, const char *q_path, const char *type = xs_dict_get(i, "type"); const char *id = xs_dict_get(i, "id"); - if (type && id && strcmp(type, "Note") == 0 && xs_startswith(id, snac.actor)) { + if (type && id && strcmp(type, "Note") == 0 && is_msg_mine(&snac, id)) { if (is_msg_public(i)) { xs *c_msg = msg_create(&snac, i); list = xs_list_append(list, c_msg); diff --git a/data.c b/data.c index c2fdccb..688d2e3 100644 --- a/data.c +++ b/data.c @@ -1467,7 +1467,7 @@ void timeline_update_indexes(snac *snac, const char *id) { object_user_cache_add(snac, id, "private"); - if (xs_startswith(id, snac->actor)) { + if (is_msg_mine(snac, id)) { xs *msg = NULL; if (valid_status(object_get(id, &msg))) { @@ -1927,7 +1927,7 @@ int pin(snac *user, const char *id) { int ret = -2; - if (xs_startswith(id, user->actor)) { + if (is_msg_mine(user, id)) { if (is_pinned(user, id)) ret = -3; else @@ -3527,7 +3527,7 @@ void enqueue_output(snac *snac, const xs_dict *msg, const xs_str *inbox, int retries, int p_status) /* enqueues an output message to an inbox */ { - if (xs_startswith(inbox, snac->actor)) { + if (is_msg_mine(snac, inbox)) { snac_debug(snac, 1, xs_str_new("refusing enqueue to myself")); return; } @@ -4055,7 +4055,7 @@ void delete_purged_posts(snac *user, int days) if (xs_is_dict(msg)) { const char *id = xs_dict_get(msg, "id"); - if (xs_is_string(id) && xs_startswith(id, user->actor)) { + if (xs_is_string(id) && is_msg_mine(user, id)) { xs *d_msg = msg_delete(user, id); enqueue_message(user, d_msg); diff --git a/html.c b/html.c index 12d269a..d6223b9 100644 --- a/html.c +++ b/html.c @@ -1898,7 +1898,7 @@ xs_html *html_entry_controls(snac *user, const char *actor, xs_html_attr("name", "redir"), xs_html_attr("value", redir)))); - if (!xs_startswith(id, user->actor)) { + if (!is_msg_mine(user, id)) { if (xs_list_in(likes, user->md5) == -1) { /* not already liked; add button */ xs_html_add(form, @@ -2426,7 +2426,7 @@ xs_html *html_entry(snac *user, xs_dict *msg, int read_only, if (read_only) closed = 1; /* non-identified page; show as closed */ else - if (user && xs_startswith(id, user->actor)) + if (user && is_msg_mine(user, id)) closed = 1; /* we questioned; closed for us */ else if (user && was_question_voted(user, id)) @@ -5022,7 +5022,7 @@ int html_post_handler(const xs_dict *req, const char *q_path, } else { /* delete an entry */ - if (xs_startswith(id, snac.actor) && !is_draft(&snac, id)) { + if (is_msg_mine(&snac, id) && !is_draft(&snac, id)) { /* it's a post by us: generate a delete */ xs *msg = msg_delete(&snac, id); diff --git a/mastoapi.c b/mastoapi.c index 94912f1..acb95a0 100644 --- a/mastoapi.c +++ b/mastoapi.c @@ -1919,7 +1919,7 @@ int mastoapi_get_handler(const xs_dict *req, const char *q_path, /* add only posts by the author */ if (!xs_is_null(msg_id) && strcmp(xs_dict_get(msg, "type"), "Note") == 0 && - xs_startswith(xs_dict_get(msg, "id"), snac2.actor) && is_msg_public(msg)) { + is_msg_mine(&snac2, xs_dict_get(msg, "id")) && is_msg_public(msg)) { /* if max_id is set, skip entries until we find it */ if (skip_until_max) { @@ -3824,7 +3824,7 @@ int mastoapi_delete_handler(const xs_dict *req, const char *q_path, if (valid_status(object_get_by_md5(p, &obj))) { const char *id = xs_dict_get(obj, "id"); - if (xs_is_string(id) && xs_startswith(id, snac.actor)) { + if (xs_is_string(id) && is_msg_mine(&snac, id)) { xs *out = mastoapi_status(&snac, obj); xs *msg = msg_delete(&snac, id); diff --git a/rss.c b/rss.c index 6e77205..6124e7a 100644 --- a/rss.c +++ b/rss.c @@ -59,7 +59,7 @@ xs_str *rss_from_timeline(snac *user, const xs_list *timeline, const char *content = xs_dict_get(msg, "content"); const char *published = xs_dict_get(msg, "published"); - if (user && !xs_startswith(id, user->actor)) + if (user && !is_msg_mine(user, id)) continue; if (!id || !content || !published) diff --git a/upgrade.c b/upgrade.c index 87ddfc8..9e0ae6e 100644 --- a/upgrade.c +++ b/upgrade.c @@ -213,7 +213,7 @@ int snac_upgrade(xs_str **error) object_add_ow(id, o); /* if it's from us, add to public */ - if (xs_startswith(id, snac.actor)) { + if (is_msg_mine(&snac, id)) { const xs_list *p; const char *v; int c; -- cgit From ee7483cf0dd007d104a815fce47a0d5f050ca870 Mon Sep 17 00:00:00 2001 From: grunfink Date: Tue, 9 Dec 2025 13:31:37 +0100 Subject: New query variable terse=anything. If set to any value, a public post page doesn't show any headline. --- html.c | 38 +++++++++++++++++++++++--------------- httpd.c | 4 ++-- snac.h | 2 +- 3 files changed, 26 insertions(+), 18 deletions(-) (limited to 'html.c') diff --git a/html.c b/html.c index 9ac57f8..32ed057 100644 --- a/html.c +++ b/html.c @@ -2996,7 +2996,7 @@ xs_html *html_footer(const snac *user) xs_str *html_timeline(snac *user, const xs_list *list, int read_only, int skip, int show, int show_more, const char *title, const char *page, - int utl, const char *error) + int utl, const char *error, int terse) /* returns the HTML for the timeline */ { xs_list *p = (xs_list *)list; @@ -3024,7 +3024,11 @@ xs_str *html_timeline(snac *user, const xs_list *list, int read_only, if (user) { head = html_user_head(user, desc, alternate); - body = html_user_body(user, read_only); + + if (terse) + body = xs_html_tag("body", NULL); + else + body = html_user_body(user, read_only); } else { head = html_instance_head(); @@ -3902,6 +3906,7 @@ int html_get_handler(const xs_dict *req, const char *q_path, int cache = 1; int save = 1; int proxy = 0; + int terse = 0; const char *v; const xs_dict *q_vars = xs_dict_get(req, "q_vars"); @@ -3914,6 +3919,9 @@ int html_get_handler(const xs_dict *req, const char *q_path, return HTTP_STATUS_NOT_FOUND; } + if (!xs_is_null(xs_dict_get(q_vars, "terse"))) + terse = 1; + if (strcmp(v, "share-bridge") == 0) { /* temporary redirect for a post */ const char *login = xs_dict_get(q_vars, "login"); @@ -4014,7 +4022,7 @@ int html_get_handler(const xs_dict *req, const char *q_path, if (xs_type(xs_dict_get(snac.config, "private")) == XSTYPE_TRUE) { /** empty public timeline for private users **/ - *body = html_timeline(&snac, NULL, 1, 0, 0, 0, NULL, "", 1, error); + *body = html_timeline(&snac, NULL, 1, 0, 0, 0, NULL, "", 1, error, terse); *b_size = strlen(*body); status = HTTP_STATUS_OK; } @@ -4037,7 +4045,7 @@ int html_get_handler(const xs_dict *req, const char *q_path, xs *pins = pinned_list(&snac); pins = xs_list_cat(pins, list); - *body = html_timeline(&snac, pins, 1, skip, show, more, NULL, "", 1, error); + *body = html_timeline(&snac, pins, 1, skip, show, more, NULL, "", 1, error, terse); *b_size = strlen(*body); status = HTTP_STATUS_OK; @@ -4161,7 +4169,7 @@ int html_get_handler(const xs_dict *req, const char *q_path, xs *title = xs_fmt(xs_list_len(tl) ? L("Search results for tag %s") : L("Nothing found for tag %s"), q); - *body = html_timeline(&snac, tl, 0, skip, show, more, title, page, 0, error); + *body = html_timeline(&snac, tl, 0, skip, show, more, title, page, 0, error, terse); *b_size = strlen(*body); status = HTTP_STATUS_OK; } @@ -4186,7 +4194,7 @@ int html_get_handler(const xs_dict *req, const char *q_path, title = xs_fmt(L("Nothing found for '%s'"), q); *body = html_timeline(&snac, tl, 0, skip, tl_len, to || tl_len == show, - title, page, 0, error); + title, page, 0, error, terse); *b_size = strlen(*body); status = HTTP_STATUS_OK; } @@ -4213,7 +4221,7 @@ int html_get_handler(const xs_dict *req, const char *q_path, xs *list = timeline_list(&snac, "private", skip, show, &more); *body = html_timeline(&snac, list, 0, skip, show, - more, NULL, "/admin", 1, error); + more, NULL, "/admin", 1, error, terse); *b_size = strlen(*body); status = HTTP_STATUS_OK; @@ -4240,7 +4248,7 @@ int html_get_handler(const xs_dict *req, const char *q_path, xs *list0 = xs_list_append(xs_list_new(), md5); xs *list = timeline_top_level(&snac, list0); - *body = html_timeline(&snac, list, 0, 0, 0, 0, NULL, "/admin", 1, error); + *body = html_timeline(&snac, list, 0, 0, 0, 0, NULL, "/admin", 1, error, terse); *b_size = strlen(*body); status = HTTP_STATUS_OK; } @@ -4281,7 +4289,7 @@ int html_get_handler(const xs_dict *req, const char *q_path, xs *next = timeline_instance_list(skip + show, 1); *body = html_timeline(&snac, list, 0, skip, show, - xs_list_len(next), L("Showing instance timeline"), "/instance", 0, error); + xs_list_len(next), L("Showing instance timeline"), "/instance", 0, error, terse); *b_size = strlen(*body); status = HTTP_STATUS_OK; } @@ -4296,7 +4304,7 @@ int html_get_handler(const xs_dict *req, const char *q_path, xs *list = pinned_list(&snac); *body = html_timeline(&snac, list, 0, skip, show, - 0, L("Pinned posts"), "", 0, error); + 0, L("Pinned posts"), "", 0, error, terse); *b_size = strlen(*body); status = HTTP_STATUS_OK; } @@ -4311,7 +4319,7 @@ int html_get_handler(const xs_dict *req, const char *q_path, xs *list = bookmark_list(&snac); *body = html_timeline(&snac, list, 0, skip, show, - 0, L("Bookmarked posts"), "", 0, error); + 0, L("Bookmarked posts"), "", 0, error, terse); *b_size = strlen(*body); status = HTTP_STATUS_OK; } @@ -4326,7 +4334,7 @@ int html_get_handler(const xs_dict *req, const char *q_path, xs *list = draft_list(&snac); *body = html_timeline(&snac, list, 0, skip, show, - 0, L("Post drafts"), "", 0, error); + 0, L("Post drafts"), "", 0, error, terse); *b_size = strlen(*body); status = HTTP_STATUS_OK; } @@ -4341,7 +4349,7 @@ int html_get_handler(const xs_dict *req, const char *q_path, xs *list = scheduled_list(&snac); *body = html_timeline(&snac, list, 0, skip, show, - 0, L("Scheduled posts"), "", 0, error); + 0, L("Scheduled posts"), "", 0, error, terse); *b_size = strlen(*body); status = HTTP_STATUS_OK; } @@ -4367,7 +4375,7 @@ int html_get_handler(const xs_dict *req, const char *q_path, xs *title = xs_fmt(L("Showing timeline for list '%s'"), name); *body = html_timeline(&snac, ttl, 0, skip, show, - xs_list_len(next), title, base, 1, error); + xs_list_len(next), title, base, 1, error, terse); *b_size = strlen(*body); status = HTTP_STATUS_OK; } @@ -4387,7 +4395,7 @@ int html_get_handler(const xs_dict *req, const char *q_path, list = xs_list_append(list, md5); - *body = html_timeline(&snac, list, 1, 0, 0, 0, NULL, "", 1, error); + *body = html_timeline(&snac, list, 1, 0, 0, 0, NULL, "", 1, error, terse); *b_size = strlen(*body); status = HTTP_STATUS_OK; } diff --git a/httpd.c b/httpd.c index a8cd849..f93f197 100644 --- a/httpd.c +++ b/httpd.c @@ -257,7 +257,7 @@ int server_get_handler(xs_dict *req, const char *q_path, else { xs *page = xs_fmt("?t=%s", t); xs *title = xs_fmt(L("Search results for tag #%s"), t); - *body = html_timeline(NULL, tl, 0, skip, show, more, title, page, 0, NULL); + *body = html_timeline(NULL, tl, 0, skip, show, more, title, page, 0, NULL, 0); } } else @@ -265,7 +265,7 @@ int server_get_handler(xs_dict *req, const char *q_path, /** instance timeline **/ xs *tl = timeline_instance_list(0, 30); *body = html_timeline(NULL, tl, 0, 0, 0, 0, - L("Recent posts by users in this instance"), NULL, 0, NULL); + L("Recent posts by users in this instance"), NULL, 0, NULL, 0); } else *body = greeting_html(); diff --git a/snac.h b/snac.h index 732d0b2..01947c5 100644 --- a/snac.h +++ b/snac.h @@ -411,7 +411,7 @@ xs_str *encode_html(const char *str); xs_str *html_timeline(snac *user, const xs_list *list, int read_only, int skip, int show, int show_more, - const char *title, const char *page, int utl, const char *error); + const char *title, const char *page, int utl, const char *error, int terse); int html_get_handler(const xs_dict *req, const char *q_path, char **body, int *b_size, char **ctype, -- cgit From 481c0315ef7ccd2eaf0ec9c6eaa17cbbd7efeb2c Mon Sep 17 00:00:00 2001 From: grunfink Date: Tue, 9 Dec 2025 13:38:56 +0100 Subject: If terse is set, history is also hidden. --- html.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'html.c') diff --git a/html.c b/html.c index 32ed057..8f7c4a9 100644 --- a/html.c +++ b/html.c @@ -3248,7 +3248,7 @@ xs_str *html_timeline(snac *user, const xs_list *list, int read_only, if (list && user && read_only) { /** history **/ - if (xs_type(xs_dict_get(srv_config, "disable_history")) != XSTYPE_TRUE) { + if (xs_type(xs_dict_get(srv_config, "disable_history")) != XSTYPE_TRUE && !terse) { xs_html *ul = xs_html_tag("ul", NULL); xs_html *history = xs_html_tag("div", -- cgit From 85ed0eb0d535700a5df837c37f51848811e461a0 Mon Sep 17 00:00:00 2001 From: violette Date: Thu, 18 Dec 2025 07:58:24 +0100 Subject: Added emoji reactions (contributed by violette). --- activitypub.c | 161 ++++++++++++++++++++++++++++- data.c | 124 +++++++++++++++++++++-- format.c | 3 + html.c | 317 +++++++++++++++++++++++++++++++++++++++++++++++++++++++--- main.c | 2 +- mastoapi.c | 160 ++++++++++++++++++++++++++++- snac.h | 10 +- xs_unicode.h | 7 ++ xs_url.h | 33 ++++++ 9 files changed, 786 insertions(+), 31 deletions(-) (limited to 'html.c') diff --git a/activitypub.c b/activitypub.c index 90230d8..19f0cc6 100644 --- a/activitypub.c +++ b/activitypub.c @@ -4,6 +4,7 @@ #include "xs.h" #include "xs_json.h" #include "xs_curl.h" +#include "xs_url.h" #include "xs_mime.h" #include "xs_openssl.h" #include "xs_regex.h" @@ -1530,11 +1531,24 @@ xs_dict *msg_update(snac *snac, const xs_dict *object) xs_dict *msg_admiration(snac *snac, const char *object, const char *type) -/* creates a Like or Announce message */ +/* creates a Like, Announce or EmojiReact message */ { xs *a_msg = NULL; xs_dict *msg = NULL; xs *wrk = NULL; + char t = 0; + + switch (*type) { + case 'L': + t = 'l'; + break; + case 'A': + t = 'a'; + break; + case 'E': + t = 'e'; + break; + } /* call the object */ timeline_request(snac, &object, &wrk, 0); @@ -1542,7 +1556,7 @@ xs_dict *msg_admiration(snac *snac, const char *object, const char *type) if (valid_status(object_get(object, &a_msg))) { xs *rcpts = xs_list_new(); xs *o_md5 = xs_md5_hex(object, strlen(object)); - xs *id = xs_fmt("%s/%s/%s", snac->actor, *type == 'L' ? "l" : "a", o_md5); + xs *id = xs_fmt("%s/%c/%s", snac->actor, t, o_md5); msg = msg_base(snac, type, id, snac->actor, "@now", object); @@ -1586,6 +1600,113 @@ xs_dict *msg_repulsion(snac *user, const char *id, const char *type) return msg; } +xs_dict *msg_emoji_init(snac *snac, const char *mid, const char *eid) +/* creates an emoji reaction from a local user */ +{ + xs_dict *n_msg = msg_admiration(snac, mid, "EmojiReact"); + + eid = xs_strip_chars_i(xs_dup(eid), ":"); + xs *content = NULL; + xs *tag = xs_list_new(); + xs *dict = xs_dict_new(); + xs *icon = xs_dict_new(); + xs *accounts = xs_list_new(); + + /* may be a default emoji */ + xs *eidd = xs_dup(eid); + const char *eidda = eid; + + if (xs_is_emoji(xs_utf8_dec(&eidda))) + content = xs_dup(eid); + + else if (*eid == '%') { + content = xs_url_dec_emoji(xs_dup(eid)); + if (content == NULL) { + return NULL; + } + } + else if (xs_dict_get(emojis(), xs_fmt(":%s:", eid)) == NULL) + return NULL; + else { + content = xs_fmt(":%s:", eid); + icon = xs_dict_set(icon, "type", "Image"); + icon = xs_dict_set(icon, "url", xs_fmt("%s/s/%s.png", snac->actor, eid)); + dict = xs_dict_set(dict, "icon", icon); + + dict = xs_dict_set(dict, "id", xs_fmt("%s/s/%s.png", snac->actor, eid)); + dict = xs_dict_set(dict, "name", content); + dict = xs_dict_set(dict, "type", "Emoji"); + tag = xs_list_append(tag, dict); + } + + accounts = xs_list_append(accounts, snac->actor); + + n_msg = xs_dict_set(n_msg, "content", content); + n_msg = xs_dict_set(n_msg, "accounts", accounts); + n_msg = xs_dict_set(n_msg, "attributedTo", xs_list_get(xs_dup(xs_dict_get(n_msg, "to")), 1)); + n_msg = xs_dict_set(n_msg, "accountId", snac->uid); + n_msg = xs_dict_set(n_msg, "tag", tag); + + int ret = timeline_admire(snac, xs_dict_get(n_msg, "object"), snac->actor, 1, n_msg); + if (ret == 200 || ret == 201) { + enqueue_message(snac, n_msg); + return n_msg; + } + + return NULL; +} + +xs_dict *msg_emoji_unreact(snac *user, const char *mid, const char *eid) +/* creates an Undo + emoji reaction message */ +{ + xs *a_msg = NULL; + xs_dict *msg = NULL; + + if (valid_status(object_get(mid, &a_msg))) { + /* create a clone of the original admiration message */ + xs *object = msg_admiration(user, mid, "EmojiReact"); + + /* delete the published date */ + object = xs_dict_del(object, "published"); + + /* create an undo message for this object */ + msg = msg_undo(user, object); + + /* copy the 'to' field */ + msg = xs_dict_set(msg, "to", xs_dict_get(object, "to")); + } + + xs *emotes = object_get_emoji_reacts(mid); + const char *v; + int c = 0; + + /* may be a default emoji */ + if (strlen(eid) == 12 && *eid == '%') { + eid = xs_url_dec(eid); + if (eid == NULL) { + return NULL; + } + } + + /* lets get all emotes for this msg, and compare it to our content */ + while (xs_list_next(emotes, &v, &c)) { + xs_dict *e = NULL; + if (valid_status(object_get_by_md5(v, &e))) { + const char *content = xs_dict_get(e, "content"); + const char *id = xs_dict_get(e, "id"); + const char *actor = xs_dict_get(e, "actor"); + /* maybe formated as :{emoteName}: too */ + if (xs_str_in(eid, content) != -1) + if (strcmp(user->actor, actor) == 0) { + object_rm_emoji_react(mid, id); + return msg; + } + } + } + + return NULL; +} + xs_dict *msg_actor_place(snac *user, const char *label) /* creates a Place object, if the user has a location defined */ @@ -2605,6 +2726,16 @@ int process_input_message(snac *snac, const xs_dict *msg, const xs_dict *req) else if (strcmp(type, "Undo") == 0) { /** **/ const char *id = xs_dict_get(object, "object"); + const char *content = xs_dict_get(object, "content"); + /* misskey sends emojis as like + tag */ + xs *cd = xs_dup(content); + const char *sna = cd; + const xs_dict *tag = xs_dict_get(object, "tag"); + unsigned int utf = xs_utf8_dec((const char **)&sna); + + int isEmoji = 0; + if (xs_is_emoji(utf) || (tag && xs_list_len(tag) > 0)) + isEmoji = 1; if (xs_type(object) != XSTYPE_DICT) { snac_debug(snac, 1, xs_fmt("undo: overriding utype %s | %s | %s", @@ -2633,8 +2764,19 @@ int process_input_message(snac *snac, const xs_dict *msg, const xs_dict *req) else snac_log(snac, xs_fmt("error deleting follower %s", actor)); } + /* *key emojis are like w/ Emoji tag */ + else + if ((isEmoji || strcmp(utype, "EmojiReact") == 0) && + (content && strcmp(content, "♥") != 0)) { + const xs_val *mid = xs_dict_get(object, "id"); + int status = object_rm_emoji_react((char *)id, mid); + /* ensure *key notifications type */ + utype = "EmojiReact"; + + snac_log(snac, xs_fmt("Undo 'EmojiReact' for %s %d", id, status)); + } else - if (strcmp(utype, "Like") == 0 || strcmp(utype, "EmojiReact") == 0) { /** **/ + if (strcmp(utype, "Like") == 0) { /** **/ int status = object_unadmire(id, actor, 1); snac_log(snac, xs_fmt("Undo '%s' for %s %d", utype, id, status)); @@ -2771,13 +2913,22 @@ int process_input_message(snac *snac, const xs_dict *msg, const xs_dict *req) } else if (strcmp(type, "Like") == 0 || strcmp(type, "EmojiReact") == 0) { /** **/ + /* misskey sends emojis as Like + tag. + * It is easier to handle them both at the same time. */ + const char *sna = xs_dict_get(msg, "content"); + const xs_dict *tag = xs_dict_get(msg, "tag"); + unsigned int utf = xs_utf8_dec((const char **)&sna); + + if (xs_is_emoji(utf) || (tag && xs_list_len(tag) > 0)) + type = "EmojiReact"; + if (xs_type(object) == XSTYPE_DICT) object = xs_dict_get(object, "id"); if (xs_is_null(object)) snac_log(snac, xs_fmt("malformed message: no 'id' field")); else - if (timeline_admire(snac, object, actor, 1) == HTTP_STATUS_CREATED) + if (timeline_admire(snac, object, actor, 1, xs_dup(msg)) == HTTP_STATUS_CREATED) snac_log(snac, xs_fmt("new '%s' %s %s", type, actor, object)); else snac_log(snac, xs_fmt("repeated '%s' from %s to %s", type, actor, object)); @@ -2818,7 +2969,7 @@ int process_input_message(snac *snac, const xs_dict *msg, const xs_dict *req) xs *this_relay = xs_fmt("%s/relay", srv_baseurl); if (strcmp(actor, this_relay) != 0) { - if (valid_status(timeline_admire(snac, object, actor, 0))) + if (valid_status(timeline_admire(snac, object, actor, 0, a_msg))) snac_log(snac, xs_fmt("new 'Announce' %s %s", actor, object)); else snac_log(snac, xs_fmt("repeated 'Announce' from %s to %s", diff --git a/data.c b/data.c index 688d2e3..07eea3c 100644 --- a/data.c +++ b/data.c @@ -1043,6 +1043,14 @@ xs_list *object_children(const char *id) } +xs_list *object_get_emoji_reacts(const char *id) +/* returns the list of an object's emoji reactions */ +{ + xs *fn = _object_index_fn(id, "_e.idx"); + return index_list(fn, XS_ALL); +} + + xs_list *object_likes(const char *id) { xs *fn = _object_index_fn(id, "_l.idx"); @@ -1086,12 +1094,26 @@ int object_admire(const char *id, const char *actor, int like) int object_unadmire(const char *id, const char *actor, int like) -/* actor no longer likes or announces this object */ +/* actor retrives their likes, announces or emojis this object */ { + switch (like) { + case 0: + like = 'a'; + break; + case 1: + like = 'l'; + break; + case 2: + like = 'e'; + break; + } int status; xs *fn = _object_fn(id); - fn = xs_replace_i(fn, ".json", like ? "_l.idx" : "_a.idx"); + char sfx[7] = "_x.idx"; + sfx[1] = like; + + fn = xs_replace_i(fn, ".json", sfx); status = index_del(fn, actor); @@ -1099,7 +1121,46 @@ int object_unadmire(const char *id, const char *actor, int like) index_gc(fn); srv_debug(0, - xs_fmt("object_unadmire (%s) %s %s %d", like ? "Like" : "Announce", actor, fn, status)); + xs_fmt("object_unadmire (%s) %s %s %d", like >= 'e' ? + (like == 'l' ? "Like" : "EmojiReact") : "Announce" , actor, fn, status)); + + return status; +} + +int object_emoji_react(const char *mid, const char *eid) +/* actor reacts w/ an emoji */ +{ + int status = HTTP_STATUS_OK; + xs *fn = _object_fn(mid); + + fn = xs_replace_i(fn, ".json", "_e.idx"); + + if (!index_in(fn, eid)) { + status = index_add(fn, eid); + + srv_debug(1, xs_fmt("object_emoji_react (%s) added %s to %s", "EmojiReact", eid, fn)); + } + + return status; +} + + +int object_rm_emoji_react(const char *mid, const char *eid) +/* actor retrives their emoji reaction */ +{ + int status; + xs *fn = _object_fn(mid); + + fn = xs_replace_i(fn, ".json", "_e.idx"); + + status = index_del(fn, eid); + object_del(eid); + + if (valid_status(status)) + index_gc(fn); + + srv_debug(0, + xs_fmt("object_unadmire (EmojiReact) %s %s %d", eid, fn, status)); return status; } @@ -1506,19 +1567,47 @@ int timeline_add(snac *snac, const char *id, const xs_dict *o_msg) } -int timeline_admire(snac *snac, const char *id, const char *admirer, int like) -/* updates a timeline entry with a new admiration */ +int timeline_emoji_react(const char *act, const char *id, xs_dict *msg) +/* adds an emoji reaction to a message */ +{ + msg = xs_dict_append(msg, "attributedTo", act); + msg = xs_dict_set(msg, "type", "EmojiReact"); + const char *emote_id = xs_dict_get(msg, "id"); + + int ret = object_add(emote_id, msg); + if (ret == HTTP_STATUS_OK || ret == HTTP_STATUS_CREATED) + ret = object_emoji_react(id, emote_id); + + return ret; +} + + +int timeline_admire(snac *snac, const char *id, + const char *admirer, int like, xs_dict *msg) +/* updates a timeline entry with a new admiration or emoji reaction */ { + int ret; + const char *content = xs_dict_get_path(msg, "content"); + const char *type = xs_dict_get_path(msg, "type"); + /* if we are admiring this, add to both timelines */ if (!like && strcmp(admirer, snac->actor) == 0) { object_user_cache_add(snac, id, "public"); object_user_cache_add(snac, id, "private"); } - int ret = object_admire(id, admirer, like); + /* use utf <3 as a like, as it is ugly */ + if (type && xs_match(type, "Like|EmojiReact|Emoji") && + content && strcmp(content, "❤") != 0) { + ret = timeline_emoji_react(xs_dup(snac->actor), id, xs_dup(msg)); + snac_debug(snac, 1, xs_fmt("timeline_emoji_react %s", id)); + } - snac_debug(snac, 1, xs_fmt("timeline_admire (%s) %s %s", - like ? "Like" : "Announce", id, admirer)); + else { + ret = object_admire(id, admirer, like); + snac_debug(snac, 1, xs_fmt("timeline_admire (%s) %s %s", + like ? "Like" : "Announce", id, admirer)); + } return ret; } @@ -1867,6 +1956,25 @@ xs_list *muted_list(snac *user) return l; } +/** emojis react **/ + +const xs_str *emoji_reacted(snac *user, const char *id) +/* returns the emoji an user reacted to a message */ +{ + xs *emojis = object_get_emoji_reacts(id); + int c = 0; + const char *v; + xs_dict *msg; + + while (xs_list_next(emojis, &v, &c)) { + if (object_get_by_md5(v, &msg)) { + const xs_val *act = xs_dict_get(msg, "actor"); + if (act && strcmp(act, user->actor) == 0) + return xs_dict_get(msg, "content"); + } + } + return NULL; +} /** bookmarking **/ diff --git a/format.c b/format.c index 84c634d..4f93b7b 100644 --- a/format.c +++ b/format.c @@ -459,6 +459,9 @@ xs_str *sanitize(const char *content) char *p; const char *v; + if (!content) + return NULL; + sl = xs_regex_split(content, "]+>"); p = sl; diff --git a/html.c b/html.c index 8f7c4a9..8cc0067 100644 --- a/html.c +++ b/html.c @@ -54,9 +54,10 @@ int login(snac *user, const xs_dict *headers) return logged_in; } - -xs_str *replace_shortnames(xs_str *s, const xs_list *tag, int ems, const char *proxy) -/* replaces all the :shortnames: with the emojis in tag */ +xs_str *_replace_shortnames(xs_str *s, const xs_list *tag, int ems, + const char *proxy, const xs_list *cl, const char *act) +/* replace but also adds a class list and an actor in its alt text. + * Used for emoji reactions */ { if (!xs_is_null(tag)) { xs *tag_list = NULL; @@ -69,11 +70,15 @@ xs_str *replace_shortnames(xs_str *s, const xs_list *tag, int ems, const char *p tag_list = xs_dup(tag); } - xs *style = xs_fmt("height: %dem; width: %dem; vertical-align: middle;", ems, ems); + xs *style = xs_fmt("max-height: %dem; max-width: %dem;", ems, ems); xs *class = xs_fmt("snac-emoji snac-emoji-%d-em", ems); + if (cl) + class = xs_str_cat(class, " ", xs_join(cl, " ")); - const xs_dict *v; int c = 0; + const xs_val *v; + + c = 0; xs_set rep_emoji; xs_set_init(&rep_emoji); @@ -100,6 +105,8 @@ xs_str *replace_shortnames(xs_str *s, const xs_list *tag, int ems, const char *p if (!xs_is_string(mt)) mt = xs_mime_by_ext(u); + act = act ? xs_fmt("%s\n%s", n, act) : xs_fmt("%s", n); + if (strcmp(mt, "image/svg+xml") == 0 && !xs_is_true(xs_dict_get(srv_config, "enable_svg"))) s = xs_replace_i(s, n, ""); else { @@ -108,8 +115,8 @@ xs_str *replace_shortnames(xs_str *s, const xs_list *tag, int ems, const char *p xs_html *img = xs_html_sctag("img", xs_html_attr("loading", "lazy"), xs_html_attr("src", url), - xs_html_attr("alt", n), - xs_html_attr("title", n), + xs_html_attr("alt", act), + xs_html_attr("title", act), xs_html_attr("class", class), xs_html_attr("style", style)); @@ -130,6 +137,13 @@ xs_str *replace_shortnames(xs_str *s, const xs_list *tag, int ems, const char *p } +xs_str *replace_shortnames(xs_str *s, const xs_list *tag, int ems, const char *proxy) +/* replaces all the :shortnames: with the emojis in tag */ +{ + return _replace_shortnames(s, tag, ems, proxy, NULL, NULL); +} + + xs_str *actor_name(xs_dict *actor, const char *proxy) /* gets the actor name */ { @@ -430,6 +444,52 @@ void html_note_render_visibility(snac* user, xs_html *form, const int scope) xs_html_add(form, paragraph); } +/* html_note but moddled for emoji's needs. here and not bellow, since the + * other one is already so complex. */ +xs_html *html_emoji(snac *user, const char *summary, + const char *div_id, const char *form_id, + const char* placeholder, const char *post_id, + const char* eid) +{ + xs *action = xs_fmt("%s/admin/action", user->actor); + + xs_html *form; + const int react = eid == NULL ? 0 : 1; + + xs_html *note = xs_html_tag("div", + xs_html_tag("details", + xs_html_tag("summary", + xs_html_text(summary)), + xs_html_tag("p", NULL), + xs_html_tag("div", + xs_html_attr("class", "snac-note"), + xs_html_attr("id", div_id), + form = xs_html_tag("form", + xs_html_attr("autocomplete", "off"), + xs_html_attr("method", "post"), + xs_html_attr("action", action), + xs_html_attr("enctype", "multipart/form-data"), + xs_html_attr("id", form_id), + xs_html_sctag("input", + xs_html_attr("type", "hidden"), + xs_html_attr("name", "id"), + xs_html_attr("value", post_id)), + xs_html_sctag("input", + xs_html_attr("type", react ? "hidden" : "text"), + xs_html_attr("name", "eid"), + xs_html_attr(react ? "value" : "placeholder", react ? eid : placeholder)), + xs_html_text(" "), + xs_html_sctag("input", + xs_html_attr("type", "submit"), + xs_html_attr("name", "action"), + xs_html_attr("eid", "action"), + xs_html_attr("value", react ? L("EmojiUnreact") : L("EmojiReact"))), + xs_html_text(" "), + xs_html_tag("p", NULL))))); + + return note; +} + xs_html *html_note(snac *user, const char *summary, const char *div_id, const char *form_id, const char *ta_plh, const char *ta_content, @@ -1356,6 +1416,28 @@ xs_html *html_top_controls(snac *user) xs_html_attr("value", L("Like"))), xs_html_text(" "), xs_html_text(L("(by URL)"))), + xs_html_tag("form", + xs_html_attr("autocomplete", "off"), + xs_html_attr("method", "post"), + xs_html_attr("action", ops_action), + xs_html_sctag("input", + xs_html_attr("type", "text"), + xs_html_attr("name", "eid"), + xs_html_attr("required", "required"), + xs_html_attr("placeholder", ":neocat:")), + xs_html_text(" "), + xs_html_sctag("input", + xs_html_attr("type", "text"), + xs_html_attr("name", "id"), + xs_html_attr("required", "required"), + xs_html_attr("placeholder", "https:/" "/fedi.example.com/bob/...")), + xs_html_text(" "), + xs_html_sctag("input", + xs_html_attr("type", "submit"), + xs_html_attr("name", "action"), + xs_html_attr("value", L("EmojiReact"))), + xs_html_text(" "), + xs_html_text(L("(by URL)"))), xs_html_tag("p", NULL))); /** user settings **/ @@ -2019,6 +2101,21 @@ xs_html *html_entry_controls(snac *user, const char *actor, xs_html_tag("p", NULL)); } + { /** emoji react **/ + /* the post textarea */ + xs *div_id = xs_fmt("%s_reply", md5); + xs *form_id = xs_fmt("%s_reply_form", md5); + + xs_html_add(controls, xs_html_tag("div", + xs_html_tag("p", NULL), + html_emoji( + user, L("Emoji react"), + div_id, form_id, + ":neocat:", id, + emoji_reacted(user, id))), + xs_html_tag("p", NULL)); + } + { /** reply **/ /* the post textarea */ xs *ct = build_mentions(user, msg); @@ -2345,6 +2442,168 @@ xs_html *html_entry(snac *user, xs_dict *msg, int read_only, xs_html_add(snac_content_wrap, snac_content); + /* add all emoji reacts */ + int is_emoji = 0; + { + int c = 0; + const xs_dict *k; + xs *ls = xs_list_new(); + xs *sfrl = xs_dict_new(); + xs *rl = object_get_emoji_reacts(id); + + xs_dict *m = NULL; + while (xs_list_next(rl, &v, &c)) { + if (valid_status(object_get_by_md5(v, &m))) { + const char *content = xs_dict_get(m, "content"); + const char *actor = xs_dict_get(m, "actor"); + const xs_list *contentl = xs_dict_get(sfrl, content); + xs *actors = xs_list_new(); + actors = xs_list_append(actors, actor); + char me = actor && user && strcmp(actor, user->actor) == 0; + int count = 1; + + if (contentl) { + count = atoi(xs_list_get(contentl, 0)) + 1; + const xs_list *actorsc = xs_list_get(contentl, 1); + if (strncmp(xs_list_get(contentl, 2), "1", 1) == 0) + me = 1; + + if (xs_list_in(actorsc, actor) != -1) { + xs_free(actors); + actors = xs_dup(actorsc); + } + else + actors = xs_list_cat(actors, actorsc); + } + + xs *fl = xs_list_new(); + fl = xs_list_append(fl, xs_fmt("%d", count), actors, xs_fmt("%d", me)); + sfrl = xs_dict_append(sfrl, content, fl); + } + } + + c = 0; + + while (xs_list_next(rl, &k, &c)) { + if (valid_status(object_get_by_md5(k, &m))) { + const xs_dict *tag = xs_dict_get(m, "tag"); + const xs_dict *ide = xs_dict_get(m, "id"); + + const char *content = xs_dict_get(m, "content"); + const char *shortname; + shortname = xs_dict_get(m, "content"); + + const xs_list *items = xs_dict_get(sfrl, content); + const char *nb = xs_list_get(items, 0); + const xs_list *actors = xs_list_get(items, 1); + const char me = *xs_list_get(items, 2) == '1'; + + if (!xs_is_null(nb)) { + is_emoji = 1; + + const char *act = atoi(nb) > 1 ? + xs_fmt("%d different actors \n\t%s", atoi(nb), xs_join(actors, ",\n\t")) : + xs_dict_get(m, "actor"); + + xs *class = xs_list_new(); + class = xs_list_append(class, "snac-reaction"); + + xs_html *ret = NULL; + if (tag && shortname) { + xs *cl = xs_list_new(); + cl = xs_list_append(cl, "snac-reaction-image"); + xs *emoji = _replace_shortnames(xs_dup(shortname), tag, 2, proxy, cl, act); + + if (me) + class = xs_list_append(class, "snac-reacted"); + + ret = xs_html_tag("button", + xs_html_attr("type", "submit"), + xs_html_attr("name", "action"), + xs_html_attr("value", me ? L("EmojiReact") : L("EmojiUnreact")), + xs_html_raw(emoji), + xs_html_tag("span", + xs_html_raw(nb), + xs_html_attr("style", "padding-left: 5px;")), + xs_html_attr("title", act), + xs_html_attr("class", xs_join(class, " "))); + + if (!(ide && xs_startswith(ide, srv_baseurl))) + xs_html_add(ret, xs_html_attr("disabled", "true")); + } + else if (shortname) { + xs *sn = xs_dup(shortname); + const char *sna = sn; + unsigned int utf = xs_utf8_dec((const char **)&sna); + + if (xs_is_emoji(utf)) { + const char *style = "font-size: large;"; + if (me) + class = xs_list_append(class, "snac-reacted"); + ret = xs_html_tag("button", + xs_html_attr("type", "submit"), + xs_html_attr("name", "action"), + xs_html_attr("value", me ? L("EmojiUnreact") : L("EmojiReact")), + xs_html_raw(xs_fmt("&#%d", utf)), + xs_html_tag("span", + xs_html_raw(nb), + xs_html_attr("style", "font-size: initial; padding-left: 5px;")), + xs_html_attr("title", act), + xs_html_attr("class", xs_join(class, " ")), + xs_html_attr("style", style)); + } + } + if (ret) { + xs *s1; + if (user) { + xs *action = xs_fmt("%s/admin/action", user->actor); + xs *form_id = xs_fmt("%s_reply_form", md5); + + xs_html *form = + xs_html_tag("form", + xs_html_attr("autocomplete", "off"), + xs_html_attr("method", "post"), + xs_html_attr("action", action), + xs_html_attr("enctype", "multipart/form-data"), + xs_html_attr("style", "display: inline-flex;" + "vertical-align: middle;"), + xs_html_attr("id", form_id), + xs_html_sctag("input", + xs_html_attr("type", "hidden"), + xs_html_attr("name", "id"), + xs_html_attr("value", id)), + xs_html_sctag("input", + xs_html_attr("type", "hidden"), + xs_html_attr("name", "eid"), + xs_html_attr("value", shortname)), + ret); + s1 = xs_html_render(form); + } + else + s1 = xs_html_render(ret); + + ls = xs_list_append(ls, s1); + sfrl = xs_dict_del(sfrl, content); + } + } + } + } + + c = 0; + + xs_html *emoji_div; + if (xs_list_len(ls) > 0) { + emoji_div = xs_html_tag("div", xs_html_text(L("Emoji reactions: ")), + xs_html_attr("class", "snac-reaction-div")); + + while (ls != NULL && xs_list_next(ls, &k, &c)) + xs_html_add(emoji_div, xs_html_raw(k)); + + xs_html_add(snac_content_wrap, emoji_div); + } + + } + { /** build the content string **/ const char *content = xs_dict_get(msg, "content"); @@ -2371,7 +2630,8 @@ xs_html *html_entry(snac *user, xs_dict *msg, int read_only, c = xs_replace_i(c, "

", "

"); - c = xs_str_cat(c, "

"); + if (is_emoji == 0) + c = xs_str_cat(c, "

"); /* replace the :shortnames: */ c = replace_shortnames(c, xs_dict_get(msg, "tag"), 2, proxy); @@ -3686,9 +3946,18 @@ xs_str *html_notifications(snac *user, int skip, int show) if (strcmp(type, "EmojiReact") == 0 || strcmp(type, "Like") == 0) { const char *content = xs_dict_get_path(noti, "msg.content"); + xs *cd = xs_dup(content); + const char *sna = cd; + const xs_dict *tag = xs_dict_get_path(noti, "msg.tag"); + unsigned int utf = xs_utf8_dec((const char **)&sna); + + int isEmoji = 0; + if (xs_is_emoji(utf) || (tag && xs_list_len(tag) > 0)) + isEmoji = 1; + if (xs_type(content) == XSTYPE_STRING) { xs *emoji = replace_shortnames(xs_dup(content), xs_dict_get_path(noti, "msg.tag"), 1, proxy); - wrk = xs_fmt("%s (%s️)", type, emoji); + wrk = xs_fmt("%s (%s️)", isEmoji ? "EmojiReact" : "Like", emoji); label = wrk; } } @@ -4583,8 +4852,8 @@ int html_get_handler(const xs_dict *req, const char *q_path, xs *msg = msg_admiration(&snac, id, *action == 'L' ? "Like" : "Announce"); if (msg != NULL) { + timeline_admire(&snac, xs_dict_get(msg, "object"), snac.actor, *action == 'L' ? 1 : 0, msg); enqueue_message(&snac, msg); - timeline_admire(&snac, xs_dict_get(msg, "object"), snac.actor, *action == 'L' ? 1 : 0); status = HTTP_STATUS_SEE_OTHER; } @@ -4892,12 +5161,36 @@ int html_post_handler(const xs_dict *req, const char *q_path, status = HTTP_STATUS_SEE_OTHER; + if (strcmp(action, L("EmojiUnreact")) == 0) { /** **/ + const char *eid = xs_dict_get(p_vars, "eid"); + + if (eid != NULL) { + xs *n_msg = msg_emoji_unreact(&snac, id, eid); + + if (n_msg != NULL) + enqueue_message(&snac, n_msg); + } + } + else + if (strcmp(action, L("EmojiReact")) == 0) { /** **/ + const char *eid = xs_dict_get(p_vars, "eid"); + + eid = xs_strip_chars_i(xs_dup(eid), ":"); + + const xs_dict *ret = msg_emoji_init(&snac, id, eid); + /* fails if either invalid or already reacted */ + if (!ret) + ret = msg_emoji_unreact(&snac, id, eid); + if (!ret) + status = HTTP_STATUS_NOT_FOUND; + } + else if (strcmp(action, L("Like")) == 0) { /** **/ xs *msg = msg_admiration(&snac, id, "Like"); if (msg != NULL) { + timeline_admire(&snac, xs_dict_get(msg, "object"), snac.actor, 1, msg); enqueue_message(&snac, msg); - timeline_admire(&snac, xs_dict_get(msg, "object"), snac.actor, 1); } } else @@ -4905,8 +5198,8 @@ int html_post_handler(const xs_dict *req, const char *q_path, xs *msg = msg_admiration(&snac, id, "Announce"); if (msg != NULL) { + timeline_admire(&snac, xs_dict_get(msg, "object"), snac.actor, 0, msg); enqueue_message(&snac, msg); - timeline_admire(&snac, xs_dict_get(msg, "object"), snac.actor, 0); } } else diff --git a/main.c b/main.c index 6915bd0..4b0463e 100644 --- a/main.c +++ b/main.c @@ -498,7 +498,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); + timeline_admire(&snac, xs_dict_get(msg, "object"), snac.actor, 0, ""); if (dbglevel) { xs_json_dump(msg, 4, stdout); diff --git a/mastoapi.c b/mastoapi.c index 9503447..1d99cd5 100644 --- a/mastoapi.c +++ b/mastoapi.c @@ -1158,6 +1158,97 @@ xs_dict *mastoapi_status(snac *snac, const xs_dict *msg) st = xs_dict_append(st, "tags", htl); st = xs_dict_append(st, "emojis", eml); } + { + xs *rl = object_get_emoji_reacts(id); + xs *frl = xs_list_new(); /* final */ + xs *sfrl = xs_dict_new(); /* seen */ + int c = 0; + const char *v; + + xs_dict *msg = NULL; + while (xs_list_next(rl, &v, &c)) { + if (valid_status(object_get_by_md5(v, &msg))) { + const char *content = xs_dict_get(msg, "content"); + const char *actor = xs_dict_get(msg, "actor"); + const xs_list *contentl = xs_dict_get(sfrl, content); + /* NOTE: idk when there are no actor, but i encountered that bug. + * Probably because of one of my previous attempts. + * Keeping this just in case, can remove later */ + const char *me = actor && strcmp(actor, snac->actor) == 0 ? + xs_stock(XSTYPE_TRUE) : xs_stock(XSTYPE_FALSE); + int count = 1; + + if (contentl) { + count = atoi(xs_list_get(contentl, 0)) + 1; + if (strncmp(xs_list_get(contentl, 1), xs_stock(XSTYPE_TRUE), 1) == 0) + me = xs_stock(XSTYPE_TRUE); + } + + xs *fl = xs_list_new(); + fl = xs_list_append(fl, xs_fmt("%d", count), me); + sfrl = xs_dict_append(sfrl, content, fl); + } + } + + c = 0; + + while (xs_list_next(rl, &v, &c)) { + if (valid_status(object_get_by_md5(v, &msg))) { + xs *d1 = xs_dict_new(); + + const xs_dict *icon = xs_dict_get(xs_list_get(xs_dict_get(msg, "tag"), 0), "icon"); + const char *o_url = xs_dict_get(icon, "url"); + const char *name = xs_dict_get(msg, "content"); + const char *actor = xs_dict_get(msg, "actor"); + + xs *nm = xs_dup(name); + xs *url = NULL; + + if (!xs_is_null(o_url)) { + if (actor && snac && !strcmp(actor, snac->actor)) + url = make_url(o_url, NULL, 1); + else + url = xs_dup(o_url); + } + + xs *accounts = xs_list_new(); + if (actor) { + xs *d2 = xs_dict_new(); + object_get(actor, &d2); + xs *e_acct = mastoapi_account(snac, d2); + accounts = xs_list_append(accounts, e_acct); + } + + const xs_list *item = xs_dict_get(sfrl, nm); + const xs_str *nb = xs_list_get(item, 0); + const xs_val *me = xs_list_get(item, 1); + if (item == NULL) + continue; + + if (nm && strcmp(nm, "")) { + if (url && strcmp(url, "")) { + d1 = xs_dict_append(d1, "name", nm); + d1 = xs_dict_append(d1, "shortcode", nm); + d1 = xs_dict_append(d1, "accounts", accounts); + d1 = xs_dict_append(d1, "me", me); + d1 = xs_dict_append(d1, "url", url); + d1 = xs_dict_append(d1, "static_url", url); + d1 = xs_dict_append(d1, "visible_in_picker", xs_stock(XSTYPE_TRUE)); + d1 = xs_dict_append(d1, "count", nb); + } else { + d1 = xs_dict_append(d1, "name", nm); + d1 = xs_dict_append(d1, "count", nb); + d1 = xs_dict_append(d1, "me", me); + d1 = xs_dict_append(d1, "visible_in_picker", xs_stock(XSTYPE_TRUE)); + } + sfrl = xs_dict_del(sfrl, nm); + frl = xs_list_append(frl, d1); + } + } + } + + st = xs_dict_append(st, "reactions", frl); + } xs_free(idx); xs_free(ixc); @@ -2202,15 +2293,24 @@ int mastoapi_get_handler(const xs_dict *req, const char *q_path, if (noti == NULL) continue; + const xs_dict *tag = xs_list_get(xs_dict_get_path(noti, "msg.tag"), 0); + const char *type = xs_dict_get(noti, "type"); const char *utype = xs_dict_get(noti, "utype"); const char *objid = xs_dict_get(noti, "objid"); const char *id = xs_dict_get(noti, "id"); const char *actid = xs_dict_get(noti, "actor"); + + int isEmoji = 0; + xs *fid = xs_replace(id, ".", ""); xs *actor = NULL; xs *entry = NULL; + if (tag) { + isEmoji = strcmp(xs_dict_get(tag, "type"), "Emoji") ? 0 : 1; + } + if (!valid_status(actor_get(actid, &actor))) continue; @@ -2234,9 +2334,12 @@ int mastoapi_get_handler(const xs_dict *req, const char *q_path, } /* convert the type */ - if (strcmp(type, "Like") == 0 || strcmp(type, "EmojiReact") == 0) + if (strcmp(type, "Like") == 0 && !isEmoji) type = "favourite"; else + if (isEmoji || strcmp(type, "EmojiReact") == 0) + type = "reaction"; + else if (strcmp(type, "Announce") == 0) type = "reblog"; else @@ -2277,8 +2380,29 @@ int mastoapi_get_handler(const xs_dict *req, const char *q_path, if (strcmp(type, "follow") != 0 && !xs_is_null(objid)) { xs *st = mastoapi_status(&snac1, entry); - if (st) + if (st) { mn = xs_dict_append(mn, "status", st); + + if (strcmp(type, "reaction") == 0 && !xs_is_null(objid)) { + const char *eid = NULL; + const char *url = NULL; + int utf = 0; + + const xs_dict *tag = xs_list_get(xs_dict_get_path(noti, "msg.tag"), 0); + const char *content = xs_dict_get_path(noti, "msg.content"); + + url = xs_dict_get(xs_dict_get(tag, "icon"), "url"); + eid = xs_dict_get(tag, "name"); + + if (eid && url) { + mn = xs_dict_append(mn, "emoji", eid); + mn = xs_dict_append(mn, "emoji_url", url); + } + + if (xs_is_emoji((utf = xs_utf8_dec(&content)))) + mn = xs_dict_append(mn, "name", xs_fmt("&#%d;", utf)); + } + } } out = xs_list_append(out, mn); @@ -2594,6 +2718,11 @@ int mastoapi_get_handler(const xs_dict *req, const char *q_path, "\"max_expiration\":2629746," "\"max_options\":8,\"min_expiration\":300}"); cfg = xs_dict_append(cfg, "polls", d14); + + + xs *d15 = xs_json_loads("{\"max_reactions\":50}"); + cfg = xs_dict_append(cfg, "reactions", d15); + } ins = xs_dict_append(ins, "configuration", cfg); @@ -3219,7 +3348,7 @@ int mastoapi_post_handler(const xs_dict *req, const char *q_path, if (n_msg != NULL) { enqueue_message(&snac, n_msg); - timeline_admire(&snac, xs_dict_get(n_msg, "object"), snac.actor, 1); + timeline_admire(&snac, xs_dict_get(n_msg, "object"), snac.actor, 1, msg); out = mastoapi_status(&snac, msg); } @@ -3234,13 +3363,36 @@ int mastoapi_post_handler(const xs_dict *req, const char *q_path, out = mastoapi_status(&snac, msg); } } + else + if (strcmp(op, "react") == 0) { /** **/ + const char *eid = xs_list_get(l, 5); + xs *n_msg = msg_emoji_init(&snac, id, eid); + if (n_msg) + out = mastoapi_status(&snac, n_msg); + } + else + if (strcmp(op, "unreact") == 0) { /** **/ + const char *eid = xs_list_get(l, 5); + xs *content = xs_fmt("%s", eid); + + if (eid) { + xs *n_msg = msg_emoji_unreact(&snac, id, content); + + if (n_msg != NULL) { + enqueue_message(&snac, n_msg); + + out = mastoapi_status(&snac, msg); + } + } + } + else if (strcmp(op, "reblog") == 0) { /** **/ xs *n_msg = msg_admiration(&snac, id, "Announce"); if (n_msg != NULL) { enqueue_message(&snac, n_msg); - timeline_admire(&snac, xs_dict_get(n_msg, "object"), snac.actor, 0); + timeline_admire(&snac, xs_dict_get(n_msg, "object"), snac.actor, 0, msg); out = mastoapi_status(&snac, msg); } diff --git a/snac.h b/snac.h index e1d25f2..3d98aac 100644 --- a/snac.h +++ b/snac.h @@ -146,12 +146,15 @@ void object_touch(const char *id); int object_admire(const char *id, const char *actor, int like); int object_unadmire(const char *id, const char *actor, int like); +int object_emoji_react(const char *mid, const char *eid); +int object_rm_emoji_react(const char *mid, const char *eid); int object_likes_len(const char *id); int object_announces_len(const char *id); xs_list *object_children(const char *id); xs_list *object_likes(const char *id); xs_list *object_announces(const char *id); +xs_list *object_get_emoji_reacts(const char *id); int object_parent(const char *md5, char parent[MD5_HEX_SIZE]); int object_user_cache_add(snac *snac, const char *id, const char *cachedir); @@ -180,7 +183,8 @@ xs_str *user_index_fn(snac *user, const char *idx_name); xs_list *timeline_simple_list(snac *user, const char *idx_name, int skip, int show, int *more); xs_list *timeline_list(snac *snac, const char *idx_name, int skip, int show, int *more); int timeline_add(snac *snac, const char *id, const xs_dict *o_msg); -int timeline_admire(snac *snac, const char *id, const char *admirer, int like); +int timeline_admire(snac *snac, const char *id, const char *admirer, int like, xs_dict *msg); +int timeline_emoji_react(const char *atto, const char *id, xs_dict *o_msg); xs_list *timeline_top_level(snac *snac, const xs_list *list); void timeline_add_mark(snac *user); @@ -201,6 +205,8 @@ void unmute(snac *snac, const char *actor); int is_muted(snac *snac, const char *actor); xs_list *muted_list(snac *user); +const xs_str *emoji_reacted(snac *user, const char *id); + int is_bookmarked(snac *user, const char *id); int bookmark(snac *user, const char *id); int unbookmark(snac *user, const char *id); @@ -358,6 +364,8 @@ xs_list *get_attachments(const xs_dict *msg); xs_dict *msg_admiration(snac *snac, const char *object, const char *type); xs_dict *msg_repulsion(snac *user, const char *id, const char *type); +xs_dict *msg_emoji_init(snac *user, const char *mid, const char *eid); +xs_dict *msg_emoji_unreact(snac *user, const char *id, const char *type); xs_dict *msg_create(snac *snac, const xs_dict *object); xs_dict *msg_follow(snac *snac, const char *actor); diff --git a/xs_unicode.h b/xs_unicode.h index 67b3827..0b4de1c 100644 --- a/xs_unicode.h +++ b/xs_unicode.h @@ -22,6 +22,7 @@ int xs_unicode_nfc(unsigned int base, unsigned int diac, unsigned int *cpoint); int xs_unicode_is_alpha(unsigned int cpoint); int xs_unicode_is_right_to_left(unsigned int cpoint); + int xs_is_emoji(unsigned int cpoint); #ifdef _XS_H xs_str *xs_utf8_insert(xs_str *str, unsigned int cpoint, int *offset); @@ -134,6 +135,12 @@ static unsigned int xs_unicode_width_table[] = { 0x20000, 0x2fffd, 2 /* more CJK */ }; +/* magic number from https://en.wikipedia.org/wiki/Emoji#Unicode_blocks */ +int xs_is_emoji(unsigned int cpoint) { +/* returns wether the input is an utf8 emoji */ + return cpoint > 0x00A9 && cpoint < 0x1ffff; +} + int xs_unicode_width(unsigned int cpoint) /* returns the width in columns of a Unicode codepoint (somewhat simplified) */ { diff --git a/xs_url.h b/xs_url.h index 7bdff49..222771f 100644 --- a/xs_url.h +++ b/xs_url.h @@ -6,6 +6,7 @@ xs_str *xs_url_dec(const char *str); xs_str *xs_url_enc(const char *str); +xs_str *xs_url_dec_emoji(const char *str); xs_dict *xs_url_vars(const char *str); xs_dict *xs_multipart_form_data(const char *payload, int p_size, const char *header); @@ -79,6 +80,38 @@ xs_str *xs_url_dec(const char *str) } +xs_str *xs_url_dec_emoji(const char *str) +/* decodes an URL, returns NULL if not every char is an encoded hex */ +{ + xs_str *s = xs_str_new(NULL); + + while (*str) { + if (!xs_is_string(str)) + break; + + if (*str == '%') { + unsigned int i; + + if (sscanf(str + 1, "%02x", &i) == 1) { + unsigned char uc = i; + + if (!xs_is_string((char *)&uc)) + break; + + s = xs_append_m(s, (char *)&uc, 1); + str += 2; + } + } + else + return NULL; + + str++; + } + + return s; +} + + xs_str *xs_url_enc(const char *str) /* URL-encodes a string (RFC 3986) */ { -- cgit From 7c065cbc9879582985cef3d3ad8b2ae197b8b851 Mon Sep 17 00:00:00 2001 From: grunfink Date: Thu, 18 Dec 2025 08:09:10 +0100 Subject: Fixed some xs_dup() leaks. --- activitypub.c | 2 +- data.c | 2 +- html.c | 4 ++-- 3 files changed, 4 insertions(+), 4 deletions(-) (limited to 'html.c') diff --git a/activitypub.c b/activitypub.c index 19f0cc6..d19a428 100644 --- a/activitypub.c +++ b/activitypub.c @@ -1643,7 +1643,7 @@ xs_dict *msg_emoji_init(snac *snac, const char *mid, const char *eid) n_msg = xs_dict_set(n_msg, "content", content); n_msg = xs_dict_set(n_msg, "accounts", accounts); - n_msg = xs_dict_set(n_msg, "attributedTo", xs_list_get(xs_dup(xs_dict_get(n_msg, "to")), 1)); + n_msg = xs_dict_set(n_msg, "attributedTo", xs_list_get(xs_dict_get(n_msg, "to"), 1)); n_msg = xs_dict_set(n_msg, "accountId", snac->uid); n_msg = xs_dict_set(n_msg, "tag", tag); diff --git a/data.c b/data.c index 07eea3c..bbe92b8 100644 --- a/data.c +++ b/data.c @@ -1599,7 +1599,7 @@ int timeline_admire(snac *snac, const char *id, /* use utf <3 as a like, as it is ugly */ if (type && xs_match(type, "Like|EmojiReact|Emoji") && content && strcmp(content, "❤") != 0) { - ret = timeline_emoji_react(xs_dup(snac->actor), id, xs_dup(msg)); + ret = timeline_emoji_react(snac->actor, id, xs_dup(msg)); snac_debug(snac, 1, xs_fmt("timeline_emoji_react %s", id)); } diff --git a/html.c b/html.c index 8cc0067..dae84b7 100644 --- a/html.c +++ b/html.c @@ -5173,9 +5173,9 @@ int html_post_handler(const xs_dict *req, const char *q_path, } else if (strcmp(action, L("EmojiReact")) == 0) { /** **/ - const char *eid = xs_dict_get(p_vars, "eid"); + xs *eid = xs_dup(xs_dict_get(p_vars, "eid")); - eid = xs_strip_chars_i(xs_dup(eid), ":"); + eid = xs_strip_chars_i(eid, ":"); const xs_dict *ret = msg_emoji_init(&snac, id, eid); /* fails if either invalid or already reacted */ -- cgit From ce667add2cfedcc6d57c521899dfb9d2e3e6e1d7 Mon Sep 17 00:00:00 2001 From: grunfink Date: Thu, 18 Dec 2025 08:13:54 +0100 Subject: Fixed some xs_join() uses. --- html.c | 19 ++++++++++++------- 1 file changed, 12 insertions(+), 7 deletions(-) (limited to 'html.c') diff --git a/html.c b/html.c index dae84b7..6ee0707 100644 --- a/html.c +++ b/html.c @@ -72,8 +72,10 @@ xs_str *_replace_shortnames(xs_str *s, const xs_list *tag, int ems, xs *style = xs_fmt("max-height: %dem; max-width: %dem;", ems, ems); xs *class = xs_fmt("snac-emoji snac-emoji-%d-em", ems); - if (cl) - class = xs_str_cat(class, " ", xs_join(cl, " ")); + if (cl) { + xs *l = xs_join(cl, " "); + class = xs_str_cat(class, " ", l); + } int c = 0; const xs_val *v; @@ -2501,9 +2503,10 @@ xs_html *html_entry(snac *user, xs_dict *msg, int read_only, if (!xs_is_null(nb)) { is_emoji = 1; - const char *act = atoi(nb) > 1 ? - xs_fmt("%d different actors \n\t%s", atoi(nb), xs_join(actors, ",\n\t")) : - xs_dict_get(m, "actor"); + xs *al = xs_join(actors, ",\n\t"); + xs *act = atoi(nb) > 1 ? + xs_fmt("%d different actors \n\t%s", atoi(nb), al) : + xs_dup(xs_dict_get(m, "actor")); xs *class = xs_list_new(); class = xs_list_append(class, "snac-reaction"); @@ -2517,6 +2520,7 @@ xs_html *html_entry(snac *user, xs_dict *msg, int read_only, if (me) class = xs_list_append(class, "snac-reacted"); + xs *l1 = xs_join(class, " "); ret = xs_html_tag("button", xs_html_attr("type", "submit"), xs_html_attr("name", "action"), @@ -2526,7 +2530,7 @@ xs_html *html_entry(snac *user, xs_dict *msg, int read_only, xs_html_raw(nb), xs_html_attr("style", "padding-left: 5px;")), xs_html_attr("title", act), - xs_html_attr("class", xs_join(class, " "))); + xs_html_attr("class", l1)); if (!(ide && xs_startswith(ide, srv_baseurl))) xs_html_add(ret, xs_html_attr("disabled", "true")); @@ -2540,6 +2544,7 @@ xs_html *html_entry(snac *user, xs_dict *msg, int read_only, const char *style = "font-size: large;"; if (me) class = xs_list_append(class, "snac-reacted"); + xs *l1 = xs_join(class, " "); ret = xs_html_tag("button", xs_html_attr("type", "submit"), xs_html_attr("name", "action"), @@ -2549,7 +2554,7 @@ xs_html *html_entry(snac *user, xs_dict *msg, int read_only, xs_html_raw(nb), xs_html_attr("style", "font-size: initial; padding-left: 5px;")), xs_html_attr("title", act), - xs_html_attr("class", xs_join(class, " ")), + xs_html_attr("class", l1), xs_html_attr("style", style)); } } -- cgit From c8ee70f6df7276b7c02f902cc544d209139697b2 Mon Sep 17 00:00:00 2001 From: grunfink Date: Thu, 18 Dec 2025 08:20:15 +0100 Subject: Fixed some xs_fmt() usages. --- html.c | 11 +++++++---- mastoapi.c | 9 ++++++--- 2 files changed, 13 insertions(+), 7 deletions(-) (limited to 'html.c') diff --git a/html.c b/html.c index 6ee0707..c317c1a 100644 --- a/html.c +++ b/html.c @@ -55,7 +55,7 @@ int login(snac *user, const xs_dict *headers) } xs_str *_replace_shortnames(xs_str *s, const xs_list *tag, int ems, - const char *proxy, const xs_list *cl, const char *act) + const char *proxy, const xs_list *cl, const char *act_o) /* replace but also adds a class list and an actor in its alt text. * Used for emoji reactions */ { @@ -107,7 +107,7 @@ xs_str *_replace_shortnames(xs_str *s, const xs_list *tag, int ems, if (!xs_is_string(mt)) mt = xs_mime_by_ext(u); - act = act ? xs_fmt("%s\n%s", n, act) : xs_fmt("%s", n); + xs *act = act_o ? xs_fmt("%s\n%s", n, act) : xs_fmt("%s", n); if (strcmp(mt, "image/svg+xml") == 0 && !xs_is_true(xs_dict_get(srv_config, "enable_svg"))) s = xs_replace_i(s, n, ""); @@ -2479,7 +2479,9 @@ xs_html *html_entry(snac *user, xs_dict *msg, int read_only, } xs *fl = xs_list_new(); - fl = xs_list_append(fl, xs_fmt("%d", count), actors, xs_fmt("%d", me)); + xs *c1 = xs_fmt("%d", count); + xs *c2 = xs_fmt("%d", me); + fl = xs_list_append(fl, c1, actors, c2); sfrl = xs_dict_append(sfrl, content, fl); } } @@ -2545,11 +2547,12 @@ xs_html *html_entry(snac *user, xs_dict *msg, int read_only, if (me) class = xs_list_append(class, "snac-reacted"); xs *l1 = xs_join(class, " "); + xs *s1 = xs_fmt("&#%d", utf); ret = xs_html_tag("button", xs_html_attr("type", "submit"), xs_html_attr("name", "action"), xs_html_attr("value", me ? L("EmojiUnreact") : L("EmojiReact")), - xs_html_raw(xs_fmt("&#%d", utf)), + xs_html_raw(s1), xs_html_tag("span", xs_html_raw(nb), xs_html_attr("style", "font-size: initial; padding-left: 5px;")), diff --git a/mastoapi.c b/mastoapi.c index 1d99cd5..72379b5 100644 --- a/mastoapi.c +++ b/mastoapi.c @@ -1185,7 +1185,8 @@ xs_dict *mastoapi_status(snac *snac, const xs_dict *msg) } xs *fl = xs_list_new(); - fl = xs_list_append(fl, xs_fmt("%d", count), me); + xs *c1 = xs_fmt("%d", count); + fl = xs_list_append(fl, c1, me); sfrl = xs_dict_append(sfrl, content, fl); } } @@ -2399,8 +2400,10 @@ int mastoapi_get_handler(const xs_dict *req, const char *q_path, mn = xs_dict_append(mn, "emoji_url", url); } - if (xs_is_emoji((utf = xs_utf8_dec(&content)))) - mn = xs_dict_append(mn, "name", xs_fmt("&#%d;", utf)); + if (xs_is_emoji((utf = xs_utf8_dec(&content)))) { + xs *s1 = xs_fmt("&#%d;", utf); + mn = xs_dict_append(mn, "name", s1); + } } } } -- cgit From 219f3e6808bdcf8a03cce404caa5cb6a008d00c5 Mon Sep 17 00:00:00 2001 From: grunfink Date: Thu, 18 Dec 2025 08:54:21 +0100 Subject: Fixed leak in emoji_reacted(). --- data.c | 6 +++--- html.c | 5 +++-- snac.h | 2 +- 3 files changed, 7 insertions(+), 6 deletions(-) (limited to 'html.c') diff --git a/data.c b/data.c index 8f7050c..023bae6 100644 --- a/data.c +++ b/data.c @@ -1959,19 +1959,19 @@ xs_list *muted_list(snac *user) /** emojis react **/ -const xs_str *emoji_reacted(snac *user, const char *id) +xs_str *emoji_reacted(snac *user, const char *id) /* returns the emoji an user reacted to a message */ { xs *emojis = object_get_emoji_reacts(id); int c = 0; const char *v; - xs_dict *msg; while (xs_list_next(emojis, &v, &c)) { + xs *msg = NULL; if (object_get_by_md5(v, &msg)) { const xs_val *act = xs_dict_get(msg, "actor"); if (act && strcmp(act, user->actor) == 0) - return xs_dict_get(msg, "content"); + return xs_dup(xs_dict_get(msg, "content")); } } return NULL; diff --git a/html.c b/html.c index c317c1a..48273f2 100644 --- a/html.c +++ b/html.c @@ -2107,14 +2107,15 @@ xs_html *html_entry_controls(snac *user, const char *actor, /* the post textarea */ xs *div_id = xs_fmt("%s_reply", md5); xs *form_id = xs_fmt("%s_reply_form", md5); + xs *e_react = emoji_reacted(user, id); xs_html_add(controls, xs_html_tag("div", xs_html_tag("p", NULL), html_emoji( - user, L("Emoji react"), + user, L("Emoji react..."), div_id, form_id, ":neocat:", id, - emoji_reacted(user, id))), + e_react)), xs_html_tag("p", NULL)); } diff --git a/snac.h b/snac.h index 23f363c..bcff7f8 100644 --- a/snac.h +++ b/snac.h @@ -205,7 +205,7 @@ void unmute(snac *snac, const char *actor); int is_muted(snac *snac, const char *actor); xs_list *muted_list(snac *user); -const xs_str *emoji_reacted(snac *user, const char *id); +xs_str *emoji_reacted(snac *user, const char *id); int is_bookmarked(snac *user, const char *id); int bookmark(snac *user, const char *id); -- cgit From 2903b29e653d8549319ceabbb65de408d2e5c073 Mon Sep 17 00:00:00 2001 From: grunfink Date: Thu, 18 Dec 2025 08:58:04 +0100 Subject: Fixed more minor leaks. --- html.c | 3 ++- mastoapi.c | 3 ++- 2 files changed, 4 insertions(+), 2 deletions(-) (limited to 'html.c') diff --git a/html.c b/html.c index 48273f2..8700ac4 100644 --- a/html.c +++ b/html.c @@ -2454,8 +2454,8 @@ xs_html *html_entry(snac *user, xs_dict *msg, int read_only, xs *sfrl = xs_dict_new(); xs *rl = object_get_emoji_reacts(id); - xs_dict *m = NULL; while (xs_list_next(rl, &v, &c)) { + xs *m = NULL; if (valid_status(object_get_by_md5(v, &m))) { const char *content = xs_dict_get(m, "content"); const char *actor = xs_dict_get(m, "actor"); @@ -2490,6 +2490,7 @@ xs_html *html_entry(snac *user, xs_dict *msg, int read_only, c = 0; while (xs_list_next(rl, &k, &c)) { + xs *m = NULL; if (valid_status(object_get_by_md5(k, &m))) { const xs_dict *tag = xs_dict_get(m, "tag"); const xs_dict *ide = xs_dict_get(m, "id"); diff --git a/mastoapi.c b/mastoapi.c index 72379b5..041f37e 100644 --- a/mastoapi.c +++ b/mastoapi.c @@ -1165,8 +1165,8 @@ xs_dict *mastoapi_status(snac *snac, const xs_dict *msg) int c = 0; const char *v; - xs_dict *msg = NULL; while (xs_list_next(rl, &v, &c)) { + xs *msg = NULL; if (valid_status(object_get_by_md5(v, &msg))) { const char *content = xs_dict_get(msg, "content"); const char *actor = xs_dict_get(msg, "actor"); @@ -1194,6 +1194,7 @@ xs_dict *mastoapi_status(snac *snac, const xs_dict *msg) c = 0; while (xs_list_next(rl, &v, &c)) { + xs *msg = NULL; if (valid_status(object_get_by_md5(v, &msg))) { xs *d1 = xs_dict_new(); -- cgit From da6d275933e9dac513386ac2bc567d3568ab0217 Mon Sep 17 00:00:00 2001 From: grunfink Date: Thu, 18 Dec 2025 09:45:55 +0100 Subject: Strip surrounding colons in emoji pictures, if they are still there. --- html.c | 2 ++ 1 file changed, 2 insertions(+) (limited to 'html.c') diff --git a/html.c b/html.c index 8700ac4..e59e622 100644 --- a/html.c +++ b/html.c @@ -2521,6 +2521,8 @@ xs_html *html_entry(snac *user, xs_dict *msg, int read_only, cl = xs_list_append(cl, "snac-reaction-image"); xs *emoji = _replace_shortnames(xs_dup(shortname), tag, 2, proxy, cl, act); + emoji = xs_strip_chars_i(emoji, ":"); + if (me) class = xs_list_append(class, "snac-reacted"); -- cgit From 9f3f8d237ac32f60c8471515dcb64095f1c4ccd6 Mon Sep 17 00:00:00 2001 From: grunfink Date: Thu, 18 Dec 2025 10:02:43 +0100 Subject: Fixed typo. --- html.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'html.c') diff --git a/html.c b/html.c index e59e622..f1b004b 100644 --- a/html.c +++ b/html.c @@ -107,7 +107,7 @@ xs_str *_replace_shortnames(xs_str *s, const xs_list *tag, int ems, if (!xs_is_string(mt)) mt = xs_mime_by_ext(u); - xs *act = act_o ? xs_fmt("%s\n%s", n, act) : xs_fmt("%s", n); + xs *act = act_o ? xs_fmt("%s\n%s", n, act_o) : xs_fmt("%s", n); if (strcmp(mt, "image/svg+xml") == 0 && !xs_is_true(xs_dict_get(srv_config, "enable_svg"))) s = xs_replace_i(s, n, ""); -- cgit From e233f3f904e64d6ecaffdf68f8532e1587681fe3 Mon Sep 17 00:00:00 2001 From: violette Date: Fri, 19 Dec 2025 09:54:50 +0100 Subject: Some fixes. --- html.c | 9 +++++---- mastoapi.c | 2 +- 2 files changed, 6 insertions(+), 5 deletions(-) (limited to 'html.c') diff --git a/html.c b/html.c index f1b004b..f6c93c7 100644 --- a/html.c +++ b/html.c @@ -2500,11 +2500,12 @@ xs_html *html_entry(snac *user, xs_dict *msg, int read_only, shortname = xs_dict_get(m, "content"); const xs_list *items = xs_dict_get(sfrl, content); - const char *nb = xs_list_get(items, 0); - const xs_list *actors = xs_list_get(items, 1); - const char me = *xs_list_get(items, 2) == '1'; - if (!xs_is_null(nb)) { + if (!xs_is_null(items)) { + const char *nb = xs_list_get(items, 0); + const xs_list *actors = xs_list_get(items, 1); + const char me = *xs_list_get(items, 2) == '1'; + is_emoji = 1; xs *al = xs_join(actors, ",\n\t"); diff --git a/mastoapi.c b/mastoapi.c index 041f37e..d38e902 100644 --- a/mastoapi.c +++ b/mastoapi.c @@ -1174,7 +1174,7 @@ xs_dict *mastoapi_status(snac *snac, const xs_dict *msg) /* NOTE: idk when there are no actor, but i encountered that bug. * Probably because of one of my previous attempts. * Keeping this just in case, can remove later */ - const char *me = actor && strcmp(actor, snac->actor) == 0 ? + const char *me = actor && snac && strcmp(actor, snac->actor) == 0 ? xs_stock(XSTYPE_TRUE) : xs_stock(XSTYPE_FALSE); int count = 1; -- cgit From a97d3ac11628cd9244f97d2304d972abfb2b4824 Mon Sep 17 00:00:00 2001 From: grunfink Date: Fri, 19 Dec 2025 15:27:51 +0100 Subject: Reindex tags when editing a post. Tags that were deleted in the post message are not deleted, though. --- html.c | 3 +++ 1 file changed, 3 insertions(+) (limited to 'html.c') diff --git a/html.c b/html.c index f6c93c7..0b84e56 100644 --- a/html.c +++ b/html.c @@ -5141,6 +5141,9 @@ int html_post_handler(const xs_dict *req, const char *q_path, /* overwrite object, not updating the indexes */ object_add_ow(edit_id, msg); + /* index tags */ + tag_index(edit_id, msg); + /* update message */ c_msg = msg_update(&snac, msg); } -- cgit From 79c32485dffa95107216c2251ffa111d7cdef432 Mon Sep 17 00:00:00 2001 From: grunfink Date: Sat, 20 Dec 2025 13:15:34 +0100 Subject: Fixed failed search when blank surround the query string. --- html.c | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) (limited to 'html.c') diff --git a/html.c b/html.c index 0b84e56..20ad839 100644 --- a/html.c +++ b/html.c @@ -4343,9 +4343,13 @@ int html_get_handler(const xs_dict *req, const char *q_path, status = HTTP_STATUS_UNAUTHORIZED; } else { - const char *q = xs_dict_get(q_vars, "q"); + xs *q = NULL; + const char *q1 = xs_dict_get(q_vars, "q"); xs *url_acct = NULL; + if (xs_is_string(q1)) + q = xs_strip_i(xs_dup(q1)); + /* searching for an URL? */ if (q && xs_match(q, "https://*|http://*")) { /* may by an actor; try a webfinger */ -- cgit From a1f0a89931ad1ac51e28c318e29ef7f1cfc25255 Mon Sep 17 00:00:00 2001 From: grunfink Date: Fri, 26 Dec 2025 21:15:19 +0100 Subject: Revert "Fixed failed search when blank surround the query string." This reverts commit 79c32485dffa95107216c2251ffa111d7cdef432. --- html.c | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) (limited to 'html.c') diff --git a/html.c b/html.c index 20ad839..0b84e56 100644 --- a/html.c +++ b/html.c @@ -4343,13 +4343,9 @@ int html_get_handler(const xs_dict *req, const char *q_path, status = HTTP_STATUS_UNAUTHORIZED; } else { - xs *q = NULL; - const char *q1 = xs_dict_get(q_vars, "q"); + const char *q = xs_dict_get(q_vars, "q"); xs *url_acct = NULL; - if (xs_is_string(q1)) - q = xs_strip_i(xs_dup(q1)); - /* searching for an URL? */ if (q && xs_match(q, "https://*|http://*")) { /* may by an actor; try a webfinger */ -- cgit From be4dd4ea7f58a31546ad873d5b464fdb8b289ec8 Mon Sep 17 00:00:00 2001 From: grunfink Date: Fri, 26 Dec 2025 21:24:08 +0100 Subject: Fixed failed search when blank surround the query string. This time, without fucking up everything. --- html.c | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) (limited to 'html.c') diff --git a/html.c b/html.c index 0b84e56..3e692bd 100644 --- a/html.c +++ b/html.c @@ -4343,9 +4343,15 @@ int html_get_handler(const xs_dict *req, const char *q_path, status = HTTP_STATUS_UNAUTHORIZED; } else { - const char *q = xs_dict_get(q_vars, "q"); + const char *q = NULL; + xs *cq = xs_dup(xs_dict_get(q_vars, "q")); xs *url_acct = NULL; + if (xs_is_string(cq)) { + cq = xs_strip_i(cq); + q = cq; + } + /* searching for an URL? */ if (q && xs_match(q, "https://*|http://*")) { /* may by an actor; try a webfinger */ -- cgit From b64bebd412f10e7a29f92220624e8423aaf5d883 Mon Sep 17 00:00:00 2001 From: Alexandre Oliva Date: Sun, 28 Dec 2025 20:59:12 +0100 Subject: introduce separate people/ pages When you have lots of followers or followees or pending follows, constructing the entire people page to look up information about a single user can take a while and be quite wasteful when you want to look up a single user. Introduce and prefer people/ over people#. While at that, fix a memory leak in webfinger search: the empty list was allocated twice. --- RELEASE_NOTES.md | 2 ++ html.c | 53 ++++++++++++++++++++++++++++++++++++++++++++++++++--- 2 files changed, 52 insertions(+), 3 deletions(-) (limited to 'html.c') diff --git a/RELEASE_NOTES.md b/RELEASE_NOTES.md index d28b125..0cb2c86 100644 --- a/RELEASE_NOTES.md +++ b/RELEASE_NOTES.md @@ -2,6 +2,8 @@ ## UNRELEASED +Link contacts to single-user people pages (contributed by lxo). + Added emoji reactions (contributed by violette). Mastodon API: Fix for some client notifications (contributed by violette), fix for a status visibility error (contributed by fruye). diff --git a/html.c b/html.c index 3e692bd..c03c867 100644 --- a/html.c +++ b/html.c @@ -221,7 +221,7 @@ xs_html *html_actor_icon(snac *user, xs_dict *actor, const char *date, anchored link to the people page instead of the actor url */ if (fwer || fwing) { xs *md5 = xs_md5_hex(actor_id, strlen(actor_id)); - href = xs_fmt("%s/people#%s", user->actor, md5); + href = xs_fmt("%s/people/%s", user->actor, md5); } } @@ -2355,7 +2355,7 @@ xs_html *html_entry(snac *user, xs_dict *msg, int read_only, } if (!read_only && (fwers || fwing)) - href = xs_fmt("%s/people#%s", user->actor, p); + href = xs_fmt("%s/people/%s", user->actor, p); else href = xs_dup(id); @@ -3856,6 +3856,29 @@ xs_str *html_people(snac *user) return xs_html_render_s(html, "\n"); } +xs_str *html_people_one(snac *user, const char *actor) +{ + const char *proxy = NULL; + + if (xs_is_true(xs_dict_get(srv_config, "proxy_media"))) + proxy = user->actor; + + xs_html *lists = xs_html_tag("div", + xs_html_attr("class", "snac-posts")); + + xs *foll = xs_list_append(xs_list_new(), actor); + + xs_html_add(lists, + html_people_list(user, foll, L("People - single"), "p", proxy)); + + xs_html *html = xs_html_tag("html", + html_user_head(user, NULL, NULL), + xs_html_add(html_user_body(user, 0), + lists, + html_footer(user))); + + return xs_html_render_s(html, "\n"); +} xs_str *html_notifications(snac *user, int skip, int show) { @@ -4415,7 +4438,7 @@ int html_get_handler(const xs_dict *req, const char *q_path, actor_add(actor, actor_obj); /* create a people list with only one element */ - l = xs_list_append(xs_list_new(), actor); + l = xs_list_append(l, actor); xs *title = xs_fmt(L("Search results for account %s"), q); @@ -4555,6 +4578,30 @@ int html_get_handler(const xs_dict *req, const char *q_path, } } else + if (xs_startswith(p_path, "people/")) { /** the list of people **/ + if (!login(&snac, req)) { + *body = xs_dup(uid); + status = HTTP_STATUS_UNAUTHORIZED; + } + else { + xs *actor_dict = NULL; + const char *actor_id = NULL; + xs *actor = NULL; + + if (valid_status(object_get_by_md5(p_path + strlen("people/"), &actor_dict)) && + (actor_id = xs_dict_get(actor_dict, "id")) != NULL && + valid_status(actor_get(actor_id, &actor))) { + *body = html_people_one(&snac, actor_id); + *b_size = strlen(*body); + status = HTTP_STATUS_OK; + } + else { + *body = xs_dup(uid); + status = HTTP_STATUS_NOT_FOUND; + } + } + } + else if (strcmp(p_path, "notifications") == 0) { /** the list of notifications **/ if (!login(&snac, req)) { *body = xs_dup(uid); -- cgit From 93f82fae9e024ae4a7338701db762e390056e990 Mon Sep 17 00:00:00 2001 From: Alexandre Oliva Date: Tue, 30 Dec 2025 13:12:46 +0100 Subject: Add posts by actor to people page Select posts by the actor from the given timeline range. Since posts by the actor may be very sparse in the timeline, add a "More (x 10)" button to bump the show count, that controls how many timeline posts we'll filter *from*. We could conceivably keep searching the timeline until we find as many posts as requested or reach the end, but that could take a very long time. Just filtering the given ranges is much simpler, and probably sufficiently intuitive despite the potential initial surprise. --- html.c | 85 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++------ 1 file changed, 78 insertions(+), 7 deletions(-) (limited to 'html.c') diff --git a/html.c b/html.c index c03c867..70c599f 100644 --- a/html.c +++ b/html.c @@ -3856,25 +3856,89 @@ xs_str *html_people(snac *user) return xs_html_render_s(html, "\n"); } -xs_str *html_people_one(snac *user, const char *actor) +/* Filter list to display only posts by actor. We'll probably show + fewer than show posts. Should we try harder to find some? */ +xs_str *html_people_one(snac *user, const char *actor, const xs_list *list, + int skip, int show, int show_more, const char *page) { const char *proxy = NULL; + xs_list *p = (xs_list *)list; + const char *v; if (xs_is_true(xs_dict_get(srv_config, "proxy_media"))) proxy = user->actor; + xs_html *body = html_user_body(user, 0); + xs_html *lists = xs_html_tag("div", xs_html_attr("class", "snac-posts")); xs *foll = xs_list_append(xs_list_new(), actor); xs_html_add(lists, - html_people_list(user, foll, L("People - single"), "p", proxy)); + html_people_list(user, foll, L("Contact's posts"), "p", proxy)); + + xs_html_add(body, lists); + + while (xs_list_iter(&p, &v)) { + xs *msg = NULL; + int status; + + status = timeline_get_by_md5(user, v, &msg); + + if (!valid_status(status)) + continue; + + const char *by = xs_dict_get(msg, "attributedTo"); + if (!by || strcmp(actor, by) != 0) + continue; + + xs_html *entry = html_entry(user, msg, 0, 0, v, 1); + + if (entry != NULL) + xs_html_add(lists, + entry); + } + + if (show_more) { + xs *m = NULL; + xs *m10 = NULL; + xs *ss = xs_fmt("skip=%d&show=%d", skip + show, show); + + xs *url = xs_dup(user == NULL ? srv_baseurl : user->actor); + + if (page != NULL) + url = xs_str_cat(url, page); + + if (xs_str_in(url, "?") != -1) + m = xs_fmt("%s&%s", url, ss); + else + m = xs_fmt("%s?%s", url, ss); + m10 = xs_fmt("%s0", m); + + xs_html *more_links = xs_html_tag("p", + xs_html_tag("a", + xs_html_attr("href", url), + xs_html_attr("name", "snac-more"), + xs_html_text(L("Back to top"))), + xs_html_text(" - "), + xs_html_tag("a", + xs_html_attr("href", m), + xs_html_attr("name", "snac-more"), + xs_html_text(L("More..."))), + xs_html_text(" - "), + xs_html_tag("a", + xs_html_attr("href", m10), + xs_html_attr("name", "snac-more"), + xs_html_text(L("More (x 10)...")))); + + xs_html_add(body, + more_links); + } xs_html *html = xs_html_tag("html", html_user_head(user, NULL, NULL), - xs_html_add(html_user_body(user, 0), - lists, + xs_html_add(body, html_footer(user))); return xs_html_render_s(html, "\n"); @@ -4578,7 +4642,7 @@ int html_get_handler(const xs_dict *req, const char *q_path, } } else - if (xs_startswith(p_path, "people/")) { /** the list of people **/ + if (xs_startswith(p_path, "people/")) { /** a single actor **/ if (!login(&snac, req)) { *body = xs_dup(uid); status = HTTP_STATUS_UNAUTHORIZED; @@ -4587,11 +4651,18 @@ int html_get_handler(const xs_dict *req, const char *q_path, xs *actor_dict = NULL; const char *actor_id = NULL; xs *actor = NULL; + xs_list *page_lst = xs_split_n(p_path, "?", 2); + xs *page = xs_str_cat(xs_str_new("/"), xs_list_get(page_lst, 0)); + xs_list *l = xs_split_n(page, "/", 3); + const char *actor_md5 = xs_list_get(l, 2); - if (valid_status(object_get_by_md5(p_path + strlen("people/"), &actor_dict)) && + if (valid_status(object_get_by_md5(actor_md5, &actor_dict)) && (actor_id = xs_dict_get(actor_dict, "id")) != NULL && valid_status(actor_get(actor_id, &actor))) { - *body = html_people_one(&snac, actor_id); + int more = 0; + xs *list = timeline_list(&snac, "private", skip, show, &more); + + *body = html_people_one(&snac, actor_id, list, skip, show, more, page); *b_size = strlen(*body); status = HTTP_STATUS_OK; } -- cgit From f99895032e33d5c9a80c45bae50fa433f4ea1c48 Mon Sep 17 00:00:00 2001 From: grunfink Date: Tue, 30 Dec 2025 13:14:50 +0100 Subject: Fixed indentation in previous patch (my fault). --- html.c | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) (limited to 'html.c') diff --git a/html.c b/html.c index 70c599f..223cf4c 100644 --- a/html.c +++ b/html.c @@ -3914,7 +3914,8 @@ xs_str *html_people_one(snac *user, const char *actor, const xs_list *list, m = xs_fmt("%s&%s", url, ss); else m = xs_fmt("%s?%s", url, ss); - m10 = xs_fmt("%s0", m); + + m10 = xs_fmt("%s0", m); xs_html *more_links = xs_html_tag("p", xs_html_tag("a", -- cgit From 20eba6f5192176ad0fc3c468cd4bd8dda62f15e6 Mon Sep 17 00:00:00 2001 From: Alexandre Oliva Date: Thu, 1 Jan 2026 16:25:00 +0100 Subject: Add boosts, likes and reacts to actor's people page Show in an actor's page anything that the user could have seen from actor in timeline or notifications, namely: - posts by actor (use get_atto to identify the actor) - boosts by actor - user's posts with likes or emojireacts by actor That said, in this view, only the latest boost is shown for a post, so it might not seem like the boost is by actor. Likes and emojireacts aren't even shown, so the reason why a post appears might be puzzling. Use timeline_simple_list, since we don't show entire conversations, and we want to identify all posts with actor's interactions. Saturate show at max_timeline_entries, so that we don't silently skip entries. --- html.c | 28 ++++++++++++++++++++++++---- 1 file changed, 24 insertions(+), 4 deletions(-) (limited to 'html.c') diff --git a/html.c b/html.c index 223cf4c..3e1d7ba 100644 --- a/html.c +++ b/html.c @@ -3889,8 +3889,22 @@ xs_str *html_people_one(snac *user, const char *actor, const xs_list *list, if (!valid_status(status)) continue; - const char *by = xs_dict_get(msg, "attributedTo"); - if (!by || strcmp(actor, by) != 0) + const char *id = xs_dict_get(msg, "id"); + const char *by = get_atto(msg); + xs *actor_md5 = NULL; + xs_list *boosts = NULL; + xs_list *likes = NULL; + xs_list *reacts = NULL; + /* Besides actor's posts, also show actor's boosts, and also + posts by user with likes or reacts by actor. I.e., any + actor's actions that user could have seen in the timeline + or in notifications. */ + if (!(by && strcmp(actor, by) == 0) && + xs_list_in((boosts = object_announces(id)), + (actor_md5 = xs_md5_hex(actor, strlen(actor)))) == -1 && + (!(by && strcmp(user->actor, by) == 0) || + (xs_list_in((likes = object_likes(id)), actor_md5) == -1 && + xs_list_in((reacts = object_get_emoji_reacts(id)), actor_md5) == -1))) continue; xs_html *entry = html_entry(user, msg, 0, 0, v, 1); @@ -4359,8 +4373,12 @@ int html_get_handler(const xs_dict *req, const char *q_path, cache = 0; int skip = 0; + const char *max_show_default = "50"; + int max_show = xs_number_get(xs_dict_get_def(srv_config, "max_timeline_entries", + max_show_default)); int def_show = xs_number_get(xs_dict_get_def(srv_config, "def_timeline_entries", - xs_dict_get_def(srv_config, "max_timeline_entries", "50"))); + xs_dict_get_def(srv_config, "max_timeline_entries", + max_show_default))); int show = def_show; if ((v = xs_dict_get(q_vars, "skip")) != NULL) @@ -4386,6 +4404,8 @@ int html_get_handler(const xs_dict *req, const char *q_path, /* a show of 0 has no sense */ if (show == 0) show = def_show; + if (show > max_show) + show = max_show; if (p_path == NULL) { /** public timeline **/ xs *h = xs_str_localtime(0, "%Y-%m.html"); @@ -4661,7 +4681,7 @@ int html_get_handler(const xs_dict *req, const char *q_path, (actor_id = xs_dict_get(actor_dict, "id")) != NULL && valid_status(actor_get(actor_id, &actor))) { int more = 0; - xs *list = timeline_list(&snac, "private", skip, show, &more); + xs *list = timeline_simple_list(&snac, "private", skip, show, &more); *body = html_people_one(&snac, actor_id, list, skip, show, more, page); *b_size = strlen(*body); -- cgit From 11af00194e3e0ec15e17a23556dc2929f92e0210 Mon Sep 17 00:00:00 2001 From: grunfink Date: Thu, 1 Jan 2026 17:01:03 +0100 Subject: Bumped copyright year. --- LICENSE | 2 +- activitypub.c | 2 +- data.c | 2 +- format.c | 2 +- html.c | 2 +- http.c | 2 +- httpd.c | 2 +- main.c | 4 ++-- mastoapi.c | 2 +- rss.c | 2 +- snac.c | 2 +- snac.h | 2 +- upgrade.c | 2 +- utils.c | 2 +- webfinger.c | 2 +- xs.h | 2 +- xs_curl.h | 2 +- xs_fcgi.h | 2 +- xs_glob.h | 2 +- xs_hex.h | 2 +- xs_html.h | 2 +- xs_http.h | 2 +- xs_httpd.h | 2 +- xs_io.h | 2 +- xs_json.h | 4 +++- xs_match.h | 2 +- xs_mime.h | 2 +- xs_openssl.h | 2 +- xs_po.h | 2 +- xs_random.h | 2 +- xs_regex.h | 2 +- xs_set.h | 2 +- xs_socket.h | 2 +- xs_time.h | 2 +- xs_unicode.h | 2 +- xs_unix_socket.h | 2 +- xs_url.h | 2 +- xs_version.h | 2 +- xs_webmention.h | 2 +- 39 files changed, 42 insertions(+), 40 deletions(-) (limited to 'html.c') diff --git a/LICENSE b/LICENSE index 702b487..3fa58d1 100644 --- a/LICENSE +++ b/LICENSE @@ -1,6 +1,6 @@ MIT License -Copyright (c) 2022 - 2025 grunfink et al. (Fediverse: @grunfink@comam.es) +Copyright (c) 2022 - 2026 grunfink et al. (Fediverse: @grunfink@comam.es) Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: diff --git a/activitypub.c b/activitypub.c index 59df31a..617fd30 100644 --- a/activitypub.c +++ b/activitypub.c @@ -1,5 +1,5 @@ /* snac - A simple, minimalistic ActivityPub instance */ -/* copyright (c) 2022 - 2025 grunfink et al. / MIT license */ +/* copyright (c) 2022 - 2026 grunfink et al. / MIT license */ #include "xs.h" #include "xs_json.h" diff --git a/data.c b/data.c index 023bae6..22ea7b0 100644 --- a/data.c +++ b/data.c @@ -1,5 +1,5 @@ /* snac - A simple, minimalistic ActivityPub instance */ -/* copyright (c) 2022 - 2025 grunfink et al. / MIT license */ +/* copyright (c) 2022 - 2026 grunfink et al. / MIT license */ #include "xs.h" #include "xs_hex.h" diff --git a/format.c b/format.c index 4f93b7b..803e5a5 100644 --- a/format.c +++ b/format.c @@ -1,5 +1,5 @@ /* snac - A simple, minimalistic ActivityPub instance */ -/* copyright (c) 2022 - 2025 grunfink et al. / MIT license */ +/* copyright (c) 2022 - 2026 grunfink et al. / MIT license */ #include "xs.h" #include "xs_regex.h" diff --git a/html.c b/html.c index 3e1d7ba..ef8816d 100644 --- a/html.c +++ b/html.c @@ -1,5 +1,5 @@ /* snac - A simple, minimalistic ActivityPub instance */ -/* copyright (c) 2022 - 2025 grunfink et al. / MIT license */ +/* copyright (c) 2022 - 2026 grunfink et al. / MIT license */ #include "xs.h" #include "xs_io.h" diff --git a/http.c b/http.c index f0b1314..4f0b2fa 100644 --- a/http.c +++ b/http.c @@ -1,5 +1,5 @@ /* snac - A simple, minimalistic ActivityPub instance */ -/* copyright (c) 2022 - 2025 grunfink et al. / MIT license */ +/* copyright (c) 2022 - 2026 grunfink et al. / MIT license */ #include "xs.h" #include "xs_io.h" diff --git a/httpd.c b/httpd.c index f93f197..f46cfca 100644 --- a/httpd.c +++ b/httpd.c @@ -1,5 +1,5 @@ /* snac - A simple, minimalistic ActivityPub instance */ -/* copyright (c) 2022 - 2025 grunfink et al. / MIT license */ +/* copyright (c) 2022 - 2026 grunfink et al. / MIT license */ #include "xs.h" #include "xs_io.h" diff --git a/main.c b/main.c index 4b0463e..29a8a51 100644 --- a/main.c +++ b/main.c @@ -1,5 +1,5 @@ /* snac - A simple, minimalistic ActivityPub instance */ -/* copyright (c) 2022 - 2025 grunfink et al. / MIT license */ +/* copyright (c) 2022 - 2026 grunfink et al. / MIT license */ #include "xs.h" #include "xs_io.h" @@ -18,7 +18,7 @@ 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("Copyright (c) 2022 - 2026 grunfink et al. / MIT license\n"); printf("\n"); if (cmd == NULL) { diff --git a/mastoapi.c b/mastoapi.c index 6a12ced..0cadf2c 100644 --- a/mastoapi.c +++ b/mastoapi.c @@ -1,5 +1,5 @@ /* snac - A simple, minimalistic ActivityPub instance */ -/* copyright (c) 2022 - 2025 grunfink et al. / MIT license */ +/* copyright (c) 2022 - 2026 grunfink et al. / MIT license */ #ifndef NO_MASTODON_API diff --git a/rss.c b/rss.c index 09446dc..dc26071 100644 --- a/rss.c +++ b/rss.c @@ -1,5 +1,5 @@ /* snac - A simple, minimalistic ActivityPub instance */ -/* copyright (c) 2025 grunfink et al. / MIT license */ +/* copyright (c) 2025 - 2026 grunfink et al. / MIT license */ #include "xs.h" #include "xs_html.h" diff --git a/snac.c b/snac.c index 31f524f..965edbb 100644 --- a/snac.c +++ b/snac.c @@ -1,5 +1,5 @@ /* snac - A simple, minimalistic ActivityPub instance */ -/* copyright (c) 2022 - 2025 grunfink et al. / MIT license */ +/* copyright (c) 2022 - 2026 grunfink et al. / MIT license */ #define XS_IMPLEMENTATION diff --git a/snac.h b/snac.h index bcff7f8..430b551 100644 --- a/snac.h +++ b/snac.h @@ -1,5 +1,5 @@ /* snac - A simple, minimalistic ActivityPub instance */ -/* copyright (c) 2022 - 2025 grunfink et al. / MIT license */ +/* copyright (c) 2022 - 2026 grunfink et al. / MIT license */ #define VERSION "2.86-dev" diff --git a/upgrade.c b/upgrade.c index 9e0ae6e..07e9c51 100644 --- a/upgrade.c +++ b/upgrade.c @@ -1,5 +1,5 @@ /* snac - A simple, minimalistic ActivityPub instance */ -/* copyright (c) 2022 - 2025 grunfink et al. / MIT license */ +/* copyright (c) 2022 - 2026 grunfink et al. / MIT license */ #include "xs.h" #include "xs_io.h" diff --git a/utils.c b/utils.c index 44e4006..4bdbaaf 100644 --- a/utils.c +++ b/utils.c @@ -1,5 +1,5 @@ /* snac - A simple, minimalistic ActivityPub instance */ -/* copyright (c) 2022 - 2025 grunfink et al. / MIT license */ +/* copyright (c) 2022 - 2026 grunfink et al. / MIT license */ #include "xs.h" #include "xs_io.h" diff --git a/webfinger.c b/webfinger.c index 1ce5e76..264cb85 100644 --- a/webfinger.c +++ b/webfinger.c @@ -1,5 +1,5 @@ /* snac - A simple, minimalistic ActivityPub instance */ -/* copyright (c) 2022 - 2025 grunfink et al. / MIT license */ +/* copyright (c) 2022 - 2026 grunfink et al. / MIT license */ #include "xs.h" #include "xs_json.h" diff --git a/xs.h b/xs.h index 5f2e6aa..11f0722 100644 --- a/xs.h +++ b/xs.h @@ -1,4 +1,4 @@ -/* copyright (c) 2022 - 2025 grunfink et al. / MIT license */ +/* copyright (c) 2022 - 2026 grunfink et al. / MIT license */ #ifndef _XS_H diff --git a/xs_curl.h b/xs_curl.h index feb23e0..4b510ac 100644 --- a/xs_curl.h +++ b/xs_curl.h @@ -1,4 +1,4 @@ -/* copyright (c) 2022 - 2025 grunfink et al. / MIT license */ +/* copyright (c) 2022 - 2026 grunfink et al. / MIT license */ #ifndef _XS_CURL_H diff --git a/xs_fcgi.h b/xs_fcgi.h index b3cb892..0af6fcc 100644 --- a/xs_fcgi.h +++ b/xs_fcgi.h @@ -1,4 +1,4 @@ -/* copyright (c) 2022 - 2025 grunfink et al. / MIT license */ +/* copyright (c) 2022 - 2026 grunfink et al. / MIT license */ /* This is an intentionally-dead-simple FastCGI implementation; diff --git a/xs_glob.h b/xs_glob.h index 6d87ca1..c38ec9a 100644 --- a/xs_glob.h +++ b/xs_glob.h @@ -1,4 +1,4 @@ -/* copyright (c) 2022 - 2025 grunfink et al. / MIT license */ +/* copyright (c) 2022 - 2026 grunfink et al. / MIT license */ #ifndef _XS_GLOB_H diff --git a/xs_hex.h b/xs_hex.h index 2507d5d..91c528e 100644 --- a/xs_hex.h +++ b/xs_hex.h @@ -1,4 +1,4 @@ -/* copyright (c) 2022 - 2025 grunfink et al. / MIT license */ +/* copyright (c) 2022 - 2026 grunfink et al. / MIT license */ #ifndef _XS_HEX_H diff --git a/xs_html.h b/xs_html.h index 29886c6..ab77499 100644 --- a/xs_html.h +++ b/xs_html.h @@ -1,4 +1,4 @@ -/* copyright (c) 2022 - 2025 grunfink et al. / MIT license */ +/* copyright (c) 2022 - 2026 grunfink et al. / MIT license */ #ifndef _XS_HTML_H diff --git a/xs_http.h b/xs_http.h index f2fd296..b9b5f80 100644 --- a/xs_http.h +++ b/xs_http.h @@ -1,4 +1,4 @@ -/* copyright (c) 2022 - 2025 grunfink et al. / MIT license */ +/* copyright (c) 2022 - 2026 grunfink et al. / MIT license */ #ifndef _XS_HTTP_H diff --git a/xs_httpd.h b/xs_httpd.h index 9be51e6..4a4689c 100644 --- a/xs_httpd.h +++ b/xs_httpd.h @@ -1,4 +1,4 @@ -/* copyright (c) 2022 - 2025 grunfink et al. / MIT license */ +/* copyright (c) 2022 - 2026 grunfink et al. / MIT license */ #ifndef _XS_HTTPD_H diff --git a/xs_io.h b/xs_io.h index 9c5018e..9a03e5b 100644 --- a/xs_io.h +++ b/xs_io.h @@ -1,4 +1,4 @@ -/* copyright (c) 2022 - 2025 grunfink et al. / MIT license */ +/* copyright (c) 2022 - 2026 grunfink et al. / MIT license */ #ifndef _XS_IO_H diff --git a/xs_json.h b/xs_json.h index 07800fa..a9577ee 100644 --- a/xs_json.h +++ b/xs_json.h @@ -1,4 +1,4 @@ -/* copyright (c) 2022 - 2025 grunfink et al. / MIT license */ +/* copyright (c) 2022 - 2026 grunfink et al. / MIT license */ #ifndef _XS_JSON_H @@ -28,6 +28,8 @@ int xs_json_load_object(FILE *f, int maxdepth, xs_dict **d); /** IMPLEMENTATION **/ +#include "xs_unicode.h" + /** JSON dumps **/ static void _xs_json_dump_str(const char *data, FILE *f) diff --git a/xs_match.h b/xs_match.h index 76c1bf3..21830e8 100644 --- a/xs_match.h +++ b/xs_match.h @@ -1,4 +1,4 @@ -/* copyright (c) 2022 - 2025 grunfink et al. / MIT license */ +/* copyright (c) 2022 - 2026 grunfink et al. / MIT license */ #ifndef _XS_MATCH_H diff --git a/xs_mime.h b/xs_mime.h index 0146385..6709ec6 100644 --- a/xs_mime.h +++ b/xs_mime.h @@ -1,4 +1,4 @@ -/* copyright (c) 2022 - 2025 grunfink et al. / MIT license */ +/* copyright (c) 2022 - 2026 grunfink et al. / MIT license */ #ifndef _XS_MIME_H diff --git a/xs_openssl.h b/xs_openssl.h index f215bcc..64b59dd 100644 --- a/xs_openssl.h +++ b/xs_openssl.h @@ -1,4 +1,4 @@ -/* copyright (c) 2022 - 2025 grunfink et al. / MIT license */ +/* copyright (c) 2022 - 2026 grunfink et al. / MIT license */ #ifndef _XS_OPENSSL_H diff --git a/xs_po.h b/xs_po.h index c6feec9..c1ad2c9 100644 --- a/xs_po.h +++ b/xs_po.h @@ -1,4 +1,4 @@ -/* copyright (c) 2025 grunfink et al. / MIT license */ +/* copyright (c) 2025 - 2026 grunfink et al. / MIT license */ #ifndef _XS_PO_H diff --git a/xs_random.h b/xs_random.h index f936099..357f9cb 100644 --- a/xs_random.h +++ b/xs_random.h @@ -1,4 +1,4 @@ -/* copyright (c) 2022 - 2025 grunfink et al. / MIT license */ +/* copyright (c) 2022 - 2026 grunfink et al. / MIT license */ #ifndef _XS_RANDOM_H diff --git a/xs_regex.h b/xs_regex.h index 1e157d2..5ccefda 100644 --- a/xs_regex.h +++ b/xs_regex.h @@ -1,4 +1,4 @@ -/* copyright (c) 2022 - 2025 grunfink et al. / MIT license */ +/* copyright (c) 2022 - 2026 grunfink et al. / MIT license */ #ifndef _XS_REGEX_H diff --git a/xs_set.h b/xs_set.h index 8946e42..ae67cc7 100644 --- a/xs_set.h +++ b/xs_set.h @@ -1,4 +1,4 @@ -/* copyright (c) 2022 - 2025 grunfink et al. / MIT license */ +/* copyright (c) 2022 - 2026 grunfink et al. / MIT license */ #ifndef _XS_SET_H diff --git a/xs_socket.h b/xs_socket.h index 7bf5298..70bfe98 100644 --- a/xs_socket.h +++ b/xs_socket.h @@ -1,4 +1,4 @@ -/* copyright (c) 2022 - 2025 grunfink et al. / MIT license */ +/* copyright (c) 2022 - 2026 grunfink et al. / MIT license */ #ifndef _XS_SOCKET_H diff --git a/xs_time.h b/xs_time.h index 507d238..46b35a1 100644 --- a/xs_time.h +++ b/xs_time.h @@ -1,4 +1,4 @@ -/* copyright (c) 2022 - 2025 grunfink et al. / MIT license */ +/* copyright (c) 2022 - 2026 grunfink et al. / MIT license */ #ifndef _XS_TIME_H diff --git a/xs_unicode.h b/xs_unicode.h index 0b4de1c..7686dcd 100644 --- a/xs_unicode.h +++ b/xs_unicode.h @@ -1,4 +1,4 @@ -/* copyright (c) 2022 - 2025 grunfink et al. / MIT license */ +/* copyright (c) 2022 - 2026 grunfink et al. / MIT license */ #ifndef _XS_UNICODE_H diff --git a/xs_unix_socket.h b/xs_unix_socket.h index 462a5b3..1ef7d39 100644 --- a/xs_unix_socket.h +++ b/xs_unix_socket.h @@ -1,4 +1,4 @@ -/* copyright (c) 2022 - 2025 grunfink et al. / MIT license */ +/* copyright (c) 2022 - 2026 grunfink et al. / MIT license */ #ifndef _XS_UNIX_SOCKET_H diff --git a/xs_url.h b/xs_url.h index 222771f..56d6d9d 100644 --- a/xs_url.h +++ b/xs_url.h @@ -1,4 +1,4 @@ -/* copyright (c) 2022 - 2025 grunfink et al. / MIT license */ +/* copyright (c) 2022 - 2026 grunfink et al. / MIT license */ #ifndef _XS_URL_H diff --git a/xs_version.h b/xs_version.h index 34a7a45..598c72e 100644 --- a/xs_version.h +++ b/xs_version.h @@ -1 +1 @@ -/* 0a8b987d7bb945fe7844411727d03ac73f417455 2025-10-14T05:21:05+02:00 */ +/* ad74258be9b1585840a5366cdb4b6ef707c0e95a 2026-01-01T16:58:39+01:00 */ diff --git a/xs_webmention.h b/xs_webmention.h index f9578b4..035b3a6 100644 --- a/xs_webmention.h +++ b/xs_webmention.h @@ -1,4 +1,4 @@ -/* copyright (c) 2025 grunfink et al. / MIT license */ +/* copyright (c) 2025 - 2026 grunfink et al. / MIT license */ #ifndef _XS_WEBMENTION_H -- cgit From 6c876107aba84d2c066b62929ab094bf6d92ef71 Mon Sep 17 00:00:00 2001 From: grunfink Date: Tue, 6 Jan 2026 14:26:57 +0100 Subject: Hide EmojiReacts from muted actors and blocked instances. --- html.c | 4 ++++ 1 file changed, 4 insertions(+) (limited to 'html.c') diff --git a/html.c b/html.c index ef8816d..d5a59d8 100644 --- a/html.c +++ b/html.c @@ -2460,6 +2460,10 @@ xs_html *html_entry(snac *user, xs_dict *msg, int read_only, const char *content = xs_dict_get(m, "content"); const char *actor = xs_dict_get(m, "actor"); const xs_list *contentl = xs_dict_get(sfrl, content); + + if (is_muted(user, actor) || is_instance_blocked(actor)) + continue; + xs *actors = xs_list_new(); actors = xs_list_append(actors, actor); char me = actor && user && strcmp(actor, user->actor) == 0; -- cgit From a01e4b148fd32a015167478171443047507c168d Mon Sep 17 00:00:00 2001 From: grunfink Date: Tue, 6 Jan 2026 18:53:38 +0100 Subject: Fixed crash. --- html.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'html.c') diff --git a/html.c b/html.c index d5a59d8..4299396 100644 --- a/html.c +++ b/html.c @@ -2461,7 +2461,7 @@ xs_html *html_entry(snac *user, xs_dict *msg, int read_only, const char *actor = xs_dict_get(m, "actor"); const xs_list *contentl = xs_dict_get(sfrl, content); - if (is_muted(user, actor) || is_instance_blocked(actor)) + if ((user && is_muted(user, actor)) || is_instance_blocked(actor)) continue; xs *actors = xs_list_new(); -- cgit From c6642e958b628426f2c7b0d71b427a7960cf063c Mon Sep 17 00:00:00 2001 From: grunfink Date: Thu, 8 Jan 2026 08:43:50 +0100 Subject: Don't show any EmojiReactions if they are disabled. --- html.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'html.c') diff --git a/html.c b/html.c index 4299396..c41d7f4 100644 --- a/html.c +++ b/html.c @@ -2447,7 +2447,7 @@ xs_html *html_entry(snac *user, xs_dict *msg, int read_only, /* add all emoji reacts */ int is_emoji = 0; - { + if (!xs_is_true(xs_dict_get(srv_config, "disable_emojireact"))) { int c = 0; const xs_dict *k; xs *ls = xs_list_new(); -- cgit From 5ef0483ab34892d7ea773a690a7c17e3d102cf46 Mon Sep 17 00:00:00 2001 From: grunfink Date: Thu, 8 Jan 2026 08:47:57 +0100 Subject: Don't show the EmojiReact dropdown if disabled. --- html.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'html.c') diff --git a/html.c b/html.c index c41d7f4..f26ac4d 100644 --- a/html.c +++ b/html.c @@ -2103,7 +2103,7 @@ xs_html *html_entry_controls(snac *user, const char *actor, xs_html_tag("p", NULL)); } - { /** emoji react **/ + if (!xs_is_true(xs_dict_get(srv_config, "disable_emojireact"))) { /** emoji react **/ /* the post textarea */ xs *div_id = xs_fmt("%s_reply", md5); xs *form_id = xs_fmt("%s_reply_form", md5); -- cgit From a8cbbeeb3fdc8ec660270b9112b50d2dee71e55d Mon Sep 17 00:00:00 2001 From: grunfink Date: Thu, 8 Jan 2026 09:05:53 +0100 Subject: Also disable EmojiReact notifications, if so configured. --- html.c | 3 +++ 1 file changed, 3 insertions(+) (limited to 'html.c') diff --git a/html.c b/html.c index f26ac4d..d5294cb 100644 --- a/html.c +++ b/html.c @@ -4030,6 +4030,9 @@ xs_str *html_notifications(snac *user, int skip, int show) if (xs_is_string(id2) && xs_set_add(&rep, id2) != 1) continue; + if (strcmp(type, "EmojiReact") == 0 && xs_is_true(xs_dict_get(srv_config, "disable_emojireact"))) + continue; + object_get(id, &obj); const char *msg_id = NULL; -- cgit From 6ad2365a41dd84ee925d7921a1c5756499d6fb17 Mon Sep 17 00:00:00 2001 From: byte Date: Mon, 12 Jan 2026 01:25:49 +0100 Subject: user-specified word mutes and matching --- activitypub.c | 2 +- html.c | 95 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 96 insertions(+), 1 deletion(-) (limited to 'html.c') diff --git a/activitypub.c b/activitypub.c index 5e55b8a..829f6d5 100644 --- a/activitypub.c +++ b/activitypub.c @@ -2958,7 +2958,7 @@ int process_input_message(snac *snac, const xs_dict *msg, const xs_dict *req) if (xs_is_null(object)) snac_log(snac, xs_fmt("malformed message: no 'id' field")); else - if (timeline_admire(snac, object, actor, 1, xs_dup(msg)) == HTTP_STATUS_CREATED) + if (timeline_admire(snac, object, actor, 1, msg) == HTTP_STATUS_CREATED) snac_log(snac, xs_fmt("new '%s' %s %s", type, actor, object)); else snac_log(snac, xs_fmt("repeated '%s' from %s to %s", type, actor, object)); diff --git a/html.c b/html.c index d5294cb..ebd9331 100644 --- a/html.c +++ b/html.c @@ -16,6 +16,7 @@ #include "xs_url.h" #include "xs_random.h" #include "xs_http.h" +#include "xs_list_tools.h" #include "snac.h" @@ -1864,6 +1865,38 @@ xs_html *html_top_controls(snac *user) xs_html_attr("class", "button"), xs_html_attr("value", L("Update hashtags"))))))); + xs *muted_words_action = xs_fmt("%s/admin/muted-words", user->actor); + xs *muted_words = xs_join(xs_dict_get_def(user->config, + "muted_words", xs_stock(XSTYPE_LIST)), "\n"); + + xs_html_add(top_controls, + xs_html_tag("details", + xs_html_tag("summary", + xs_html_text(L("Muted words..."))), + xs_html_tag("p", + xs_html_text(L("One word per line, partial matches count"))), + xs_html_tag("div", + xs_html_attr("class", "snac-muted-words"), + xs_html_tag("form", + xs_html_attr("autocomplete", "off"), + xs_html_attr("method", "post"), + xs_html_attr("action", muted_words_action), + xs_html_attr("enctype", "multipart/form-data"), + + xs_html_tag("textarea", + xs_html_attr("name", "muted_words"), + xs_html_attr("cols", "40"), + xs_html_attr("rows", "4"), + xs_html_attr("placeholder", "nascar\nsuperbowl\nFIFA"), + xs_html_text(muted_words)), + + xs_html_tag("br", NULL), + + xs_html_sctag("input", + xs_html_attr("type", "submit"), + xs_html_attr("class", "button"), + xs_html_attr("value", L("Update muted words"))))))); + return top_controls; } @@ -2144,6 +2177,30 @@ xs_html *html_entry_controls(snac *user, const char *actor, } +static const xs_str* words_in_content(const xs_list *words, const xs_val *content) +/* returns a word that matches any of the words in content */ +{ + if (!xs_is_list(words) || !xs_is_string(content)) { + return NULL; + } + xs *c = xs_split(content, " "); + xs *sc = xs_list_sort(c, NULL); + + const xs_str *wv; + const xs_str *cv; + xs_list_foreach(words, wv) { + xs_list_foreach(sc, cv) { + xs_tolower_i((xs_str*)cv); + if(xs_str_in(cv, wv) != -1){ + return wv; + } + } + } + + return NULL; +} + + xs_html *html_entry(snac *user, xs_dict *msg, int read_only, int level, const char *md5, int hide_children) { @@ -2438,6 +2495,17 @@ xs_html *html_entry(snac *user, xs_dict *msg, int read_only, xs_html_text(v), xs_html_text(L(" [SENSITIVE CONTENT]")))); } + else + if (user && + /* muted_words is all lowercase and sorted for performance */ + (v = words_in_content(xs_dict_get(user->config, "muted_words"), + xs_dict_get(msg, "content"))) != NULL) { + snac_debug(user, 1, xs_fmt("word %s muted by user preferences: %s", v, id)); + snac_content = xs_html_tag("details", + xs_html_tag("summary", + xs_html_text(L("Muted: ")), + xs_html_text(v))); + } else { snac_content = xs_html_tag("div", NULL); } @@ -5813,6 +5881,33 @@ int html_post_handler(const xs_dict *req, const char *q_path, status = HTTP_STATUS_SEE_OTHER; } + else + if (p_path && strcmp(p_path, "admin/muted-words") == 0) { + const char *words = xs_dict_get(p_vars, "muted_words"); + + if (xs_is_string(words)) { + xs *new_words = xs_list_new(); + xs *l = xs_split(words, "\n"); + const char *v; + + xs_list_foreach(l, v) { + xs *s1 = xs_strip_i(xs_dup(v)); + s1 = xs_replace_i(s1, " ", ""); + + if (*s1 == '\0') + continue; + + xs *s2 = xs_utf8_to_lower(s1); + + new_words = xs_list_insert_sorted(new_words, s2); + } + + snac.config = xs_dict_set(snac.config, "muted_words", new_words); + user_persist(&snac, 0); + } + + status = HTTP_STATUS_SEE_OTHER; + } if (status == HTTP_STATUS_SEE_OTHER) { const char *hard_redir = xs_dict_get(p_vars, "hard-redir"); -- cgit From f5ad346f7d819a1fe71ac6f1bc98e16fe1aead01 Mon Sep 17 00:00:00 2001 From: violette Date: Mon, 12 Jan 2026 05:09:21 +0100 Subject: Use the metadata pronouns. --- doc/style.css | 1 + html.c | 38 ++++++++++++++++++++++++++++++++++++++ utils.c | 1 + 3 files changed, 40 insertions(+) (limited to 'html.c') diff --git a/doc/style.css b/doc/style.css index 2981926..9c8764a 100644 --- a/doc/style.css +++ b/doc/style.css @@ -40,3 +40,4 @@ blockquote { font-style: italic; } a { color: #7799dd } a:visited { color: #aa99dd } } +select { max-width: 40%; } diff --git a/html.c b/html.c index ebd9331..9aaaa50 100644 --- a/html.c +++ b/html.c @@ -162,6 +162,31 @@ xs_str *actor_name(xs_dict *actor, const char *proxy) } +xs_str *actor_pronouns(xs_dict *actor) +/* gets the actor name */ +{ + const xs_list *attachment; + const xs_dict *d; + const char *pronouns = ""; + char *ret; + + if (!xs_is_null((attachment = xs_dict_get(actor, "attachment")))) { + xs_list_foreach(attachment, d) { + char *prop = xs_utf8_to_lower(xs_dict_get(d, "name")); + /* make sure that we are reading the correct metadata */ + if (strlen(prop) == 8 && strcmp(prop, "pronouns") == 0) + pronouns = xs_dict_get(d, "value"); + } + } + + /*

breaks page, cannot nest them */ + ret = xs_replace_i(xs_dup(pronouns), "

", ""); + ret = xs_replace_i(ret, "

", ""); + + return ret; +} + + xs_str *format_text_with_emoji(snac *user, const char *text, int ems, const char *proxy) /* needed when we have local text with no tags attached */ { @@ -195,6 +220,10 @@ xs_html *html_actor_icon(snac *user, xs_dict *actor, const char *date, int fwer = 0; xs *name = actor_name(actor, proxy); + xs *pronouns = actor_pronouns(actor); + char pronouns_c = 0; + if (*pronouns != '\0') + pronouns_c = 1; /* get the avatar */ if ((v = xs_dict_get(actor, "icon")) != NULL) { @@ -251,6 +280,15 @@ xs_html *html_actor_icon(snac *user, xs_dict *actor, const char *date, xs_html_text("»"))); } + if (pronouns_c > 0) + xs_html_add(actor_icon, + xs_html_text(" "), + xs_html_tag("span", + xs_html_attr("class", "snac-pronouns"), + xs_html_attr("title", "user's pronouns"), + xs_html_raw(pronouns))); + + if (strcmp(xs_dict_get(actor, "type"), "Service") == 0) { xs_html_add(actor_icon, xs_html_text(" "), diff --git a/utils.c b/utils.c index fd077f6..9f4e1b7 100644 --- a/utils.c +++ b/utils.c @@ -88,6 +88,7 @@ static const char * const default_css = " a { color: #7799dd }\n" " a:visited { color: #aa99dd }\n" "}\n" + "select { max-width: 40%; }\n" ; const char *snac_blurb = -- cgit From b1612de2102d9c30bf42402f38ffdc52ab482674 Mon Sep 17 00:00:00 2001 From: grunfink Date: Mon, 12 Jan 2026 05:11:45 +0100 Subject: Minor tweak. --- html.c | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) (limited to 'html.c') diff --git a/html.c b/html.c index 9aaaa50..9e96108 100644 --- a/html.c +++ b/html.c @@ -168,14 +168,16 @@ xs_str *actor_pronouns(xs_dict *actor) const xs_list *attachment; const xs_dict *d; const char *pronouns = ""; - char *ret; + xs_str *ret; - if (!xs_is_null((attachment = xs_dict_get(actor, "attachment")))) { + if (xs_is_list((attachment = xs_dict_get(actor, "attachment")))) { xs_list_foreach(attachment, d) { - char *prop = xs_utf8_to_lower(xs_dict_get(d, "name")); + xs *prop = xs_utf8_to_lower(xs_dict_get(d, "name")); /* make sure that we are reading the correct metadata */ - if (strlen(prop) == 8 && strcmp(prop, "pronouns") == 0) + if (strlen(prop) == 8 && strcmp(prop, "pronouns") == 0) { pronouns = xs_dict_get(d, "value"); + break; + } } } -- cgit From 7fe8761ed7992762603c01610a115fde199dcaaf Mon Sep 17 00:00:00 2001 From: grunfink Date: Mon, 12 Jan 2026 05:20:52 +0100 Subject: Strip all HTML tags from pronouns. --- html.c | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) (limited to 'html.c') diff --git a/html.c b/html.c index 9e96108..2107609 100644 --- a/html.c +++ b/html.c @@ -181,9 +181,8 @@ xs_str *actor_pronouns(xs_dict *actor) } } - /*

breaks page, cannot nest them */ - ret = xs_replace_i(xs_dup(pronouns), "

", ""); - ret = xs_replace_i(ret, "

", ""); + /* strip all HTML tags */ + ret = xs_regex_replace(pronouns, "]+>", ""); return ret; } -- cgit From a84e74e8f22e0be502d1b4b98cadaf284db5af23 Mon Sep 17 00:00:00 2001 From: grunfink Date: Mon, 12 Jan 2026 06:14:58 +0100 Subject: Move the pronouns closer to the account name. --- html.c | 31 ++++++++++++++++--------------- 1 file changed, 16 insertions(+), 15 deletions(-) (limited to 'html.c') diff --git a/html.c b/html.c index 2107609..2ff6f79 100644 --- a/html.c +++ b/html.c @@ -222,9 +222,6 @@ xs_html *html_actor_icon(snac *user, xs_dict *actor, const char *date, xs *name = actor_name(actor, proxy); xs *pronouns = actor_pronouns(actor); - char pronouns_c = 0; - if (*pronouns != '\0') - pronouns_c = 1; /* get the avatar */ if ((v = xs_dict_get(actor, "icon")) != NULL) { @@ -259,16 +256,28 @@ xs_html *html_actor_icon(snac *user, xs_dict *actor, const char *date, if (href == NULL) href = xs_dup(actor_id); + xs_html *name_link = xs_html_tag("a", + xs_html_attr("href", href), + xs_html_attr("class", "p-author h-card snac-author"), + xs_html_raw(name)); /* name is already html-escaped */ + + if (*pronouns) { + xs_html_add(name_link, + xs_html_text(" ["), + xs_html_tag("span", + xs_html_attr("class", "snac-pronouns"), + xs_html_attr("title", "user's pronouns"), + xs_html_raw(pronouns)), + xs_html_text("]")); + } + xs_html_add(actor_icon, xs_html_sctag("img", xs_html_attr("loading", "lazy"), xs_html_attr("class", "snac-avatar"), xs_html_attr("src", avatar), xs_html_attr("alt", "[?]")), - xs_html_tag("a", - xs_html_attr("href", href), - xs_html_attr("class", "p-author h-card snac-author"), - xs_html_raw(name))); /* name is already html-escaped */ + name_link); if (!xs_is_null(url)) { xs *md5 = xs_md5_hex(url, strlen(url)); @@ -281,14 +290,6 @@ xs_html *html_actor_icon(snac *user, xs_dict *actor, const char *date, xs_html_text("»"))); } - if (pronouns_c > 0) - xs_html_add(actor_icon, - xs_html_text(" "), - xs_html_tag("span", - xs_html_attr("class", "snac-pronouns"), - xs_html_attr("title", "user's pronouns"), - xs_html_raw(pronouns))); - if (strcmp(xs_dict_get(actor, "type"), "Service") == 0) { xs_html_add(actor_icon, -- cgit From 3f3e4892ff0d0c46b49cae88e7e74ba5c6c11a3b Mon Sep 17 00:00:00 2001 From: grunfink Date: Mon, 12 Jan 2026 18:08:34 +0100 Subject: Fixed typo. --- html.c | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) (limited to 'html.c') diff --git a/html.c b/html.c index 2ff6f79..a11b448 100644 --- a/html.c +++ b/html.c @@ -182,8 +182,7 @@ xs_str *actor_pronouns(xs_dict *actor) } /* strip all HTML tags */ - ret = xs_regex_replace(pronouns, "]+>", ""); - + ret = xs_regex_replace(pronouns, "]+>", ""); return ret; } -- cgit From 485986dab9ab0ee59c5829f85c6b066f6bfa3d72 Mon Sep 17 00:00:00 2001 From: byte Date: Fri, 16 Jan 2026 21:11:42 +0000 Subject: simple cosmetic counters --- html.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'html.c') diff --git a/html.c b/html.c index a11b448..86620fb 100644 --- a/html.c +++ b/html.c @@ -3710,7 +3710,7 @@ xs_html *html_people_list(snac *user, xs_list *list, const char *header, const c xs_html *people = xs_html_tag("div", xs_html_tag("h2", xs_html_attr("class", "snac-header"), - xs_html_text(header)), + xs_html_raw(xs_fmt("%s - %d\n", header, xs_list_len(list)))), snac_posts = xs_html_tag("details", xs_html_attr("open", NULL), xs_html_tag("summary", -- cgit From 1719c1fe95017faa1a75ba58c5391e05eb2e772e Mon Sep 17 00:00:00 2001 From: byte Date: Sat, 17 Jan 2026 02:45:33 +0000 Subject: doing count conditional --- html.c | 20 +++++++++++++------- 1 file changed, 13 insertions(+), 7 deletions(-) (limited to 'html.c') diff --git a/html.c b/html.c index 86620fb..c834fcf 100644 --- a/html.c +++ b/html.c @@ -3704,13 +3704,19 @@ xs_str *html_timeline(snac *user, const xs_list *list, int read_only, } -xs_html *html_people_list(snac *user, xs_list *list, const char *header, const char *t, const char *proxy) +xs_html *html_people_list(snac *user, xs_list *list, const char *header, const char *t, const char *proxy, int do_count) { xs_html *snac_posts; + const char *header_cnt; + if (do_count) + header_cnt = xs_fmt("%s - %d\n", header, xs_list_len(list)); + else + header_cnt = xs_fmt("%s\n", header); + xs_html *people = xs_html_tag("div", xs_html_tag("h2", xs_html_attr("class", "snac-header"), - xs_html_raw(xs_fmt("%s - %d\n", header, xs_list_len(list)))), + xs_html_raw(header_cnt)), snac_posts = xs_html_tag("details", xs_html_attr("open", NULL), xs_html_tag("summary", @@ -3951,12 +3957,12 @@ xs_str *html_people(snac *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)); + html_people_list(user, pending, L("Pending follow confirmations"), "p", proxy, 1)); } xs_html_add(lists, - html_people_list(user, wing, L("People you follow"), "i", proxy), - html_people_list(user, wers, L("People that follow you"), "e", proxy)); + html_people_list(user, wing, L("People you follow"), "i", proxy, 1), + html_people_list(user, wers, L("People that follow you"), "e", proxy, 1)); xs_html *html = xs_html_tag("html", html_user_head(user, NULL, NULL), @@ -3987,7 +3993,7 @@ xs_str *html_people_one(snac *user, const char *actor, const xs_list *list, xs *foll = xs_list_append(xs_list_new(), actor); xs_html_add(lists, - html_people_list(user, foll, L("Contact's posts"), "p", proxy)); + html_people_list(user, foll, L("Contact's posts"), "p", proxy, 0)); xs_html_add(body, lists); @@ -4641,7 +4647,7 @@ int html_get_handler(const xs_dict *req, const char *q_path, xs *title = xs_fmt(L("Search results for account %s"), q); - page = html_people_list(&snac, l, title, "wf", NULL); + page = html_people_list(&snac, l, title, "wf", NULL, 1); } } -- cgit From 84c0ee63cf4a67e7fb8484ef874b121160b63382 Mon Sep 17 00:00:00 2001 From: byte Date: Sat, 17 Jan 2026 02:49:56 +0000 Subject: segfault fix --- html.c | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) (limited to 'html.c') diff --git a/html.c b/html.c index a11b448..b4cb1c7 100644 --- a/html.c +++ b/html.c @@ -167,6 +167,7 @@ xs_str *actor_pronouns(xs_dict *actor) { const xs_list *attachment; const xs_dict *d; + const char *v; const char *pronouns = ""; xs_str *ret; @@ -175,7 +176,9 @@ xs_str *actor_pronouns(xs_dict *actor) xs *prop = xs_utf8_to_lower(xs_dict_get(d, "name")); /* make sure that we are reading the correct metadata */ if (strlen(prop) == 8 && strcmp(prop, "pronouns") == 0) { - pronouns = xs_dict_get(d, "value"); + /* safeguard from NULL values */ + v = xs_dict_get(d, "value"); + pronouns = v ? v : pronouns; break; } } -- cgit From 8ce4bd725dc9c37a58a1924a2ba355c176a2dd4a Mon Sep 17 00:00:00 2001 From: grunfink Date: Sat, 17 Jan 2026 05:24:50 +0100 Subject: Fixed small memory leak. --- html.c | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) (limited to 'html.c') diff --git a/html.c b/html.c index 1a25eaa..3f5435c 100644 --- a/html.c +++ b/html.c @@ -3710,7 +3710,7 @@ xs_str *html_timeline(snac *user, const xs_list *list, int read_only, xs_html *html_people_list(snac *user, xs_list *list, const char *header, const char *t, const char *proxy, int do_count) { xs_html *snac_posts; - const char *header_cnt; + xs *header_cnt; if (do_count) header_cnt = xs_fmt("%s - %d\n", header, xs_list_len(list)); else @@ -3719,7 +3719,7 @@ xs_html *html_people_list(snac *user, xs_list *list, const char *header, const c xs_html *people = xs_html_tag("div", xs_html_tag("h2", xs_html_attr("class", "snac-header"), - xs_html_raw(header_cnt)), + xs_html_text(header_cnt)), snac_posts = xs_html_tag("details", xs_html_attr("open", NULL), xs_html_tag("summary", -- cgit