commit 7eb2556
Merge remote-tracking branch 'upstream/master' into curl-smtp
16 files changed,
+1062,
-127
M
Makefile
+14,
-7
1@@ -38,24 +38,31 @@ uninstall:
2 rm $(PREFIX_MAN)/man5/snac.5
3 rm $(PREFIX_MAN)/man8/snac.8
4
5+update-po:
6+ mkdir -p po
7+ [ -f "po/en.po" ] || xgettext -o po/en.po --language=C --keyword=L --from-code=utf-8 *.c
8+ for a in po/*.po ; do \
9+ xgettext --omit-header -j -o $$a --language=C --keyword=L --from-code=utf-8 *.c ; \
10+ done
11+
12 activitypub.o: activitypub.c xs.h xs_json.h xs_curl.h xs_mime.h \
13 xs_openssl.h xs_regex.h xs_time.h xs_set.h xs_match.h xs_unicode.h \
14 snac.h http_codes.h
15 data.o: data.c xs.h xs_hex.h xs_io.h xs_json.h xs_openssl.h xs_glob.h \
16- xs_set.h xs_time.h xs_regex.h xs_match.h xs_unicode.h xs_random.h snac.h \
17- http_codes.h
18+ xs_set.h xs_time.h xs_regex.h xs_match.h xs_unicode.h xs_random.h \
19+ xs_po.h snac.h http_codes.h
20 format.o: format.c xs.h xs_regex.h xs_mime.h xs_html.h xs_json.h \
21 xs_time.h xs_match.h snac.h http_codes.h
22 html.o: html.c xs.h xs_io.h xs_json.h xs_regex.h xs_set.h xs_openssl.h \
23- xs_time.h xs_mime.h xs_match.h xs_html.h xs_curl.h xs_unicode.h snac.h \
24- http_codes.h
25+ xs_time.h xs_mime.h xs_match.h xs_html.h xs_curl.h xs_unicode.h xs_url.h \
26+ snac.h http_codes.h
27 http.o: http.c xs.h xs_io.h xs_openssl.h xs_curl.h xs_time.h xs_json.h \
28 snac.h http_codes.h
29 httpd.o: httpd.c xs.h xs_io.h xs_json.h xs_socket.h xs_unix_socket.h \
30 xs_httpd.h xs_mime.h xs_time.h xs_openssl.h xs_fcgi.h xs_html.h snac.h \
31 http_codes.h
32-main.o: main.c xs.h xs_io.h xs_json.h xs_time.h xs_openssl.h snac.h \
33- http_codes.h
34+main.o: main.c xs.h xs_io.h xs_json.h xs_time.h xs_openssl.h xs_match.h \
35+ snac.h http_codes.h
36 mastoapi.o: mastoapi.c xs.h xs_hex.h xs_openssl.h xs_json.h xs_io.h \
37 xs_time.h xs_glob.h xs_set.h xs_random.h xs_url.h xs_mime.h xs_match.h \
38 snac.h http_codes.h
39@@ -63,7 +70,7 @@ sandbox.o: sandbox.c xs.h snac.h http_codes.h
40 snac.o: snac.c xs.h xs_hex.h xs_io.h xs_unicode_tbl.h xs_unicode.h \
41 xs_json.h xs_curl.h xs_openssl.h xs_socket.h xs_unix_socket.h xs_url.h \
42 xs_httpd.h xs_mime.h xs_regex.h xs_set.h xs_time.h xs_glob.h xs_random.h \
43- xs_match.h xs_fcgi.h xs_html.h snac.h http_codes.h
44+ xs_match.h xs_fcgi.h xs_html.h xs_po.h snac.h http_codes.h
45 upgrade.o: upgrade.c xs.h xs_io.h xs_json.h xs_glob.h snac.h http_codes.h
46 utils.o: utils.c xs.h xs_io.h xs_json.h xs_time.h xs_openssl.h \
47 xs_random.h xs_glob.h xs_curl.h xs_regex.h snac.h http_codes.h
+7,
-7
1@@ -39,20 +39,20 @@ activitypub.o: activitypub.c xs.h xs_json.h xs_curl.h xs_mime.h \
2 xs_openssl.h xs_regex.h xs_time.h xs_set.h xs_match.h xs_unicode.h \
3 snac.h http_codes.h
4 data.o: data.c xs.h xs_hex.h xs_io.h xs_json.h xs_openssl.h xs_glob.h \
5- xs_set.h xs_time.h xs_regex.h xs_match.h xs_unicode.h xs_random.h snac.h \
6- http_codes.h
7+ xs_set.h xs_time.h xs_regex.h xs_match.h xs_unicode.h xs_random.h \
8+ xs_po.h snac.h http_codes.h
9 format.o: format.c xs.h xs_regex.h xs_mime.h xs_html.h xs_json.h \
10 xs_time.h xs_match.h snac.h http_codes.h
11 html.o: html.c xs.h xs_io.h xs_json.h xs_regex.h xs_set.h xs_openssl.h \
12- xs_time.h xs_mime.h xs_match.h xs_html.h xs_curl.h xs_unicode.h snac.h \
13- http_codes.h
14+ xs_time.h xs_mime.h xs_match.h xs_html.h xs_curl.h xs_unicode.h xs_url.h \
15+ snac.h http_codes.h
16 http.o: http.c xs.h xs_io.h xs_openssl.h xs_curl.h xs_time.h xs_json.h \
17 snac.h http_codes.h
18 httpd.o: httpd.c xs.h xs_io.h xs_json.h xs_socket.h xs_unix_socket.h \
19 xs_httpd.h xs_mime.h xs_time.h xs_openssl.h xs_fcgi.h xs_html.h snac.h \
20 http_codes.h
21-main.o: main.c xs.h xs_io.h xs_json.h xs_time.h xs_openssl.h snac.h \
22- http_codes.h
23+main.o: main.c xs.h xs_io.h xs_json.h xs_time.h xs_openssl.h xs_match.h \
24+ snac.h http_codes.h
25 mastoapi.o: mastoapi.c xs.h xs_hex.h xs_openssl.h xs_json.h xs_io.h \
26 xs_time.h xs_glob.h xs_set.h xs_random.h xs_url.h xs_mime.h xs_match.h \
27 snac.h http_codes.h
28@@ -60,7 +60,7 @@ sandbox.o: sandbox.c xs.h snac.h http_codes.h
29 snac.o: snac.c xs.h xs_hex.h xs_io.h xs_unicode_tbl.h xs_unicode.h \
30 xs_json.h xs_curl.h xs_openssl.h xs_socket.h xs_unix_socket.h xs_url.h \
31 xs_httpd.h xs_mime.h xs_regex.h xs_set.h xs_time.h xs_glob.h xs_random.h \
32- xs_match.h xs_fcgi.h xs_html.h snac.h http_codes.h
33+ xs_match.h xs_fcgi.h xs_html.h xs_po.h snac.h http_codes.h
34 upgrade.o: upgrade.c xs.h xs_io.h xs_json.h xs_glob.h snac.h http_codes.h
35 utils.o: utils.c xs.h xs_io.h xs_json.h xs_time.h xs_openssl.h \
36 xs_random.h xs_glob.h xs_curl.h xs_regex.h snac.h http_codes.h
+11,
-3
1@@ -2715,6 +2715,12 @@ int process_user_queue(snac *snac)
2 }
3
4
5+xs_str *str_status(int status)
6+{
7+ return xs_fmt("%d %s", status, status < 0 ? xs_curl_strerr(status) : http_status_text(status));
8+}
9+
10+
11 void process_queue_item(xs_dict *q_item)
12 /* processes an item from the global queue */
13 {
14@@ -2771,7 +2777,9 @@ void process_queue_item(xs_dict *q_item)
15 else
16 payload = xs_str_new(NULL);
17
18- srv_log(xs_fmt("output message: sent to inbox %s %d%s", inbox, status, payload));
19+ xs *s_status = str_status(status);
20+
21+ srv_log(xs_fmt("output message: sent to inbox %s (%s)%s", inbox, s_status, payload));
22
23 if (!valid_status(status)) {
24 retries++;
25@@ -2789,10 +2797,10 @@ void process_queue_item(xs_dict *q_item)
26 || status == HTTP_STATUS_UNPROCESSABLE_CONTENT
27 || status < 0)
28 /* explicit error: discard */
29- srv_log(xs_fmt("output message: error %s %d", inbox, status));
30+ srv_log(xs_fmt("output message: error %s (%s)", inbox, s_status));
31 else
32 if (retries > queue_retry_max)
33- srv_log(xs_fmt("output message: giving up %s %d", inbox, status));
34+ srv_log(xs_fmt("output message: giving up %s (%s)", inbox, s_status));
35 else {
36 /* requeue */
37 enqueue_output_raw(keyid, seckey, msg, inbox, retries, status);
M
data.c
+45,
-0
1@@ -13,6 +13,7 @@
2 #include "xs_match.h"
3 #include "xs_unicode.h"
4 #include "xs_random.h"
5+#include "xs_po.h"
6
7 #include "snac.h"
8
9@@ -98,6 +99,9 @@ int srv_open(const char *basedir, int auto_upgrade)
10 if (error != NULL)
11 srv_log(error);
12
13+ if (!ret)
14+ return ret;
15+
16 /* create the queue/ subdir, just in case */
17 xs *qdir = xs_fmt("%s/queue", srv_basedir);
18 mkdirx(qdir);
19@@ -148,6 +152,29 @@ int srv_open(const char *basedir, int auto_upgrade)
20 mkdirx(expdir);
21 }
22
23+ /* languages */
24+ srv_langs = xs_dict_new();
25+ srv_langs = xs_dict_set(srv_langs, "en", xs_stock(XSTYPE_NULL));
26+
27+ xs *l_dir = xs_fmt("%s/lang/", srv_basedir);
28+ mkdirx(l_dir);
29+
30+ l_dir = xs_str_cat(l_dir, "*.po");
31+ xs *pos = xs_glob(l_dir, 0, 0);
32+ const char *po;
33+
34+ xs_list_foreach(pos, po) {
35+ xs *d = xs_po_to_dict(po);
36+
37+ if (xs_is_dict(d)) {
38+ xs *l = xs_split(po, "/");
39+ xs *id = xs_dup(xs_list_get(l, -1));
40+ id = xs_replace_i(id, ".po", "");
41+
42+ srv_langs = xs_dict_set(srv_langs, id, d);
43+ }
44+ }
45+
46 return ret;
47 }
48
49@@ -4064,3 +4091,21 @@ void badlogin_inc(const char *user, const char *addr)
50 pthread_mutex_unlock(&data_mutex);
51 }
52 }
53+
54+
55+/** language strings **/
56+
57+const char *lang_str(const char *str, const snac *user)
58+/* returns a translated string */
59+{
60+ const char *n_str = str;
61+
62+ if (user && xs_is_dict(user->lang) && xs_is_string(str)) {
63+ n_str = xs_dict_get(user->lang, str);
64+
65+ if (xs_is_null(n_str) || *n_str == '\0')
66+ n_str = str;
67+ }
68+
69+ return n_str;
70+}
+10,
-7
1@@ -23,8 +23,12 @@ Ultrix machine in your grandfather basement, probably MacOS) support hard
2 links on their native filesystems. Don't do fancy things like moving the
3 subdirectories to different filesystems. Also, if you move your
4 .Nm
5-installation to another server, do it with a tool that respect hard
6-link counts. Remember:
7+installation to another server, do it with a tool that keeps hard
8+links, like
9+.Xr tar 1
10+or
11+.Xr rsync 1
12+with the -H switch. Remember:
13 .Nm
14 is a very UNIXy program that loves hard links.
15 .Ss Building and Installation
16@@ -194,9 +198,7 @@ By setting this to true, no inbox collection is done. Inbox collection helps
17 being discovered from remote instances, but also increases network traffic.
18 .It Ic http_headers
19 If you need to add more HTTP response headers for whatever reason, you can
20-fill this object with the required header/value pairs. For example, for enhanced
21-XSS security, you can set the "Content-Security-Policy" header to "script-src ;"
22-to be totally sure that no JavaScript is executed.
23+fill this object with the required header/value pairs.
24 .It Ic show_instance_timeline
25 If this is set to true, the instance base URL will show a timeline with the latest
26 user posts instead of the default greeting static page. If other information
27@@ -327,8 +329,9 @@ These weapons of mass destruction can be written into the
28 file in the server base directory, one per line; if this file exists,
29 all posts' content will be matched (after being stripped of HTML tags)
30 against these regexes, one by one, and any match will make the post to
31-be rejected. If you don't know about regular expressions, don't use this
32-option (or learn about them in some tutorial, there are gazillions of
33+be rejected. Use lower case, the regex will be case insensitive by default.
34+If you don't know about regular expressions, don't use this
35+option (or learn about them inw some tutorial, there are gazillions of
36 them out there), as you and your users may start missing posts. Also,
37 given that every regular expression implementation supports a different
38 set of features, consider reading the documentation about the one
M
format.c
+1,
-1
1@@ -458,7 +458,7 @@ xs_str *sanitize(const char *content)
2
3 if (valid_tags[i]) {
4 /* accepted tag: rebuild it with only the accepted elements */
5- xs *el = xs_regex_select(v, "(src|href|rel|class|target)=\"[^\"]*\"");
6+ xs *el = xs_regex_select(v, "(src|href|rel|class|target)=(\"[^\"]*\"|'[^']*')");
7 xs *s3 = xs_join(el, " ");
8
9 s2 = xs_fmt("<%s%s%s%s>",
M
html.c
+144,
-80
1@@ -17,7 +17,7 @@
2
3 #include "snac.h"
4
5-int login(snac *snac, const xs_dict *headers)
6+int login(snac *user, const xs_dict *headers)
7 /* tries a login */
8 {
9 int logged_in = 0;
10@@ -31,23 +31,23 @@ int login(snac *snac, const xs_dict *headers)
11 xs *l1 = xs_split_n(s2, ":", 1);
12
13 if (xs_list_len(l1) == 2) {
14- const char *user = xs_list_get(l1, 0);
15+ const char *uid = xs_list_get(l1, 0);
16 const char *pwd = xs_list_get(l1, 1);
17 const char *addr = xs_or(xs_dict_get(headers, "remote-addr"),
18 xs_dict_get(headers, "x-forwarded-for"));
19
20- if (badlogin_check(user, addr)) {
21- logged_in = check_password(user, pwd,
22- xs_dict_get(snac->config, "passwd"));
23+ if (badlogin_check(uid, addr)) {
24+ logged_in = check_password(uid, pwd,
25+ xs_dict_get(user->config, "passwd"));
26
27 if (!logged_in)
28- badlogin_inc(user, addr);
29+ badlogin_inc(uid, addr);
30 }
31 }
32 }
33
34 if (logged_in)
35- lastlog_write(snac, "web");
36+ lastlog_write(user, "web");
37
38 return logged_in;
39 }
40@@ -69,7 +69,7 @@ xs_str *replace_shortnames(xs_str *s, const xs_list *tag, int ems, const char *p
41
42 xs *style = xs_fmt("height: %dem; width: %dem; vertical-align: middle;", ems, ems);
43
44- const char *v;
45+ const xs_dict *v;
46 int c = 0;
47
48 while (xs_list_next(tag_list, &v, &c)) {
49@@ -77,19 +77,25 @@ xs_str *replace_shortnames(xs_str *s, const xs_list *tag, int ems, const char *p
50
51 if (t && strcmp(t, "Emoji") == 0) {
52 const char *n = xs_dict_get(v, "name");
53- const char *i = xs_dict_get(v, "icon");
54+ const xs_dict *i = xs_dict_get(v, "icon");
55
56- if (n && i) {
57+ if (xs_is_string(n) && xs_is_dict(i)) {
58 const char *u = xs_dict_get(i, "url");
59- xs *url = make_url(u, proxy, 0);
60+ const char *mt = xs_dict_get(i, "mediaType");
61+
62+ if (xs_is_string(u) && xs_is_string(mt) && strcmp(mt, "image/svg+xml")) {
63+ xs *url = make_url(u, proxy, 0);
64
65- xs_html *img = xs_html_sctag("img",
66- xs_html_attr("loading", "lazy"),
67- xs_html_attr("src", url),
68- xs_html_attr("style", style));
69+ xs_html *img = xs_html_sctag("img",
70+ xs_html_attr("loading", "lazy"),
71+ xs_html_attr("src", url),
72+ xs_html_attr("style", style));
73
74- xs *s1 = xs_html_render(img);
75- s = xs_replace_i(s, n, s1);
76+ xs *s1 = xs_html_render(img);
77+ s = xs_replace_i(s, n, s1);
78+ }
79+ else
80+ s = xs_replace_i(s, n, "");
81 }
82 }
83 }
84@@ -621,6 +627,9 @@ static xs_html *html_instance_body(void)
85 const char *email = xs_dict_get(srv_config, "admin_email");
86 const char *acct = xs_dict_get(srv_config, "admin_account");
87
88+ /* for L() */
89+ const snac *user = NULL;
90+
91 xs *blurb = xs_replace(snac_blurb, "%host%", host);
92
93 xs_html *dl;
94@@ -735,9 +744,11 @@ xs_html *html_user_head(snac *user, const char *desc, const char *url)
95 xs *fwers = follower_list(user);
96 xs *fwing = following_list(user);
97
98- xs *s1 = xs_fmt(L("%d following, %d followers · "),
99+ xs *s1 = xs_fmt(L("%d following, %d followers"),
100 xs_list_len(fwing), xs_list_len(fwers));
101
102+ s1 = xs_str_cat(s1, " · ");
103+
104 s_desc = xs_str_prepend_i(s_desc, s1);
105 }
106
107@@ -1052,8 +1063,8 @@ static xs_html *html_user_body(snac *user, int read_only)
108 const char *longitude = xs_dict_get_def(user->config, "longitude", "");
109
110 if (*latitude && *longitude) {
111- xs *label = xs_fmt(L("%s,%s"), latitude, longitude);
112- xs *url = xs_fmt(L("https://openstreetmap.org/search?query=%s,%s"),
113+ xs *label = xs_fmt("%s,%s", latitude, longitude);
114+ xs *url = xs_fmt("https://openstreetmap.org/search?query=%s,%s",
115 latitude, longitude);
116
117 xs_html_add(top_user,
118@@ -1069,7 +1080,7 @@ static xs_html *html_user_body(snac *user, int read_only)
119 xs *fwers = follower_list(user);
120 xs *fwing = following_list(user);
121
122- xs *s1 = xs_fmt(L("%d following %d followers"),
123+ xs *s1 = xs_fmt(L("%d following, %d followers"),
124 xs_list_len(fwing), xs_list_len(fwers));
125
126 xs_html_add(top_user,
127@@ -1085,16 +1096,16 @@ static xs_html *html_user_body(snac *user, int read_only)
128 }
129
130
131-xs_html *html_top_controls(snac *snac)
132+xs_html *html_top_controls(snac *user)
133 /* generates the top controls */
134 {
135- xs *ops_action = xs_fmt("%s/admin/action", snac->actor);
136+ xs *ops_action = xs_fmt("%s/admin/action", user->actor);
137
138 xs_html *top_controls = xs_html_tag("div",
139 xs_html_attr("class", "snac-top-controls"),
140
141 /** new post **/
142- html_note(snac, L("New Post..."),
143+ html_note(user, L("New Post..."),
144 "new_post_div", "new_post_form",
145 L("What's on your mind?"), "",
146 NULL, NULL,
147@@ -1164,53 +1175,53 @@ xs_html *html_top_controls(snac *snac)
148 const char *email = "[disabled by admin]";
149
150 if (xs_type(xs_dict_get(srv_config, "disable_email_notifications")) != XSTYPE_TRUE) {
151- email = xs_dict_get(snac->config_o, "email");
152+ email = xs_dict_get(user->config_o, "email");
153 if (xs_is_null(email)) {
154- email = xs_dict_get(snac->config, "email");
155+ email = xs_dict_get(user->config, "email");
156
157 if (xs_is_null(email))
158 email = "";
159 }
160 }
161
162- const char *cw = xs_dict_get(snac->config, "cw");
163+ const char *cw = xs_dict_get(user->config, "cw");
164 if (xs_is_null(cw))
165 cw = "";
166
167- const char *telegram_bot = xs_dict_get(snac->config, "telegram_bot");
168+ const char *telegram_bot = xs_dict_get(user->config, "telegram_bot");
169 if (xs_is_null(telegram_bot))
170 telegram_bot = "";
171
172- const char *telegram_chat_id = xs_dict_get(snac->config, "telegram_chat_id");
173+ const char *telegram_chat_id = xs_dict_get(user->config, "telegram_chat_id");
174 if (xs_is_null(telegram_chat_id))
175 telegram_chat_id = "";
176
177- const char *ntfy_server = xs_dict_get(snac->config, "ntfy_server");
178+ const char *ntfy_server = xs_dict_get(user->config, "ntfy_server");
179 if (xs_is_null(ntfy_server))
180 ntfy_server = "";
181
182- const char *ntfy_token = xs_dict_get(snac->config, "ntfy_token");
183+ const char *ntfy_token = xs_dict_get(user->config, "ntfy_token");
184 if (xs_is_null(ntfy_token))
185 ntfy_token = "";
186
187- const char *purge_days = xs_dict_get(snac->config, "purge_days");
188+ const char *purge_days = xs_dict_get(user->config, "purge_days");
189 if (!xs_is_null(purge_days) && xs_type(purge_days) == XSTYPE_NUMBER)
190 purge_days = (char *)xs_number_str(purge_days);
191 else
192 purge_days = "0";
193
194- const xs_val *d_dm_f_u = xs_dict_get(snac->config, "drop_dm_from_unknown");
195- const xs_val *bot = xs_dict_get(snac->config, "bot");
196- const xs_val *a_private = xs_dict_get(snac->config, "private");
197- const xs_val *auto_boost = xs_dict_get(snac->config, "auto_boost");
198- const xs_val *coll_thrds = xs_dict_get(snac->config, "collapse_threads");
199- const xs_val *pending = xs_dict_get(snac->config, "approve_followers");
200- const xs_val *show_foll = xs_dict_get(snac->config, "show_contact_metrics");
201- const char *latitude = xs_dict_get_def(snac->config, "latitude", "");
202- const char *longitude = xs_dict_get_def(snac->config, "longitude", "");
203+ const xs_val *d_dm_f_u = xs_dict_get(user->config, "drop_dm_from_unknown");
204+ const xs_val *bot = xs_dict_get(user->config, "bot");
205+ const xs_val *a_private = xs_dict_get(user->config, "private");
206+ const xs_val *auto_boost = xs_dict_get(user->config, "auto_boost");
207+ const xs_val *coll_thrds = xs_dict_get(user->config, "collapse_threads");
208+ const xs_val *pending = xs_dict_get(user->config, "approve_followers");
209+ const xs_val *show_foll = xs_dict_get(user->config, "show_contact_metrics");
210+ const char *latitude = xs_dict_get_def(user->config, "latitude", "");
211+ const char *longitude = xs_dict_get_def(user->config, "longitude", "");
212
213 xs *metadata = NULL;
214- const xs_dict *md = xs_dict_get(snac->config, "metadata");
215+ const xs_dict *md = xs_dict_get(user->config, "metadata");
216
217 if (xs_type(md) == XSTYPE_DICT) {
218 const xs_str *k;
219@@ -1232,7 +1243,29 @@ xs_html *html_top_controls(snac *snac)
220 else
221 metadata = xs_str_new(NULL);
222
223- xs *user_setup_action = xs_fmt("%s/admin/user-setup", snac->actor);
224+ /* ui language */
225+ xs_html *lang_select = xs_html_tag("select",
226+ xs_html_attr("name", "web_ui_lang"));
227+
228+ const char *u_lang = xs_dict_get_def(user->config, "lang", "en");
229+ const char *lang;
230+ const xs_dict *langs;
231+
232+ xs_dict_foreach(srv_langs, lang, langs) {
233+ if (strcmp(u_lang, lang) == 0)
234+ xs_html_add(lang_select,
235+ xs_html_tag("option",
236+ xs_html_text(lang),
237+ xs_html_attr("value", lang),
238+ xs_html_attr("selected", "selected")));
239+ else
240+ xs_html_add(lang_select,
241+ xs_html_tag("option",
242+ xs_html_text(lang),
243+ xs_html_attr("value", lang)));
244+ }
245+
246+ xs *user_setup_action = xs_fmt("%s/admin/user-setup", user->actor);
247
248 xs_html_add(top_controls,
249 xs_html_tag("details",
250@@ -1251,7 +1284,7 @@ xs_html *html_top_controls(snac *snac)
251 xs_html_sctag("input",
252 xs_html_attr("type", "text"),
253 xs_html_attr("name", "name"),
254- xs_html_attr("value", xs_dict_get(snac->config, "name")),
255+ xs_html_attr("value", xs_dict_get(user->config, "name")),
256 xs_html_attr("placeholder", L("Your name")))),
257 xs_html_tag("p",
258 xs_html_text(L("Avatar: ")),
259@@ -1281,7 +1314,7 @@ xs_html *html_top_controls(snac *snac)
260 xs_html_attr("cols", "40"),
261 xs_html_attr("rows", "4"),
262 xs_html_attr("placeholder", L("Write about yourself here...")),
263- xs_html_text(xs_dict_get(snac->config, "bio")))),
264+ xs_html_text(xs_dict_get(user->config, "bio")))),
265 xs_html_sctag("input",
266 xs_html_attr("type", "checkbox"),
267 xs_html_attr("name", "cw"),
268@@ -1422,6 +1455,11 @@ xs_html *html_top_controls(snac *snac)
269 "/example.com/my-blog\nGPG Key=1FA54\n..."),
270 xs_html_text(metadata))),
271
272+ xs_html_tag("p",
273+ xs_html_text(L("Web interface language:")),
274+ xs_html_sctag("br", NULL),
275+ lang_select),
276+
277 xs_html_tag("p",
278 xs_html_text(L("New password:")),
279 xs_html_sctag("br", NULL),
280@@ -1444,8 +1482,8 @@ xs_html *html_top_controls(snac *snac)
281
282 xs_html_tag("p", NULL)))));
283
284- xs *followed_hashtags_action = xs_fmt("%s/admin/followed-hashtags", snac->actor);
285- xs *followed_hashtags = xs_join(xs_dict_get_def(snac->config,
286+ xs *followed_hashtags_action = xs_fmt("%s/admin/followed-hashtags", user->actor);
287+ xs *followed_hashtags = xs_join(xs_dict_get_def(user->config,
288 "followed_hashtags", xs_stock(XSTYPE_LIST)), "\n");
289
290 xs_html_add(top_controls,
291@@ -1480,7 +1518,7 @@ xs_html *html_top_controls(snac *snac)
292 }
293
294
295-static xs_html *html_button(char *clss, char *label, char *hint)
296+static xs_html *html_button(const char *clss, const char *label, const char *hint)
297 {
298 xs *c = xs_fmt("snac-btn-%s", clss);
299
300@@ -1496,7 +1534,7 @@ static xs_html *html_button(char *clss, char *label, char *hint)
301 }
302
303
304-xs_str *build_mentions(snac *snac, const xs_dict *msg)
305+xs_str *build_mentions(snac *user, const xs_dict *msg)
306 /* returns a string with the mentions in msg */
307 {
308 xs_str *s = xs_str_new(NULL);
309@@ -1510,7 +1548,7 @@ xs_str *build_mentions(snac *snac, const xs_dict *msg)
310 const char *name = xs_dict_get(v, "name");
311
312 if (type && strcmp(type, "Mention") == 0 &&
313- href && strcmp(href, snac->actor) != 0 && name) {
314+ href && strcmp(href, user->actor) != 0 && name) {
315 xs *s1 = NULL;
316
317 if (name[0] != '@') {
318@@ -1551,7 +1589,7 @@ xs_str *build_mentions(snac *snac, const xs_dict *msg)
319 }
320
321
322-xs_html *html_entry_controls(snac *snac, const char *actor,
323+xs_html *html_entry_controls(snac *user, const char *actor,
324 const xs_dict *msg, const char *md5)
325 {
326 const char *id = xs_dict_get(msg, "id");
327@@ -1560,7 +1598,7 @@ xs_html *html_entry_controls(snac *snac, const char *actor,
328 xs *likes = object_likes(id);
329 xs *boosts = object_announces(id);
330
331- xs *action = xs_fmt("%s/admin/action", snac->actor);
332+ xs *action = xs_fmt("%s/admin/action", user->actor);
333 xs *redir = xs_fmt("%s_entry", md5);
334
335 xs_html *form;
336@@ -1587,8 +1625,8 @@ xs_html *html_entry_controls(snac *snac, const char *actor,
337 xs_html_attr("name", "redir"),
338 xs_html_attr("value", redir))));
339
340- if (!xs_startswith(id, snac->actor)) {
341- if (xs_list_in(likes, snac->md5) == -1) {
342+ if (!xs_startswith(id, user->actor)) {
343+ if (xs_list_in(likes, user->md5) == -1) {
344 /* not already liked; add button */
345 xs_html_add(form,
346 html_button("like", L("Like"), L("Say you like this post")));
347@@ -1600,7 +1638,7 @@ xs_html *html_entry_controls(snac *snac, const char *actor,
348 }
349 }
350 else {
351- if (is_pinned(snac, id))
352+ if (is_pinned(user, id))
353 xs_html_add(form,
354 html_button("unpin", L("Unpin"), L("Unpin this post from your timeline")));
355 else
356@@ -1609,7 +1647,7 @@ xs_html *html_entry_controls(snac *snac, const char *actor,
357 }
358
359 if (is_msg_public(msg)) {
360- if (xs_list_in(boosts, snac->md5) == -1) {
361+ if (xs_list_in(boosts, user->md5) == -1) {
362 /* not already boosted; add button */
363 xs_html_add(form,
364 html_button("boost", L("Boost"), L("Announce this post to your followers")));
365@@ -1621,16 +1659,16 @@ xs_html *html_entry_controls(snac *snac, const char *actor,
366 }
367 }
368
369- if (is_bookmarked(snac, id))
370+ if (is_bookmarked(user, id))
371 xs_html_add(form,
372 html_button("unbookmark", L("Unbookmark"), L("Delete this post from your bookmarks")));
373 else
374 xs_html_add(form,
375 html_button("bookmark", L("Bookmark"), L("Add this post to your bookmarks")));
376
377- if (strcmp(actor, snac->actor) != 0) {
378+ if (strcmp(actor, user->actor) != 0) {
379 /* controls for other actors than this one */
380- if (following_check(snac, actor)) {
381+ if (following_check(user, actor)) {
382 xs_html_add(form,
383 html_button("unfollow", L("Unfollow"), L("Stop following this user's activity")));
384 }
385@@ -1640,7 +1678,7 @@ xs_html *html_entry_controls(snac *snac, const char *actor,
386 }
387
388 if (!xs_is_null(group)) {
389- if (following_check(snac, group)) {
390+ if (following_check(user, group)) {
391 xs_html_add(form,
392 html_button("unfollow", L("Unfollow Group"),
393 L("Stop following this group or channel")));
394@@ -1666,7 +1704,7 @@ xs_html *html_entry_controls(snac *snac, const char *actor,
395
396 const char *prev_src = xs_dict_get(msg, "sourceContent");
397
398- if (!xs_is_null(prev_src) && strcmp(actor, snac->actor) == 0) { /** edit **/
399+ if (!xs_is_null(prev_src) && strcmp(actor, user->actor) == 0) { /** edit **/
400 /* post can be edited */
401 xs *div_id = xs_fmt("%s_edit", md5);
402 xs *form_id = xs_fmt("%s_edit_form", md5);
403@@ -1693,26 +1731,26 @@ xs_html *html_entry_controls(snac *snac, const char *actor,
404
405 xs_html_add(controls, xs_html_tag("div",
406 xs_html_tag("p", NULL),
407- html_note(snac, L("Edit..."),
408+ html_note(user, L("Edit..."),
409 div_id, form_id,
410 "", prev_src,
411 id, NULL,
412 xs_dict_get(msg, "sensitive"), xs_dict_get(msg, "summary"),
413 xs_stock(is_msg_public(msg) ? XSTYPE_FALSE : XSTYPE_TRUE), redir,
414- NULL, 0, att_files, att_alt_texts, is_draft(snac, id))),
415+ NULL, 0, att_files, att_alt_texts, is_draft(user, id))),
416 xs_html_tag("p", NULL));
417 }
418
419 { /** reply **/
420 /* the post textarea */
421- xs *ct = build_mentions(snac, msg);
422+ xs *ct = build_mentions(user, msg);
423 xs *div_id = xs_fmt("%s_reply", md5);
424 xs *form_id = xs_fmt("%s_reply_form", md5);
425 xs *redir = xs_fmt("%s_entry", md5);
426
427 xs_html_add(controls, xs_html_tag("div",
428 xs_html_tag("p", NULL),
429- html_note(snac, L("Reply..."),
430+ html_note(user, L("Reply..."),
431 div_id, form_id,
432 "", ct,
433 NULL, NULL,
434@@ -1839,7 +1877,7 @@ xs_html *html_entry(snac *user, xs_dict *msg, int read_only,
435 xs_html_raw(" 📌 ")));
436 }
437
438- if (user && is_bookmarked(user, id)) {
439+ if (user && !read_only && is_bookmarked(user, id)) {
440 /* add a bookmark emoji */
441 xs_html_add(score,
442 xs_html_tag("span",
443@@ -2242,6 +2280,11 @@ xs_html *html_entry(snac *user, xs_dict *msg, int read_only,
444 if (content && xs_str_in(content, o_href) != -1)
445 continue;
446
447+ /* drop silently any attachment that may include JavaScript */
448+ if (strcmp(type, "image/svg+xml") == 0 ||
449+ strcmp(type, "text/html") == 0)
450+ continue;
451+
452 /* do this attachment include an icon? */
453 const xs_dict *icon = xs_dict_get(a, "icon");
454 if (xs_type(icon) == XSTYPE_DICT) {
455@@ -2571,7 +2614,7 @@ xs_html *html_entry(snac *user, xs_dict *msg, int read_only,
456 }
457
458
459-xs_html *html_footer(void)
460+xs_html *html_footer(const snac *user)
461 {
462 return xs_html_tag("div",
463 xs_html_attr("class", "snac-footer"),
464@@ -2879,13 +2922,13 @@ xs_str *html_timeline(snac *user, const xs_list *list, int read_only,
465 }
466
467 xs_html_add(body,
468- html_footer());
469+ html_footer(user));
470
471 return xs_html_render_s(html, "<!DOCTYPE html>\n");
472 }
473
474
475-xs_html *html_people_list(snac *snac, xs_list *list, char *header, char *t, const char *proxy)
476+xs_html *html_people_list(snac *user, xs_list *list, const char *header, const char *t, const char *proxy)
477 {
478 xs_html *snac_posts;
479 xs_html *people = xs_html_tag("div",
480@@ -2910,7 +2953,7 @@ xs_html *html_people_list(snac *snac, xs_list *list, char *header, char *t, cons
481 xs_html_attr("name", md5)),
482 xs_html_tag("div",
483 xs_html_attr("class", "snac-post-header"),
484- html_actor_icon(snac, actor, xs_dict_get(actor, "published"),
485+ html_actor_icon(user, actor, xs_dict_get(actor, "published"),
486 NULL, NULL, 0, 1, proxy, NULL, NULL)));
487
488 /* content (user bio) */
489@@ -2934,7 +2977,7 @@ xs_html *html_people_list(snac *snac, xs_list *list, char *header, char *t, cons
490 }
491
492 /* buttons */
493- xs *btn_form_action = xs_fmt("%s/admin/action", snac->actor);
494+ xs *btn_form_action = xs_fmt("%s/admin/action", user->actor);
495
496 xs_html *snac_controls = xs_html_tag("div",
497 xs_html_attr("class", "snac-controls"));
498@@ -2954,12 +2997,12 @@ xs_html *html_people_list(snac *snac, xs_list *list, char *header, char *t, cons
499
500 xs_html_add(snac_controls, form);
501
502- if (following_check(snac, actor_id)) {
503+ if (following_check(user, actor_id)) {
504 xs_html_add(form,
505 html_button("unfollow", L("Unfollow"),
506 L("Stop following this user's activity")));
507
508- if (is_limited(snac, actor_id))
509+ if (is_limited(user, actor_id))
510 xs_html_add(form,
511 html_button("unlimit", L("Unlimit"),
512 L("Allow announces (boosts) from this user")));
513@@ -2973,12 +3016,12 @@ xs_html *html_people_list(snac *snac, xs_list *list, char *header, char *t, cons
514 html_button("follow", L("Follow"),
515 L("Start following this user's activity")));
516
517- if (follower_check(snac, actor_id))
518+ if (follower_check(user, actor_id))
519 xs_html_add(form,
520 html_button("delete", L("Delete"), L("Delete this user")));
521 }
522
523- if (pending_check(snac, actor_id)) {
524+ if (pending_check(user, actor_id)) {
525 xs_html_add(form,
526 html_button("approve", L("Approve"),
527 L("Approve this follow request")));
528@@ -2987,7 +3030,7 @@ xs_html *html_people_list(snac *snac, xs_list *list, char *header, char *t, cons
529 html_button("discard", L("Discard"), L("Discard this follow request")));
530 }
531
532- if (is_muted(snac, actor_id))
533+ if (is_muted(user, actor_id))
534 xs_html_add(form,
535 html_button("unmute", L("Unmute"),
536 L("Stop blocking activities from this user")));
537@@ -3002,7 +3045,7 @@ xs_html *html_people_list(snac *snac, xs_list *list, char *header, char *t, cons
538
539 xs_html_add(snac_controls,
540 xs_html_tag("p", NULL),
541- html_note(snac, L("Direct Message..."),
542+ html_note(user, L("Direct Message..."),
543 dm_div_id, dm_form_id,
544 "", "",
545 NULL, actor_id,
546@@ -3048,7 +3091,7 @@ xs_str *html_people(snac *user)
547 html_user_head(user, NULL, NULL),
548 xs_html_add(html_user_body(user, 0),
549 lists,
550- html_footer()));
551+ html_footer(user)));
552
553 return xs_html_render_s(html, "<!DOCTYPE html>\n");
554 }
555@@ -3298,7 +3341,7 @@ xs_str *html_notifications(snac *user, int skip, int show)
556 xs_set_free(&rep);
557
558 xs_html_add(body,
559- html_footer());
560+ html_footer(user));
561
562 /* set the check time to now */
563 xs *dummy = notify_check_time(user, 1);
564@@ -3310,12 +3353,24 @@ xs_str *html_notifications(snac *user, int skip, int show)
565 }
566
567
568+void set_user_lang(snac *user)
569+/* sets the language dict according to user configuration */
570+{
571+ user->lang = NULL;
572+ const char *lang = xs_dict_get(user->config, "lang");
573+
574+ if (xs_is_string(lang))
575+ user->lang = xs_dict_get(srv_langs, lang);
576+}
577+
578+
579 int html_get_handler(const xs_dict *req, const char *q_path,
580 char **body, int *b_size, char **ctype,
581 xs_str **etag, xs_str **last_modified)
582 {
583 const char *accept = xs_dict_get(req, "accept");
584 int status = HTTP_STATUS_NOT_FOUND;
585+ const snac *user = NULL;
586 snac snac;
587 xs *uid = NULL;
588 const char *p_path;
589@@ -3382,6 +3437,9 @@ int html_get_handler(const xs_dict *req, const char *q_path,
590 return HTTP_STATUS_NOT_FOUND;
591 }
592
593+ user = &snac; /* for L() */
594+ set_user_lang(&snac);
595+
596 if (xs_is_true(xs_dict_get(srv_config, "proxy_media")))
597 proxy = 1;
598
599@@ -3550,7 +3608,7 @@ int html_get_handler(const xs_dict *req, const char *q_path,
600 html_user_head(&snac, NULL, NULL),
601 xs_html_add(html_user_body(&snac, 0),
602 page,
603- html_footer()));
604+ html_footer(user)));
605
606 *body = xs_html_render_s(html, "<!DOCTYPE html>\n");
607 *b_size = strlen(*body);
608@@ -4005,6 +4063,7 @@ int html_post_handler(const xs_dict *req, const char *q_path,
609 (void)ctype;
610
611 int status = 0;
612+ const snac *user = NULL;
613 snac snac;
614 const char *uid;
615 const char *p_path;
616@@ -4028,6 +4087,9 @@ int html_post_handler(const xs_dict *req, const char *q_path,
617 return HTTP_STATUS_UNAUTHORIZED;
618 }
619
620+ user = &snac; /* for L() */
621+ set_user_lang(&snac);
622+
623 p_vars = xs_dict_get(req, "p_vars");
624
625 if (p_path && strcmp(p_path, "admin/note") == 0) { /** **/
626@@ -4470,6 +4532,8 @@ int html_post_handler(const xs_dict *req, const char *q_path,
627 snac.config = xs_dict_set(snac.config, "show_contact_metrics", xs_stock(XSTYPE_TRUE));
628 else
629 snac.config = xs_dict_set(snac.config, "show_contact_metrics", xs_stock(XSTYPE_FALSE));
630+ if ((v = xs_dict_get(p_vars, "web_ui_lang")) != NULL)
631+ snac.config = xs_dict_set(snac.config, "lang", v);
632
633 snac.config = xs_dict_set(snac.config, "latitude", xs_dict_get_def(p_vars, "latitude", ""));
634 snac.config = xs_dict_set(snac.config, "longitude", xs_dict_get_def(p_vars, "longitude", ""));
M
httpd.c
+5,
-0
1@@ -211,6 +211,8 @@ int server_get_handler(xs_dict *req, const char *q_path,
2 {
3 int status = 0;
4
5+ const snac *user = NULL;
6+
7 /* is it the server root? */
8 if (*q_path == '\0' || strcmp(q_path, "/") == 0) {
9 const xs_dict *q_vars = xs_dict_get(req, "q_vars");
10@@ -553,6 +555,9 @@ void httpd_connection(FILE *f)
11 headers = xs_dict_append(headers, "access-control-allow-origin", "*");
12 headers = xs_dict_append(headers, "access-control-allow-headers", "*");
13
14+ /* disable any form of fucking JavaScript */
15+ headers = xs_dict_append(headers, "Content-Security-Policy", "script-src ;");
16+
17 if (p_state->use_fcgi)
18 xs_fcgi_response(f, status, headers, body, b_size, fcgi_id);
19 else
A
po/en.po
+687,
-0
1@@ -0,0 +1,687 @@
2+# snac message translation file
3+#
4+#, fuzzy
5+msgid ""
6+msgstr ""
7+"Project-Id-Version: snac\n"
8+"Last-Translator: grunfink\n"
9+"Language: en\n"
10+"Content-Type: text/plain; charset=UTF-8\n"
11+
12+#: html.c:367
13+msgid "Sensitive content: "
14+msgstr ""
15+
16+#: html.c:375
17+msgid "Sensitive content description"
18+msgstr ""
19+
20+#: html.c:388
21+msgid "Only for mentioned people: "
22+msgstr ""
23+
24+#: html.c:411
25+msgid "Reply to (URL): "
26+msgstr ""
27+
28+#: html.c:420
29+msgid "Don't send, but store as a draft"
30+msgstr ""
31+
32+#: html.c:421
33+msgid "Draft:"
34+msgstr ""
35+
36+#: html.c:441
37+msgid "Attachments..."
38+msgstr ""
39+
40+#: html.c:464
41+msgid "File:"
42+msgstr ""
43+
44+#: html.c:468
45+msgid "Clear this field to delete the attachment"
46+msgstr ""
47+
48+#: html.c:477 html.c:502
49+msgid "Attachment description"
50+msgstr ""
51+
52+#: html.c:513
53+msgid "Poll..."
54+msgstr ""
55+
56+#: html.c:515
57+msgid "Poll options (one per line, up to 8):"
58+msgstr ""
59+
60+#: html.c:527
61+msgid "One choice"
62+msgstr ""
63+
64+#: html.c:530
65+msgid "Multiple choices"
66+msgstr ""
67+
68+#: html.c:536
69+msgid "End in 5 minutes"
70+msgstr ""
71+
72+#: html.c:540
73+msgid "End in 1 hour"
74+msgstr ""
75+
76+#: html.c:543
77+msgid "End in 1 day"
78+msgstr ""
79+
80+#: html.c:551
81+msgid "Post"
82+msgstr ""
83+
84+#: html.c:648 html.c:655
85+msgid "Site description"
86+msgstr ""
87+
88+#: html.c:666
89+msgid "Admin email"
90+msgstr ""
91+
92+#: html.c:679
93+msgid "Admin account"
94+msgstr ""
95+
96+#: html.c:747 html.c:1083
97+#, c-format
98+msgid "%d following, %d followers"
99+msgstr ""
100+
101+#: html.c:837
102+msgid "RSS"
103+msgstr ""
104+
105+#: html.c:842 html.c:870
106+msgid "private"
107+msgstr ""
108+
109+#: html.c:866
110+msgid "public"
111+msgstr ""
112+
113+#: html.c:874
114+msgid "notifications"
115+msgstr ""
116+
117+#: html.c:879
118+msgid "people"
119+msgstr ""
120+
121+#: html.c:883
122+msgid "instance"
123+msgstr ""
124+
125+#: html.c:892
126+msgid ""
127+"Search posts by URL or content (regular expression), @user@host accounts, or "
128+"#tag"
129+msgstr ""
130+
131+#: html.c:893
132+msgid "Content search"
133+msgstr ""
134+
135+#: html.c:1015
136+msgid "verified link"
137+msgstr ""
138+
139+#: html.c:1072 html.c:2420 html.c:2433 html.c:2442
140+msgid "Location: "
141+msgstr ""
142+
143+#: html.c:1108
144+msgid "New Post..."
145+msgstr ""
146+
147+#: html.c:1110
148+msgid "What's on your mind?"
149+msgstr ""
150+
151+#: html.c:1119
152+msgid "Operations..."
153+msgstr ""
154+
155+#: html.c:1134 html.c:1677 html.c:3016 html.c:4333
156+msgid "Follow"
157+msgstr ""
158+
159+#: html.c:1136
160+msgid "(by URL or user@host)"
161+msgstr ""
162+
163+#: html.c:1151 html.c:1653 html.c:4285
164+msgid "Boost"
165+msgstr ""
166+
167+#: html.c:1153 html.c:1170
168+msgid "(by URL)"
169+msgstr ""
170+
171+#: html.c:1168 html.c:1632 html.c:4276
172+msgid "Like"
173+msgstr ""
174+
175+#: html.c:1273
176+msgid "User Settings..."
177+msgstr ""
178+
179+#: html.c:1282
180+msgid "Display name:"
181+msgstr ""
182+
183+#: html.c:1288
184+msgid "Your name"
185+msgstr ""
186+
187+#: html.c:1290
188+msgid "Avatar: "
189+msgstr ""
190+
191+#: html.c:1298
192+msgid "Delete current avatar"
193+msgstr ""
194+
195+#: html.c:1300
196+msgid "Header image (banner): "
197+msgstr ""
198+
199+#: html.c:1308
200+msgid "Delete current header image"
201+msgstr ""
202+
203+#: html.c:1310
204+msgid "Bio:"
205+msgstr ""
206+
207+#: html.c:1316
208+msgid "Write about yourself here..."
209+msgstr ""
210+
211+#: html.c:1325
212+msgid "Always show sensitive content"
213+msgstr ""
214+
215+#: html.c:1327
216+msgid "Email address for notifications:"
217+msgstr ""
218+
219+#: html.c:1335
220+msgid "Telegram notifications (bot key and chat id):"
221+msgstr ""
222+
223+#: html.c:1349
224+msgid "ntfy notifications (ntfy server and token):"
225+msgstr ""
226+
227+#: html.c:1363
228+msgid "Maximum days to keep posts (0: server settings):"
229+msgstr ""
230+
231+#: html.c:1377
232+msgid "Drop direct messages from people you don't follow"
233+msgstr ""
234+
235+#: html.c:1386
236+msgid "This account is a bot"
237+msgstr ""
238+
239+#: html.c:1395
240+msgid "Auto-boost all mentions to this account"
241+msgstr ""
242+
243+#: html.c:1404
244+msgid "This account is private (posts are not shown through the web)"
245+msgstr ""
246+
247+#: html.c:1414
248+msgid "Collapse top threads by default"
249+msgstr ""
250+
251+#: html.c:1423
252+msgid "Follow requests must be approved"
253+msgstr ""
254+
255+#: html.c:1432
256+msgid "Publish follower and following metrics"
257+msgstr ""
258+
259+#: html.c:1434
260+msgid "Current location:"
261+msgstr ""
262+
263+#: html.c:1448
264+msgid "Profile metadata (key=value pairs in each line):"
265+msgstr ""
266+
267+#: html.c:1459
268+msgid "Web interface language:"
269+msgstr ""
270+
271+#: html.c:1464
272+msgid "New password:"
273+msgstr ""
274+
275+#: html.c:1471
276+msgid "Repeat new password:"
277+msgstr ""
278+
279+#: html.c:1481
280+msgid "Update user info"
281+msgstr ""
282+
283+#: html.c:1492
284+msgid "Followed hashtags..."
285+msgstr ""
286+
287+#: html.c:1494
288+msgid "One hashtag per line"
289+msgstr ""
290+
291+#: html.c:1515
292+msgid "Update hashtags"
293+msgstr ""
294+
295+#: html.c:1632
296+msgid "Say you like this post"
297+msgstr ""
298+
299+#: html.c:1637 html.c:4294
300+msgid "Unlike"
301+msgstr ""
302+
303+#: html.c:1637
304+msgid "Nah don't like it that much"
305+msgstr ""
306+
307+#: html.c:1643 html.c:4426
308+msgid "Unpin"
309+msgstr ""
310+
311+#: html.c:1643
312+msgid "Unpin this post from your timeline"
313+msgstr ""
314+
315+#: html.c:1646 html.c:4421
316+msgid "Pin"
317+msgstr ""
318+
319+#: html.c:1646
320+msgid "Pin this post to the top of your timeline"
321+msgstr ""
322+
323+#: html.c:1653
324+msgid "Announce this post to your followers"
325+msgstr ""
326+
327+#: html.c:1658 html.c:4302
328+msgid "Unboost"
329+msgstr ""
330+
331+#: html.c:1658
332+msgid "I regret I boosted this"
333+msgstr ""
334+
335+#: html.c:1664 html.c:4436
336+msgid "Unbookmark"
337+msgstr ""
338+
339+#: html.c:1664
340+msgid "Delete this post from your bookmarks"
341+msgstr ""
342+
343+#: html.c:1667 html.c:4431
344+msgid "Bookmark"
345+msgstr ""
346+
347+#: html.c:1667
348+msgid "Add this post to your bookmarks"
349+msgstr ""
350+
351+#: html.c:1673 html.c:3002 html.c:3190 html.c:4346
352+msgid "Unfollow"
353+msgstr ""
354+
355+#: html.c:1673 html.c:3003
356+msgid "Stop following this user's activity"
357+msgstr ""
358+
359+#: html.c:1677 html.c:3017
360+msgid "Start following this user's activity"
361+msgstr ""
362+
363+#: html.c:1683 html.c:4376
364+msgid "Unfollow Group"
365+msgstr ""
366+
367+#: html.c:1684
368+msgid "Stop following this group or channel"
369+msgstr ""
370+
371+#: html.c:1688 html.c:4363
372+msgid "Follow Group"
373+msgstr ""
374+
375+#: html.c:1689
376+msgid "Start following this group or channel"
377+msgstr ""
378+
379+#: html.c:1694 html.c:3039 html.c:4310
380+msgid "MUTE"
381+msgstr ""
382+
383+#: html.c:1695
384+msgid "Block any activity from this user forever"
385+msgstr ""
386+
387+#: html.c:1700 html.c:3021 html.c:4393
388+msgid "Delete"
389+msgstr ""
390+
391+#: html.c:1700
392+msgid "Delete this post"
393+msgstr ""
394+
395+#: html.c:1703 html.c:4318
396+msgid "Hide"
397+msgstr ""
398+
399+#: html.c:1703
400+msgid "Hide this post and its children"
401+msgstr ""
402+
403+#: html.c:1734
404+msgid "Edit..."
405+msgstr ""
406+
407+#: html.c:1753
408+msgid "Reply..."
409+msgstr ""
410+
411+#: html.c:1804
412+msgid "Truncated (too deep)"
413+msgstr ""
414+
415+#: html.c:1813
416+msgid "follows you"
417+msgstr ""
418+
419+#: html.c:1876
420+msgid "Pinned"
421+msgstr ""
422+
423+#: html.c:1884
424+msgid "Bookmarked"
425+msgstr ""
426+
427+#: html.c:1892
428+msgid "Poll"
429+msgstr ""
430+
431+#: html.c:1899
432+msgid "Voted"
433+msgstr ""
434+
435+#: html.c:1908
436+msgid "Event"
437+msgstr ""
438+
439+#: html.c:1940 html.c:1969
440+msgid "boosted"
441+msgstr ""
442+
443+#: html.c:1985
444+msgid "in reply to"
445+msgstr ""
446+
447+#: html.c:2036
448+msgid " [SENSITIVE CONTENT]"
449+msgstr ""
450+
451+#: html.c:2213
452+msgid "Vote"
453+msgstr ""
454+
455+#: html.c:2223
456+msgid "Closed"
457+msgstr ""
458+
459+#: html.c:2248
460+msgid "Closes in"
461+msgstr ""
462+
463+#: html.c:2327
464+msgid "Video"
465+msgstr ""
466+
467+#: html.c:2342
468+msgid "Audio"
469+msgstr ""
470+
471+#: html.c:2364
472+msgid "Attachment"
473+msgstr ""
474+
475+#: html.c:2378
476+msgid "Alt..."
477+msgstr ""
478+
479+#: html.c:2391
480+msgid "Source channel or community"
481+msgstr ""
482+
483+#: html.c:2485
484+msgid "Time: "
485+msgstr ""
486+
487+#: html.c:2560
488+msgid "Older..."
489+msgstr ""
490+
491+#: html.c:2623
492+msgid "about this site"
493+msgstr ""
494+
495+#: html.c:2625
496+msgid "powered by "
497+msgstr ""
498+
499+#: html.c:2690
500+msgid "Dismiss"
501+msgstr ""
502+
503+#: html.c:2707
504+#, c-format
505+msgid "Timeline for list '%s'"
506+msgstr ""
507+
508+#: html.c:2726 html.c:3767
509+msgid "Pinned posts"
510+msgstr ""
511+
512+#: html.c:2738 html.c:3782
513+msgid "Bookmarked posts"
514+msgstr ""
515+
516+#: html.c:2750 html.c:3797
517+msgid "Post drafts"
518+msgstr ""
519+
520+#: html.c:2809
521+msgid "No more unseen posts"
522+msgstr ""
523+
524+#: html.c:2813 html.c:2913
525+msgid "Back to top"
526+msgstr ""
527+
528+#: html.c:2866
529+msgid "History"
530+msgstr ""
531+
532+#: html.c:2918 html.c:3338
533+msgid "More..."
534+msgstr ""
535+
536+#: html.c:3007 html.c:4329
537+msgid "Unlimit"
538+msgstr ""
539+
540+#: html.c:3008
541+msgid "Allow announces (boosts) from this user"
542+msgstr ""
543+
544+#: html.c:3011 html.c:4325
545+msgid "Limit"
546+msgstr ""
547+
548+#: html.c:3012
549+msgid "Block announces (boosts) from this user"
550+msgstr ""
551+
552+#: html.c:3021
553+msgid "Delete this user"
554+msgstr ""
555+
556+#: html.c:3026 html.c:4441
557+msgid "Approve"
558+msgstr ""
559+
560+#: html.c:3027
561+msgid "Approve this follow request"
562+msgstr ""
563+
564+#: html.c:3030 html.c:4465
565+msgid "Discard"
566+msgstr ""
567+
568+#: html.c:3030
569+msgid "Discard this follow request"
570+msgstr ""
571+
572+#: html.c:3035 html.c:4314
573+msgid "Unmute"
574+msgstr ""
575+
576+#: html.c:3036
577+msgid "Stop blocking activities from this user"
578+msgstr ""
579+
580+#: html.c:3040
581+msgid "Block any activity from this user"
582+msgstr ""
583+
584+#: html.c:3048
585+msgid "Direct Message..."
586+msgstr ""
587+
588+#: html.c:3083
589+msgid "Pending follow confirmations"
590+msgstr ""
591+
592+#: html.c:3087
593+msgid "People you follow"
594+msgstr ""
595+
596+#: html.c:3088
597+msgid "People that follow you"
598+msgstr ""
599+
600+#: html.c:3127
601+msgid "Clear all"
602+msgstr ""
603+
604+#: html.c:3184
605+msgid "Mention"
606+msgstr ""
607+
608+#: html.c:3187
609+msgid "Finished poll"
610+msgstr ""
611+
612+#: html.c:3202
613+msgid "Follow Request"
614+msgstr ""
615+
616+#: html.c:3285
617+msgid "Context"
618+msgstr ""
619+
620+#: html.c:3296
621+msgid "New"
622+msgstr ""
623+
624+#: html.c:3311
625+msgid "Already seen"
626+msgstr ""
627+
628+#: html.c:3326
629+msgid "None"
630+msgstr ""
631+
632+#: html.c:3592
633+#, c-format
634+msgid "Search results for account %s"
635+msgstr ""
636+
637+#: html.c:3599
638+#, c-format
639+msgid "Account %s not found"
640+msgstr ""
641+
642+#: html.c:3630
643+#, c-format
644+msgid "Search results for tag %s"
645+msgstr ""
646+
647+#: html.c:3630
648+#, c-format
649+msgid "Nothing found for tag %s"
650+msgstr ""
651+
652+#: html.c:3646
653+#, c-format
654+msgid "Search results for '%s' (may be more)"
655+msgstr ""
656+
657+#: html.c:3649
658+#, c-format
659+msgid "Search results for '%s'"
660+msgstr ""
661+
662+#: html.c:3652
663+#, c-format
664+msgid "No more matches for '%s'"
665+msgstr ""
666+
667+#: html.c:3654
668+#, c-format
669+msgid "Nothing found for '%s'"
670+msgstr ""
671+
672+#: html.c:3752
673+msgid "Showing instance timeline"
674+msgstr ""
675+
676+#: html.c:3820
677+#, c-format
678+msgid "Showing timeline for list '%s'"
679+msgstr ""
680+
681+#: httpd.c:250
682+#, c-format
683+msgid "Search results for tag #%s"
684+msgstr ""
685+
686+#: httpd.c:259
687+msgid "Recent posts by users in this instance"
688+msgstr ""
M
snac.c
+3,
-0
1@@ -24,6 +24,7 @@
2 #include "xs_match.h"
3 #include "xs_fcgi.h"
4 #include "xs_html.h"
5+#include "xs_po.h"
6
7 #include "snac.h"
8
9@@ -34,6 +35,7 @@ xs_str *srv_basedir = NULL;
10 xs_dict *srv_config = NULL;
11 xs_str *srv_baseurl = NULL;
12 xs_str *srv_proxy_token_seed = NULL;
13+xs_dict *srv_langs = NULL;
14
15 int dbglevel = 0;
16
17@@ -179,6 +181,7 @@ const char *http_status_text(int status)
18 /* translate status codes to canonical status texts */
19 {
20 switch (status) {
21+ case 399: return "Timeout";
22 #define HTTP_STATUS(code, name, text) case HTTP_STATUS_ ## name: return #text;
23 #include "http_codes.h"
24 #undef HTTP_STATUS
M
snac.h
+10,
-2
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.72"
6+#define VERSION "2.73-dev"
7
8 #define USER_AGENT "snac/" VERSION
9
10@@ -16,6 +16,10 @@
11 #define MAX_THREADS 256
12 #endif
13
14+#ifndef MAX_JSON_DEPTH
15+#define MAX_JSON_DEPTH 8
16+#endif
17+
18 #ifndef MAX_CONVERSATION_LEVELS
19 #define MAX_CONVERSATION_LEVELS 48
20 #endif
21@@ -29,10 +33,11 @@ extern xs_str *srv_basedir;
22 extern xs_dict *srv_config;
23 extern xs_str *srv_baseurl;
24 extern xs_str *srv_proxy_token_seed;
25+extern xs_dict *srv_langs;
26
27 extern int dbglevel;
28
29-#define L(s) (s)
30+#define L(s) lang_str((s), user)
31
32 #define POSTLIKE_OBJECT_TYPE "Note|Question|Page|Article|Video|Audio|Event"
33
34@@ -55,6 +60,7 @@ typedef struct {
35 xs_dict *links; /* validated links */
36 xs_str *actor; /* actor url */
37 xs_str *md5; /* actor url md5 */
38+ const xs_dict *lang;/* string translation dict */
39 } snac;
40
41 typedef struct {
42@@ -441,3 +447,5 @@ xs_str *make_url(const char *href, const char *proxy, int by_token);
43
44 int badlogin_check(const char *user, const char *addr);
45 void badlogin_inc(const char *user, const char *addr);
46+
47+const char *lang_str(const char *str, const snac *user);
+11,
-0
1@@ -13,6 +13,8 @@ int xs_smtp_request(const char *url, const char *user, const char *pass,
2 const char *from, const char *to, const xs_str *body,
3 int use_ssl);
4
5+const char *xs_curl_strerr(int errnum);
6+
7 #ifdef XS_IMPLEMENTATION
8
9 #include <curl/curl.h>
10@@ -240,6 +242,15 @@ int xs_smtp_request(const char *url, const char *user, const char *pass,
11 return (int)res;
12 }
13
14+
15+const char *xs_curl_strerr(int errnum)
16+{
17+ CURLcode cc = errnum < 0 ? -errnum : errnum;
18+
19+ return curl_easy_strerror(cc);
20+}
21+
22+
23 #endif /* XS_IMPLEMENTATION */
24
25 #endif /* _XS_CURL_H */
+23,
-17
1@@ -4,17 +4,23 @@
2
3 #define _XS_JSON_H
4
5+#ifndef MAX_JSON_DEPTH
6+#define MAX_JSON_DEPTH 32
7+#endif
8+
9 int xs_json_dump(const xs_val *data, int indent, FILE *f);
10 xs_str *xs_json_dumps(const xs_val *data, int indent);
11
12-xs_val *xs_json_load(FILE *f);
13-xs_val *xs_json_loads(const xs_str *json);
14+xs_val *xs_json_load_full(FILE *f, int maxdepth);
15+xs_val *xs_json_loads_full(const xs_str *json, int maxdepth);
16+#define xs_json_load(f) xs_json_load_full(f, MAX_JSON_DEPTH)
17+#define xs_json_loads(s) xs_json_loads_full(s, MAX_JSON_DEPTH)
18
19 xstype xs_json_load_type(FILE *f);
20 int xs_json_load_array_iter(FILE *f, xs_val **value, xstype *pt, int *c);
21 int xs_json_load_object_iter(FILE *f, xs_str **key, xs_val **value, xstype *pt, int *c);
22-xs_list *xs_json_load_array(FILE *f);
23-xs_dict *xs_json_load_object(FILE *f);
24+xs_list *xs_json_load_array(FILE *f, int maxdepth);
25+xs_dict *xs_json_load_object(FILE *f, int maxdepth);
26
27
28 #ifdef XS_IMPLEMENTATION
29@@ -371,7 +377,7 @@ int xs_json_load_array_iter(FILE *f, xs_val **value, xstype *pt, int *c)
30 }
31
32
33-xs_list *xs_json_load_array(FILE *f)
34+xs_list *xs_json_load_array(FILE *f, int maxdepth)
35 /* loads a full JSON array (after the initial OBRACK) */
36 {
37 xstype t;
38@@ -387,12 +393,12 @@ xs_list *xs_json_load_array(FILE *f)
39
40 if (r == 1) {
41 /* partial load? */
42- if (v == NULL) {
43+ if (v == NULL && maxdepth != 0) {
44 if (t == XSTYPE_LIST)
45- v = xs_json_load_array(f);
46+ v = xs_json_load_array(f, maxdepth - 1);
47 else
48 if (t == XSTYPE_DICT)
49- v = xs_json_load_object(f);
50+ v = xs_json_load_object(f, maxdepth - 1);
51 }
52
53 /* still null? fail */
54@@ -459,7 +465,7 @@ int xs_json_load_object_iter(FILE *f, xs_str **key, xs_val **value, xstype *pt,
55 }
56
57
58-xs_dict *xs_json_load_object(FILE *f)
59+xs_dict *xs_json_load_object(FILE *f, int maxdepth)
60 /* loads a full JSON object (after the initial OCURLY) */
61 {
62 xstype t;
63@@ -476,12 +482,12 @@ xs_dict *xs_json_load_object(FILE *f)
64
65 if (r == 1) {
66 /* partial load? */
67- if (v == NULL) {
68+ if (v == NULL && maxdepth != 0) {
69 if (t == XSTYPE_LIST)
70- v = xs_json_load_array(f);
71+ v = xs_json_load_array(f, maxdepth - 1);
72 else
73 if (t == XSTYPE_DICT)
74- v = xs_json_load_object(f);
75+ v = xs_json_load_object(f, maxdepth - 1);
76 }
77
78 /* still null? fail */
79@@ -500,14 +506,14 @@ xs_dict *xs_json_load_object(FILE *f)
80 }
81
82
83-xs_val *xs_json_loads(const xs_str *json)
84+xs_val *xs_json_loads_full(const xs_str *json, int maxdepth)
85 /* loads a string in JSON format and converts to a multiple data */
86 {
87 FILE *f;
88 xs_val *v = NULL;
89
90 if ((f = fmemopen((char *)json, strlen(json), "r")) != NULL) {
91- v = xs_json_load(f);
92+ v = xs_json_load_full(f, maxdepth);
93 fclose(f);
94 }
95
96@@ -533,17 +539,17 @@ xstype xs_json_load_type(FILE *f)
97 }
98
99
100-xs_val *xs_json_load(FILE *f)
101+xs_val *xs_json_load_full(FILE *f, int maxdepth)
102 /* loads a JSON file */
103 {
104 xs_val *v = NULL;
105 xstype t = xs_json_load_type(f);
106
107 if (t == XSTYPE_LIST)
108- v = xs_json_load_array(f);
109+ v = xs_json_load_array(f, maxdepth);
110 else
111 if (t == XSTYPE_DICT)
112- v = xs_json_load_object(f);
113+ v = xs_json_load_object(f, maxdepth);
114
115 return v;
116 }
A
xs_po.h
+86,
-0
1@@ -0,0 +1,86 @@
2+/* copyright (c) 2025 grunfink et al. / MIT license */
3+
4+#ifndef _XS_PO_H
5+
6+#define _XS_PO_H
7+
8+xs_dict *xs_po_to_dict(const char *fn);
9+
10+#ifdef XS_IMPLEMENTATION
11+
12+xs_dict *xs_po_to_dict(const char *fn)
13+/* converts a PO file to a dict */
14+{
15+ xs_dict *d = NULL;
16+ FILE *f;
17+
18+ if ((f = fopen(fn, "r")) != NULL) {
19+ d = xs_dict_new();
20+
21+ xs *k = NULL;
22+ xs *v = NULL;
23+ enum { IN_NONE, IN_K, IN_V } mode = IN_NONE;
24+
25+ while (!feof(f)) {
26+ xs *l = xs_strip_i(xs_readline(f));
27+
28+ /* discard empty lines and comments */
29+ if (*l == '\0' || *l == '#')
30+ continue;
31+
32+ if (xs_startswith(l, "msgid ")) {
33+ if (mode == IN_V) {
34+ /* flush */
35+ if (xs_is_string(k) && xs_is_string(v) && *v)
36+ d = xs_dict_set(d, k, v);
37+
38+ k = xs_free(k);
39+ v = xs_free(v);
40+ }
41+
42+ l = xs_replace_i(l, "msgid ", "");
43+ mode = IN_K;
44+
45+ k = xs_str_new(NULL);
46+ }
47+ else
48+ if (xs_startswith(l, "msgstr ")) {
49+ if (mode != IN_K)
50+ break;
51+
52+ l = xs_replace_i(l, "msgstr ", "");
53+ mode = IN_V;
54+
55+ v = xs_str_new(NULL);
56+ }
57+
58+ l = xs_replace_i(l, "\\n", "\n");
59+ l = xs_strip_chars_i(l, "\"");
60+
61+ switch (mode) {
62+ case IN_K:
63+ k = xs_str_cat(k, l);
64+ break;
65+
66+ case IN_V:
67+ v = xs_str_cat(v, l);
68+ break;
69+
70+ case IN_NONE:
71+ break;
72+ }
73+ }
74+
75+ /* final flush */
76+ if (xs_is_string(k) && xs_is_string(v) && *v)
77+ d = xs_dict_set(d, k, v);
78+
79+ fclose(f);
80+ }
81+
82+ return d;
83+}
84+
85+#endif /* XS_IMPLEMENTATION */
86+
87+#endif /* XS_PO_H */
+4,
-2
1@@ -43,11 +43,13 @@ xs_list *xs_regex_split_n(const char *str, const char *rx, int count)
2 while (count > 0 && !regexec(&re, (p = str + offset), 1, &rm, offset > 0 ? REG_NOTBOL : 0)) {
3 /* add first the leading part of the string */
4 xs *s1 = xs_str_new_sz(p, rm.rm_so);
5- list = xs_list_append(list, s1);
6+
7+ list = xs_list_append(list, xs_is_string(s1) ? s1 : "");
8
9 /* add now the matched text as the separator */
10 xs *s2 = xs_str_new_sz(p + rm.rm_so, rm.rm_eo - rm.rm_so);
11- list = xs_list_append(list, s2);
12+
13+ list = xs_list_append(list, xs_is_string(s2) ? s2 : "");
14
15 /* move forward */
16 offset += rm.rm_eo;
+1,
-1
1@@ -1 +1 @@
2-/* 2f43b93e9d2b63360c802e09f4c68adfef74c673 2025-01-28T07:40:50+01:00 */
3+/* d467dc71e518603250a55c8a67e26cf40e1710e9 2025-02-14T10:21:15+01:00 */