commit 85be7f3
Merge remote-tracking branch 'upstream/master' into curl-smtp
+4,
-2
1@@ -1,6 +1,6 @@
2 # Release Notes
3
4-## UNRELEASED
5+## 2.69 "Yin/Yang of Love"
6
7 Added support for subscribing to LitePub (Pleroma-style) Fediverse Relays like e.g. https://fedi-relay.gyptazy.com to improve federation. See `snac(8)` (the Administrator Manual) for more information on how to use this feature.
8
9@@ -18,7 +18,7 @@ Fixed broken NetBSD build (missing dependency in Makefile.NetBSD).
10
11 The user profile can now include longitude and latitude data for your current location.
12
13-Mastodon API: implemented limit= on notification fetches (contributed by nowster), implemented faster min_id handling (contributed by nowster), obey the quiet public visibility set for posts.
14+Mastodon API: implemented limit= on notification fetches (contributed by nowster), implemented faster min_id handling (contributed by nowster), obey the quiet public visibility set for posts, other timeline improvements (contributed by nowster).
15
16 Reduced RSA key size for new users from 4096 to 2048. This will be friendlier to smaller machines, and everybody else out there is using 2048.
17
18@@ -26,6 +26,8 @@ If the `SNAC_BASEDIR` environment variable is defined and set to the base direct
19
20 Fixed a bug in the generation of the top page (contributed by an-im-dugud).
21
22+Added support for Markdown headers and underlining (contributed by an-im-dugud).
23+
24 ## 2.68
25
26 Fixed regression in link verification code (contributed by nowster).
+97,
-34
1@@ -587,6 +587,70 @@ int is_msg_from_private_user(const xs_dict *msg)
2 }
3
4
5+int followed_hashtag_check(snac *user, const xs_dict *msg)
6+/* returns true if this message contains a hashtag followed by me */
7+{
8+ const xs_list *fw_tags = xs_dict_get(user->config, "followed_hashtags");
9+
10+ if (xs_is_list(fw_tags)) {
11+ const xs_list *tags_in_msg = xs_dict_get(msg, "tag");
12+
13+ if (xs_is_list(tags_in_msg)) {
14+ const xs_dict *te;
15+
16+ /* iterate the tags in the message */
17+ xs_list_foreach(tags_in_msg, te) {
18+ if (xs_is_dict(te)) {
19+ const char *type = xs_dict_get(te, "type");
20+ const char *name = xs_dict_get(te, "name");
21+
22+ if (xs_is_string(type) && xs_is_string(name)) {
23+ if (strcmp(type, "Hashtag") == 0) {
24+ xs *lc_name = xs_utf8_to_lower(name);
25+
26+ if (xs_list_in(fw_tags, lc_name) != -1)
27+ return 1;
28+ }
29+ }
30+ }
31+ }
32+ }
33+ }
34+
35+ return 0;
36+}
37+
38+
39+void followed_hashtag_distribute(const xs_dict *msg)
40+/* distribute this post to all users following the included hashtags */
41+{
42+ const char *id = xs_dict_get(msg, "id");
43+ const xs_list *tags_in_msg = xs_dict_get(msg, "tag");
44+
45+ if (!xs_is_string(id) || !xs_is_list(tags_in_msg) || xs_list_len(tags_in_msg) == 0)
46+ return;
47+
48+ srv_debug(1, xs_fmt("followed_hashtag_distribute check for %s", id));
49+
50+ xs *users = user_list();
51+ const char *uid;
52+
53+ xs_list_foreach(users, uid) {
54+ snac user;
55+
56+ if (user_open(&user, uid)) {
57+ if (followed_hashtag_check(&user, msg)) {
58+ timeline_add(&user, id, msg);
59+
60+ snac_log(&user, xs_fmt("followed hashtag in %s", id));
61+ }
62+
63+ user_free(&user);
64+ }
65+ }
66+}
67+
68+
69 int is_msg_for_me(snac *snac, const xs_dict *c_msg)
70 /* checks if this message is for me */
71 {
72@@ -602,19 +666,32 @@ int is_msg_for_me(snac *snac, const xs_dict *c_msg)
73 if (xs_match(type, "Like|Announce|EmojiReact")) {
74 const char *object = xs_dict_get(c_msg, "object");
75
76- if (xs_type(object) == XSTYPE_DICT)
77+ if (xs_is_dict(object))
78 object = xs_dict_get(object, "id");
79
80 /* bad object id? reject */
81- if (xs_type(object) != XSTYPE_STRING)
82+ if (!xs_is_string(object))
83 return 0;
84
85 /* if it's about one of our posts, accept it */
86 if (xs_startswith(object, snac->actor))
87 return 2;
88
89- /* if it's by someone we don't follow, reject */
90- return following_check(snac, actor);
91+ /* if it's by someone we follow, accept it */
92+ if (following_check(snac, actor))
93+ return 1;
94+
95+ /* do we follow any hashtag? */
96+ if (xs_is_list(xs_dict_get(snac->config, "followed_hashtags"))) {
97+ xs *obj = NULL;
98+
99+ /* if the admired object contains any followed hashtag, accept it */
100+ if (valid_status(object_get(object, &obj)) &&
101+ followed_hashtag_check(snac, obj))
102+ return 7;
103+ }
104+
105+ return 0;
106 }
107
108 /* if it's an Undo, it must be from someone related to us */
109@@ -708,30 +785,8 @@ int is_msg_for_me(snac *snac, const xs_dict *c_msg)
110 }
111
112 /* does this message contain a tag we are following? */
113- const xs_list *fw_tags = xs_dict_get(snac->config, "followed_hashtags");
114- if (pub_msg && xs_type(fw_tags) == XSTYPE_LIST) {
115- const xs_list *tags_in_msg = xs_dict_get(msg, "tag");
116- if (xs_type(tags_in_msg) == XSTYPE_LIST) {
117- const xs_dict *te;
118-
119- /* iterate the tags in the message */
120- xs_list_foreach(tags_in_msg, te) {
121- if (xs_type(te) == XSTYPE_DICT) {
122- const char *type = xs_dict_get(te, "type");
123- const char *name = xs_dict_get(te, "name");
124-
125- if (xs_type(type) == XSTYPE_STRING && xs_type(name) == XSTYPE_STRING) {
126- if (strcmp(type, "Hashtag") == 0) {
127- xs *lc_name = xs_utf8_to_lower(name);
128-
129- if (xs_list_in(fw_tags, lc_name) != -1)
130- return 7;
131- }
132- }
133- }
134- }
135- }
136- }
137+ if (pub_msg && followed_hashtag_check(snac, msg))
138+ return 7;
139
140 return 0;
141 }
142@@ -2277,15 +2332,23 @@ int process_input_message(snac *snac, const xs_dict *msg, const xs_dict *req)
143 xs *who_o = NULL;
144
145 if (valid_status(actor_request(snac, who, &who_o))) {
146- if (timeline_admire(snac, object, actor, 0) == HTTP_STATUS_CREATED)
147- snac_log(snac, xs_fmt("new 'Announce' %s %s", actor, object));
148- else
149- snac_log(snac, xs_fmt("repeated 'Announce' from %s to %s",
150- actor, object));
151+ /* don't account as such announces by our own relay */
152+ xs *this_relay = xs_fmt("%s/relay", srv_baseurl);
153+
154+ if (strcmp(actor, this_relay) != 0) {
155+ if (timeline_admire(snac, object, actor, 0) == HTTP_STATUS_CREATED)
156+ snac_log(snac, xs_fmt("new 'Announce' %s %s", actor, object));
157+ else
158+ snac_log(snac, xs_fmt("repeated 'Announce' from %s to %s",
159+ actor, object));
160+ }
161
162 /* distribute the post with the actor as 'proxy' */
163 list_distribute(snac, actor, a_msg);
164
165+ /* distribute the post to users following these hashtags */
166+ followed_hashtag_distribute(a_msg);
167+
168 do_notify = 1;
169 }
170 else
171@@ -2300,7 +2363,7 @@ int process_input_message(snac *snac, const xs_dict *msg, const xs_dict *req)
172 }
173 else
174 if (strcmp(type, "Update") == 0) { /** **/
175- if (xs_match(utype, "Person|Service")) { /** **/
176+ if (xs_match(utype, "Person|Service|Application")) { /** **/
177 actor_add(actor, xs_dict_get(msg, "object"));
178 timeline_touch(snac);
179
+6,
-1
1@@ -24,9 +24,11 @@ A special subset of Markdown is allowed, including:
2 .It bold
3 **text between two pairs of asterisks**
4 .It italic
5-*text between a pair of asterisks*
6+*text between a pair of asterisks* or _between a pair of underscores_
7 .It strikethrough text
8 ~~text between a pair of tildes~~
9+.It underlined text
10+__text between two pairs of underscores__
11 .It code
12 Text `between backticks` is formatted as code.
13 .Bd -literal
14@@ -53,6 +55,9 @@ Horizonal rules can be inserted by typing three minus symbols
15 alone in a line.
16 .It quoted text
17 Lines starting with >.
18+.It headers
19+One, two or three # at the beginning of a line plus a space plus
20+some text are converted to HTML headers.
21 .It user mentions
22 Strings in the format @user@host are requested using the Webfinger
23 protocol and converted to links and mentions if something reasonable
+6,
-3
1@@ -591,17 +591,20 @@ instance can subscribe to LitePub (Pleroma-style) Fediverse Relays. Doing this i
2 visibility and allows following hashtags. To do this, you must create a special user named
3 relay and, from it, follow the relay actor(s) like you do with regular actor URLs. This
4 special user will start receiving boosts from the relay server of posts from other instances
5-also following it. It any other user of the same
6+also following it. If any other user of the same
7 .Nm
8 instance follows any of the hashtags included in these boosted posts coming from the relay,
9-they will received as if they were for them.
10+they will be received as if they were for them.
11 .Pp
12 Example:
13 .Bd -literal -offset indent
14 snac adduser $SNAC_BASEDIR relay # only needed once
15-snac follow $SNAC_BASEDIR relay https://fedi-relay.gyptazy.com/actor
16+snac follow $SNAC_BASEDIR relay https://relay.example.com/actor
17 .Ed
18 .Pp
19+Users on your instance do NOT need to follow the local relay user to benefit from following
20+hashtags.
21+.Pp
22 Please take note that subscribing to relays can increase the traffic towards your instance
23 significantly. In any case, lowering the "Maximum days to keep posts" value for the relay
24 special user is recommended (e.g. setting to just 1 day).
M
format.c
+46,
-1
1@@ -92,6 +92,8 @@ static xs_str *format_line(const char *line, xs_list **attach)
2 "`[^`]+`" "|"
3 "~~[^~]+~~" "|"
4 "\\*\\*?\\*?[^\\*]+\\*?\\*?\\*" "|"
5+ "_[^_]+_" "|" //anzu
6+ "__[^_]+__" "|" //anzu
7 "!\\[[^]]+\\]\\([^\\)]+\\)" "|"
8 "\\[[^]]+\\]\\([^\\)]+\\)" "|"
9 "[a-z]+:/" "/[^[:space:]]+" "|"
10@@ -127,6 +129,20 @@ static xs_str *format_line(const char *line, xs_list **attach)
11 xs *s2 = xs_fmt("<i>%s</i>", s1);
12 s = xs_str_cat(s, s2);
13 }
14+ //anzu - begin
15+ else
16+ if (xs_startswith(v, "__")) {
17+ xs *s1 = xs_strip_chars_i(xs_dup(v), "_");
18+ xs *s2 = xs_fmt("<u>%s</u>", s1);
19+ s = xs_str_cat(s, s2);
20+ }
21+ else
22+ if (xs_startswith(v, "_")) {
23+ xs *s1 = xs_strip_chars_i(xs_dup(v), "_");
24+ xs *s2 = xs_fmt("<i>%s</i>", s1);
25+ s = xs_str_cat(s, s2);
26+ }
27+ //anzu - end
28 else
29 if (xs_startswith(v, "~~")) {
30 xs *s1 = xs_strip_chars_i(xs_dup(v), "~");
31@@ -303,6 +319,31 @@ xs_str *not_really_markdown(const char *content, xs_list **attach, xs_list **tag
32 continue;
33 }
34
35+ //anzu - begin
36+ // h1 reserved for snac?
37+ if (xs_startswith(ss, "# ")) {
38+ ss = xs_strip_i(xs_crop_i(ss, 2, 0));
39+ s = xs_str_cat(s, "<h2>");
40+ s = xs_str_cat(s, ss);
41+ s = xs_str_cat(s, "</h2>");
42+ continue;
43+ }
44+ if (xs_startswith(ss, "## ")) {
45+ ss = xs_strip_i(xs_crop_i(ss, 3, 0));
46+ s = xs_str_cat(s, "<h2>");
47+ s = xs_str_cat(s, ss);
48+ s = xs_str_cat(s, "</h2>");
49+ continue;
50+ }
51+ if (xs_startswith(ss, "### ")) {
52+ ss = xs_strip_i(xs_crop_i(ss, 4, 0));
53+ s = xs_str_cat(s, "<h3>");
54+ s = xs_str_cat(s, ss);
55+ s = xs_str_cat(s, "</h3>");
56+ continue;
57+ }
58+ //anzu - end
59+
60 if (xs_startswith(ss, ">")) {
61 /* delete the > and subsequent spaces */
62 ss = xs_strip_i(xs_crop_i(ss, 1, 0));
63@@ -336,6 +377,8 @@ xs_str *not_really_markdown(const char *content, xs_list **attach, xs_list **tag
64 s = xs_replace_i(s, "<br><br><blockquote>", "<br><blockquote>");
65 s = xs_replace_i(s, "</blockquote><br>", "</blockquote>");
66 s = xs_replace_i(s, "</pre><br>", "</pre>");
67+ s = xs_replace_i(s, "</h2><br>", "</h2>"); //anzu ???
68+ s = xs_replace_i(s, "</h3><br>", "</h3>"); //anzu ???
69
70 {
71 /* traditional emoticons */
72@@ -378,7 +421,9 @@ xs_str *not_really_markdown(const char *content, xs_list **attach, xs_list **tag
73
74 const char *valid_tags[] = {
75 "a", "p", "br", "br/", "blockquote", "ul", "ol", "li", "cite", "small",
76- "span", "i", "b", "u", "s", "pre", "code", "em", "strong", "hr", "img", "del", "bdi", NULL
77+ "span", "i", "b", "u", "s", "pre", "code", "em", "strong", "hr", "img", "del", "bdi",
78+ "h2","h3", //anzu
79+ NULL
80 };
81
82 xs_str *sanitize(const char *content)
M
html.c
+20,
-8
1@@ -1855,13 +1855,15 @@ xs_html *html_entry(snac *user, xs_dict *msg, int read_only,
2 }
3 }
4 }
5- else
6- if (strcmp(type, "Note") == 0) {
7- if (level == 0) {
8- /* is the parent not here? */
9- const char *parent = get_in_reply_to(msg);
10
11- if (user && !xs_is_null(parent) && *parent && !timeline_here(user, parent)) {
12+ if (user && strcmp(type, "Note") == 0) {
13+ /* is the parent not here? */
14+ const char *parent = get_in_reply_to(msg);
15+
16+ if (!xs_is_null(parent) && *parent) {
17+ xs *md5 = xs_md5_hex(parent, strlen(parent));
18+
19+ if (!timeline_here(user, md5)) {
20 xs_html_add(post_header,
21 xs_html_tag("div",
22 xs_html_attr("class", "snac-origin"),
23@@ -2965,9 +2967,12 @@ xs_str *html_notifications(snac *user, int skip, int show)
24 xs_html_attr("class", "snac-posts"));
25 xs_html_add(body, posts);
26
27- xs_list *p = n_list;
28+ xs_set rep;
29+ xs_set_init(&rep);
30+
31 const xs_str *v;
32- while (xs_list_iter(&p, &v)) {
33+
34+ xs_list_foreach(n_list, v) {
35 xs *noti = notify_get(user, v);
36
37 if (noti == NULL)
38@@ -2988,6 +2993,11 @@ xs_str *html_notifications(snac *user, int skip, int show)
39
40 object_get(id, &obj);
41
42+ const char *msg_id = NULL;
43+
44+ if (xs_is_dict(obj) && (msg_id = xs_dict_get(obj, "id")) && xs_set_add(&rep, msg_id) != 1)
45+ continue;
46+
47 const char *actor_id = xs_dict_get(noti, "actor");
48 xs *actor = NULL;
49
50@@ -3101,6 +3111,8 @@ xs_str *html_notifications(snac *user, int skip, int show)
51 }
52 }
53
54+ xs_set_free(&rep);
55+
56 if (noti_new == NULL && noti_seen == NULL)
57 xs_html_add(body,
58 xs_html_tag("h2",
M
httpd.c
+1,
-1
1@@ -138,7 +138,7 @@ static xs_str *greeting_html(void)
2 while (xs_list_iter(&p, &uid)) {
3 snac user;
4
5- if (user_open(&user, uid)) {
6+ if (strcmp(uid, "relay") && user_open(&user, uid)) {
7 xs_html_add(ul,
8 xs_html_tag("li",
9 xs_html_tag("a",
+4,
-1
1@@ -1453,7 +1453,10 @@ xs_list *mastoapi_timeline(snac *user, const xs_dict *args, const char *index_fn
2 xs *st = mastoapi_status(user, msg);
3
4 if (st != NULL) {
5- out = xs_list_append(out, st);
6+ if (ascending)
7+ out = xs_list_insert(out, 0, st);
8+ else
9+ out = xs_list_append(out, st);
10 cnt++;
11 }
12
+8,
-1
1@@ -63,15 +63,22 @@ LL_BEGIN(sbox_enter_linux_, const char* basedir, const char *address, int smtp_p
2 LANDLOCK_ACCESS_FS_REFER_COMPAT,
3 s = LANDLOCK_ACCESS_FS_MAKE_SOCK,
4 x = LANDLOCK_ACCESS_FS_EXECUTE;
5+ char *resolved_path = NULL;
6
7 LL_PATH(basedir, rf|rd|w|c);
8 LL_PATH("/tmp", rf|rd|w|c);
9 #ifndef WITHOUT_SHM
10 LL_PATH("/dev/shm", rf|w|c );
11 #endif
12+ LL_PATH("/dev/urandom", rf );
13 LL_PATH("/etc/resolv.conf", rf );
14 LL_PATH("/etc/hosts", rf );
15- LL_PATH("/etc/ssl", rf );
16+ LL_PATH("/etc/ssl", rf|rd );
17+ if ((resolved_path = realpath("/etc/ssl/cert.pem", NULL))) {
18+ /* some distros like cert.pem to be a symlink */
19+ LL_PATH(resolved_path, rf );
20+ free(resolved_path);
21+ }
22 LL_PATH("/usr/share/zoneinfo", rf );
23
24 if (mtime("/etc/pki") > 0)
M
snac.h
+1,
-1
1@@ -1,7 +1,7 @@
2 /* snac - A simple, minimalistic ActivityPub instance */
3 /* copyright (c) 2022 - 2025 grunfink et al. / MIT license */
4
5-#define VERSION "2.69-dev"
6+#define VERSION "2.69"
7
8 #define USER_AGENT "snac/" VERSION
9
M
utils.c
+4,
-0
1@@ -318,6 +318,10 @@ int adduser(const char *uid)
2 mkdirx(d);
3 }
4
5+ /* add a specially short data retention time for the relay */
6+ if (strcmp(uid, "relay") == 0)
7+ config = xs_dict_set(config, "purge_days", xs_stock(1));
8+
9 xs *cfn = xs_fmt("%s/user.json", basedir);
10
11 if ((f = fopen(cfn, "w")) == NULL) {