summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--LICENSE2
-rw-r--r--RELEASE_NOTES.md12
-rw-r--r--activitypub.c7
-rw-r--r--data.c2
-rw-r--r--doc/snac.82
-rw-r--r--format.c2
-rw-r--r--html.c160
-rw-r--r--http.c2
-rw-r--r--httpd.c2
-rw-r--r--main.c4
-rw-r--r--mastoapi.c6
-rw-r--r--po/pt_BR.po12
-rw-r--r--rss.c12
-rw-r--r--snac.c2
-rw-r--r--snac.h4
-rw-r--r--upgrade.c2
-rw-r--r--utils.c2
-rw-r--r--webfinger.c2
-rw-r--r--xs.h2
-rw-r--r--xs_curl.h2
-rw-r--r--xs_fcgi.h2
-rw-r--r--xs_glob.h2
-rw-r--r--xs_hex.h2
-rw-r--r--xs_html.h2
-rw-r--r--xs_http.h2
-rw-r--r--xs_httpd.h2
-rw-r--r--xs_io.h2
-rw-r--r--xs_json.h4
-rw-r--r--xs_match.h2
-rw-r--r--xs_mime.h2
-rw-r--r--xs_openssl.h2
-rw-r--r--xs_po.h2
-rw-r--r--xs_random.h2
-rw-r--r--xs_regex.h2
-rw-r--r--xs_set.h2
-rw-r--r--xs_socket.h2
-rw-r--r--xs_time.h2
-rw-r--r--xs_unicode.h2
-rw-r--r--xs_unix_socket.h2
-rw-r--r--xs_url.h2
-rw-r--r--xs_version.h2
-rw-r--r--xs_webmention.h2
42 files changed, 233 insertions, 54 deletions
diff --git a/LICENSE b/LICENSE
index 702b487..3fa58d1 100644
--- a/LICENSE
+++ b/LICENSE
@@ -1,6 +1,6 @@
1MIT License 1MIT License
2 2
3Copyright (c) 2022 - 2025 grunfink et al. (Fediverse: @grunfink@comam.es) 3Copyright (c) 2022 - 2026 grunfink et al. (Fediverse: @grunfink@comam.es)
4 4
5Permission 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: 5Permission 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:
6 6
diff --git a/RELEASE_NOTES.md b/RELEASE_NOTES.md
index d28b125..0dc7fc1 100644
--- a/RELEASE_NOTES.md
+++ b/RELEASE_NOTES.md
@@ -2,6 +2,18 @@
2 2
3## UNRELEASED 3## UNRELEASED
4 4
5If `disable_emojireact` is set to `true` in `server.json`, EmojiReacts (incoming and outgoing) are totally disabled.
6
7## 2.87
8
9Hide EmojiReacts from muted actors and blocked instances.
10
11## 2.86
12
13Truncate RSS titles at UTF-8 character boundaries (contributed by lxo).
14
15Link contacts to single-user people pages. Also, user's posts are shown (contributed by lxo).
16
5Added emoji reactions (contributed by violette). 17Added emoji reactions (contributed by violette).
6 18
7Mastodon API: Fix for some client notifications (contributed by violette), fix for a status visibility error (contributed by fruye). 19Mastodon API: Fix for some client notifications (contributed by violette), fix for a status visibility error (contributed by fruye).
diff --git a/activitypub.c b/activitypub.c
index 59df31a..5e55b8a 100644
--- a/activitypub.c
+++ b/activitypub.c
@@ -1,5 +1,5 @@
1/* snac - A simple, minimalistic ActivityPub instance */ 1/* snac - A simple, minimalistic ActivityPub instance */
2/* copyright (c) 2022 - 2025 grunfink et al. / MIT license */ 2/* copyright (c) 2022 - 2026 grunfink et al. / MIT license */
3 3
4#include "xs.h" 4#include "xs.h"
5#include "xs_json.h" 5#include "xs_json.h"
@@ -2578,6 +2578,11 @@ int process_input_message(snac *snac, const xs_dict *msg, const xs_dict *req)
2578 return -1; 2578 return -1;
2579 } 2579 }
2580 2580
2581 if (strcmp(type, "EmojiReact") == 0 && xs_is_true(xs_dict_get(srv_config, "disable_emojireact"))) {
2582 srv_log(xs_fmt("Dropping EmojiReact from %s due to admin configuration", actor));
2583 return -1;
2584 }
2585
2581 const char *object, *utype; 2586 const char *object, *utype;
2582 2587
2583 object = xs_dict_get(msg, "object"); 2588 object = xs_dict_get(msg, "object");
diff --git a/data.c b/data.c
index 023bae6..22ea7b0 100644
--- a/data.c
+++ b/data.c
@@ -1,5 +1,5 @@
1/* snac - A simple, minimalistic ActivityPub instance */ 1/* snac - A simple, minimalistic ActivityPub instance */
2/* copyright (c) 2022 - 2025 grunfink et al. / MIT license */ 2/* copyright (c) 2022 - 2026 grunfink et al. / MIT license */
3 3
4#include "xs.h" 4#include "xs.h"
5#include "xs_hex.h" 5#include "xs_hex.h"
diff --git a/doc/snac.8 b/doc/snac.8
index b8a75fa..6f98648 100644
--- a/doc/snac.8
+++ b/doc/snac.8
@@ -296,6 +296,8 @@ outgoing messages (default: 15). Anyway, whenever any incoming activity from a
296failed instance is detected, this counter is reset for it. 296failed instance is detected, this counter is reset for it.
297.It Ic vkey 297.It Ic vkey
298Public vapid key. Used for notification on some client. 298Public vapid key. Used for notification on some client.
299.It Ic disable_emojireact
300If set to true, all EmojiReact support (for input and output) is disabled.
299.El 301.El
300.Pp 302.Pp
301You must restart the server to make effective these changes. 303You must restart the server to make effective these changes.
diff --git a/format.c b/format.c
index 4f93b7b..803e5a5 100644
--- a/format.c
+++ b/format.c
@@ -1,5 +1,5 @@
1/* snac - A simple, minimalistic ActivityPub instance */ 1/* snac - A simple, minimalistic ActivityPub instance */
2/* copyright (c) 2022 - 2025 grunfink et al. / MIT license */ 2/* copyright (c) 2022 - 2026 grunfink et al. / MIT license */
3 3
4#include "xs.h" 4#include "xs.h"
5#include "xs_regex.h" 5#include "xs_regex.h"
diff --git a/html.c b/html.c
index 3e692bd..d5294cb 100644
--- a/html.c
+++ b/html.c
@@ -1,5 +1,5 @@
1/* snac - A simple, minimalistic ActivityPub instance */ 1/* snac - A simple, minimalistic ActivityPub instance */
2/* copyright (c) 2022 - 2025 grunfink et al. / MIT license */ 2/* copyright (c) 2022 - 2026 grunfink et al. / MIT license */
3 3
4#include "xs.h" 4#include "xs.h"
5#include "xs_io.h" 5#include "xs_io.h"
@@ -221,7 +221,7 @@ xs_html *html_actor_icon(snac *user, xs_dict *actor, const char *date,
221 anchored link to the people page instead of the actor url */ 221 anchored link to the people page instead of the actor url */
222 if (fwer || fwing) { 222 if (fwer || fwing) {
223 xs *md5 = xs_md5_hex(actor_id, strlen(actor_id)); 223 xs *md5 = xs_md5_hex(actor_id, strlen(actor_id));
224 href = xs_fmt("%s/people#%s", user->actor, md5); 224 href = xs_fmt("%s/people/%s", user->actor, md5);
225 } 225 }
226 } 226 }
227 227
@@ -2103,7 +2103,7 @@ xs_html *html_entry_controls(snac *user, const char *actor,
2103 xs_html_tag("p", NULL)); 2103 xs_html_tag("p", NULL));
2104 } 2104 }
2105 2105
2106 { /** emoji react **/ 2106 if (!xs_is_true(xs_dict_get(srv_config, "disable_emojireact"))) { /** emoji react **/
2107 /* the post textarea */ 2107 /* the post textarea */
2108 xs *div_id = xs_fmt("%s_reply", md5); 2108 xs *div_id = xs_fmt("%s_reply", md5);
2109 xs *form_id = xs_fmt("%s_reply_form", md5); 2109 xs *form_id = xs_fmt("%s_reply_form", md5);
@@ -2355,7 +2355,7 @@ xs_html *html_entry(snac *user, xs_dict *msg, int read_only,
2355 } 2355 }
2356 2356
2357 if (!read_only && (fwers || fwing)) 2357 if (!read_only && (fwers || fwing))
2358 href = xs_fmt("%s/people#%s", user->actor, p); 2358 href = xs_fmt("%s/people/%s", user->actor, p);
2359 else 2359 else
2360 href = xs_dup(id); 2360 href = xs_dup(id);
2361 2361
@@ -2447,7 +2447,7 @@ xs_html *html_entry(snac *user, xs_dict *msg, int read_only,
2447 2447
2448 /* add all emoji reacts */ 2448 /* add all emoji reacts */
2449 int is_emoji = 0; 2449 int is_emoji = 0;
2450 { 2450 if (!xs_is_true(xs_dict_get(srv_config, "disable_emojireact"))) {
2451 int c = 0; 2451 int c = 0;
2452 const xs_dict *k; 2452 const xs_dict *k;
2453 xs *ls = xs_list_new(); 2453 xs *ls = xs_list_new();
@@ -2460,6 +2460,10 @@ xs_html *html_entry(snac *user, xs_dict *msg, int read_only,
2460 const char *content = xs_dict_get(m, "content"); 2460 const char *content = xs_dict_get(m, "content");
2461 const char *actor = xs_dict_get(m, "actor"); 2461 const char *actor = xs_dict_get(m, "actor");
2462 const xs_list *contentl = xs_dict_get(sfrl, content); 2462 const xs_list *contentl = xs_dict_get(sfrl, content);
2463
2464 if ((user && is_muted(user, actor)) || is_instance_blocked(actor))
2465 continue;
2466
2463 xs *actors = xs_list_new(); 2467 xs *actors = xs_list_new();
2464 actors = xs_list_append(actors, actor); 2468 actors = xs_list_append(actors, actor);
2465 char me = actor && user && strcmp(actor, user->actor) == 0; 2469 char me = actor && user && strcmp(actor, user->actor) == 0;
@@ -3856,6 +3860,108 @@ xs_str *html_people(snac *user)
3856 return xs_html_render_s(html, "<!DOCTYPE html>\n"); 3860 return xs_html_render_s(html, "<!DOCTYPE html>\n");
3857} 3861}
3858 3862
3863/* Filter list to display only posts by actor. We'll probably show
3864 fewer than show posts. Should we try harder to find some? */
3865xs_str *html_people_one(snac *user, const char *actor, const xs_list *list,
3866 int skip, int show, int show_more, const char *page)
3867{
3868 const char *proxy = NULL;
3869 xs_list *p = (xs_list *)list;
3870 const char *v;
3871
3872 if (xs_is_true(xs_dict_get(srv_config, "proxy_media")))
3873 proxy = user->actor;
3874
3875 xs_html *body = html_user_body(user, 0);
3876
3877 xs_html *lists = xs_html_tag("div",
3878 xs_html_attr("class", "snac-posts"));
3879
3880 xs *foll = xs_list_append(xs_list_new(), actor);
3881
3882 xs_html_add(lists,
3883 html_people_list(user, foll, L("Contact's posts"), "p", proxy));
3884
3885 xs_html_add(body, lists);
3886
3887 while (xs_list_iter(&p, &v)) {
3888 xs *msg = NULL;
3889 int status;
3890
3891 status = timeline_get_by_md5(user, v, &msg);
3892
3893 if (!valid_status(status))
3894 continue;
3895
3896 const char *id = xs_dict_get(msg, "id");
3897 const char *by = get_atto(msg);
3898 xs *actor_md5 = NULL;
3899 xs_list *boosts = NULL;
3900 xs_list *likes = NULL;
3901 xs_list *reacts = NULL;
3902 /* Besides actor's posts, also show actor's boosts, and also
3903 posts by user with likes or reacts by actor. I.e., any
3904 actor's actions that user could have seen in the timeline
3905 or in notifications. */
3906 if (!(by && strcmp(actor, by) == 0) &&
3907 xs_list_in((boosts = object_announces(id)),
3908 (actor_md5 = xs_md5_hex(actor, strlen(actor)))) == -1 &&
3909 (!(by && strcmp(user->actor, by) == 0) ||
3910 (xs_list_in((likes = object_likes(id)), actor_md5) == -1 &&
3911 xs_list_in((reacts = object_get_emoji_reacts(id)), actor_md5) == -1)))
3912 continue;
3913
3914 xs_html *entry = html_entry(user, msg, 0, 0, v, 1);
3915
3916 if (entry != NULL)
3917 xs_html_add(lists,
3918 entry);
3919 }
3920
3921 if (show_more) {
3922 xs *m = NULL;
3923 xs *m10 = NULL;
3924 xs *ss = xs_fmt("skip=%d&show=%d", skip + show, show);
3925
3926 xs *url = xs_dup(user == NULL ? srv_baseurl : user->actor);
3927
3928 if (page != NULL)
3929 url = xs_str_cat(url, page);
3930
3931 if (xs_str_in(url, "?") != -1)
3932 m = xs_fmt("%s&%s", url, ss);
3933 else
3934 m = xs_fmt("%s?%s", url, ss);
3935
3936 m10 = xs_fmt("%s0", m);
3937
3938 xs_html *more_links = xs_html_tag("p",
3939 xs_html_tag("a",
3940 xs_html_attr("href", url),
3941 xs_html_attr("name", "snac-more"),
3942 xs_html_text(L("Back to top"))),
3943 xs_html_text(" - "),
3944 xs_html_tag("a",
3945 xs_html_attr("href", m),
3946 xs_html_attr("name", "snac-more"),
3947 xs_html_text(L("More..."))),
3948 xs_html_text(" - "),
3949 xs_html_tag("a",
3950 xs_html_attr("href", m10),
3951 xs_html_attr("name", "snac-more"),
3952 xs_html_text(L("More (x 10)..."))));
3953
3954 xs_html_add(body,
3955 more_links);
3956 }
3957
3958 xs_html *html = xs_html_tag("html",
3959 html_user_head(user, NULL, NULL),
3960 xs_html_add(body,
3961 html_footer(user)));
3962
3963 return xs_html_render_s(html, "<!DOCTYPE html>\n");
3964}
3859 3965
3860xs_str *html_notifications(snac *user, int skip, int show) 3966xs_str *html_notifications(snac *user, int skip, int show)
3861{ 3967{
@@ -3924,6 +4030,9 @@ xs_str *html_notifications(snac *user, int skip, int show)
3924 if (xs_is_string(id2) && xs_set_add(&rep, id2) != 1) 4030 if (xs_is_string(id2) && xs_set_add(&rep, id2) != 1)
3925 continue; 4031 continue;
3926 4032
4033 if (strcmp(type, "EmojiReact") == 0 && xs_is_true(xs_dict_get(srv_config, "disable_emojireact")))
4034 continue;
4035
3927 object_get(id, &obj); 4036 object_get(id, &obj);
3928 4037
3929 const char *msg_id = NULL; 4038 const char *msg_id = NULL;
@@ -4271,8 +4380,12 @@ int html_get_handler(const xs_dict *req, const char *q_path,
4271 cache = 0; 4380 cache = 0;
4272 4381
4273 int skip = 0; 4382 int skip = 0;
4383 const char *max_show_default = "50";
4384 int max_show = xs_number_get(xs_dict_get_def(srv_config, "max_timeline_entries",
4385 max_show_default));
4274 int def_show = xs_number_get(xs_dict_get_def(srv_config, "def_timeline_entries", 4386 int def_show = xs_number_get(xs_dict_get_def(srv_config, "def_timeline_entries",
4275 xs_dict_get_def(srv_config, "max_timeline_entries", "50"))); 4387 xs_dict_get_def(srv_config, "max_timeline_entries",
4388 max_show_default)));
4276 int show = def_show; 4389 int show = def_show;
4277 4390
4278 if ((v = xs_dict_get(q_vars, "skip")) != NULL) 4391 if ((v = xs_dict_get(q_vars, "skip")) != NULL)
@@ -4298,6 +4411,8 @@ int html_get_handler(const xs_dict *req, const char *q_path,
4298 /* a show of 0 has no sense */ 4411 /* a show of 0 has no sense */
4299 if (show == 0) 4412 if (show == 0)
4300 show = def_show; 4413 show = def_show;
4414 if (show > max_show)
4415 show = max_show;
4301 4416
4302 if (p_path == NULL) { /** public timeline **/ 4417 if (p_path == NULL) { /** public timeline **/
4303 xs *h = xs_str_localtime(0, "%Y-%m.html"); 4418 xs *h = xs_str_localtime(0, "%Y-%m.html");
@@ -4415,7 +4530,7 @@ int html_get_handler(const xs_dict *req, const char *q_path,
4415 actor_add(actor, actor_obj); 4530 actor_add(actor, actor_obj);
4416 4531
4417 /* create a people list with only one element */ 4532 /* create a people list with only one element */
4418 l = xs_list_append(xs_list_new(), actor); 4533 l = xs_list_append(l, actor);
4419 4534
4420 xs *title = xs_fmt(L("Search results for account %s"), q); 4535 xs *title = xs_fmt(L("Search results for account %s"), q);
4421 4536
@@ -4555,6 +4670,37 @@ int html_get_handler(const xs_dict *req, const char *q_path,
4555 } 4670 }
4556 } 4671 }
4557 else 4672 else
4673 if (xs_startswith(p_path, "people/")) { /** a single actor **/
4674 if (!login(&snac, req)) {
4675 *body = xs_dup(uid);
4676 status = HTTP_STATUS_UNAUTHORIZED;
4677 }
4678 else {
4679 xs *actor_dict = NULL;
4680 const char *actor_id = NULL;
4681 xs *actor = NULL;
4682 xs_list *page_lst = xs_split_n(p_path, "?", 2);
4683 xs *page = xs_str_cat(xs_str_new("/"), xs_list_get(page_lst, 0));
4684 xs_list *l = xs_split_n(page, "/", 3);
4685 const char *actor_md5 = xs_list_get(l, 2);
4686
4687 if (valid_status(object_get_by_md5(actor_md5, &actor_dict)) &&
4688 (actor_id = xs_dict_get(actor_dict, "id")) != NULL &&
4689 valid_status(actor_get(actor_id, &actor))) {
4690 int more = 0;
4691 xs *list = timeline_simple_list(&snac, "private", skip, show, &more);
4692
4693 *body = html_people_one(&snac, actor_id, list, skip, show, more, page);
4694 *b_size = strlen(*body);
4695 status = HTTP_STATUS_OK;
4696 }
4697 else {
4698 *body = xs_dup(uid);
4699 status = HTTP_STATUS_NOT_FOUND;
4700 }
4701 }
4702 }
4703 else
4558 if (strcmp(p_path, "notifications") == 0) { /** the list of notifications **/ 4704 if (strcmp(p_path, "notifications") == 0) { /** the list of notifications **/
4559 if (!login(&snac, req)) { 4705 if (!login(&snac, req)) {
4560 *body = xs_dup(uid); 4706 *body = xs_dup(uid);
diff --git a/http.c b/http.c
index f0b1314..4f0b2fa 100644
--- a/http.c
+++ b/http.c
@@ -1,5 +1,5 @@
1/* snac - A simple, minimalistic ActivityPub instance */ 1/* snac - A simple, minimalistic ActivityPub instance */
2/* copyright (c) 2022 - 2025 grunfink et al. / MIT license */ 2/* copyright (c) 2022 - 2026 grunfink et al. / MIT license */
3 3
4#include "xs.h" 4#include "xs.h"
5#include "xs_io.h" 5#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 @@
1/* snac - A simple, minimalistic ActivityPub instance */ 1/* snac - A simple, minimalistic ActivityPub instance */
2/* copyright (c) 2022 - 2025 grunfink et al. / MIT license */ 2/* copyright (c) 2022 - 2026 grunfink et al. / MIT license */
3 3
4#include "xs.h" 4#include "xs.h"
5#include "xs_io.h" 5#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 @@
1/* snac - A simple, minimalistic ActivityPub instance */ 1/* snac - A simple, minimalistic ActivityPub instance */
2/* copyright (c) 2022 - 2025 grunfink et al. / MIT license */ 2/* copyright (c) 2022 - 2026 grunfink et al. / MIT license */
3 3
4#include "xs.h" 4#include "xs.h"
5#include "xs_io.h" 5#include "xs_io.h"
@@ -18,7 +18,7 @@
18int usage(const char *cmd) 18int usage(const char *cmd)
19{ 19{
20 printf("snac " VERSION " - A simple, minimalistic ActivityPub instance\n"); 20 printf("snac " VERSION " - A simple, minimalistic ActivityPub instance\n");
21 printf("Copyright (c) 2022 - 2025 grunfink et al. / MIT license\n"); 21 printf("Copyright (c) 2022 - 2026 grunfink et al. / MIT license\n");
22 printf("\n"); 22 printf("\n");
23 23
24 if (cmd == NULL) { 24 if (cmd == NULL) {
diff --git a/mastoapi.c b/mastoapi.c
index 6a12ced..52aca08 100644
--- a/mastoapi.c
+++ b/mastoapi.c
@@ -1,5 +1,5 @@
1/* snac - A simple, minimalistic ActivityPub instance */ 1/* snac - A simple, minimalistic ActivityPub instance */
2/* copyright (c) 2022 - 2025 grunfink et al. / MIT license */ 2/* copyright (c) 2022 - 2026 grunfink et al. / MIT license */
3 3
4#ifndef NO_MASTODON_API 4#ifndef NO_MASTODON_API
5 5
@@ -1171,6 +1171,10 @@ xs_dict *mastoapi_status(snac *snac, const xs_dict *msg)
1171 const char *content = xs_dict_get(msg, "content"); 1171 const char *content = xs_dict_get(msg, "content");
1172 const char *actor = xs_dict_get(msg, "actor"); 1172 const char *actor = xs_dict_get(msg, "actor");
1173 const xs_list *contentl = xs_dict_get(sfrl, content); 1173 const xs_list *contentl = xs_dict_get(sfrl, content);
1174
1175 if ((snac && is_muted(snac, actor)) || is_instance_blocked(actor))
1176 continue;
1177
1174 /* NOTE: idk when there are no actor, but i encountered that bug. 1178 /* NOTE: idk when there are no actor, but i encountered that bug.
1175 * Probably because of one of my previous attempts. 1179 * Probably because of one of my previous attempts.
1176 * Keeping this just in case, can remove later */ 1180 * Keeping this just in case, can remove later */
diff --git a/po/pt_BR.po b/po/pt_BR.po
index e930490..71b0132 100644
--- a/po/pt_BR.po
+++ b/po/pt_BR.po
@@ -4,13 +4,13 @@
4msgid "" 4msgid ""
5msgstr "" 5msgstr ""
6"Project-Id-Version: snac\n" 6"Project-Id-Version: snac\n"
7"PO-Revision-Date: 2025-10-11 16:48-0300\n" 7"PO-Revision-Date: 2025-12-22 07:55-0300\n"
8"Last-Translator: Daltux <@daltux@snac.daltux.net>\n" 8"Last-Translator: Daltux <@daltux@snac.daltux.net>\n"
9"Language: pt_BR\n" 9"Language: pt_BR\n"
10"MIME-Version: 1.0\n" 10"MIME-Version: 1.0\n"
11"Content-Type: text/plain; charset=UTF-8\n" 11"Content-Type: text/plain; charset=UTF-8\n"
12"Content-Transfer-Encoding: 8bit\n" 12"Content-Transfer-Encoding: 8bit\n"
13"X-Generator: Poedit 3.7\n" 13"X-Generator: Geany / PoHelper 2.0\n"
14 14
15#: html.c:534 15#: html.c:534
16msgid "Sensitive content: " 16msgid "Sensitive content: "
@@ -800,17 +800,17 @@ msgid "Direct Message"
800msgstr "Mensagem direta" 800msgstr "Mensagem direta"
801 801
802#: html.c:488 html.c:2534 html.c:2559 html.c:5177 802#: html.c:488 html.c:2534 html.c:2559 html.c:5177
803msgid "EmojiUnreact" 803msgid "Desfazer EmojiReação"
804msgstr "" 804msgstr ""
805 805
806#: html.c:488 html.c:1440 html.c:2534 html.c:2559 html.c:5188 806#: html.c:488 html.c:1440 html.c:2534 html.c:2559 html.c:5188
807msgid "EmojiReact" 807msgid "EmojiReact"
808msgstr "" 808msgstr "EmojiReação"
809 809
810#: html.c:2115 810#: html.c:2115
811msgid "Emoji react..." 811msgid "Emoji react..."
812msgstr "" 812msgstr "EmojiReagir..."
813 813
814#: html.c:2609 814#: html.c:2609
815msgid "Emoji reactions: " 815msgid "Emoji reactions: "
816msgstr "" 816msgstr "EmojiReações: "
diff --git a/rss.c b/rss.c
index 6124e7a..dc26071 100644
--- a/rss.c
+++ b/rss.c
@@ -1,5 +1,5 @@
1/* snac - A simple, minimalistic ActivityPub instance */ 1/* snac - A simple, minimalistic ActivityPub instance */
2/* copyright (c) 2025 grunfink et al. / MIT license */ 2/* copyright (c) 2025 - 2026 grunfink et al. / MIT license */
3 3
4#include "xs.h" 4#include "xs.h"
5#include "xs_html.h" 5#include "xs_html.h"
@@ -10,6 +10,7 @@
10#include "xs_openssl.h" 10#include "xs_openssl.h"
11#include "xs_json.h" 11#include "xs_json.h"
12#include "xs_http.h" 12#include "xs_http.h"
13#include "xs_unicode.h"
13 14
14#include "snac.h" 15#include "snac.h"
15 16
@@ -74,7 +75,14 @@ xs_str *rss_from_timeline(snac *user, const xs_list *timeline,
74 title = xs_regex_replace_i(title, "&[^;]+;", " "); 75 title = xs_regex_replace_i(title, "&[^;]+;", " ");
75 int i; 76 int i;
76 77
77 for (i = 0; title[i] && title[i] != '\n' && i < 50; i++); 78 for (i = 0; title[i] && title[i] != '\n' && i < 50; ) {
79 const char *p = &title[i];
80 unsigned int cp = xs_utf8_dec(&p);
81 int n = p - title;
82 if (cp == 0xfffd || n > 50)
83 break;
84 i = n;
85 }
78 86
79 if (title[i] != '\0') { 87 if (title[i] != '\0') {
80 title[i] = '\0'; 88 title[i] = '\0';
diff --git a/snac.c b/snac.c
index 31f524f..965edbb 100644
--- a/snac.c
+++ b/snac.c
@@ -1,5 +1,5 @@
1/* snac - A simple, minimalistic ActivityPub instance */ 1/* snac - A simple, minimalistic ActivityPub instance */
2/* copyright (c) 2022 - 2025 grunfink et al. / MIT license */ 2/* copyright (c) 2022 - 2026 grunfink et al. / MIT license */
3 3
4#define XS_IMPLEMENTATION 4#define XS_IMPLEMENTATION
5 5
diff --git a/snac.h b/snac.h
index bcff7f8..2364f72 100644
--- a/snac.h
+++ b/snac.h
@@ -1,7 +1,7 @@
1/* snac - A simple, minimalistic ActivityPub instance */ 1/* snac - A simple, minimalistic ActivityPub instance */
2/* copyright (c) 2022 - 2025 grunfink et al. / MIT license */ 2/* copyright (c) 2022 - 2026 grunfink et al. / MIT license */
3 3
4#define VERSION "2.86-dev" 4#define VERSION "2.87"
5 5
6#define USER_AGENT "snac/" VERSION 6#define USER_AGENT "snac/" VERSION
7 7
diff --git a/upgrade.c b/upgrade.c
index 9e0ae6e..07e9c51 100644
--- a/upgrade.c
+++ b/upgrade.c
@@ -1,5 +1,5 @@
1/* snac - A simple, minimalistic ActivityPub instance */ 1/* snac - A simple, minimalistic ActivityPub instance */
2/* copyright (c) 2022 - 2025 grunfink et al. / MIT license */ 2/* copyright (c) 2022 - 2026 grunfink et al. / MIT license */
3 3
4#include "xs.h" 4#include "xs.h"
5#include "xs_io.h" 5#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 @@
1/* snac - A simple, minimalistic ActivityPub instance */ 1/* snac - A simple, minimalistic ActivityPub instance */
2/* copyright (c) 2022 - 2025 grunfink et al. / MIT license */ 2/* copyright (c) 2022 - 2026 grunfink et al. / MIT license */
3 3
4#include "xs.h" 4#include "xs.h"
5#include "xs_io.h" 5#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 @@
1/* snac - A simple, minimalistic ActivityPub instance */ 1/* snac - A simple, minimalistic ActivityPub instance */
2/* copyright (c) 2022 - 2025 grunfink et al. / MIT license */ 2/* copyright (c) 2022 - 2026 grunfink et al. / MIT license */
3 3
4#include "xs.h" 4#include "xs.h"
5#include "xs_json.h" 5#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 @@
1/* copyright (c) 2022 - 2025 grunfink et al. / MIT license */ 1/* copyright (c) 2022 - 2026 grunfink et al. / MIT license */
2 2
3#ifndef _XS_H 3#ifndef _XS_H
4 4
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 @@
1/* copyright (c) 2022 - 2025 grunfink et al. / MIT license */ 1/* copyright (c) 2022 - 2026 grunfink et al. / MIT license */
2 2
3#ifndef _XS_CURL_H 3#ifndef _XS_CURL_H
4 4
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 @@
1/* copyright (c) 2022 - 2025 grunfink et al. / MIT license */ 1/* copyright (c) 2022 - 2026 grunfink et al. / MIT license */
2 2
3/* 3/*
4 This is an intentionally-dead-simple FastCGI implementation; 4 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 @@
1/* copyright (c) 2022 - 2025 grunfink et al. / MIT license */ 1/* copyright (c) 2022 - 2026 grunfink et al. / MIT license */
2 2
3#ifndef _XS_GLOB_H 3#ifndef _XS_GLOB_H
4 4
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 @@
1/* copyright (c) 2022 - 2025 grunfink et al. / MIT license */ 1/* copyright (c) 2022 - 2026 grunfink et al. / MIT license */
2 2
3#ifndef _XS_HEX_H 3#ifndef _XS_HEX_H
4 4
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 @@
1/* copyright (c) 2022 - 2025 grunfink et al. / MIT license */ 1/* copyright (c) 2022 - 2026 grunfink et al. / MIT license */
2 2
3#ifndef _XS_HTML_H 3#ifndef _XS_HTML_H
4 4
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 @@
1/* copyright (c) 2022 - 2025 grunfink et al. / MIT license */ 1/* copyright (c) 2022 - 2026 grunfink et al. / MIT license */
2 2
3#ifndef _XS_HTTP_H 3#ifndef _XS_HTTP_H
4 4
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 @@
1/* copyright (c) 2022 - 2025 grunfink et al. / MIT license */ 1/* copyright (c) 2022 - 2026 grunfink et al. / MIT license */
2 2
3#ifndef _XS_HTTPD_H 3#ifndef _XS_HTTPD_H
4 4
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 @@
1/* copyright (c) 2022 - 2025 grunfink et al. / MIT license */ 1/* copyright (c) 2022 - 2026 grunfink et al. / MIT license */
2 2
3#ifndef _XS_IO_H 3#ifndef _XS_IO_H
4 4
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 @@
1/* copyright (c) 2022 - 2025 grunfink et al. / MIT license */ 1/* copyright (c) 2022 - 2026 grunfink et al. / MIT license */
2 2
3#ifndef _XS_JSON_H 3#ifndef _XS_JSON_H
4 4
@@ -28,6 +28,8 @@ int xs_json_load_object(FILE *f, int maxdepth, xs_dict **d);
28 28
29/** IMPLEMENTATION **/ 29/** IMPLEMENTATION **/
30 30
31#include "xs_unicode.h"
32
31/** JSON dumps **/ 33/** JSON dumps **/
32 34
33static void _xs_json_dump_str(const char *data, FILE *f) 35static 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 @@
1/* copyright (c) 2022 - 2025 grunfink et al. / MIT license */ 1/* copyright (c) 2022 - 2026 grunfink et al. / MIT license */
2 2
3#ifndef _XS_MATCH_H 3#ifndef _XS_MATCH_H
4 4
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 @@
1/* copyright (c) 2022 - 2025 grunfink et al. / MIT license */ 1/* copyright (c) 2022 - 2026 grunfink et al. / MIT license */
2 2
3#ifndef _XS_MIME_H 3#ifndef _XS_MIME_H
4 4
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 @@
1/* copyright (c) 2022 - 2025 grunfink et al. / MIT license */ 1/* copyright (c) 2022 - 2026 grunfink et al. / MIT license */
2 2
3#ifndef _XS_OPENSSL_H 3#ifndef _XS_OPENSSL_H
4 4
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 @@
1/* copyright (c) 2025 grunfink et al. / MIT license */ 1/* copyright (c) 2025 - 2026 grunfink et al. / MIT license */
2 2
3#ifndef _XS_PO_H 3#ifndef _XS_PO_H
4 4
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 @@
1/* copyright (c) 2022 - 2025 grunfink et al. / MIT license */ 1/* copyright (c) 2022 - 2026 grunfink et al. / MIT license */
2 2
3#ifndef _XS_RANDOM_H 3#ifndef _XS_RANDOM_H
4 4
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 @@
1/* copyright (c) 2022 - 2025 grunfink et al. / MIT license */ 1/* copyright (c) 2022 - 2026 grunfink et al. / MIT license */
2 2
3#ifndef _XS_REGEX_H 3#ifndef _XS_REGEX_H
4 4
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 @@
1/* copyright (c) 2022 - 2025 grunfink et al. / MIT license */ 1/* copyright (c) 2022 - 2026 grunfink et al. / MIT license */
2 2
3#ifndef _XS_SET_H 3#ifndef _XS_SET_H
4 4
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 @@
1/* copyright (c) 2022 - 2025 grunfink et al. / MIT license */ 1/* copyright (c) 2022 - 2026 grunfink et al. / MIT license */
2 2
3#ifndef _XS_SOCKET_H 3#ifndef _XS_SOCKET_H
4 4
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 @@
1/* copyright (c) 2022 - 2025 grunfink et al. / MIT license */ 1/* copyright (c) 2022 - 2026 grunfink et al. / MIT license */
2 2
3#ifndef _XS_TIME_H 3#ifndef _XS_TIME_H
4 4
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 @@
1/* copyright (c) 2022 - 2025 grunfink et al. / MIT license */ 1/* copyright (c) 2022 - 2026 grunfink et al. / MIT license */
2 2
3#ifndef _XS_UNICODE_H 3#ifndef _XS_UNICODE_H
4 4
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 @@
1/* copyright (c) 2022 - 2025 grunfink et al. / MIT license */ 1/* copyright (c) 2022 - 2026 grunfink et al. / MIT license */
2 2
3#ifndef _XS_UNIX_SOCKET_H 3#ifndef _XS_UNIX_SOCKET_H
4 4
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 @@
1/* copyright (c) 2022 - 2025 grunfink et al. / MIT license */ 1/* copyright (c) 2022 - 2026 grunfink et al. / MIT license */
2 2
3#ifndef _XS_URL_H 3#ifndef _XS_URL_H
4 4
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 @@
1/* copyright (c) 2025 grunfink et al. / MIT license */ 1/* copyright (c) 2025 - 2026 grunfink et al. / MIT license */
2 2
3#ifndef _XS_WEBMENTION_H 3#ifndef _XS_WEBMENTION_H
4 4