summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--Makefile21
-rw-r--r--Makefile.NetBSD14
-rw-r--r--activitypub.c14
-rw-r--r--data.c45
-rw-r--r--doc/snac.817
-rw-r--r--format.c2
-rw-r--r--html.c224
-rw-r--r--httpd.c5
-rw-r--r--po/en.po687
-rw-r--r--snac.c3
-rw-r--r--snac.h12
-rw-r--r--xs_curl.h11
-rw-r--r--xs_json.h40
-rw-r--r--xs_po.h86
-rw-r--r--xs_regex.h6
-rw-r--r--xs_version.h2
16 files changed, 1062 insertions, 127 deletions
diff --git a/Makefile b/Makefile
index 764456d..4ba6862 100644
--- a/Makefile
+++ b/Makefile
@@ -38,24 +38,31 @@ uninstall:
38 rm $(PREFIX_MAN)/man5/snac.5 38 rm $(PREFIX_MAN)/man5/snac.5
39 rm $(PREFIX_MAN)/man8/snac.8 39 rm $(PREFIX_MAN)/man8/snac.8
40 40
41update-po:
42 mkdir -p po
43 [ -f "po/en.po" ] || xgettext -o po/en.po --language=C --keyword=L --from-code=utf-8 *.c
44 for a in po/*.po ; do \
45 xgettext --omit-header -j -o $$a --language=C --keyword=L --from-code=utf-8 *.c ; \
46 done
47
41activitypub.o: activitypub.c xs.h xs_json.h xs_curl.h xs_mime.h \ 48activitypub.o: activitypub.c xs.h xs_json.h xs_curl.h xs_mime.h \
42 xs_openssl.h xs_regex.h xs_time.h xs_set.h xs_match.h xs_unicode.h \ 49 xs_openssl.h xs_regex.h xs_time.h xs_set.h xs_match.h xs_unicode.h \
43 snac.h http_codes.h 50 snac.h http_codes.h
44data.o: data.c xs.h xs_hex.h xs_io.h xs_json.h xs_openssl.h xs_glob.h \ 51data.o: data.c xs.h xs_hex.h xs_io.h xs_json.h xs_openssl.h xs_glob.h \
45 xs_set.h xs_time.h xs_regex.h xs_match.h xs_unicode.h xs_random.h snac.h \ 52 xs_set.h xs_time.h xs_regex.h xs_match.h xs_unicode.h xs_random.h \
46 http_codes.h 53 xs_po.h snac.h http_codes.h
47format.o: format.c xs.h xs_regex.h xs_mime.h xs_html.h xs_json.h \ 54format.o: format.c xs.h xs_regex.h xs_mime.h xs_html.h xs_json.h \
48 xs_time.h xs_match.h snac.h http_codes.h 55 xs_time.h xs_match.h snac.h http_codes.h
49html.o: html.c xs.h xs_io.h xs_json.h xs_regex.h xs_set.h xs_openssl.h \ 56html.o: html.c xs.h xs_io.h xs_json.h xs_regex.h xs_set.h xs_openssl.h \
50 xs_time.h xs_mime.h xs_match.h xs_html.h xs_curl.h xs_unicode.h snac.h \ 57 xs_time.h xs_mime.h xs_match.h xs_html.h xs_curl.h xs_unicode.h xs_url.h \
51 http_codes.h 58 snac.h http_codes.h
52http.o: http.c xs.h xs_io.h xs_openssl.h xs_curl.h xs_time.h xs_json.h \ 59http.o: http.c xs.h xs_io.h xs_openssl.h xs_curl.h xs_time.h xs_json.h \
53 snac.h http_codes.h 60 snac.h http_codes.h
54httpd.o: httpd.c xs.h xs_io.h xs_json.h xs_socket.h xs_unix_socket.h \ 61httpd.o: httpd.c xs.h xs_io.h xs_json.h xs_socket.h xs_unix_socket.h \
55 xs_httpd.h xs_mime.h xs_time.h xs_openssl.h xs_fcgi.h xs_html.h snac.h \ 62 xs_httpd.h xs_mime.h xs_time.h xs_openssl.h xs_fcgi.h xs_html.h snac.h \
56 http_codes.h 63 http_codes.h
57main.o: main.c xs.h xs_io.h xs_json.h xs_time.h xs_openssl.h snac.h \ 64main.o: main.c xs.h xs_io.h xs_json.h xs_time.h xs_openssl.h xs_match.h \
58 http_codes.h 65 snac.h http_codes.h
59mastoapi.o: mastoapi.c xs.h xs_hex.h xs_openssl.h xs_json.h xs_io.h \ 66mastoapi.o: mastoapi.c xs.h xs_hex.h xs_openssl.h xs_json.h xs_io.h \
60 xs_time.h xs_glob.h xs_set.h xs_random.h xs_url.h xs_mime.h xs_match.h \ 67 xs_time.h xs_glob.h xs_set.h xs_random.h xs_url.h xs_mime.h xs_match.h \
61 snac.h http_codes.h 68 snac.h http_codes.h
@@ -63,7 +70,7 @@ sandbox.o: sandbox.c xs.h snac.h http_codes.h
63snac.o: snac.c xs.h xs_hex.h xs_io.h xs_unicode_tbl.h xs_unicode.h \ 70snac.o: snac.c xs.h xs_hex.h xs_io.h xs_unicode_tbl.h xs_unicode.h \
64 xs_json.h xs_curl.h xs_openssl.h xs_socket.h xs_unix_socket.h xs_url.h \ 71 xs_json.h xs_curl.h xs_openssl.h xs_socket.h xs_unix_socket.h xs_url.h \
65 xs_httpd.h xs_mime.h xs_regex.h xs_set.h xs_time.h xs_glob.h xs_random.h \ 72 xs_httpd.h xs_mime.h xs_regex.h xs_set.h xs_time.h xs_glob.h xs_random.h \
66 xs_match.h xs_fcgi.h xs_html.h snac.h http_codes.h 73 xs_match.h xs_fcgi.h xs_html.h xs_po.h snac.h http_codes.h
67upgrade.o: upgrade.c xs.h xs_io.h xs_json.h xs_glob.h snac.h http_codes.h 74upgrade.o: upgrade.c xs.h xs_io.h xs_json.h xs_glob.h snac.h http_codes.h
68utils.o: utils.c xs.h xs_io.h xs_json.h xs_time.h xs_openssl.h \ 75utils.o: utils.c xs.h xs_io.h xs_json.h xs_time.h xs_openssl.h \
69 xs_random.h xs_glob.h xs_curl.h xs_regex.h snac.h http_codes.h 76 xs_random.h xs_glob.h xs_curl.h xs_regex.h snac.h http_codes.h
diff --git a/Makefile.NetBSD b/Makefile.NetBSD
index 93222b2..51c8181 100644
--- a/Makefile.NetBSD
+++ b/Makefile.NetBSD
@@ -39,20 +39,20 @@ activitypub.o: activitypub.c xs.h xs_json.h xs_curl.h xs_mime.h \
39 xs_openssl.h xs_regex.h xs_time.h xs_set.h xs_match.h xs_unicode.h \ 39 xs_openssl.h xs_regex.h xs_time.h xs_set.h xs_match.h xs_unicode.h \
40 snac.h http_codes.h 40 snac.h http_codes.h
41data.o: data.c xs.h xs_hex.h xs_io.h xs_json.h xs_openssl.h xs_glob.h \ 41data.o: data.c xs.h xs_hex.h xs_io.h xs_json.h xs_openssl.h xs_glob.h \
42 xs_set.h xs_time.h xs_regex.h xs_match.h xs_unicode.h xs_random.h snac.h \ 42 xs_set.h xs_time.h xs_regex.h xs_match.h xs_unicode.h xs_random.h \
43 http_codes.h 43 xs_po.h snac.h http_codes.h
44format.o: format.c xs.h xs_regex.h xs_mime.h xs_html.h xs_json.h \ 44format.o: format.c xs.h xs_regex.h xs_mime.h xs_html.h xs_json.h \
45 xs_time.h xs_match.h snac.h http_codes.h 45 xs_time.h xs_match.h snac.h http_codes.h
46html.o: html.c xs.h xs_io.h xs_json.h xs_regex.h xs_set.h xs_openssl.h \ 46html.o: html.c xs.h xs_io.h xs_json.h xs_regex.h xs_set.h xs_openssl.h \
47 xs_time.h xs_mime.h xs_match.h xs_html.h xs_curl.h xs_unicode.h snac.h \ 47 xs_time.h xs_mime.h xs_match.h xs_html.h xs_curl.h xs_unicode.h xs_url.h \
48 http_codes.h 48 snac.h http_codes.h
49http.o: http.c xs.h xs_io.h xs_openssl.h xs_curl.h xs_time.h xs_json.h \ 49http.o: http.c xs.h xs_io.h xs_openssl.h xs_curl.h xs_time.h xs_json.h \
50 snac.h http_codes.h 50 snac.h http_codes.h
51httpd.o: httpd.c xs.h xs_io.h xs_json.h xs_socket.h xs_unix_socket.h \ 51httpd.o: httpd.c xs.h xs_io.h xs_json.h xs_socket.h xs_unix_socket.h \
52 xs_httpd.h xs_mime.h xs_time.h xs_openssl.h xs_fcgi.h xs_html.h snac.h \ 52 xs_httpd.h xs_mime.h xs_time.h xs_openssl.h xs_fcgi.h xs_html.h snac.h \
53 http_codes.h 53 http_codes.h
54main.o: main.c xs.h xs_io.h xs_json.h xs_time.h xs_openssl.h snac.h \ 54main.o: main.c xs.h xs_io.h xs_json.h xs_time.h xs_openssl.h xs_match.h \
55 http_codes.h 55 snac.h http_codes.h
56mastoapi.o: mastoapi.c xs.h xs_hex.h xs_openssl.h xs_json.h xs_io.h \ 56mastoapi.o: mastoapi.c xs.h xs_hex.h xs_openssl.h xs_json.h xs_io.h \
57 xs_time.h xs_glob.h xs_set.h xs_random.h xs_url.h xs_mime.h xs_match.h \ 57 xs_time.h xs_glob.h xs_set.h xs_random.h xs_url.h xs_mime.h xs_match.h \
58 snac.h http_codes.h 58 snac.h http_codes.h
@@ -60,7 +60,7 @@ sandbox.o: sandbox.c xs.h snac.h http_codes.h
60snac.o: snac.c xs.h xs_hex.h xs_io.h xs_unicode_tbl.h xs_unicode.h \ 60snac.o: snac.c xs.h xs_hex.h xs_io.h xs_unicode_tbl.h xs_unicode.h \
61 xs_json.h xs_curl.h xs_openssl.h xs_socket.h xs_unix_socket.h xs_url.h \ 61 xs_json.h xs_curl.h xs_openssl.h xs_socket.h xs_unix_socket.h xs_url.h \
62 xs_httpd.h xs_mime.h xs_regex.h xs_set.h xs_time.h xs_glob.h xs_random.h \ 62 xs_httpd.h xs_mime.h xs_regex.h xs_set.h xs_time.h xs_glob.h xs_random.h \
63 xs_match.h xs_fcgi.h xs_html.h snac.h http_codes.h 63 xs_match.h xs_fcgi.h xs_html.h xs_po.h snac.h http_codes.h
64upgrade.o: upgrade.c xs.h xs_io.h xs_json.h xs_glob.h snac.h http_codes.h 64upgrade.o: upgrade.c xs.h xs_io.h xs_json.h xs_glob.h snac.h http_codes.h
65utils.o: utils.c xs.h xs_io.h xs_json.h xs_time.h xs_openssl.h \ 65utils.o: utils.c xs.h xs_io.h xs_json.h xs_time.h xs_openssl.h \
66 xs_random.h xs_glob.h xs_curl.h xs_regex.h snac.h http_codes.h 66 xs_random.h xs_glob.h xs_curl.h xs_regex.h snac.h http_codes.h
diff --git a/activitypub.c b/activitypub.c
index 643baba..e2519e6 100644
--- a/activitypub.c
+++ b/activitypub.c
@@ -2715,6 +2715,12 @@ int process_user_queue(snac *snac)
2715} 2715}
2716 2716
2717 2717
2718xs_str *str_status(int status)
2719{
2720 return xs_fmt("%d %s", status, status < 0 ? xs_curl_strerr(status) : http_status_text(status));
2721}
2722
2723
2718void process_queue_item(xs_dict *q_item) 2724void process_queue_item(xs_dict *q_item)
2719/* processes an item from the global queue */ 2725/* processes an item from the global queue */
2720{ 2726{
@@ -2771,7 +2777,9 @@ void process_queue_item(xs_dict *q_item)
2771 else 2777 else
2772 payload = xs_str_new(NULL); 2778 payload = xs_str_new(NULL);
2773 2779
2774 srv_log(xs_fmt("output message: sent to inbox %s %d%s", inbox, status, payload)); 2780 xs *s_status = str_status(status);
2781
2782 srv_log(xs_fmt("output message: sent to inbox %s (%s)%s", inbox, s_status, payload));
2775 2783
2776 if (!valid_status(status)) { 2784 if (!valid_status(status)) {
2777 retries++; 2785 retries++;
@@ -2789,10 +2797,10 @@ void process_queue_item(xs_dict *q_item)
2789 || status == HTTP_STATUS_UNPROCESSABLE_CONTENT 2797 || status == HTTP_STATUS_UNPROCESSABLE_CONTENT
2790 || status < 0) 2798 || status < 0)
2791 /* explicit error: discard */ 2799 /* explicit error: discard */
2792 srv_log(xs_fmt("output message: error %s %d", inbox, status)); 2800 srv_log(xs_fmt("output message: error %s (%s)", inbox, s_status));
2793 else 2801 else
2794 if (retries > queue_retry_max) 2802 if (retries > queue_retry_max)
2795 srv_log(xs_fmt("output message: giving up %s %d", inbox, status)); 2803 srv_log(xs_fmt("output message: giving up %s (%s)", inbox, s_status));
2796 else { 2804 else {
2797 /* requeue */ 2805 /* requeue */
2798 enqueue_output_raw(keyid, seckey, msg, inbox, retries, status); 2806 enqueue_output_raw(keyid, seckey, msg, inbox, retries, status);
diff --git a/data.c b/data.c
index 6995611..ae85aaf 100644
--- a/data.c
+++ b/data.c
@@ -13,6 +13,7 @@
13#include "xs_match.h" 13#include "xs_match.h"
14#include "xs_unicode.h" 14#include "xs_unicode.h"
15#include "xs_random.h" 15#include "xs_random.h"
16#include "xs_po.h"
16 17
17#include "snac.h" 18#include "snac.h"
18 19
@@ -98,6 +99,9 @@ int srv_open(const char *basedir, int auto_upgrade)
98 if (error != NULL) 99 if (error != NULL)
99 srv_log(error); 100 srv_log(error);
100 101
102 if (!ret)
103 return ret;
104
101 /* create the queue/ subdir, just in case */ 105 /* create the queue/ subdir, just in case */
102 xs *qdir = xs_fmt("%s/queue", srv_basedir); 106 xs *qdir = xs_fmt("%s/queue", srv_basedir);
103 mkdirx(qdir); 107 mkdirx(qdir);
@@ -148,6 +152,29 @@ int srv_open(const char *basedir, int auto_upgrade)
148 mkdirx(expdir); 152 mkdirx(expdir);
149 } 153 }
150 154
155 /* languages */
156 srv_langs = xs_dict_new();
157 srv_langs = xs_dict_set(srv_langs, "en", xs_stock(XSTYPE_NULL));
158
159 xs *l_dir = xs_fmt("%s/lang/", srv_basedir);
160 mkdirx(l_dir);
161
162 l_dir = xs_str_cat(l_dir, "*.po");
163 xs *pos = xs_glob(l_dir, 0, 0);
164 const char *po;
165
166 xs_list_foreach(pos, po) {
167 xs *d = xs_po_to_dict(po);
168
169 if (xs_is_dict(d)) {
170 xs *l = xs_split(po, "/");
171 xs *id = xs_dup(xs_list_get(l, -1));
172 id = xs_replace_i(id, ".po", "");
173
174 srv_langs = xs_dict_set(srv_langs, id, d);
175 }
176 }
177
151 return ret; 178 return ret;
152} 179}
153 180
@@ -4064,3 +4091,21 @@ void badlogin_inc(const char *user, const char *addr)
4064 pthread_mutex_unlock(&data_mutex); 4091 pthread_mutex_unlock(&data_mutex);
4065 } 4092 }
4066} 4093}
4094
4095
4096/** language strings **/
4097
4098const char *lang_str(const char *str, const snac *user)
4099/* returns a translated string */
4100{
4101 const char *n_str = str;
4102
4103 if (user && xs_is_dict(user->lang) && xs_is_string(str)) {
4104 n_str = xs_dict_get(user->lang, str);
4105
4106 if (xs_is_null(n_str) || *n_str == '\0')
4107 n_str = str;
4108 }
4109
4110 return n_str;
4111}
diff --git a/doc/snac.8 b/doc/snac.8
index 7e3213b..f1e5590 100644
--- a/doc/snac.8
+++ b/doc/snac.8
@@ -23,8 +23,12 @@ Ultrix machine in your grandfather basement, probably MacOS) support hard
23links on their native filesystems. Don't do fancy things like moving the 23links on their native filesystems. Don't do fancy things like moving the
24subdirectories to different filesystems. Also, if you move your 24subdirectories to different filesystems. Also, if you move your
25.Nm 25.Nm
26installation to another server, do it with a tool that respect hard 26installation to another server, do it with a tool that keeps hard
27link counts. Remember: 27links, like
28.Xr tar 1
29or
30.Xr rsync 1
31with the -H switch. Remember:
28.Nm 32.Nm
29is a very UNIXy program that loves hard links. 33is a very UNIXy program that loves hard links.
30.Ss Building and Installation 34.Ss Building and Installation
@@ -194,9 +198,7 @@ By setting this to true, no inbox collection is done. Inbox collection helps
194being discovered from remote instances, but also increases network traffic. 198being discovered from remote instances, but also increases network traffic.
195.It Ic http_headers 199.It Ic http_headers
196If you need to add more HTTP response headers for whatever reason, you can 200If you need to add more HTTP response headers for whatever reason, you can
197fill this object with the required header/value pairs. For example, for enhanced 201fill this object with the required header/value pairs.
198XSS security, you can set the "Content-Security-Policy" header to "script-src ;"
199to be totally sure that no JavaScript is executed.
200.It Ic show_instance_timeline 202.It Ic show_instance_timeline
201If this is set to true, the instance base URL will show a timeline with the latest 203If this is set to true, the instance base URL will show a timeline with the latest
202user posts instead of the default greeting static page. If other information 204user posts instead of the default greeting static page. If other information
@@ -327,8 +329,9 @@ These weapons of mass destruction can be written into the
327file in the server base directory, one per line; if this file exists, 329file in the server base directory, one per line; if this file exists,
328all posts' content will be matched (after being stripped of HTML tags) 330all posts' content will be matched (after being stripped of HTML tags)
329against these regexes, one by one, and any match will make the post to 331against these regexes, one by one, and any match will make the post to
330be rejected. If you don't know about regular expressions, don't use this 332be rejected. Use lower case, the regex will be case insensitive by default.
331option (or learn about them in some tutorial, there are gazillions of 333If you don't know about regular expressions, don't use this
334option (or learn about them inw some tutorial, there are gazillions of
332them out there), as you and your users may start missing posts. Also, 335them out there), as you and your users may start missing posts. Also,
333given that every regular expression implementation supports a different 336given that every regular expression implementation supports a different
334set of features, consider reading the documentation about the one 337set of features, consider reading the documentation about the one
diff --git a/format.c b/format.c
index 4c8ed29..755aeb2 100644
--- a/format.c
+++ b/format.c
@@ -458,7 +458,7 @@ xs_str *sanitize(const char *content)
458 458
459 if (valid_tags[i]) { 459 if (valid_tags[i]) {
460 /* accepted tag: rebuild it with only the accepted elements */ 460 /* accepted tag: rebuild it with only the accepted elements */
461 xs *el = xs_regex_select(v, "(src|href|rel|class|target)=\"[^\"]*\""); 461 xs *el = xs_regex_select(v, "(src|href|rel|class|target)=(\"[^\"]*\"|'[^']*')");
462 xs *s3 = xs_join(el, " "); 462 xs *s3 = xs_join(el, " ");
463 463
464 s2 = xs_fmt("<%s%s%s%s>", 464 s2 = xs_fmt("<%s%s%s%s>",
diff --git a/html.c b/html.c
index e742e94..6573630 100644
--- a/html.c
+++ b/html.c
@@ -17,7 +17,7 @@
17 17
18#include "snac.h" 18#include "snac.h"
19 19
20int login(snac *snac, const xs_dict *headers) 20int login(snac *user, const xs_dict *headers)
21/* tries a login */ 21/* tries a login */
22{ 22{
23 int logged_in = 0; 23 int logged_in = 0;
@@ -31,23 +31,23 @@ int login(snac *snac, const xs_dict *headers)
31 xs *l1 = xs_split_n(s2, ":", 1); 31 xs *l1 = xs_split_n(s2, ":", 1);
32 32
33 if (xs_list_len(l1) == 2) { 33 if (xs_list_len(l1) == 2) {
34 const char *user = xs_list_get(l1, 0); 34 const char *uid = xs_list_get(l1, 0);
35 const char *pwd = xs_list_get(l1, 1); 35 const char *pwd = xs_list_get(l1, 1);
36 const char *addr = xs_or(xs_dict_get(headers, "remote-addr"), 36 const char *addr = xs_or(xs_dict_get(headers, "remote-addr"),
37 xs_dict_get(headers, "x-forwarded-for")); 37 xs_dict_get(headers, "x-forwarded-for"));
38 38
39 if (badlogin_check(user, addr)) { 39 if (badlogin_check(uid, addr)) {
40 logged_in = check_password(user, pwd, 40 logged_in = check_password(uid, pwd,
41 xs_dict_get(snac->config, "passwd")); 41 xs_dict_get(user->config, "passwd"));
42 42
43 if (!logged_in) 43 if (!logged_in)
44 badlogin_inc(user, addr); 44 badlogin_inc(uid, addr);
45 } 45 }
46 } 46 }
47 } 47 }
48 48
49 if (logged_in) 49 if (logged_in)
50 lastlog_write(snac, "web"); 50 lastlog_write(user, "web");
51 51
52 return logged_in; 52 return logged_in;
53} 53}
@@ -69,7 +69,7 @@ xs_str *replace_shortnames(xs_str *s, const xs_list *tag, int ems, const char *p
69 69
70 xs *style = xs_fmt("height: %dem; width: %dem; vertical-align: middle;", ems, ems); 70 xs *style = xs_fmt("height: %dem; width: %dem; vertical-align: middle;", ems, ems);
71 71
72 const char *v; 72 const xs_dict *v;
73 int c = 0; 73 int c = 0;
74 74
75 while (xs_list_next(tag_list, &v, &c)) { 75 while (xs_list_next(tag_list, &v, &c)) {
@@ -77,19 +77,25 @@ xs_str *replace_shortnames(xs_str *s, const xs_list *tag, int ems, const char *p
77 77
78 if (t && strcmp(t, "Emoji") == 0) { 78 if (t && strcmp(t, "Emoji") == 0) {
79 const char *n = xs_dict_get(v, "name"); 79 const char *n = xs_dict_get(v, "name");
80 const char *i = xs_dict_get(v, "icon"); 80 const xs_dict *i = xs_dict_get(v, "icon");
81 81
82 if (n && i) { 82 if (xs_is_string(n) && xs_is_dict(i)) {
83 const char *u = xs_dict_get(i, "url"); 83 const char *u = xs_dict_get(i, "url");
84 xs *url = make_url(u, proxy, 0); 84 const char *mt = xs_dict_get(i, "mediaType");
85
86 if (xs_is_string(u) && xs_is_string(mt) && strcmp(mt, "image/svg+xml")) {
87 xs *url = make_url(u, proxy, 0);
85 88
86 xs_html *img = xs_html_sctag("img", 89 xs_html *img = xs_html_sctag("img",
87 xs_html_attr("loading", "lazy"), 90 xs_html_attr("loading", "lazy"),
88 xs_html_attr("src", url), 91 xs_html_attr("src", url),
89 xs_html_attr("style", style)); 92 xs_html_attr("style", style));
90 93
91 xs *s1 = xs_html_render(img); 94 xs *s1 = xs_html_render(img);
92 s = xs_replace_i(s, n, s1); 95 s = xs_replace_i(s, n, s1);
96 }
97 else
98 s = xs_replace_i(s, n, "");
93 } 99 }
94 } 100 }
95 } 101 }
@@ -621,6 +627,9 @@ static xs_html *html_instance_body(void)
621 const char *email = xs_dict_get(srv_config, "admin_email"); 627 const char *email = xs_dict_get(srv_config, "admin_email");
622 const char *acct = xs_dict_get(srv_config, "admin_account"); 628 const char *acct = xs_dict_get(srv_config, "admin_account");
623 629
630 /* for L() */
631 const snac *user = NULL;
632
624 xs *blurb = xs_replace(snac_blurb, "%host%", host); 633 xs *blurb = xs_replace(snac_blurb, "%host%", host);
625 634
626 xs_html *dl; 635 xs_html *dl;
@@ -735,9 +744,11 @@ xs_html *html_user_head(snac *user, const char *desc, const char *url)
735 xs *fwers = follower_list(user); 744 xs *fwers = follower_list(user);
736 xs *fwing = following_list(user); 745 xs *fwing = following_list(user);
737 746
738 xs *s1 = xs_fmt(L("%d following, %d followers · "), 747 xs *s1 = xs_fmt(L("%d following, %d followers"),
739 xs_list_len(fwing), xs_list_len(fwers)); 748 xs_list_len(fwing), xs_list_len(fwers));
740 749
750 s1 = xs_str_cat(s1, " · ");
751
741 s_desc = xs_str_prepend_i(s_desc, s1); 752 s_desc = xs_str_prepend_i(s_desc, s1);
742 } 753 }
743 754
@@ -1052,8 +1063,8 @@ static xs_html *html_user_body(snac *user, int read_only)
1052 const char *longitude = xs_dict_get_def(user->config, "longitude", ""); 1063 const char *longitude = xs_dict_get_def(user->config, "longitude", "");
1053 1064
1054 if (*latitude && *longitude) { 1065 if (*latitude && *longitude) {
1055 xs *label = xs_fmt(L("%s,%s"), latitude, longitude); 1066 xs *label = xs_fmt("%s,%s", latitude, longitude);
1056 xs *url = xs_fmt(L("https://openstreetmap.org/search?query=%s,%s"), 1067 xs *url = xs_fmt("https://openstreetmap.org/search?query=%s,%s",
1057 latitude, longitude); 1068 latitude, longitude);
1058 1069
1059 xs_html_add(top_user, 1070 xs_html_add(top_user,
@@ -1069,7 +1080,7 @@ static xs_html *html_user_body(snac *user, int read_only)
1069 xs *fwers = follower_list(user); 1080 xs *fwers = follower_list(user);
1070 xs *fwing = following_list(user); 1081 xs *fwing = following_list(user);
1071 1082
1072 xs *s1 = xs_fmt(L("%d following %d followers"), 1083 xs *s1 = xs_fmt(L("%d following, %d followers"),
1073 xs_list_len(fwing), xs_list_len(fwers)); 1084 xs_list_len(fwing), xs_list_len(fwers));
1074 1085
1075 xs_html_add(top_user, 1086 xs_html_add(top_user,
@@ -1085,16 +1096,16 @@ static xs_html *html_user_body(snac *user, int read_only)
1085} 1096}
1086 1097
1087 1098
1088xs_html *html_top_controls(snac *snac) 1099xs_html *html_top_controls(snac *user)
1089/* generates the top controls */ 1100/* generates the top controls */
1090{ 1101{
1091 xs *ops_action = xs_fmt("%s/admin/action", snac->actor); 1102 xs *ops_action = xs_fmt("%s/admin/action", user->actor);
1092 1103
1093 xs_html *top_controls = xs_html_tag("div", 1104 xs_html *top_controls = xs_html_tag("div",
1094 xs_html_attr("class", "snac-top-controls"), 1105 xs_html_attr("class", "snac-top-controls"),
1095 1106
1096 /** new post **/ 1107 /** new post **/
1097 html_note(snac, L("New Post..."), 1108 html_note(user, L("New Post..."),
1098 "new_post_div", "new_post_form", 1109 "new_post_div", "new_post_form",
1099 L("What's on your mind?"), "", 1110 L("What's on your mind?"), "",
1100 NULL, NULL, 1111 NULL, NULL,
@@ -1164,53 +1175,53 @@ xs_html *html_top_controls(snac *snac)
1164 const char *email = "[disabled by admin]"; 1175 const char *email = "[disabled by admin]";
1165 1176
1166 if (xs_type(xs_dict_get(srv_config, "disable_email_notifications")) != XSTYPE_TRUE) { 1177 if (xs_type(xs_dict_get(srv_config, "disable_email_notifications")) != XSTYPE_TRUE) {
1167 email = xs_dict_get(snac->config_o, "email"); 1178 email = xs_dict_get(user->config_o, "email");
1168 if (xs_is_null(email)) { 1179 if (xs_is_null(email)) {
1169 email = xs_dict_get(snac->config, "email"); 1180 email = xs_dict_get(user->config, "email");
1170 1181
1171 if (xs_is_null(email)) 1182 if (xs_is_null(email))
1172 email = ""; 1183 email = "";
1173 } 1184 }
1174 } 1185 }
1175 1186
1176 const char *cw = xs_dict_get(snac->config, "cw"); 1187 const char *cw = xs_dict_get(user->config, "cw");
1177 if (xs_is_null(cw)) 1188 if (xs_is_null(cw))
1178 cw = ""; 1189 cw = "";
1179 1190
1180 const char *telegram_bot = xs_dict_get(snac->config, "telegram_bot"); 1191 const char *telegram_bot = xs_dict_get(user->config, "telegram_bot");
1181 if (xs_is_null(telegram_bot)) 1192 if (xs_is_null(telegram_bot))
1182 telegram_bot = ""; 1193 telegram_bot = "";
1183 1194
1184 const char *telegram_chat_id = xs_dict_get(snac->config, "telegram_chat_id"); 1195 const char *telegram_chat_id = xs_dict_get(user->config, "telegram_chat_id");
1185 if (xs_is_null(telegram_chat_id)) 1196 if (xs_is_null(telegram_chat_id))
1186 telegram_chat_id = ""; 1197 telegram_chat_id = "";
1187 1198
1188 const char *ntfy_server = xs_dict_get(snac->config, "ntfy_server"); 1199 const char *ntfy_server = xs_dict_get(user->config, "ntfy_server");
1189 if (xs_is_null(ntfy_server)) 1200 if (xs_is_null(ntfy_server))
1190 ntfy_server = ""; 1201 ntfy_server = "";
1191 1202
1192 const char *ntfy_token = xs_dict_get(snac->config, "ntfy_token"); 1203 const char *ntfy_token = xs_dict_get(user->config, "ntfy_token");
1193 if (xs_is_null(ntfy_token)) 1204 if (xs_is_null(ntfy_token))
1194 ntfy_token = ""; 1205 ntfy_token = "";
1195 1206
1196 const char *purge_days = xs_dict_get(snac->config, "purge_days"); 1207 const char *purge_days = xs_dict_get(user->config, "purge_days");
1197 if (!xs_is_null(purge_days) && xs_type(purge_days) == XSTYPE_NUMBER) 1208 if (!xs_is_null(purge_days) && xs_type(purge_days) == XSTYPE_NUMBER)
1198 purge_days = (char *)xs_number_str(purge_days); 1209 purge_days = (char *)xs_number_str(purge_days);
1199 else 1210 else
1200 purge_days = "0"; 1211 purge_days = "0";
1201 1212
1202 const xs_val *d_dm_f_u = xs_dict_get(snac->config, "drop_dm_from_unknown"); 1213 const xs_val *d_dm_f_u = xs_dict_get(user->config, "drop_dm_from_unknown");
1203 const xs_val *bot = xs_dict_get(snac->config, "bot"); 1214 const xs_val *bot = xs_dict_get(user->config, "bot");
1204 const xs_val *a_private = xs_dict_get(snac->config, "private"); 1215 const xs_val *a_private = xs_dict_get(user->config, "private");
1205 const xs_val *auto_boost = xs_dict_get(snac->config, "auto_boost"); 1216 const xs_val *auto_boost = xs_dict_get(user->config, "auto_boost");
1206 const xs_val *coll_thrds = xs_dict_get(snac->config, "collapse_threads"); 1217 const xs_val *coll_thrds = xs_dict_get(user->config, "collapse_threads");
1207 const xs_val *pending = xs_dict_get(snac->config, "approve_followers"); 1218 const xs_val *pending = xs_dict_get(user->config, "approve_followers");
1208 const xs_val *show_foll = xs_dict_get(snac->config, "show_contact_metrics"); 1219 const xs_val *show_foll = xs_dict_get(user->config, "show_contact_metrics");
1209 const char *latitude = xs_dict_get_def(snac->config, "latitude", ""); 1220 const char *latitude = xs_dict_get_def(user->config, "latitude", "");
1210 const char *longitude = xs_dict_get_def(snac->config, "longitude", ""); 1221 const char *longitude = xs_dict_get_def(user->config, "longitude", "");
1211 1222
1212 xs *metadata = NULL; 1223 xs *metadata = NULL;
1213 const xs_dict *md = xs_dict_get(snac->config, "metadata"); 1224 const xs_dict *md = xs_dict_get(user->config, "metadata");
1214 1225
1215 if (xs_type(md) == XSTYPE_DICT) { 1226 if (xs_type(md) == XSTYPE_DICT) {
1216 const xs_str *k; 1227 const xs_str *k;
@@ -1232,7 +1243,29 @@ xs_html *html_top_controls(snac *snac)
1232 else 1243 else
1233 metadata = xs_str_new(NULL); 1244 metadata = xs_str_new(NULL);
1234 1245
1235 xs *user_setup_action = xs_fmt("%s/admin/user-setup", snac->actor); 1246 /* ui language */
1247 xs_html *lang_select = xs_html_tag("select",
1248 xs_html_attr("name", "web_ui_lang"));
1249
1250 const char *u_lang = xs_dict_get_def(user->config, "lang", "en");
1251 const char *lang;
1252 const xs_dict *langs;
1253
1254 xs_dict_foreach(srv_langs, lang, langs) {
1255 if (strcmp(u_lang, lang) == 0)
1256 xs_html_add(lang_select,
1257 xs_html_tag("option",
1258 xs_html_text(lang),
1259 xs_html_attr("value", lang),
1260 xs_html_attr("selected", "selected")));
1261 else
1262 xs_html_add(lang_select,
1263 xs_html_tag("option",
1264 xs_html_text(lang),
1265 xs_html_attr("value", lang)));
1266 }
1267
1268 xs *user_setup_action = xs_fmt("%s/admin/user-setup", user->actor);
1236 1269
1237 xs_html_add(top_controls, 1270 xs_html_add(top_controls,
1238 xs_html_tag("details", 1271 xs_html_tag("details",
@@ -1251,7 +1284,7 @@ xs_html *html_top_controls(snac *snac)
1251 xs_html_sctag("input", 1284 xs_html_sctag("input",
1252 xs_html_attr("type", "text"), 1285 xs_html_attr("type", "text"),
1253 xs_html_attr("name", "name"), 1286 xs_html_attr("name", "name"),
1254 xs_html_attr("value", xs_dict_get(snac->config, "name")), 1287 xs_html_attr("value", xs_dict_get(user->config, "name")),
1255 xs_html_attr("placeholder", L("Your name")))), 1288 xs_html_attr("placeholder", L("Your name")))),
1256 xs_html_tag("p", 1289 xs_html_tag("p",
1257 xs_html_text(L("Avatar: ")), 1290 xs_html_text(L("Avatar: ")),
@@ -1281,7 +1314,7 @@ xs_html *html_top_controls(snac *snac)
1281 xs_html_attr("cols", "40"), 1314 xs_html_attr("cols", "40"),
1282 xs_html_attr("rows", "4"), 1315 xs_html_attr("rows", "4"),
1283 xs_html_attr("placeholder", L("Write about yourself here...")), 1316 xs_html_attr("placeholder", L("Write about yourself here...")),
1284 xs_html_text(xs_dict_get(snac->config, "bio")))), 1317 xs_html_text(xs_dict_get(user->config, "bio")))),
1285 xs_html_sctag("input", 1318 xs_html_sctag("input",
1286 xs_html_attr("type", "checkbox"), 1319 xs_html_attr("type", "checkbox"),
1287 xs_html_attr("name", "cw"), 1320 xs_html_attr("name", "cw"),
@@ -1423,6 +1456,11 @@ xs_html *html_top_controls(snac *snac)
1423 xs_html_text(metadata))), 1456 xs_html_text(metadata))),
1424 1457
1425 xs_html_tag("p", 1458 xs_html_tag("p",
1459 xs_html_text(L("Web interface language:")),
1460 xs_html_sctag("br", NULL),
1461 lang_select),
1462
1463 xs_html_tag("p",
1426 xs_html_text(L("New password:")), 1464 xs_html_text(L("New password:")),
1427 xs_html_sctag("br", NULL), 1465 xs_html_sctag("br", NULL),
1428 xs_html_sctag("input", 1466 xs_html_sctag("input",
@@ -1444,8 +1482,8 @@ xs_html *html_top_controls(snac *snac)
1444 1482
1445 xs_html_tag("p", NULL))))); 1483 xs_html_tag("p", NULL)))));
1446 1484
1447 xs *followed_hashtags_action = xs_fmt("%s/admin/followed-hashtags", snac->actor); 1485 xs *followed_hashtags_action = xs_fmt("%s/admin/followed-hashtags", user->actor);
1448 xs *followed_hashtags = xs_join(xs_dict_get_def(snac->config, 1486 xs *followed_hashtags = xs_join(xs_dict_get_def(user->config,
1449 "followed_hashtags", xs_stock(XSTYPE_LIST)), "\n"); 1487 "followed_hashtags", xs_stock(XSTYPE_LIST)), "\n");
1450 1488
1451 xs_html_add(top_controls, 1489 xs_html_add(top_controls,
@@ -1480,7 +1518,7 @@ xs_html *html_top_controls(snac *snac)
1480} 1518}
1481 1519
1482 1520
1483static xs_html *html_button(char *clss, char *label, char *hint) 1521static xs_html *html_button(const char *clss, const char *label, const char *hint)
1484{ 1522{
1485 xs *c = xs_fmt("snac-btn-%s", clss); 1523 xs *c = xs_fmt("snac-btn-%s", clss);
1486 1524
@@ -1496,7 +1534,7 @@ static xs_html *html_button(char *clss, char *label, char *hint)
1496} 1534}
1497 1535
1498 1536
1499xs_str *build_mentions(snac *snac, const xs_dict *msg) 1537xs_str *build_mentions(snac *user, const xs_dict *msg)
1500/* returns a string with the mentions in msg */ 1538/* returns a string with the mentions in msg */
1501{ 1539{
1502 xs_str *s = xs_str_new(NULL); 1540 xs_str *s = xs_str_new(NULL);
@@ -1510,7 +1548,7 @@ xs_str *build_mentions(snac *snac, const xs_dict *msg)
1510 const char *name = xs_dict_get(v, "name"); 1548 const char *name = xs_dict_get(v, "name");
1511 1549
1512 if (type && strcmp(type, "Mention") == 0 && 1550 if (type && strcmp(type, "Mention") == 0 &&
1513 href && strcmp(href, snac->actor) != 0 && name) { 1551 href && strcmp(href, user->actor) != 0 && name) {
1514 xs *s1 = NULL; 1552 xs *s1 = NULL;
1515 1553
1516 if (name[0] != '@') { 1554 if (name[0] != '@') {
@@ -1551,7 +1589,7 @@ xs_str *build_mentions(snac *snac, const xs_dict *msg)
1551} 1589}
1552 1590
1553 1591
1554xs_html *html_entry_controls(snac *snac, const char *actor, 1592xs_html *html_entry_controls(snac *user, const char *actor,
1555 const xs_dict *msg, const char *md5) 1593 const xs_dict *msg, const char *md5)
1556{ 1594{
1557 const char *id = xs_dict_get(msg, "id"); 1595 const char *id = xs_dict_get(msg, "id");
@@ -1560,7 +1598,7 @@ xs_html *html_entry_controls(snac *snac, const char *actor,
1560 xs *likes = object_likes(id); 1598 xs *likes = object_likes(id);
1561 xs *boosts = object_announces(id); 1599 xs *boosts = object_announces(id);
1562 1600
1563 xs *action = xs_fmt("%s/admin/action", snac->actor); 1601 xs *action = xs_fmt("%s/admin/action", user->actor);
1564 xs *redir = xs_fmt("%s_entry", md5); 1602 xs *redir = xs_fmt("%s_entry", md5);
1565 1603
1566 xs_html *form; 1604 xs_html *form;
@@ -1587,8 +1625,8 @@ xs_html *html_entry_controls(snac *snac, const char *actor,
1587 xs_html_attr("name", "redir"), 1625 xs_html_attr("name", "redir"),
1588 xs_html_attr("value", redir)))); 1626 xs_html_attr("value", redir))));
1589 1627
1590 if (!xs_startswith(id, snac->actor)) { 1628 if (!xs_startswith(id, user->actor)) {
1591 if (xs_list_in(likes, snac->md5) == -1) { 1629 if (xs_list_in(likes, user->md5) == -1) {
1592 /* not already liked; add button */ 1630 /* not already liked; add button */
1593 xs_html_add(form, 1631 xs_html_add(form,
1594 html_button("like", L("Like"), L("Say you like this post"))); 1632 html_button("like", L("Like"), L("Say you like this post")));
@@ -1600,7 +1638,7 @@ xs_html *html_entry_controls(snac *snac, const char *actor,
1600 } 1638 }
1601 } 1639 }
1602 else { 1640 else {
1603 if (is_pinned(snac, id)) 1641 if (is_pinned(user, id))
1604 xs_html_add(form, 1642 xs_html_add(form,
1605 html_button("unpin", L("Unpin"), L("Unpin this post from your timeline"))); 1643 html_button("unpin", L("Unpin"), L("Unpin this post from your timeline")));
1606 else 1644 else
@@ -1609,7 +1647,7 @@ xs_html *html_entry_controls(snac *snac, const char *actor,
1609 } 1647 }
1610 1648
1611 if (is_msg_public(msg)) { 1649 if (is_msg_public(msg)) {
1612 if (xs_list_in(boosts, snac->md5) == -1) { 1650 if (xs_list_in(boosts, user->md5) == -1) {
1613 /* not already boosted; add button */ 1651 /* not already boosted; add button */
1614 xs_html_add(form, 1652 xs_html_add(form,
1615 html_button("boost", L("Boost"), L("Announce this post to your followers"))); 1653 html_button("boost", L("Boost"), L("Announce this post to your followers")));
@@ -1621,16 +1659,16 @@ xs_html *html_entry_controls(snac *snac, const char *actor,
1621 } 1659 }
1622 } 1660 }
1623 1661
1624 if (is_bookmarked(snac, id)) 1662 if (is_bookmarked(user, id))
1625 xs_html_add(form, 1663 xs_html_add(form,
1626 html_button("unbookmark", L("Unbookmark"), L("Delete this post from your bookmarks"))); 1664 html_button("unbookmark", L("Unbookmark"), L("Delete this post from your bookmarks")));
1627 else 1665 else
1628 xs_html_add(form, 1666 xs_html_add(form,
1629 html_button("bookmark", L("Bookmark"), L("Add this post to your bookmarks"))); 1667 html_button("bookmark", L("Bookmark"), L("Add this post to your bookmarks")));
1630 1668
1631 if (strcmp(actor, snac->actor) != 0) { 1669 if (strcmp(actor, user->actor) != 0) {
1632 /* controls for other actors than this one */ 1670 /* controls for other actors than this one */
1633 if (following_check(snac, actor)) { 1671 if (following_check(user, actor)) {
1634 xs_html_add(form, 1672 xs_html_add(form,
1635 html_button("unfollow", L("Unfollow"), L("Stop following this user's activity"))); 1673 html_button("unfollow", L("Unfollow"), L("Stop following this user's activity")));
1636 } 1674 }
@@ -1640,7 +1678,7 @@ xs_html *html_entry_controls(snac *snac, const char *actor,
1640 } 1678 }
1641 1679
1642 if (!xs_is_null(group)) { 1680 if (!xs_is_null(group)) {
1643 if (following_check(snac, group)) { 1681 if (following_check(user, group)) {
1644 xs_html_add(form, 1682 xs_html_add(form,
1645 html_button("unfollow", L("Unfollow Group"), 1683 html_button("unfollow", L("Unfollow Group"),
1646 L("Stop following this group or channel"))); 1684 L("Stop following this group or channel")));
@@ -1666,7 +1704,7 @@ xs_html *html_entry_controls(snac *snac, const char *actor,
1666 1704
1667 const char *prev_src = xs_dict_get(msg, "sourceContent"); 1705 const char *prev_src = xs_dict_get(msg, "sourceContent");
1668 1706
1669 if (!xs_is_null(prev_src) && strcmp(actor, snac->actor) == 0) { /** edit **/ 1707 if (!xs_is_null(prev_src) && strcmp(actor, user->actor) == 0) { /** edit **/
1670 /* post can be edited */ 1708 /* post can be edited */
1671 xs *div_id = xs_fmt("%s_edit", md5); 1709 xs *div_id = xs_fmt("%s_edit", md5);
1672 xs *form_id = xs_fmt("%s_edit_form", md5); 1710 xs *form_id = xs_fmt("%s_edit_form", md5);
@@ -1693,26 +1731,26 @@ xs_html *html_entry_controls(snac *snac, const char *actor,
1693 1731
1694 xs_html_add(controls, xs_html_tag("div", 1732 xs_html_add(controls, xs_html_tag("div",
1695 xs_html_tag("p", NULL), 1733 xs_html_tag("p", NULL),
1696 html_note(snac, L("Edit..."), 1734 html_note(user, L("Edit..."),
1697 div_id, form_id, 1735 div_id, form_id,
1698 "", prev_src, 1736 "", prev_src,
1699 id, NULL, 1737 id, NULL,
1700 xs_dict_get(msg, "sensitive"), xs_dict_get(msg, "summary"), 1738 xs_dict_get(msg, "sensitive"), xs_dict_get(msg, "summary"),
1701 xs_stock(is_msg_public(msg) ? XSTYPE_FALSE : XSTYPE_TRUE), redir, 1739 xs_stock(is_msg_public(msg) ? XSTYPE_FALSE : XSTYPE_TRUE), redir,
1702 NULL, 0, att_files, att_alt_texts, is_draft(snac, id))), 1740 NULL, 0, att_files, att_alt_texts, is_draft(user, id))),
1703 xs_html_tag("p", NULL)); 1741 xs_html_tag("p", NULL));
1704 } 1742 }
1705 1743
1706 { /** reply **/ 1744 { /** reply **/
1707 /* the post textarea */ 1745 /* the post textarea */
1708 xs *ct = build_mentions(snac, msg); 1746 xs *ct = build_mentions(user, msg);
1709 xs *div_id = xs_fmt("%s_reply", md5); 1747 xs *div_id = xs_fmt("%s_reply", md5);
1710 xs *form_id = xs_fmt("%s_reply_form", md5); 1748 xs *form_id = xs_fmt("%s_reply_form", md5);
1711 xs *redir = xs_fmt("%s_entry", md5); 1749 xs *redir = xs_fmt("%s_entry", md5);
1712 1750
1713 xs_html_add(controls, xs_html_tag("div", 1751 xs_html_add(controls, xs_html_tag("div",
1714 xs_html_tag("p", NULL), 1752 xs_html_tag("p", NULL),
1715 html_note(snac, L("Reply..."), 1753 html_note(user, L("Reply..."),
1716 div_id, form_id, 1754 div_id, form_id,
1717 "", ct, 1755 "", ct,
1718 NULL, NULL, 1756 NULL, NULL,
@@ -1839,7 +1877,7 @@ xs_html *html_entry(snac *user, xs_dict *msg, int read_only,
1839 xs_html_raw(" &#128204; "))); 1877 xs_html_raw(" &#128204; ")));
1840 } 1878 }
1841 1879
1842 if (user && is_bookmarked(user, id)) { 1880 if (user && !read_only && is_bookmarked(user, id)) {
1843 /* add a bookmark emoji */ 1881 /* add a bookmark emoji */
1844 xs_html_add(score, 1882 xs_html_add(score,
1845 xs_html_tag("span", 1883 xs_html_tag("span",
@@ -2242,6 +2280,11 @@ xs_html *html_entry(snac *user, xs_dict *msg, int read_only,
2242 if (content && xs_str_in(content, o_href) != -1) 2280 if (content && xs_str_in(content, o_href) != -1)
2243 continue; 2281 continue;
2244 2282
2283 /* drop silently any attachment that may include JavaScript */
2284 if (strcmp(type, "image/svg+xml") == 0 ||
2285 strcmp(type, "text/html") == 0)
2286 continue;
2287
2245 /* do this attachment include an icon? */ 2288 /* do this attachment include an icon? */
2246 const xs_dict *icon = xs_dict_get(a, "icon"); 2289 const xs_dict *icon = xs_dict_get(a, "icon");
2247 if (xs_type(icon) == XSTYPE_DICT) { 2290 if (xs_type(icon) == XSTYPE_DICT) {
@@ -2571,7 +2614,7 @@ xs_html *html_entry(snac *user, xs_dict *msg, int read_only,
2571} 2614}
2572 2615
2573 2616
2574xs_html *html_footer(void) 2617xs_html *html_footer(const snac *user)
2575{ 2618{
2576 return xs_html_tag("div", 2619 return xs_html_tag("div",
2577 xs_html_attr("class", "snac-footer"), 2620 xs_html_attr("class", "snac-footer"),
@@ -2879,13 +2922,13 @@ xs_str *html_timeline(snac *user, const xs_list *list, int read_only,
2879 } 2922 }
2880 2923
2881 xs_html_add(body, 2924 xs_html_add(body,
2882 html_footer()); 2925 html_footer(user));
2883 2926
2884 return xs_html_render_s(html, "<!DOCTYPE html>\n"); 2927 return xs_html_render_s(html, "<!DOCTYPE html>\n");
2885} 2928}
2886 2929
2887 2930
2888xs_html *html_people_list(snac *snac, xs_list *list, char *header, char *t, const char *proxy) 2931xs_html *html_people_list(snac *user, xs_list *list, const char *header, const char *t, const char *proxy)
2889{ 2932{
2890 xs_html *snac_posts; 2933 xs_html *snac_posts;
2891 xs_html *people = xs_html_tag("div", 2934 xs_html *people = xs_html_tag("div",
@@ -2910,7 +2953,7 @@ xs_html *html_people_list(snac *snac, xs_list *list, char *header, char *t, cons
2910 xs_html_attr("name", md5)), 2953 xs_html_attr("name", md5)),
2911 xs_html_tag("div", 2954 xs_html_tag("div",
2912 xs_html_attr("class", "snac-post-header"), 2955 xs_html_attr("class", "snac-post-header"),
2913 html_actor_icon(snac, actor, xs_dict_get(actor, "published"), 2956 html_actor_icon(user, actor, xs_dict_get(actor, "published"),
2914 NULL, NULL, 0, 1, proxy, NULL, NULL))); 2957 NULL, NULL, 0, 1, proxy, NULL, NULL)));
2915 2958
2916 /* content (user bio) */ 2959 /* content (user bio) */
@@ -2934,7 +2977,7 @@ xs_html *html_people_list(snac *snac, xs_list *list, char *header, char *t, cons
2934 } 2977 }
2935 2978
2936 /* buttons */ 2979 /* buttons */
2937 xs *btn_form_action = xs_fmt("%s/admin/action", snac->actor); 2980 xs *btn_form_action = xs_fmt("%s/admin/action", user->actor);
2938 2981
2939 xs_html *snac_controls = xs_html_tag("div", 2982 xs_html *snac_controls = xs_html_tag("div",
2940 xs_html_attr("class", "snac-controls")); 2983 xs_html_attr("class", "snac-controls"));
@@ -2954,12 +2997,12 @@ xs_html *html_people_list(snac *snac, xs_list *list, char *header, char *t, cons
2954 2997
2955 xs_html_add(snac_controls, form); 2998 xs_html_add(snac_controls, form);
2956 2999
2957 if (following_check(snac, actor_id)) { 3000 if (following_check(user, actor_id)) {
2958 xs_html_add(form, 3001 xs_html_add(form,
2959 html_button("unfollow", L("Unfollow"), 3002 html_button("unfollow", L("Unfollow"),
2960 L("Stop following this user's activity"))); 3003 L("Stop following this user's activity")));
2961 3004
2962 if (is_limited(snac, actor_id)) 3005 if (is_limited(user, actor_id))
2963 xs_html_add(form, 3006 xs_html_add(form,
2964 html_button("unlimit", L("Unlimit"), 3007 html_button("unlimit", L("Unlimit"),
2965 L("Allow announces (boosts) from this user"))); 3008 L("Allow announces (boosts) from this user")));
@@ -2973,12 +3016,12 @@ xs_html *html_people_list(snac *snac, xs_list *list, char *header, char *t, cons
2973 html_button("follow", L("Follow"), 3016 html_button("follow", L("Follow"),
2974 L("Start following this user's activity"))); 3017 L("Start following this user's activity")));
2975 3018
2976 if (follower_check(snac, actor_id)) 3019 if (follower_check(user, actor_id))
2977 xs_html_add(form, 3020 xs_html_add(form,
2978 html_button("delete", L("Delete"), L("Delete this user"))); 3021 html_button("delete", L("Delete"), L("Delete this user")));
2979 } 3022 }
2980 3023
2981 if (pending_check(snac, actor_id)) { 3024 if (pending_check(user, actor_id)) {
2982 xs_html_add(form, 3025 xs_html_add(form,
2983 html_button("approve", L("Approve"), 3026 html_button("approve", L("Approve"),
2984 L("Approve this follow request"))); 3027 L("Approve this follow request")));
@@ -2987,7 +3030,7 @@ xs_html *html_people_list(snac *snac, xs_list *list, char *header, char *t, cons
2987 html_button("discard", L("Discard"), L("Discard this follow request"))); 3030 html_button("discard", L("Discard"), L("Discard this follow request")));
2988 } 3031 }
2989 3032
2990 if (is_muted(snac, actor_id)) 3033 if (is_muted(user, actor_id))
2991 xs_html_add(form, 3034 xs_html_add(form,
2992 html_button("unmute", L("Unmute"), 3035 html_button("unmute", L("Unmute"),
2993 L("Stop blocking activities from this user"))); 3036 L("Stop blocking activities from this user")));
@@ -3002,7 +3045,7 @@ xs_html *html_people_list(snac *snac, xs_list *list, char *header, char *t, cons
3002 3045
3003 xs_html_add(snac_controls, 3046 xs_html_add(snac_controls,
3004 xs_html_tag("p", NULL), 3047 xs_html_tag("p", NULL),
3005 html_note(snac, L("Direct Message..."), 3048 html_note(user, L("Direct Message..."),
3006 dm_div_id, dm_form_id, 3049 dm_div_id, dm_form_id,
3007 "", "", 3050 "", "",
3008 NULL, actor_id, 3051 NULL, actor_id,
@@ -3048,7 +3091,7 @@ xs_str *html_people(snac *user)
3048 html_user_head(user, NULL, NULL), 3091 html_user_head(user, NULL, NULL),
3049 xs_html_add(html_user_body(user, 0), 3092 xs_html_add(html_user_body(user, 0),
3050 lists, 3093 lists,
3051 html_footer())); 3094 html_footer(user)));
3052 3095
3053 return xs_html_render_s(html, "<!DOCTYPE html>\n"); 3096 return xs_html_render_s(html, "<!DOCTYPE html>\n");
3054} 3097}
@@ -3298,7 +3341,7 @@ xs_str *html_notifications(snac *user, int skip, int show)
3298 xs_set_free(&rep); 3341 xs_set_free(&rep);
3299 3342
3300 xs_html_add(body, 3343 xs_html_add(body,
3301 html_footer()); 3344 html_footer(user));
3302 3345
3303 /* set the check time to now */ 3346 /* set the check time to now */
3304 xs *dummy = notify_check_time(user, 1); 3347 xs *dummy = notify_check_time(user, 1);
@@ -3310,12 +3353,24 @@ xs_str *html_notifications(snac *user, int skip, int show)
3310} 3353}
3311 3354
3312 3355
3356void set_user_lang(snac *user)
3357/* sets the language dict according to user configuration */
3358{
3359 user->lang = NULL;
3360 const char *lang = xs_dict_get(user->config, "lang");
3361
3362 if (xs_is_string(lang))
3363 user->lang = xs_dict_get(srv_langs, lang);
3364}
3365
3366
3313int html_get_handler(const xs_dict *req, const char *q_path, 3367int html_get_handler(const xs_dict *req, const char *q_path,
3314 char **body, int *b_size, char **ctype, 3368 char **body, int *b_size, char **ctype,
3315 xs_str **etag, xs_str **last_modified) 3369 xs_str **etag, xs_str **last_modified)
3316{ 3370{
3317 const char *accept = xs_dict_get(req, "accept"); 3371 const char *accept = xs_dict_get(req, "accept");
3318 int status = HTTP_STATUS_NOT_FOUND; 3372 int status = HTTP_STATUS_NOT_FOUND;
3373 const snac *user = NULL;
3319 snac snac; 3374 snac snac;
3320 xs *uid = NULL; 3375 xs *uid = NULL;
3321 const char *p_path; 3376 const char *p_path;
@@ -3382,6 +3437,9 @@ int html_get_handler(const xs_dict *req, const char *q_path,
3382 return HTTP_STATUS_NOT_FOUND; 3437 return HTTP_STATUS_NOT_FOUND;
3383 } 3438 }
3384 3439
3440 user = &snac; /* for L() */
3441 set_user_lang(&snac);
3442
3385 if (xs_is_true(xs_dict_get(srv_config, "proxy_media"))) 3443 if (xs_is_true(xs_dict_get(srv_config, "proxy_media")))
3386 proxy = 1; 3444 proxy = 1;
3387 3445
@@ -3550,7 +3608,7 @@ int html_get_handler(const xs_dict *req, const char *q_path,
3550 html_user_head(&snac, NULL, NULL), 3608 html_user_head(&snac, NULL, NULL),
3551 xs_html_add(html_user_body(&snac, 0), 3609 xs_html_add(html_user_body(&snac, 0),
3552 page, 3610 page,
3553 html_footer())); 3611 html_footer(user)));
3554 3612
3555 *body = xs_html_render_s(html, "<!DOCTYPE html>\n"); 3613 *body = xs_html_render_s(html, "<!DOCTYPE html>\n");
3556 *b_size = strlen(*body); 3614 *b_size = strlen(*body);
@@ -4005,6 +4063,7 @@ int html_post_handler(const xs_dict *req, const char *q_path,
4005 (void)ctype; 4063 (void)ctype;
4006 4064
4007 int status = 0; 4065 int status = 0;
4066 const snac *user = NULL;
4008 snac snac; 4067 snac snac;
4009 const char *uid; 4068 const char *uid;
4010 const char *p_path; 4069 const char *p_path;
@@ -4028,6 +4087,9 @@ int html_post_handler(const xs_dict *req, const char *q_path,
4028 return HTTP_STATUS_UNAUTHORIZED; 4087 return HTTP_STATUS_UNAUTHORIZED;
4029 } 4088 }
4030 4089
4090 user = &snac; /* for L() */
4091 set_user_lang(&snac);
4092
4031 p_vars = xs_dict_get(req, "p_vars"); 4093 p_vars = xs_dict_get(req, "p_vars");
4032 4094
4033 if (p_path && strcmp(p_path, "admin/note") == 0) { /** **/ 4095 if (p_path && strcmp(p_path, "admin/note") == 0) { /** **/
@@ -4470,6 +4532,8 @@ int html_post_handler(const xs_dict *req, const char *q_path,
4470 snac.config = xs_dict_set(snac.config, "show_contact_metrics", xs_stock(XSTYPE_TRUE)); 4532 snac.config = xs_dict_set(snac.config, "show_contact_metrics", xs_stock(XSTYPE_TRUE));
4471 else 4533 else
4472 snac.config = xs_dict_set(snac.config, "show_contact_metrics", xs_stock(XSTYPE_FALSE)); 4534 snac.config = xs_dict_set(snac.config, "show_contact_metrics", xs_stock(XSTYPE_FALSE));
4535 if ((v = xs_dict_get(p_vars, "web_ui_lang")) != NULL)
4536 snac.config = xs_dict_set(snac.config, "lang", v);
4473 4537
4474 snac.config = xs_dict_set(snac.config, "latitude", xs_dict_get_def(p_vars, "latitude", "")); 4538 snac.config = xs_dict_set(snac.config, "latitude", xs_dict_get_def(p_vars, "latitude", ""));
4475 snac.config = xs_dict_set(snac.config, "longitude", xs_dict_get_def(p_vars, "longitude", "")); 4539 snac.config = xs_dict_set(snac.config, "longitude", xs_dict_get_def(p_vars, "longitude", ""));
diff --git a/httpd.c b/httpd.c
index 5a38aff..22a148d 100644
--- a/httpd.c
+++ b/httpd.c
@@ -211,6 +211,8 @@ int server_get_handler(xs_dict *req, const char *q_path,
211{ 211{
212 int status = 0; 212 int status = 0;
213 213
214 const snac *user = NULL;
215
214 /* is it the server root? */ 216 /* is it the server root? */
215 if (*q_path == '\0' || strcmp(q_path, "/") == 0) { 217 if (*q_path == '\0' || strcmp(q_path, "/") == 0) {
216 const xs_dict *q_vars = xs_dict_get(req, "q_vars"); 218 const xs_dict *q_vars = xs_dict_get(req, "q_vars");
@@ -553,6 +555,9 @@ void httpd_connection(FILE *f)
553 headers = xs_dict_append(headers, "access-control-allow-origin", "*"); 555 headers = xs_dict_append(headers, "access-control-allow-origin", "*");
554 headers = xs_dict_append(headers, "access-control-allow-headers", "*"); 556 headers = xs_dict_append(headers, "access-control-allow-headers", "*");
555 557
558 /* disable any form of fucking JavaScript */
559 headers = xs_dict_append(headers, "Content-Security-Policy", "script-src ;");
560
556 if (p_state->use_fcgi) 561 if (p_state->use_fcgi)
557 xs_fcgi_response(f, status, headers, body, b_size, fcgi_id); 562 xs_fcgi_response(f, status, headers, body, b_size, fcgi_id);
558 else 563 else
diff --git a/po/en.po b/po/en.po
new file mode 100644
index 0000000..ca5e816
--- /dev/null
+++ b/po/en.po
@@ -0,0 +1,687 @@
1# snac message translation file
2#
3#, fuzzy
4msgid ""
5msgstr ""
6"Project-Id-Version: snac\n"
7"Last-Translator: grunfink\n"
8"Language: en\n"
9"Content-Type: text/plain; charset=UTF-8\n"
10
11#: html.c:367
12msgid "Sensitive content: "
13msgstr ""
14
15#: html.c:375
16msgid "Sensitive content description"
17msgstr ""
18
19#: html.c:388
20msgid "Only for mentioned people: "
21msgstr ""
22
23#: html.c:411
24msgid "Reply to (URL): "
25msgstr ""
26
27#: html.c:420
28msgid "Don't send, but store as a draft"
29msgstr ""
30
31#: html.c:421
32msgid "Draft:"
33msgstr ""
34
35#: html.c:441
36msgid "Attachments..."
37msgstr ""
38
39#: html.c:464
40msgid "File:"
41msgstr ""
42
43#: html.c:468
44msgid "Clear this field to delete the attachment"
45msgstr ""
46
47#: html.c:477 html.c:502
48msgid "Attachment description"
49msgstr ""
50
51#: html.c:513
52msgid "Poll..."
53msgstr ""
54
55#: html.c:515
56msgid "Poll options (one per line, up to 8):"
57msgstr ""
58
59#: html.c:527
60msgid "One choice"
61msgstr ""
62
63#: html.c:530
64msgid "Multiple choices"
65msgstr ""
66
67#: html.c:536
68msgid "End in 5 minutes"
69msgstr ""
70
71#: html.c:540
72msgid "End in 1 hour"
73msgstr ""
74
75#: html.c:543
76msgid "End in 1 day"
77msgstr ""
78
79#: html.c:551
80msgid "Post"
81msgstr ""
82
83#: html.c:648 html.c:655
84msgid "Site description"
85msgstr ""
86
87#: html.c:666
88msgid "Admin email"
89msgstr ""
90
91#: html.c:679
92msgid "Admin account"
93msgstr ""
94
95#: html.c:747 html.c:1083
96#, c-format
97msgid "%d following, %d followers"
98msgstr ""
99
100#: html.c:837
101msgid "RSS"
102msgstr ""
103
104#: html.c:842 html.c:870
105msgid "private"
106msgstr ""
107
108#: html.c:866
109msgid "public"
110msgstr ""
111
112#: html.c:874
113msgid "notifications"
114msgstr ""
115
116#: html.c:879
117msgid "people"
118msgstr ""
119
120#: html.c:883
121msgid "instance"
122msgstr ""
123
124#: html.c:892
125msgid ""
126"Search posts by URL or content (regular expression), @user@host accounts, or "
127"#tag"
128msgstr ""
129
130#: html.c:893
131msgid "Content search"
132msgstr ""
133
134#: html.c:1015
135msgid "verified link"
136msgstr ""
137
138#: html.c:1072 html.c:2420 html.c:2433 html.c:2442
139msgid "Location: "
140msgstr ""
141
142#: html.c:1108
143msgid "New Post..."
144msgstr ""
145
146#: html.c:1110
147msgid "What's on your mind?"
148msgstr ""
149
150#: html.c:1119
151msgid "Operations..."
152msgstr ""
153
154#: html.c:1134 html.c:1677 html.c:3016 html.c:4333
155msgid "Follow"
156msgstr ""
157
158#: html.c:1136
159msgid "(by URL or user@host)"
160msgstr ""
161
162#: html.c:1151 html.c:1653 html.c:4285
163msgid "Boost"
164msgstr ""
165
166#: html.c:1153 html.c:1170
167msgid "(by URL)"
168msgstr ""
169
170#: html.c:1168 html.c:1632 html.c:4276
171msgid "Like"
172msgstr ""
173
174#: html.c:1273
175msgid "User Settings..."
176msgstr ""
177
178#: html.c:1282
179msgid "Display name:"
180msgstr ""
181
182#: html.c:1288
183msgid "Your name"
184msgstr ""
185
186#: html.c:1290
187msgid "Avatar: "
188msgstr ""
189
190#: html.c:1298
191msgid "Delete current avatar"
192msgstr ""
193
194#: html.c:1300
195msgid "Header image (banner): "
196msgstr ""
197
198#: html.c:1308
199msgid "Delete current header image"
200msgstr ""
201
202#: html.c:1310
203msgid "Bio:"
204msgstr ""
205
206#: html.c:1316
207msgid "Write about yourself here..."
208msgstr ""
209
210#: html.c:1325
211msgid "Always show sensitive content"
212msgstr ""
213
214#: html.c:1327
215msgid "Email address for notifications:"
216msgstr ""
217
218#: html.c:1335
219msgid "Telegram notifications (bot key and chat id):"
220msgstr ""
221
222#: html.c:1349
223msgid "ntfy notifications (ntfy server and token):"
224msgstr ""
225
226#: html.c:1363
227msgid "Maximum days to keep posts (0: server settings):"
228msgstr ""
229
230#: html.c:1377
231msgid "Drop direct messages from people you don't follow"
232msgstr ""
233
234#: html.c:1386
235msgid "This account is a bot"
236msgstr ""
237
238#: html.c:1395
239msgid "Auto-boost all mentions to this account"
240msgstr ""
241
242#: html.c:1404
243msgid "This account is private (posts are not shown through the web)"
244msgstr ""
245
246#: html.c:1414
247msgid "Collapse top threads by default"
248msgstr ""
249
250#: html.c:1423
251msgid "Follow requests must be approved"
252msgstr ""
253
254#: html.c:1432
255msgid "Publish follower and following metrics"
256msgstr ""
257
258#: html.c:1434
259msgid "Current location:"
260msgstr ""
261
262#: html.c:1448
263msgid "Profile metadata (key=value pairs in each line):"
264msgstr ""
265
266#: html.c:1459
267msgid "Web interface language:"
268msgstr ""
269
270#: html.c:1464
271msgid "New password:"
272msgstr ""
273
274#: html.c:1471
275msgid "Repeat new password:"
276msgstr ""
277
278#: html.c:1481
279msgid "Update user info"
280msgstr ""
281
282#: html.c:1492
283msgid "Followed hashtags..."
284msgstr ""
285
286#: html.c:1494
287msgid "One hashtag per line"
288msgstr ""
289
290#: html.c:1515
291msgid "Update hashtags"
292msgstr ""
293
294#: html.c:1632
295msgid "Say you like this post"
296msgstr ""
297
298#: html.c:1637 html.c:4294
299msgid "Unlike"
300msgstr ""
301
302#: html.c:1637
303msgid "Nah don't like it that much"
304msgstr ""
305
306#: html.c:1643 html.c:4426
307msgid "Unpin"
308msgstr ""
309
310#: html.c:1643
311msgid "Unpin this post from your timeline"
312msgstr ""
313
314#: html.c:1646 html.c:4421
315msgid "Pin"
316msgstr ""
317
318#: html.c:1646
319msgid "Pin this post to the top of your timeline"
320msgstr ""
321
322#: html.c:1653
323msgid "Announce this post to your followers"
324msgstr ""
325
326#: html.c:1658 html.c:4302
327msgid "Unboost"
328msgstr ""
329
330#: html.c:1658
331msgid "I regret I boosted this"
332msgstr ""
333
334#: html.c:1664 html.c:4436
335msgid "Unbookmark"
336msgstr ""
337
338#: html.c:1664
339msgid "Delete this post from your bookmarks"
340msgstr ""
341
342#: html.c:1667 html.c:4431
343msgid "Bookmark"
344msgstr ""
345
346#: html.c:1667
347msgid "Add this post to your bookmarks"
348msgstr ""
349
350#: html.c:1673 html.c:3002 html.c:3190 html.c:4346
351msgid "Unfollow"
352msgstr ""
353
354#: html.c:1673 html.c:3003
355msgid "Stop following this user's activity"
356msgstr ""
357
358#: html.c:1677 html.c:3017
359msgid "Start following this user's activity"
360msgstr ""
361
362#: html.c:1683 html.c:4376
363msgid "Unfollow Group"
364msgstr ""
365
366#: html.c:1684
367msgid "Stop following this group or channel"
368msgstr ""
369
370#: html.c:1688 html.c:4363
371msgid "Follow Group"
372msgstr ""
373
374#: html.c:1689
375msgid "Start following this group or channel"
376msgstr ""
377
378#: html.c:1694 html.c:3039 html.c:4310
379msgid "MUTE"
380msgstr ""
381
382#: html.c:1695
383msgid "Block any activity from this user forever"
384msgstr ""
385
386#: html.c:1700 html.c:3021 html.c:4393
387msgid "Delete"
388msgstr ""
389
390#: html.c:1700
391msgid "Delete this post"
392msgstr ""
393
394#: html.c:1703 html.c:4318
395msgid "Hide"
396msgstr ""
397
398#: html.c:1703
399msgid "Hide this post and its children"
400msgstr ""
401
402#: html.c:1734
403msgid "Edit..."
404msgstr ""
405
406#: html.c:1753
407msgid "Reply..."
408msgstr ""
409
410#: html.c:1804
411msgid "Truncated (too deep)"
412msgstr ""
413
414#: html.c:1813
415msgid "follows you"
416msgstr ""
417
418#: html.c:1876
419msgid "Pinned"
420msgstr ""
421
422#: html.c:1884
423msgid "Bookmarked"
424msgstr ""
425
426#: html.c:1892
427msgid "Poll"
428msgstr ""
429
430#: html.c:1899
431msgid "Voted"
432msgstr ""
433
434#: html.c:1908
435msgid "Event"
436msgstr ""
437
438#: html.c:1940 html.c:1969
439msgid "boosted"
440msgstr ""
441
442#: html.c:1985
443msgid "in reply to"
444msgstr ""
445
446#: html.c:2036
447msgid " [SENSITIVE CONTENT]"
448msgstr ""
449
450#: html.c:2213
451msgid "Vote"
452msgstr ""
453
454#: html.c:2223
455msgid "Closed"
456msgstr ""
457
458#: html.c:2248
459msgid "Closes in"
460msgstr ""
461
462#: html.c:2327
463msgid "Video"
464msgstr ""
465
466#: html.c:2342
467msgid "Audio"
468msgstr ""
469
470#: html.c:2364
471msgid "Attachment"
472msgstr ""
473
474#: html.c:2378
475msgid "Alt..."
476msgstr ""
477
478#: html.c:2391
479msgid "Source channel or community"
480msgstr ""
481
482#: html.c:2485
483msgid "Time: "
484msgstr ""
485
486#: html.c:2560
487msgid "Older..."
488msgstr ""
489
490#: html.c:2623
491msgid "about this site"
492msgstr ""
493
494#: html.c:2625
495msgid "powered by "
496msgstr ""
497
498#: html.c:2690
499msgid "Dismiss"
500msgstr ""
501
502#: html.c:2707
503#, c-format
504msgid "Timeline for list '%s'"
505msgstr ""
506
507#: html.c:2726 html.c:3767
508msgid "Pinned posts"
509msgstr ""
510
511#: html.c:2738 html.c:3782
512msgid "Bookmarked posts"
513msgstr ""
514
515#: html.c:2750 html.c:3797
516msgid "Post drafts"
517msgstr ""
518
519#: html.c:2809
520msgid "No more unseen posts"
521msgstr ""
522
523#: html.c:2813 html.c:2913
524msgid "Back to top"
525msgstr ""
526
527#: html.c:2866
528msgid "History"
529msgstr ""
530
531#: html.c:2918 html.c:3338
532msgid "More..."
533msgstr ""
534
535#: html.c:3007 html.c:4329
536msgid "Unlimit"
537msgstr ""
538
539#: html.c:3008
540msgid "Allow announces (boosts) from this user"
541msgstr ""
542
543#: html.c:3011 html.c:4325
544msgid "Limit"
545msgstr ""
546
547#: html.c:3012
548msgid "Block announces (boosts) from this user"
549msgstr ""
550
551#: html.c:3021
552msgid "Delete this user"
553msgstr ""
554
555#: html.c:3026 html.c:4441
556msgid "Approve"
557msgstr ""
558
559#: html.c:3027
560msgid "Approve this follow request"
561msgstr ""
562
563#: html.c:3030 html.c:4465
564msgid "Discard"
565msgstr ""
566
567#: html.c:3030
568msgid "Discard this follow request"
569msgstr ""
570
571#: html.c:3035 html.c:4314
572msgid "Unmute"
573msgstr ""
574
575#: html.c:3036
576msgid "Stop blocking activities from this user"
577msgstr ""
578
579#: html.c:3040
580msgid "Block any activity from this user"
581msgstr ""
582
583#: html.c:3048
584msgid "Direct Message..."
585msgstr ""
586
587#: html.c:3083
588msgid "Pending follow confirmations"
589msgstr ""
590
591#: html.c:3087
592msgid "People you follow"
593msgstr ""
594
595#: html.c:3088
596msgid "People that follow you"
597msgstr ""
598
599#: html.c:3127
600msgid "Clear all"
601msgstr ""
602
603#: html.c:3184
604msgid "Mention"
605msgstr ""
606
607#: html.c:3187
608msgid "Finished poll"
609msgstr ""
610
611#: html.c:3202
612msgid "Follow Request"
613msgstr ""
614
615#: html.c:3285
616msgid "Context"
617msgstr ""
618
619#: html.c:3296
620msgid "New"
621msgstr ""
622
623#: html.c:3311
624msgid "Already seen"
625msgstr ""
626
627#: html.c:3326
628msgid "None"
629msgstr ""
630
631#: html.c:3592
632#, c-format
633msgid "Search results for account %s"
634msgstr ""
635
636#: html.c:3599
637#, c-format
638msgid "Account %s not found"
639msgstr ""
640
641#: html.c:3630
642#, c-format
643msgid "Search results for tag %s"
644msgstr ""
645
646#: html.c:3630
647#, c-format
648msgid "Nothing found for tag %s"
649msgstr ""
650
651#: html.c:3646
652#, c-format
653msgid "Search results for '%s' (may be more)"
654msgstr ""
655
656#: html.c:3649
657#, c-format
658msgid "Search results for '%s'"
659msgstr ""
660
661#: html.c:3652
662#, c-format
663msgid "No more matches for '%s'"
664msgstr ""
665
666#: html.c:3654
667#, c-format
668msgid "Nothing found for '%s'"
669msgstr ""
670
671#: html.c:3752
672msgid "Showing instance timeline"
673msgstr ""
674
675#: html.c:3820
676#, c-format
677msgid "Showing timeline for list '%s'"
678msgstr ""
679
680#: httpd.c:250
681#, c-format
682msgid "Search results for tag #%s"
683msgstr ""
684
685#: httpd.c:259
686msgid "Recent posts by users in this instance"
687msgstr ""
diff --git a/snac.c b/snac.c
index 9f5b50e..4e5aaba 100644
--- a/snac.c
+++ b/snac.c
@@ -24,6 +24,7 @@
24#include "xs_match.h" 24#include "xs_match.h"
25#include "xs_fcgi.h" 25#include "xs_fcgi.h"
26#include "xs_html.h" 26#include "xs_html.h"
27#include "xs_po.h"
27 28
28#include "snac.h" 29#include "snac.h"
29 30
@@ -34,6 +35,7 @@ xs_str *srv_basedir = NULL;
34xs_dict *srv_config = NULL; 35xs_dict *srv_config = NULL;
35xs_str *srv_baseurl = NULL; 36xs_str *srv_baseurl = NULL;
36xs_str *srv_proxy_token_seed = NULL; 37xs_str *srv_proxy_token_seed = NULL;
38xs_dict *srv_langs = NULL;
37 39
38int dbglevel = 0; 40int dbglevel = 0;
39 41
@@ -179,6 +181,7 @@ const char *http_status_text(int status)
179/* translate status codes to canonical status texts */ 181/* translate status codes to canonical status texts */
180{ 182{
181 switch (status) { 183 switch (status) {
184 case 399: return "Timeout";
182#define HTTP_STATUS(code, name, text) case HTTP_STATUS_ ## name: return #text; 185#define HTTP_STATUS(code, name, text) case HTTP_STATUS_ ## name: return #text;
183#include "http_codes.h" 186#include "http_codes.h"
184#undef HTTP_STATUS 187#undef HTTP_STATUS
diff --git a/snac.h b/snac.h
index 5769b9b..92ffb3f 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 - 2025 grunfink et al. / MIT license */
3 3
4#define VERSION "2.72" 4#define VERSION "2.73-dev"
5 5
6#define USER_AGENT "snac/" VERSION 6#define USER_AGENT "snac/" VERSION
7 7
@@ -16,6 +16,10 @@
16#define MAX_THREADS 256 16#define MAX_THREADS 256
17#endif 17#endif
18 18
19#ifndef MAX_JSON_DEPTH
20#define MAX_JSON_DEPTH 8
21#endif
22
19#ifndef MAX_CONVERSATION_LEVELS 23#ifndef MAX_CONVERSATION_LEVELS
20#define MAX_CONVERSATION_LEVELS 48 24#define MAX_CONVERSATION_LEVELS 48
21#endif 25#endif
@@ -29,10 +33,11 @@ extern xs_str *srv_basedir;
29extern xs_dict *srv_config; 33extern xs_dict *srv_config;
30extern xs_str *srv_baseurl; 34extern xs_str *srv_baseurl;
31extern xs_str *srv_proxy_token_seed; 35extern xs_str *srv_proxy_token_seed;
36extern xs_dict *srv_langs;
32 37
33extern int dbglevel; 38extern int dbglevel;
34 39
35#define L(s) (s) 40#define L(s) lang_str((s), user)
36 41
37#define POSTLIKE_OBJECT_TYPE "Note|Question|Page|Article|Video|Audio|Event" 42#define POSTLIKE_OBJECT_TYPE "Note|Question|Page|Article|Video|Audio|Event"
38 43
@@ -55,6 +60,7 @@ typedef struct {
55 xs_dict *links; /* validated links */ 60 xs_dict *links; /* validated links */
56 xs_str *actor; /* actor url */ 61 xs_str *actor; /* actor url */
57 xs_str *md5; /* actor url md5 */ 62 xs_str *md5; /* actor url md5 */
63 const xs_dict *lang;/* string translation dict */
58} snac; 64} snac;
59 65
60typedef struct { 66typedef struct {
@@ -441,3 +447,5 @@ xs_str *make_url(const char *href, const char *proxy, int by_token);
441 447
442int badlogin_check(const char *user, const char *addr); 448int badlogin_check(const char *user, const char *addr);
443void badlogin_inc(const char *user, const char *addr); 449void badlogin_inc(const char *user, const char *addr);
450
451const char *lang_str(const char *str, const snac *user);
diff --git a/xs_curl.h b/xs_curl.h
index d98ac4c..0609a08 100644
--- a/xs_curl.h
+++ b/xs_curl.h
@@ -13,6 +13,8 @@ int xs_smtp_request(const char *url, const char *user, const char *pass,
13 const char *from, const char *to, const xs_str *body, 13 const char *from, const char *to, const xs_str *body,
14 int use_ssl); 14 int use_ssl);
15 15
16const char *xs_curl_strerr(int errnum);
17
16#ifdef XS_IMPLEMENTATION 18#ifdef XS_IMPLEMENTATION
17 19
18#include <curl/curl.h> 20#include <curl/curl.h>
@@ -240,6 +242,15 @@ int xs_smtp_request(const char *url, const char *user, const char *pass,
240 return (int)res; 242 return (int)res;
241} 243}
242 244
245
246const char *xs_curl_strerr(int errnum)
247{
248 CURLcode cc = errnum < 0 ? -errnum : errnum;
249
250 return curl_easy_strerror(cc);
251}
252
253
243#endif /* XS_IMPLEMENTATION */ 254#endif /* XS_IMPLEMENTATION */
244 255
245#endif /* _XS_CURL_H */ 256#endif /* _XS_CURL_H */
diff --git a/xs_json.h b/xs_json.h
index d1b18e4..8b449a9 100644
--- a/xs_json.h
+++ b/xs_json.h
@@ -4,17 +4,23 @@
4 4
5#define _XS_JSON_H 5#define _XS_JSON_H
6 6
7#ifndef MAX_JSON_DEPTH
8#define MAX_JSON_DEPTH 32
9#endif
10
7int xs_json_dump(const xs_val *data, int indent, FILE *f); 11int xs_json_dump(const xs_val *data, int indent, FILE *f);
8xs_str *xs_json_dumps(const xs_val *data, int indent); 12xs_str *xs_json_dumps(const xs_val *data, int indent);
9 13
10xs_val *xs_json_load(FILE *f); 14xs_val *xs_json_load_full(FILE *f, int maxdepth);
11xs_val *xs_json_loads(const xs_str *json); 15xs_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)
12 18
13xstype xs_json_load_type(FILE *f); 19xstype xs_json_load_type(FILE *f);
14int xs_json_load_array_iter(FILE *f, xs_val **value, xstype *pt, int *c); 20int xs_json_load_array_iter(FILE *f, xs_val **value, xstype *pt, int *c);
15int xs_json_load_object_iter(FILE *f, xs_str **key, xs_val **value, xstype *pt, int *c); 21int xs_json_load_object_iter(FILE *f, xs_str **key, xs_val **value, xstype *pt, int *c);
16xs_list *xs_json_load_array(FILE *f); 22xs_list *xs_json_load_array(FILE *f, int maxdepth);
17xs_dict *xs_json_load_object(FILE *f); 23xs_dict *xs_json_load_object(FILE *f, int maxdepth);
18 24
19 25
20#ifdef XS_IMPLEMENTATION 26#ifdef XS_IMPLEMENTATION
@@ -371,7 +377,7 @@ int xs_json_load_array_iter(FILE *f, xs_val **value, xstype *pt, int *c)
371} 377}
372 378
373 379
374xs_list *xs_json_load_array(FILE *f) 380xs_list *xs_json_load_array(FILE *f, int maxdepth)
375/* loads a full JSON array (after the initial OBRACK) */ 381/* loads a full JSON array (after the initial OBRACK) */
376{ 382{
377 xstype t; 383 xstype t;
@@ -387,12 +393,12 @@ xs_list *xs_json_load_array(FILE *f)
387 393
388 if (r == 1) { 394 if (r == 1) {
389 /* partial load? */ 395 /* partial load? */
390 if (v == NULL) { 396 if (v == NULL && maxdepth != 0) {
391 if (t == XSTYPE_LIST) 397 if (t == XSTYPE_LIST)
392 v = xs_json_load_array(f); 398 v = xs_json_load_array(f, maxdepth - 1);
393 else 399 else
394 if (t == XSTYPE_DICT) 400 if (t == XSTYPE_DICT)
395 v = xs_json_load_object(f); 401 v = xs_json_load_object(f, maxdepth - 1);
396 } 402 }
397 403
398 /* still null? fail */ 404 /* still null? fail */
@@ -459,7 +465,7 @@ int xs_json_load_object_iter(FILE *f, xs_str **key, xs_val **value, xstype *pt,
459} 465}
460 466
461 467
462xs_dict *xs_json_load_object(FILE *f) 468xs_dict *xs_json_load_object(FILE *f, int maxdepth)
463/* loads a full JSON object (after the initial OCURLY) */ 469/* loads a full JSON object (after the initial OCURLY) */
464{ 470{
465 xstype t; 471 xstype t;
@@ -476,12 +482,12 @@ xs_dict *xs_json_load_object(FILE *f)
476 482
477 if (r == 1) { 483 if (r == 1) {
478 /* partial load? */ 484 /* partial load? */
479 if (v == NULL) { 485 if (v == NULL && maxdepth != 0) {
480 if (t == XSTYPE_LIST) 486 if (t == XSTYPE_LIST)
481 v = xs_json_load_array(f); 487 v = xs_json_load_array(f, maxdepth - 1);
482 else 488 else
483 if (t == XSTYPE_DICT) 489 if (t == XSTYPE_DICT)
484 v = xs_json_load_object(f); 490 v = xs_json_load_object(f, maxdepth - 1);
485 } 491 }
486 492
487 /* still null? fail */ 493 /* still null? fail */
@@ -500,14 +506,14 @@ xs_dict *xs_json_load_object(FILE *f)
500} 506}
501 507
502 508
503xs_val *xs_json_loads(const xs_str *json) 509xs_val *xs_json_loads_full(const xs_str *json, int maxdepth)
504/* loads a string in JSON format and converts to a multiple data */ 510/* loads a string in JSON format and converts to a multiple data */
505{ 511{
506 FILE *f; 512 FILE *f;
507 xs_val *v = NULL; 513 xs_val *v = NULL;
508 514
509 if ((f = fmemopen((char *)json, strlen(json), "r")) != NULL) { 515 if ((f = fmemopen((char *)json, strlen(json), "r")) != NULL) {
510 v = xs_json_load(f); 516 v = xs_json_load_full(f, maxdepth);
511 fclose(f); 517 fclose(f);
512 } 518 }
513 519
@@ -533,17 +539,17 @@ xstype xs_json_load_type(FILE *f)
533} 539}
534 540
535 541
536xs_val *xs_json_load(FILE *f) 542xs_val *xs_json_load_full(FILE *f, int maxdepth)
537/* loads a JSON file */ 543/* loads a JSON file */
538{ 544{
539 xs_val *v = NULL; 545 xs_val *v = NULL;
540 xstype t = xs_json_load_type(f); 546 xstype t = xs_json_load_type(f);
541 547
542 if (t == XSTYPE_LIST) 548 if (t == XSTYPE_LIST)
543 v = xs_json_load_array(f); 549 v = xs_json_load_array(f, maxdepth);
544 else 550 else
545 if (t == XSTYPE_DICT) 551 if (t == XSTYPE_DICT)
546 v = xs_json_load_object(f); 552 v = xs_json_load_object(f, maxdepth);
547 553
548 return v; 554 return v;
549} 555}
diff --git a/xs_po.h b/xs_po.h
new file mode 100644
index 0000000..c6feec9
--- /dev/null
+++ b/xs_po.h
@@ -0,0 +1,86 @@
1/* copyright (c) 2025 grunfink et al. / MIT license */
2
3#ifndef _XS_PO_H
4
5#define _XS_PO_H
6
7xs_dict *xs_po_to_dict(const char *fn);
8
9#ifdef XS_IMPLEMENTATION
10
11xs_dict *xs_po_to_dict(const char *fn)
12/* converts a PO file to a dict */
13{
14 xs_dict *d = NULL;
15 FILE *f;
16
17 if ((f = fopen(fn, "r")) != NULL) {
18 d = xs_dict_new();
19
20 xs *k = NULL;
21 xs *v = NULL;
22 enum { IN_NONE, IN_K, IN_V } mode = IN_NONE;
23
24 while (!feof(f)) {
25 xs *l = xs_strip_i(xs_readline(f));
26
27 /* discard empty lines and comments */
28 if (*l == '\0' || *l == '#')
29 continue;
30
31 if (xs_startswith(l, "msgid ")) {
32 if (mode == IN_V) {
33 /* flush */
34 if (xs_is_string(k) && xs_is_string(v) && *v)
35 d = xs_dict_set(d, k, v);
36
37 k = xs_free(k);
38 v = xs_free(v);
39 }
40
41 l = xs_replace_i(l, "msgid ", "");
42 mode = IN_K;
43
44 k = xs_str_new(NULL);
45 }
46 else
47 if (xs_startswith(l, "msgstr ")) {
48 if (mode != IN_K)
49 break;
50
51 l = xs_replace_i(l, "msgstr ", "");
52 mode = IN_V;
53
54 v = xs_str_new(NULL);
55 }
56
57 l = xs_replace_i(l, "\\n", "\n");
58 l = xs_strip_chars_i(l, "\"");
59
60 switch (mode) {
61 case IN_K:
62 k = xs_str_cat(k, l);
63 break;
64
65 case IN_V:
66 v = xs_str_cat(v, l);
67 break;
68
69 case IN_NONE:
70 break;
71 }
72 }
73
74 /* final flush */
75 if (xs_is_string(k) && xs_is_string(v) && *v)
76 d = xs_dict_set(d, k, v);
77
78 fclose(f);
79 }
80
81 return d;
82}
83
84#endif /* XS_IMPLEMENTATION */
85
86#endif /* XS_PO_H */
diff --git a/xs_regex.h b/xs_regex.h
index a4db447..1e157d2 100644
--- a/xs_regex.h
+++ b/xs_regex.h
@@ -43,11 +43,13 @@ xs_list *xs_regex_split_n(const char *str, const char *rx, int count)
43 while (count > 0 && !regexec(&re, (p = str + offset), 1, &rm, offset > 0 ? REG_NOTBOL : 0)) { 43 while (count > 0 && !regexec(&re, (p = str + offset), 1, &rm, offset > 0 ? REG_NOTBOL : 0)) {
44 /* add first the leading part of the string */ 44 /* add first the leading part of the string */
45 xs *s1 = xs_str_new_sz(p, rm.rm_so); 45 xs *s1 = xs_str_new_sz(p, rm.rm_so);
46 list = xs_list_append(list, s1); 46
47 list = xs_list_append(list, xs_is_string(s1) ? s1 : "");
47 48
48 /* add now the matched text as the separator */ 49 /* add now the matched text as the separator */
49 xs *s2 = xs_str_new_sz(p + rm.rm_so, rm.rm_eo - rm.rm_so); 50 xs *s2 = xs_str_new_sz(p + rm.rm_so, rm.rm_eo - rm.rm_so);
50 list = xs_list_append(list, s2); 51
52 list = xs_list_append(list, xs_is_string(s2) ? s2 : "");
51 53
52 /* move forward */ 54 /* move forward */
53 offset += rm.rm_eo; 55 offset += rm.rm_eo;
diff --git a/xs_version.h b/xs_version.h
index 7314133..f899dcb 100644
--- a/xs_version.h
+++ b/xs_version.h
@@ -1 +1 @@
/* 2f43b93e9d2b63360c802e09f4c68adfef74c673 2025-01-28T07:40:50+01:00 */ /* d467dc71e518603250a55c8a67e26cf40e1710e9 2025-02-14T10:21:15+01:00 */