snac2

Fork of https://codeberg.org/grunfink/snac2
git clone https://git.inz.fi/snac2
Log | Files | Refs | README | LICENSE

commit 7eb2556f26baf8ff79fcb7388712d8b714efc4f6
parent 7611a6bee4bcbad2f1710aafa99aba730e5cf995
Author: shtrophic <christoph@liebender.dev>
Date:   Mon, 17 Feb 2025 20:54:36 +0100

Merge remote-tracking branch 'upstream/master' into curl-smtp

Diffstat:
MMakefile | 21++++++++++++++-------
MMakefile.NetBSD | 14+++++++-------
Mactivitypub.c | 14+++++++++++---
Mdata.c | 45+++++++++++++++++++++++++++++++++++++++++++++
Mdoc/snac.8 | 17++++++++++-------
Mformat.c | 2+-
Mhtml.c | 224+++++++++++++++++++++++++++++++++++++++++++++++++++----------------------------
Mhttpd.c | 5+++++
Apo/en.po | 687+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Msnac.c | 3+++
Msnac.h | 12++++++++++--
Mxs_curl.h | 11+++++++++++
Mxs_json.h | 40+++++++++++++++++++++++-----------------
Axs_po.h | 86+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mxs_regex.h | 6++++--
Mxs_version.h | 2+-
16 files changed, 1062 insertions(+), 127 deletions(-)

diff --git a/Makefile b/Makefile @@ -38,24 +38,31 @@ uninstall: rm $(PREFIX_MAN)/man5/snac.5 rm $(PREFIX_MAN)/man8/snac.8 +update-po: + mkdir -p po + [ -f "po/en.po" ] || xgettext -o po/en.po --language=C --keyword=L --from-code=utf-8 *.c + for a in po/*.po ; do \ + xgettext --omit-header -j -o $$a --language=C --keyword=L --from-code=utf-8 *.c ; \ + done + activitypub.o: activitypub.c xs.h xs_json.h xs_curl.h xs_mime.h \ xs_openssl.h xs_regex.h xs_time.h xs_set.h xs_match.h xs_unicode.h \ snac.h http_codes.h data.o: data.c xs.h xs_hex.h xs_io.h xs_json.h xs_openssl.h xs_glob.h \ - xs_set.h xs_time.h xs_regex.h xs_match.h xs_unicode.h xs_random.h snac.h \ - http_codes.h + xs_set.h xs_time.h xs_regex.h xs_match.h xs_unicode.h xs_random.h \ + xs_po.h snac.h http_codes.h format.o: format.c xs.h xs_regex.h xs_mime.h xs_html.h xs_json.h \ xs_time.h xs_match.h snac.h http_codes.h html.o: html.c xs.h xs_io.h xs_json.h xs_regex.h xs_set.h xs_openssl.h \ - xs_time.h xs_mime.h xs_match.h xs_html.h xs_curl.h xs_unicode.h snac.h \ - http_codes.h + xs_time.h xs_mime.h xs_match.h xs_html.h xs_curl.h xs_unicode.h xs_url.h \ + snac.h http_codes.h http.o: http.c xs.h xs_io.h xs_openssl.h xs_curl.h xs_time.h xs_json.h \ snac.h http_codes.h httpd.o: httpd.c xs.h xs_io.h xs_json.h xs_socket.h xs_unix_socket.h \ xs_httpd.h xs_mime.h xs_time.h xs_openssl.h xs_fcgi.h xs_html.h snac.h \ http_codes.h -main.o: main.c xs.h xs_io.h xs_json.h xs_time.h xs_openssl.h snac.h \ - http_codes.h +main.o: main.c xs.h xs_io.h xs_json.h xs_time.h xs_openssl.h xs_match.h \ + snac.h http_codes.h mastoapi.o: mastoapi.c xs.h xs_hex.h xs_openssl.h xs_json.h xs_io.h \ xs_time.h xs_glob.h xs_set.h xs_random.h xs_url.h xs_mime.h xs_match.h \ snac.h http_codes.h @@ -63,7 +70,7 @@ sandbox.o: sandbox.c xs.h snac.h http_codes.h snac.o: snac.c xs.h xs_hex.h xs_io.h xs_unicode_tbl.h xs_unicode.h \ xs_json.h xs_curl.h xs_openssl.h xs_socket.h xs_unix_socket.h xs_url.h \ xs_httpd.h xs_mime.h xs_regex.h xs_set.h xs_time.h xs_glob.h xs_random.h \ - xs_match.h xs_fcgi.h xs_html.h snac.h http_codes.h + xs_match.h xs_fcgi.h xs_html.h xs_po.h snac.h http_codes.h upgrade.o: upgrade.c xs.h xs_io.h xs_json.h xs_glob.h snac.h http_codes.h utils.o: utils.c xs.h xs_io.h xs_json.h xs_time.h xs_openssl.h \ 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 @@ -39,20 +39,20 @@ activitypub.o: activitypub.c xs.h xs_json.h xs_curl.h xs_mime.h \ xs_openssl.h xs_regex.h xs_time.h xs_set.h xs_match.h xs_unicode.h \ snac.h http_codes.h data.o: data.c xs.h xs_hex.h xs_io.h xs_json.h xs_openssl.h xs_glob.h \ - xs_set.h xs_time.h xs_regex.h xs_match.h xs_unicode.h xs_random.h snac.h \ - http_codes.h + xs_set.h xs_time.h xs_regex.h xs_match.h xs_unicode.h xs_random.h \ + xs_po.h snac.h http_codes.h format.o: format.c xs.h xs_regex.h xs_mime.h xs_html.h xs_json.h \ xs_time.h xs_match.h snac.h http_codes.h html.o: html.c xs.h xs_io.h xs_json.h xs_regex.h xs_set.h xs_openssl.h \ - xs_time.h xs_mime.h xs_match.h xs_html.h xs_curl.h xs_unicode.h snac.h \ - http_codes.h + xs_time.h xs_mime.h xs_match.h xs_html.h xs_curl.h xs_unicode.h xs_url.h \ + snac.h http_codes.h http.o: http.c xs.h xs_io.h xs_openssl.h xs_curl.h xs_time.h xs_json.h \ snac.h http_codes.h httpd.o: httpd.c xs.h xs_io.h xs_json.h xs_socket.h xs_unix_socket.h \ xs_httpd.h xs_mime.h xs_time.h xs_openssl.h xs_fcgi.h xs_html.h snac.h \ http_codes.h -main.o: main.c xs.h xs_io.h xs_json.h xs_time.h xs_openssl.h snac.h \ - http_codes.h +main.o: main.c xs.h xs_io.h xs_json.h xs_time.h xs_openssl.h xs_match.h \ + snac.h http_codes.h mastoapi.o: mastoapi.c xs.h xs_hex.h xs_openssl.h xs_json.h xs_io.h \ xs_time.h xs_glob.h xs_set.h xs_random.h xs_url.h xs_mime.h xs_match.h \ snac.h http_codes.h @@ -60,7 +60,7 @@ sandbox.o: sandbox.c xs.h snac.h http_codes.h snac.o: snac.c xs.h xs_hex.h xs_io.h xs_unicode_tbl.h xs_unicode.h \ xs_json.h xs_curl.h xs_openssl.h xs_socket.h xs_unix_socket.h xs_url.h \ xs_httpd.h xs_mime.h xs_regex.h xs_set.h xs_time.h xs_glob.h xs_random.h \ - xs_match.h xs_fcgi.h xs_html.h snac.h http_codes.h + xs_match.h xs_fcgi.h xs_html.h xs_po.h snac.h http_codes.h upgrade.o: upgrade.c xs.h xs_io.h xs_json.h xs_glob.h snac.h http_codes.h utils.o: utils.c xs.h xs_io.h xs_json.h xs_time.h xs_openssl.h \ 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 @@ -2715,6 +2715,12 @@ int process_user_queue(snac *snac) } +xs_str *str_status(int status) +{ + return xs_fmt("%d %s", status, status < 0 ? xs_curl_strerr(status) : http_status_text(status)); +} + + void process_queue_item(xs_dict *q_item) /* processes an item from the global queue */ { @@ -2771,7 +2777,9 @@ void process_queue_item(xs_dict *q_item) else payload = xs_str_new(NULL); - srv_log(xs_fmt("output message: sent to inbox %s %d%s", inbox, status, payload)); + xs *s_status = str_status(status); + + srv_log(xs_fmt("output message: sent to inbox %s (%s)%s", inbox, s_status, payload)); if (!valid_status(status)) { retries++; @@ -2789,10 +2797,10 @@ void process_queue_item(xs_dict *q_item) || status == HTTP_STATUS_UNPROCESSABLE_CONTENT || status < 0) /* explicit error: discard */ - srv_log(xs_fmt("output message: error %s %d", inbox, status)); + srv_log(xs_fmt("output message: error %s (%s)", inbox, s_status)); else if (retries > queue_retry_max) - srv_log(xs_fmt("output message: giving up %s %d", inbox, status)); + srv_log(xs_fmt("output message: giving up %s (%s)", inbox, s_status)); else { /* requeue */ enqueue_output_raw(keyid, seckey, msg, inbox, retries, status); diff --git a/data.c b/data.c @@ -13,6 +13,7 @@ #include "xs_match.h" #include "xs_unicode.h" #include "xs_random.h" +#include "xs_po.h" #include "snac.h" @@ -98,6 +99,9 @@ int srv_open(const char *basedir, int auto_upgrade) if (error != NULL) srv_log(error); + if (!ret) + return ret; + /* create the queue/ subdir, just in case */ xs *qdir = xs_fmt("%s/queue", srv_basedir); mkdirx(qdir); @@ -148,6 +152,29 @@ int srv_open(const char *basedir, int auto_upgrade) mkdirx(expdir); } + /* languages */ + srv_langs = xs_dict_new(); + srv_langs = xs_dict_set(srv_langs, "en", xs_stock(XSTYPE_NULL)); + + xs *l_dir = xs_fmt("%s/lang/", srv_basedir); + mkdirx(l_dir); + + l_dir = xs_str_cat(l_dir, "*.po"); + xs *pos = xs_glob(l_dir, 0, 0); + const char *po; + + xs_list_foreach(pos, po) { + xs *d = xs_po_to_dict(po); + + if (xs_is_dict(d)) { + xs *l = xs_split(po, "/"); + xs *id = xs_dup(xs_list_get(l, -1)); + id = xs_replace_i(id, ".po", ""); + + srv_langs = xs_dict_set(srv_langs, id, d); + } + } + return ret; } @@ -4064,3 +4091,21 @@ void badlogin_inc(const char *user, const char *addr) pthread_mutex_unlock(&data_mutex); } } + + +/** language strings **/ + +const char *lang_str(const char *str, const snac *user) +/* returns a translated string */ +{ + const char *n_str = str; + + if (user && xs_is_dict(user->lang) && xs_is_string(str)) { + n_str = xs_dict_get(user->lang, str); + + if (xs_is_null(n_str) || *n_str == '\0') + n_str = str; + } + + return n_str; +} diff --git a/doc/snac.8 b/doc/snac.8 @@ -23,8 +23,12 @@ Ultrix machine in your grandfather basement, probably MacOS) support hard links on their native filesystems. Don't do fancy things like moving the subdirectories to different filesystems. Also, if you move your .Nm -installation to another server, do it with a tool that respect hard -link counts. Remember: +installation to another server, do it with a tool that keeps hard +links, like +.Xr tar 1 +or +.Xr rsync 1 +with the -H switch. Remember: .Nm is a very UNIXy program that loves hard links. .Ss Building and Installation @@ -194,9 +198,7 @@ By setting this to true, no inbox collection is done. Inbox collection helps being discovered from remote instances, but also increases network traffic. .It Ic http_headers If you need to add more HTTP response headers for whatever reason, you can -fill this object with the required header/value pairs. For example, for enhanced -XSS security, you can set the "Content-Security-Policy" header to "script-src ;" -to be totally sure that no JavaScript is executed. +fill this object with the required header/value pairs. .It Ic show_instance_timeline If this is set to true, the instance base URL will show a timeline with the latest user 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 file in the server base directory, one per line; if this file exists, all posts' content will be matched (after being stripped of HTML tags) against these regexes, one by one, and any match will make the post to -be rejected. If you don't know about regular expressions, don't use this -option (or learn about them in some tutorial, there are gazillions of +be rejected. Use lower case, the regex will be case insensitive by default. +If you don't know about regular expressions, don't use this +option (or learn about them inw some tutorial, there are gazillions of them out there), as you and your users may start missing posts. Also, given that every regular expression implementation supports a different set of features, consider reading the documentation about the one diff --git a/format.c b/format.c @@ -458,7 +458,7 @@ xs_str *sanitize(const char *content) if (valid_tags[i]) { /* accepted tag: rebuild it with only the accepted elements */ - xs *el = xs_regex_select(v, "(src|href|rel|class|target)=\"[^\"]*\""); + xs *el = xs_regex_select(v, "(src|href|rel|class|target)=(\"[^\"]*\"|'[^']*')"); xs *s3 = xs_join(el, " "); s2 = xs_fmt("<%s%s%s%s>", diff --git a/html.c b/html.c @@ -17,7 +17,7 @@ #include "snac.h" -int login(snac *snac, const xs_dict *headers) +int login(snac *user, const xs_dict *headers) /* tries a login */ { int logged_in = 0; @@ -31,23 +31,23 @@ int login(snac *snac, const xs_dict *headers) xs *l1 = xs_split_n(s2, ":", 1); if (xs_list_len(l1) == 2) { - const char *user = xs_list_get(l1, 0); + const char *uid = xs_list_get(l1, 0); const char *pwd = xs_list_get(l1, 1); const char *addr = xs_or(xs_dict_get(headers, "remote-addr"), xs_dict_get(headers, "x-forwarded-for")); - if (badlogin_check(user, addr)) { - logged_in = check_password(user, pwd, - xs_dict_get(snac->config, "passwd")); + if (badlogin_check(uid, addr)) { + logged_in = check_password(uid, pwd, + xs_dict_get(user->config, "passwd")); if (!logged_in) - badlogin_inc(user, addr); + badlogin_inc(uid, addr); } } } if (logged_in) - lastlog_write(snac, "web"); + lastlog_write(user, "web"); return logged_in; } @@ -69,7 +69,7 @@ xs_str *replace_shortnames(xs_str *s, const xs_list *tag, int ems, const char *p xs *style = xs_fmt("height: %dem; width: %dem; vertical-align: middle;", ems, ems); - const char *v; + const xs_dict *v; int c = 0; 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 if (t && strcmp(t, "Emoji") == 0) { const char *n = xs_dict_get(v, "name"); - const char *i = xs_dict_get(v, "icon"); + const xs_dict *i = xs_dict_get(v, "icon"); - if (n && i) { + if (xs_is_string(n) && xs_is_dict(i)) { const char *u = xs_dict_get(i, "url"); - xs *url = make_url(u, proxy, 0); + const char *mt = xs_dict_get(i, "mediaType"); + + if (xs_is_string(u) && xs_is_string(mt) && strcmp(mt, "image/svg+xml")) { + xs *url = make_url(u, proxy, 0); - xs_html *img = xs_html_sctag("img", - xs_html_attr("loading", "lazy"), - xs_html_attr("src", url), - xs_html_attr("style", style)); + xs_html *img = xs_html_sctag("img", + xs_html_attr("loading", "lazy"), + xs_html_attr("src", url), + xs_html_attr("style", style)); - xs *s1 = xs_html_render(img); - s = xs_replace_i(s, n, s1); + xs *s1 = xs_html_render(img); + s = xs_replace_i(s, n, s1); + } + else + s = xs_replace_i(s, n, ""); } } } @@ -621,6 +627,9 @@ static xs_html *html_instance_body(void) const char *email = xs_dict_get(srv_config, "admin_email"); const char *acct = xs_dict_get(srv_config, "admin_account"); + /* for L() */ + const snac *user = NULL; + xs *blurb = xs_replace(snac_blurb, "%host%", host); xs_html *dl; @@ -735,9 +744,11 @@ xs_html *html_user_head(snac *user, const char *desc, const char *url) xs *fwers = follower_list(user); xs *fwing = following_list(user); - xs *s1 = xs_fmt(L("%d following, %d followers · "), + xs *s1 = xs_fmt(L("%d following, %d followers"), xs_list_len(fwing), xs_list_len(fwers)); + s1 = xs_str_cat(s1, " · "); + s_desc = xs_str_prepend_i(s_desc, s1); } @@ -1052,8 +1063,8 @@ static xs_html *html_user_body(snac *user, int read_only) const char *longitude = xs_dict_get_def(user->config, "longitude", ""); if (*latitude && *longitude) { - xs *label = xs_fmt(L("%s,%s"), latitude, longitude); - xs *url = xs_fmt(L("https://openstreetmap.org/search?query=%s,%s"), + xs *label = xs_fmt("%s,%s", latitude, longitude); + xs *url = xs_fmt("https://openstreetmap.org/search?query=%s,%s", latitude, longitude); xs_html_add(top_user, @@ -1069,7 +1080,7 @@ static xs_html *html_user_body(snac *user, int read_only) xs *fwers = follower_list(user); xs *fwing = following_list(user); - xs *s1 = xs_fmt(L("%d following %d followers"), + xs *s1 = xs_fmt(L("%d following, %d followers"), xs_list_len(fwing), xs_list_len(fwers)); xs_html_add(top_user, @@ -1085,16 +1096,16 @@ static xs_html *html_user_body(snac *user, int read_only) } -xs_html *html_top_controls(snac *snac) +xs_html *html_top_controls(snac *user) /* generates the top controls */ { - xs *ops_action = xs_fmt("%s/admin/action", snac->actor); + xs *ops_action = xs_fmt("%s/admin/action", user->actor); xs_html *top_controls = xs_html_tag("div", xs_html_attr("class", "snac-top-controls"), /** new post **/ - html_note(snac, L("New Post..."), + html_note(user, L("New Post..."), "new_post_div", "new_post_form", L("What's on your mind?"), "", NULL, NULL, @@ -1164,53 +1175,53 @@ xs_html *html_top_controls(snac *snac) const char *email = "[disabled by admin]"; if (xs_type(xs_dict_get(srv_config, "disable_email_notifications")) != XSTYPE_TRUE) { - email = xs_dict_get(snac->config_o, "email"); + email = xs_dict_get(user->config_o, "email"); if (xs_is_null(email)) { - email = xs_dict_get(snac->config, "email"); + email = xs_dict_get(user->config, "email"); if (xs_is_null(email)) email = ""; } } - const char *cw = xs_dict_get(snac->config, "cw"); + const char *cw = xs_dict_get(user->config, "cw"); if (xs_is_null(cw)) cw = ""; - const char *telegram_bot = xs_dict_get(snac->config, "telegram_bot"); + const char *telegram_bot = xs_dict_get(user->config, "telegram_bot"); if (xs_is_null(telegram_bot)) telegram_bot = ""; - const char *telegram_chat_id = xs_dict_get(snac->config, "telegram_chat_id"); + const char *telegram_chat_id = xs_dict_get(user->config, "telegram_chat_id"); if (xs_is_null(telegram_chat_id)) telegram_chat_id = ""; - const char *ntfy_server = xs_dict_get(snac->config, "ntfy_server"); + const char *ntfy_server = xs_dict_get(user->config, "ntfy_server"); if (xs_is_null(ntfy_server)) ntfy_server = ""; - const char *ntfy_token = xs_dict_get(snac->config, "ntfy_token"); + const char *ntfy_token = xs_dict_get(user->config, "ntfy_token"); if (xs_is_null(ntfy_token)) ntfy_token = ""; - const char *purge_days = xs_dict_get(snac->config, "purge_days"); + const char *purge_days = xs_dict_get(user->config, "purge_days"); if (!xs_is_null(purge_days) && xs_type(purge_days) == XSTYPE_NUMBER) purge_days = (char *)xs_number_str(purge_days); else purge_days = "0"; - const xs_val *d_dm_f_u = xs_dict_get(snac->config, "drop_dm_from_unknown"); - const xs_val *bot = xs_dict_get(snac->config, "bot"); - const xs_val *a_private = xs_dict_get(snac->config, "private"); - const xs_val *auto_boost = xs_dict_get(snac->config, "auto_boost"); - const xs_val *coll_thrds = xs_dict_get(snac->config, "collapse_threads"); - const xs_val *pending = xs_dict_get(snac->config, "approve_followers"); - const xs_val *show_foll = xs_dict_get(snac->config, "show_contact_metrics"); - const char *latitude = xs_dict_get_def(snac->config, "latitude", ""); - const char *longitude = xs_dict_get_def(snac->config, "longitude", ""); + const xs_val *d_dm_f_u = xs_dict_get(user->config, "drop_dm_from_unknown"); + const xs_val *bot = xs_dict_get(user->config, "bot"); + const xs_val *a_private = xs_dict_get(user->config, "private"); + const xs_val *auto_boost = xs_dict_get(user->config, "auto_boost"); + const xs_val *coll_thrds = xs_dict_get(user->config, "collapse_threads"); + const xs_val *pending = xs_dict_get(user->config, "approve_followers"); + const xs_val *show_foll = xs_dict_get(user->config, "show_contact_metrics"); + const char *latitude = xs_dict_get_def(user->config, "latitude", ""); + const char *longitude = xs_dict_get_def(user->config, "longitude", ""); xs *metadata = NULL; - const xs_dict *md = xs_dict_get(snac->config, "metadata"); + const xs_dict *md = xs_dict_get(user->config, "metadata"); if (xs_type(md) == XSTYPE_DICT) { const xs_str *k; @@ -1232,7 +1243,29 @@ xs_html *html_top_controls(snac *snac) else metadata = xs_str_new(NULL); - xs *user_setup_action = xs_fmt("%s/admin/user-setup", snac->actor); + /* ui language */ + xs_html *lang_select = xs_html_tag("select", + xs_html_attr("name", "web_ui_lang")); + + const char *u_lang = xs_dict_get_def(user->config, "lang", "en"); + const char *lang; + const xs_dict *langs; + + xs_dict_foreach(srv_langs, lang, langs) { + if (strcmp(u_lang, lang) == 0) + xs_html_add(lang_select, + xs_html_tag("option", + xs_html_text(lang), + xs_html_attr("value", lang), + xs_html_attr("selected", "selected"))); + else + xs_html_add(lang_select, + xs_html_tag("option", + xs_html_text(lang), + xs_html_attr("value", lang))); + } + + xs *user_setup_action = xs_fmt("%s/admin/user-setup", user->actor); xs_html_add(top_controls, xs_html_tag("details", @@ -1251,7 +1284,7 @@ xs_html *html_top_controls(snac *snac) xs_html_sctag("input", xs_html_attr("type", "text"), xs_html_attr("name", "name"), - xs_html_attr("value", xs_dict_get(snac->config, "name")), + xs_html_attr("value", xs_dict_get(user->config, "name")), xs_html_attr("placeholder", L("Your name")))), xs_html_tag("p", xs_html_text(L("Avatar: ")), @@ -1281,7 +1314,7 @@ xs_html *html_top_controls(snac *snac) xs_html_attr("cols", "40"), xs_html_attr("rows", "4"), xs_html_attr("placeholder", L("Write about yourself here...")), - xs_html_text(xs_dict_get(snac->config, "bio")))), + xs_html_text(xs_dict_get(user->config, "bio")))), xs_html_sctag("input", xs_html_attr("type", "checkbox"), xs_html_attr("name", "cw"), @@ -1423,6 +1456,11 @@ xs_html *html_top_controls(snac *snac) xs_html_text(metadata))), xs_html_tag("p", + xs_html_text(L("Web interface language:")), + xs_html_sctag("br", NULL), + lang_select), + + xs_html_tag("p", xs_html_text(L("New password:")), xs_html_sctag("br", NULL), xs_html_sctag("input", @@ -1444,8 +1482,8 @@ xs_html *html_top_controls(snac *snac) xs_html_tag("p", NULL))))); - xs *followed_hashtags_action = xs_fmt("%s/admin/followed-hashtags", snac->actor); - xs *followed_hashtags = xs_join(xs_dict_get_def(snac->config, + xs *followed_hashtags_action = xs_fmt("%s/admin/followed-hashtags", user->actor); + xs *followed_hashtags = xs_join(xs_dict_get_def(user->config, "followed_hashtags", xs_stock(XSTYPE_LIST)), "\n"); xs_html_add(top_controls, @@ -1480,7 +1518,7 @@ xs_html *html_top_controls(snac *snac) } -static xs_html *html_button(char *clss, char *label, char *hint) +static xs_html *html_button(const char *clss, const char *label, const char *hint) { xs *c = xs_fmt("snac-btn-%s", clss); @@ -1496,7 +1534,7 @@ static xs_html *html_button(char *clss, char *label, char *hint) } -xs_str *build_mentions(snac *snac, const xs_dict *msg) +xs_str *build_mentions(snac *user, const xs_dict *msg) /* returns a string with the mentions in msg */ { xs_str *s = xs_str_new(NULL); @@ -1510,7 +1548,7 @@ xs_str *build_mentions(snac *snac, const xs_dict *msg) const char *name = xs_dict_get(v, "name"); if (type && strcmp(type, "Mention") == 0 && - href && strcmp(href, snac->actor) != 0 && name) { + href && strcmp(href, user->actor) != 0 && name) { xs *s1 = NULL; if (name[0] != '@') { @@ -1551,7 +1589,7 @@ xs_str *build_mentions(snac *snac, const xs_dict *msg) } -xs_html *html_entry_controls(snac *snac, const char *actor, +xs_html *html_entry_controls(snac *user, const char *actor, const xs_dict *msg, const char *md5) { const char *id = xs_dict_get(msg, "id"); @@ -1560,7 +1598,7 @@ xs_html *html_entry_controls(snac *snac, const char *actor, xs *likes = object_likes(id); xs *boosts = object_announces(id); - xs *action = xs_fmt("%s/admin/action", snac->actor); + xs *action = xs_fmt("%s/admin/action", user->actor); xs *redir = xs_fmt("%s_entry", md5); xs_html *form; @@ -1587,8 +1625,8 @@ xs_html *html_entry_controls(snac *snac, const char *actor, xs_html_attr("name", "redir"), xs_html_attr("value", redir)))); - if (!xs_startswith(id, snac->actor)) { - if (xs_list_in(likes, snac->md5) == -1) { + if (!xs_startswith(id, user->actor)) { + if (xs_list_in(likes, user->md5) == -1) { /* not already liked; add button */ xs_html_add(form, 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, } } else { - if (is_pinned(snac, id)) + if (is_pinned(user, id)) xs_html_add(form, html_button("unpin", L("Unpin"), L("Unpin this post from your timeline"))); else @@ -1609,7 +1647,7 @@ xs_html *html_entry_controls(snac *snac, const char *actor, } if (is_msg_public(msg)) { - if (xs_list_in(boosts, snac->md5) == -1) { + if (xs_list_in(boosts, user->md5) == -1) { /* not already boosted; add button */ xs_html_add(form, 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, } } - if (is_bookmarked(snac, id)) + if (is_bookmarked(user, id)) xs_html_add(form, html_button("unbookmark", L("Unbookmark"), L("Delete this post from your bookmarks"))); else xs_html_add(form, html_button("bookmark", L("Bookmark"), L("Add this post to your bookmarks"))); - if (strcmp(actor, snac->actor) != 0) { + if (strcmp(actor, user->actor) != 0) { /* controls for other actors than this one */ - if (following_check(snac, actor)) { + if (following_check(user, actor)) { xs_html_add(form, html_button("unfollow", L("Unfollow"), L("Stop following this user's activity"))); } @@ -1640,7 +1678,7 @@ xs_html *html_entry_controls(snac *snac, const char *actor, } if (!xs_is_null(group)) { - if (following_check(snac, group)) { + if (following_check(user, group)) { xs_html_add(form, html_button("unfollow", L("Unfollow Group"), L("Stop following this group or channel"))); @@ -1666,7 +1704,7 @@ xs_html *html_entry_controls(snac *snac, const char *actor, const char *prev_src = xs_dict_get(msg, "sourceContent"); - if (!xs_is_null(prev_src) && strcmp(actor, snac->actor) == 0) { /** edit **/ + if (!xs_is_null(prev_src) && strcmp(actor, user->actor) == 0) { /** edit **/ /* post can be edited */ xs *div_id = xs_fmt("%s_edit", md5); xs *form_id = xs_fmt("%s_edit_form", md5); @@ -1693,26 +1731,26 @@ xs_html *html_entry_controls(snac *snac, const char *actor, xs_html_add(controls, xs_html_tag("div", xs_html_tag("p", NULL), - html_note(snac, L("Edit..."), + html_note(user, L("Edit..."), div_id, form_id, "", prev_src, id, NULL, xs_dict_get(msg, "sensitive"), xs_dict_get(msg, "summary"), xs_stock(is_msg_public(msg) ? XSTYPE_FALSE : XSTYPE_TRUE), redir, - NULL, 0, att_files, att_alt_texts, is_draft(snac, id))), + NULL, 0, att_files, att_alt_texts, is_draft(user, id))), xs_html_tag("p", NULL)); } { /** reply **/ /* the post textarea */ - xs *ct = build_mentions(snac, msg); + xs *ct = build_mentions(user, msg); xs *div_id = xs_fmt("%s_reply", md5); xs *form_id = xs_fmt("%s_reply_form", md5); xs *redir = xs_fmt("%s_entry", md5); xs_html_add(controls, xs_html_tag("div", xs_html_tag("p", NULL), - html_note(snac, L("Reply..."), + html_note(user, L("Reply..."), div_id, form_id, "", ct, NULL, NULL, @@ -1839,7 +1877,7 @@ xs_html *html_entry(snac *user, xs_dict *msg, int read_only, xs_html_raw(" &#128204; "))); } - if (user && is_bookmarked(user, id)) { + if (user && !read_only && is_bookmarked(user, id)) { /* add a bookmark emoji */ xs_html_add(score, xs_html_tag("span", @@ -2242,6 +2280,11 @@ xs_html *html_entry(snac *user, xs_dict *msg, int read_only, if (content && xs_str_in(content, o_href) != -1) continue; + /* drop silently any attachment that may include JavaScript */ + if (strcmp(type, "image/svg+xml") == 0 || + strcmp(type, "text/html") == 0) + continue; + /* do this attachment include an icon? */ const xs_dict *icon = xs_dict_get(a, "icon"); if (xs_type(icon) == XSTYPE_DICT) { @@ -2571,7 +2614,7 @@ xs_html *html_entry(snac *user, xs_dict *msg, int read_only, } -xs_html *html_footer(void) +xs_html *html_footer(const snac *user) { return xs_html_tag("div", xs_html_attr("class", "snac-footer"), @@ -2879,13 +2922,13 @@ xs_str *html_timeline(snac *user, const xs_list *list, int read_only, } xs_html_add(body, - html_footer()); + html_footer(user)); return xs_html_render_s(html, "<!DOCTYPE html>\n"); } -xs_html *html_people_list(snac *snac, xs_list *list, char *header, char *t, const char *proxy) +xs_html *html_people_list(snac *user, xs_list *list, const char *header, const char *t, const char *proxy) { xs_html *snac_posts; 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 xs_html_attr("name", md5)), xs_html_tag("div", xs_html_attr("class", "snac-post-header"), - html_actor_icon(snac, actor, xs_dict_get(actor, "published"), + html_actor_icon(user, actor, xs_dict_get(actor, "published"), NULL, NULL, 0, 1, proxy, NULL, NULL))); /* content (user bio) */ @@ -2934,7 +2977,7 @@ xs_html *html_people_list(snac *snac, xs_list *list, char *header, char *t, cons } /* buttons */ - xs *btn_form_action = xs_fmt("%s/admin/action", snac->actor); + xs *btn_form_action = xs_fmt("%s/admin/action", user->actor); xs_html *snac_controls = xs_html_tag("div", 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 xs_html_add(snac_controls, form); - if (following_check(snac, actor_id)) { + if (following_check(user, actor_id)) { xs_html_add(form, html_button("unfollow", L("Unfollow"), L("Stop following this user's activity"))); - if (is_limited(snac, actor_id)) + if (is_limited(user, actor_id)) xs_html_add(form, html_button("unlimit", L("Unlimit"), 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 html_button("follow", L("Follow"), L("Start following this user's activity"))); - if (follower_check(snac, actor_id)) + if (follower_check(user, actor_id)) xs_html_add(form, html_button("delete", L("Delete"), L("Delete this user"))); } - if (pending_check(snac, actor_id)) { + if (pending_check(user, actor_id)) { xs_html_add(form, html_button("approve", L("Approve"), L("Approve this follow request"))); @@ -2987,7 +3030,7 @@ xs_html *html_people_list(snac *snac, xs_list *list, char *header, char *t, cons html_button("discard", L("Discard"), L("Discard this follow request"))); } - if (is_muted(snac, actor_id)) + if (is_muted(user, actor_id)) xs_html_add(form, html_button("unmute", L("Unmute"), 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 xs_html_add(snac_controls, xs_html_tag("p", NULL), - html_note(snac, L("Direct Message..."), + html_note(user, L("Direct Message..."), dm_div_id, dm_form_id, "", "", NULL, actor_id, @@ -3048,7 +3091,7 @@ xs_str *html_people(snac *user) html_user_head(user, NULL, NULL), xs_html_add(html_user_body(user, 0), lists, - html_footer())); + html_footer(user))); return xs_html_render_s(html, "<!DOCTYPE html>\n"); } @@ -3298,7 +3341,7 @@ xs_str *html_notifications(snac *user, int skip, int show) xs_set_free(&rep); xs_html_add(body, - html_footer()); + html_footer(user)); /* set the check time to now */ xs *dummy = notify_check_time(user, 1); @@ -3310,12 +3353,24 @@ xs_str *html_notifications(snac *user, int skip, int show) } +void set_user_lang(snac *user) +/* sets the language dict according to user configuration */ +{ + user->lang = NULL; + const char *lang = xs_dict_get(user->config, "lang"); + + if (xs_is_string(lang)) + user->lang = xs_dict_get(srv_langs, lang); +} + + int html_get_handler(const xs_dict *req, const char *q_path, char **body, int *b_size, char **ctype, xs_str **etag, xs_str **last_modified) { const char *accept = xs_dict_get(req, "accept"); int status = HTTP_STATUS_NOT_FOUND; + const snac *user = NULL; snac snac; xs *uid = NULL; const char *p_path; @@ -3382,6 +3437,9 @@ int html_get_handler(const xs_dict *req, const char *q_path, return HTTP_STATUS_NOT_FOUND; } + user = &snac; /* for L() */ + set_user_lang(&snac); + if (xs_is_true(xs_dict_get(srv_config, "proxy_media"))) proxy = 1; @@ -3550,7 +3608,7 @@ int html_get_handler(const xs_dict *req, const char *q_path, html_user_head(&snac, NULL, NULL), xs_html_add(html_user_body(&snac, 0), page, - html_footer())); + html_footer(user))); *body = xs_html_render_s(html, "<!DOCTYPE html>\n"); *b_size = strlen(*body); @@ -4005,6 +4063,7 @@ int html_post_handler(const xs_dict *req, const char *q_path, (void)ctype; int status = 0; + const snac *user = NULL; snac snac; const char *uid; const char *p_path; @@ -4028,6 +4087,9 @@ int html_post_handler(const xs_dict *req, const char *q_path, return HTTP_STATUS_UNAUTHORIZED; } + user = &snac; /* for L() */ + set_user_lang(&snac); + p_vars = xs_dict_get(req, "p_vars"); 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, snac.config = xs_dict_set(snac.config, "show_contact_metrics", xs_stock(XSTYPE_TRUE)); else snac.config = xs_dict_set(snac.config, "show_contact_metrics", xs_stock(XSTYPE_FALSE)); + if ((v = xs_dict_get(p_vars, "web_ui_lang")) != NULL) + snac.config = xs_dict_set(snac.config, "lang", v); snac.config = xs_dict_set(snac.config, "latitude", xs_dict_get_def(p_vars, "latitude", "")); snac.config = xs_dict_set(snac.config, "longitude", xs_dict_get_def(p_vars, "longitude", "")); diff --git a/httpd.c b/httpd.c @@ -211,6 +211,8 @@ int server_get_handler(xs_dict *req, const char *q_path, { int status = 0; + const snac *user = NULL; + /* is it the server root? */ if (*q_path == '\0' || strcmp(q_path, "/") == 0) { const xs_dict *q_vars = xs_dict_get(req, "q_vars"); @@ -553,6 +555,9 @@ void httpd_connection(FILE *f) headers = xs_dict_append(headers, "access-control-allow-origin", "*"); headers = xs_dict_append(headers, "access-control-allow-headers", "*"); + /* disable any form of fucking JavaScript */ + headers = xs_dict_append(headers, "Content-Security-Policy", "script-src ;"); + if (p_state->use_fcgi) xs_fcgi_response(f, status, headers, body, b_size, fcgi_id); else diff --git a/po/en.po b/po/en.po @@ -0,0 +1,687 @@ +# snac message translation file +# +#, fuzzy +msgid "" +msgstr "" +"Project-Id-Version: snac\n" +"Last-Translator: grunfink\n" +"Language: en\n" +"Content-Type: text/plain; charset=UTF-8\n" + +#: html.c:367 +msgid "Sensitive content: " +msgstr "" + +#: html.c:375 +msgid "Sensitive content description" +msgstr "" + +#: html.c:388 +msgid "Only for mentioned people: " +msgstr "" + +#: html.c:411 +msgid "Reply to (URL): " +msgstr "" + +#: html.c:420 +msgid "Don't send, but store as a draft" +msgstr "" + +#: html.c:421 +msgid "Draft:" +msgstr "" + +#: html.c:441 +msgid "Attachments..." +msgstr "" + +#: html.c:464 +msgid "File:" +msgstr "" + +#: html.c:468 +msgid "Clear this field to delete the attachment" +msgstr "" + +#: html.c:477 html.c:502 +msgid "Attachment description" +msgstr "" + +#: html.c:513 +msgid "Poll..." +msgstr "" + +#: html.c:515 +msgid "Poll options (one per line, up to 8):" +msgstr "" + +#: html.c:527 +msgid "One choice" +msgstr "" + +#: html.c:530 +msgid "Multiple choices" +msgstr "" + +#: html.c:536 +msgid "End in 5 minutes" +msgstr "" + +#: html.c:540 +msgid "End in 1 hour" +msgstr "" + +#: html.c:543 +msgid "End in 1 day" +msgstr "" + +#: html.c:551 +msgid "Post" +msgstr "" + +#: html.c:648 html.c:655 +msgid "Site description" +msgstr "" + +#: html.c:666 +msgid "Admin email" +msgstr "" + +#: html.c:679 +msgid "Admin account" +msgstr "" + +#: html.c:747 html.c:1083 +#, c-format +msgid "%d following, %d followers" +msgstr "" + +#: html.c:837 +msgid "RSS" +msgstr "" + +#: html.c:842 html.c:870 +msgid "private" +msgstr "" + +#: html.c:866 +msgid "public" +msgstr "" + +#: html.c:874 +msgid "notifications" +msgstr "" + +#: html.c:879 +msgid "people" +msgstr "" + +#: html.c:883 +msgid "instance" +msgstr "" + +#: html.c:892 +msgid "" +"Search posts by URL or content (regular expression), @user@host accounts, or " +"#tag" +msgstr "" + +#: html.c:893 +msgid "Content search" +msgstr "" + +#: html.c:1015 +msgid "verified link" +msgstr "" + +#: html.c:1072 html.c:2420 html.c:2433 html.c:2442 +msgid "Location: " +msgstr "" + +#: html.c:1108 +msgid "New Post..." +msgstr "" + +#: html.c:1110 +msgid "What's on your mind?" +msgstr "" + +#: html.c:1119 +msgid "Operations..." +msgstr "" + +#: html.c:1134 html.c:1677 html.c:3016 html.c:4333 +msgid "Follow" +msgstr "" + +#: html.c:1136 +msgid "(by URL or user@host)" +msgstr "" + +#: html.c:1151 html.c:1653 html.c:4285 +msgid "Boost" +msgstr "" + +#: html.c:1153 html.c:1170 +msgid "(by URL)" +msgstr "" + +#: html.c:1168 html.c:1632 html.c:4276 +msgid "Like" +msgstr "" + +#: html.c:1273 +msgid "User Settings..." +msgstr "" + +#: html.c:1282 +msgid "Display name:" +msgstr "" + +#: html.c:1288 +msgid "Your name" +msgstr "" + +#: html.c:1290 +msgid "Avatar: " +msgstr "" + +#: html.c:1298 +msgid "Delete current avatar" +msgstr "" + +#: html.c:1300 +msgid "Header image (banner): " +msgstr "" + +#: html.c:1308 +msgid "Delete current header image" +msgstr "" + +#: html.c:1310 +msgid "Bio:" +msgstr "" + +#: html.c:1316 +msgid "Write about yourself here..." +msgstr "" + +#: html.c:1325 +msgid "Always show sensitive content" +msgstr "" + +#: html.c:1327 +msgid "Email address for notifications:" +msgstr "" + +#: html.c:1335 +msgid "Telegram notifications (bot key and chat id):" +msgstr "" + +#: html.c:1349 +msgid "ntfy notifications (ntfy server and token):" +msgstr "" + +#: html.c:1363 +msgid "Maximum days to keep posts (0: server settings):" +msgstr "" + +#: html.c:1377 +msgid "Drop direct messages from people you don't follow" +msgstr "" + +#: html.c:1386 +msgid "This account is a bot" +msgstr "" + +#: html.c:1395 +msgid "Auto-boost all mentions to this account" +msgstr "" + +#: html.c:1404 +msgid "This account is private (posts are not shown through the web)" +msgstr "" + +#: html.c:1414 +msgid "Collapse top threads by default" +msgstr "" + +#: html.c:1423 +msgid "Follow requests must be approved" +msgstr "" + +#: html.c:1432 +msgid "Publish follower and following metrics" +msgstr "" + +#: html.c:1434 +msgid "Current location:" +msgstr "" + +#: html.c:1448 +msgid "Profile metadata (key=value pairs in each line):" +msgstr "" + +#: html.c:1459 +msgid "Web interface language:" +msgstr "" + +#: html.c:1464 +msgid "New password:" +msgstr "" + +#: html.c:1471 +msgid "Repeat new password:" +msgstr "" + +#: html.c:1481 +msgid "Update user info" +msgstr "" + +#: html.c:1492 +msgid "Followed hashtags..." +msgstr "" + +#: html.c:1494 +msgid "One hashtag per line" +msgstr "" + +#: html.c:1515 +msgid "Update hashtags" +msgstr "" + +#: html.c:1632 +msgid "Say you like this post" +msgstr "" + +#: html.c:1637 html.c:4294 +msgid "Unlike" +msgstr "" + +#: html.c:1637 +msgid "Nah don't like it that much" +msgstr "" + +#: html.c:1643 html.c:4426 +msgid "Unpin" +msgstr "" + +#: html.c:1643 +msgid "Unpin this post from your timeline" +msgstr "" + +#: html.c:1646 html.c:4421 +msgid "Pin" +msgstr "" + +#: html.c:1646 +msgid "Pin this post to the top of your timeline" +msgstr "" + +#: html.c:1653 +msgid "Announce this post to your followers" +msgstr "" + +#: html.c:1658 html.c:4302 +msgid "Unboost" +msgstr "" + +#: html.c:1658 +msgid "I regret I boosted this" +msgstr "" + +#: html.c:1664 html.c:4436 +msgid "Unbookmark" +msgstr "" + +#: html.c:1664 +msgid "Delete this post from your bookmarks" +msgstr "" + +#: html.c:1667 html.c:4431 +msgid "Bookmark" +msgstr "" + +#: html.c:1667 +msgid "Add this post to your bookmarks" +msgstr "" + +#: html.c:1673 html.c:3002 html.c:3190 html.c:4346 +msgid "Unfollow" +msgstr "" + +#: html.c:1673 html.c:3003 +msgid "Stop following this user's activity" +msgstr "" + +#: html.c:1677 html.c:3017 +msgid "Start following this user's activity" +msgstr "" + +#: html.c:1683 html.c:4376 +msgid "Unfollow Group" +msgstr "" + +#: html.c:1684 +msgid "Stop following this group or channel" +msgstr "" + +#: html.c:1688 html.c:4363 +msgid "Follow Group" +msgstr "" + +#: html.c:1689 +msgid "Start following this group or channel" +msgstr "" + +#: html.c:1694 html.c:3039 html.c:4310 +msgid "MUTE" +msgstr "" + +#: html.c:1695 +msgid "Block any activity from this user forever" +msgstr "" + +#: html.c:1700 html.c:3021 html.c:4393 +msgid "Delete" +msgstr "" + +#: html.c:1700 +msgid "Delete this post" +msgstr "" + +#: html.c:1703 html.c:4318 +msgid "Hide" +msgstr "" + +#: html.c:1703 +msgid "Hide this post and its children" +msgstr "" + +#: html.c:1734 +msgid "Edit..." +msgstr "" + +#: html.c:1753 +msgid "Reply..." +msgstr "" + +#: html.c:1804 +msgid "Truncated (too deep)" +msgstr "" + +#: html.c:1813 +msgid "follows you" +msgstr "" + +#: html.c:1876 +msgid "Pinned" +msgstr "" + +#: html.c:1884 +msgid "Bookmarked" +msgstr "" + +#: html.c:1892 +msgid "Poll" +msgstr "" + +#: html.c:1899 +msgid "Voted" +msgstr "" + +#: html.c:1908 +msgid "Event" +msgstr "" + +#: html.c:1940 html.c:1969 +msgid "boosted" +msgstr "" + +#: html.c:1985 +msgid "in reply to" +msgstr "" + +#: html.c:2036 +msgid " [SENSITIVE CONTENT]" +msgstr "" + +#: html.c:2213 +msgid "Vote" +msgstr "" + +#: html.c:2223 +msgid "Closed" +msgstr "" + +#: html.c:2248 +msgid "Closes in" +msgstr "" + +#: html.c:2327 +msgid "Video" +msgstr "" + +#: html.c:2342 +msgid "Audio" +msgstr "" + +#: html.c:2364 +msgid "Attachment" +msgstr "" + +#: html.c:2378 +msgid "Alt..." +msgstr "" + +#: html.c:2391 +msgid "Source channel or community" +msgstr "" + +#: html.c:2485 +msgid "Time: " +msgstr "" + +#: html.c:2560 +msgid "Older..." +msgstr "" + +#: html.c:2623 +msgid "about this site" +msgstr "" + +#: html.c:2625 +msgid "powered by " +msgstr "" + +#: html.c:2690 +msgid "Dismiss" +msgstr "" + +#: html.c:2707 +#, c-format +msgid "Timeline for list '%s'" +msgstr "" + +#: html.c:2726 html.c:3767 +msgid "Pinned posts" +msgstr "" + +#: html.c:2738 html.c:3782 +msgid "Bookmarked posts" +msgstr "" + +#: html.c:2750 html.c:3797 +msgid "Post drafts" +msgstr "" + +#: html.c:2809 +msgid "No more unseen posts" +msgstr "" + +#: html.c:2813 html.c:2913 +msgid "Back to top" +msgstr "" + +#: html.c:2866 +msgid "History" +msgstr "" + +#: html.c:2918 html.c:3338 +msgid "More..." +msgstr "" + +#: html.c:3007 html.c:4329 +msgid "Unlimit" +msgstr "" + +#: html.c:3008 +msgid "Allow announces (boosts) from this user" +msgstr "" + +#: html.c:3011 html.c:4325 +msgid "Limit" +msgstr "" + +#: html.c:3012 +msgid "Block announces (boosts) from this user" +msgstr "" + +#: html.c:3021 +msgid "Delete this user" +msgstr "" + +#: html.c:3026 html.c:4441 +msgid "Approve" +msgstr "" + +#: html.c:3027 +msgid "Approve this follow request" +msgstr "" + +#: html.c:3030 html.c:4465 +msgid "Discard" +msgstr "" + +#: html.c:3030 +msgid "Discard this follow request" +msgstr "" + +#: html.c:3035 html.c:4314 +msgid "Unmute" +msgstr "" + +#: html.c:3036 +msgid "Stop blocking activities from this user" +msgstr "" + +#: html.c:3040 +msgid "Block any activity from this user" +msgstr "" + +#: html.c:3048 +msgid "Direct Message..." +msgstr "" + +#: html.c:3083 +msgid "Pending follow confirmations" +msgstr "" + +#: html.c:3087 +msgid "People you follow" +msgstr "" + +#: html.c:3088 +msgid "People that follow you" +msgstr "" + +#: html.c:3127 +msgid "Clear all" +msgstr "" + +#: html.c:3184 +msgid "Mention" +msgstr "" + +#: html.c:3187 +msgid "Finished poll" +msgstr "" + +#: html.c:3202 +msgid "Follow Request" +msgstr "" + +#: html.c:3285 +msgid "Context" +msgstr "" + +#: html.c:3296 +msgid "New" +msgstr "" + +#: html.c:3311 +msgid "Already seen" +msgstr "" + +#: html.c:3326 +msgid "None" +msgstr "" + +#: html.c:3592 +#, c-format +msgid "Search results for account %s" +msgstr "" + +#: html.c:3599 +#, c-format +msgid "Account %s not found" +msgstr "" + +#: html.c:3630 +#, c-format +msgid "Search results for tag %s" +msgstr "" + +#: html.c:3630 +#, c-format +msgid "Nothing found for tag %s" +msgstr "" + +#: html.c:3646 +#, c-format +msgid "Search results for '%s' (may be more)" +msgstr "" + +#: html.c:3649 +#, c-format +msgid "Search results for '%s'" +msgstr "" + +#: html.c:3652 +#, c-format +msgid "No more matches for '%s'" +msgstr "" + +#: html.c:3654 +#, c-format +msgid "Nothing found for '%s'" +msgstr "" + +#: html.c:3752 +msgid "Showing instance timeline" +msgstr "" + +#: html.c:3820 +#, c-format +msgid "Showing timeline for list '%s'" +msgstr "" + +#: httpd.c:250 +#, c-format +msgid "Search results for tag #%s" +msgstr "" + +#: httpd.c:259 +msgid "Recent posts by users in this instance" +msgstr "" diff --git a/snac.c b/snac.c @@ -24,6 +24,7 @@ #include "xs_match.h" #include "xs_fcgi.h" #include "xs_html.h" +#include "xs_po.h" #include "snac.h" @@ -34,6 +35,7 @@ xs_str *srv_basedir = NULL; xs_dict *srv_config = NULL; xs_str *srv_baseurl = NULL; xs_str *srv_proxy_token_seed = NULL; +xs_dict *srv_langs = NULL; int dbglevel = 0; @@ -179,6 +181,7 @@ const char *http_status_text(int status) /* translate status codes to canonical status texts */ { switch (status) { + case 399: return "Timeout"; #define HTTP_STATUS(code, name, text) case HTTP_STATUS_ ## name: return #text; #include "http_codes.h" #undef HTTP_STATUS diff --git a/snac.h b/snac.h @@ -1,7 +1,7 @@ /* snac - A simple, minimalistic ActivityPub instance */ /* copyright (c) 2022 - 2025 grunfink et al. / MIT license */ -#define VERSION "2.72" +#define VERSION "2.73-dev" #define USER_AGENT "snac/" VERSION @@ -16,6 +16,10 @@ #define MAX_THREADS 256 #endif +#ifndef MAX_JSON_DEPTH +#define MAX_JSON_DEPTH 8 +#endif + #ifndef MAX_CONVERSATION_LEVELS #define MAX_CONVERSATION_LEVELS 48 #endif @@ -29,10 +33,11 @@ extern xs_str *srv_basedir; extern xs_dict *srv_config; extern xs_str *srv_baseurl; extern xs_str *srv_proxy_token_seed; +extern xs_dict *srv_langs; extern int dbglevel; -#define L(s) (s) +#define L(s) lang_str((s), user) #define POSTLIKE_OBJECT_TYPE "Note|Question|Page|Article|Video|Audio|Event" @@ -55,6 +60,7 @@ typedef struct { xs_dict *links; /* validated links */ xs_str *actor; /* actor url */ xs_str *md5; /* actor url md5 */ + const xs_dict *lang;/* string translation dict */ } snac; typedef struct { @@ -441,3 +447,5 @@ xs_str *make_url(const char *href, const char *proxy, int by_token); int badlogin_check(const char *user, const char *addr); void badlogin_inc(const char *user, const char *addr); + +const char *lang_str(const char *str, const snac *user); diff --git 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, const char *from, const char *to, const xs_str *body, int use_ssl); +const char *xs_curl_strerr(int errnum); + #ifdef XS_IMPLEMENTATION #include <curl/curl.h> @@ -240,6 +242,15 @@ int xs_smtp_request(const char *url, const char *user, const char *pass, return (int)res; } + +const char *xs_curl_strerr(int errnum) +{ + CURLcode cc = errnum < 0 ? -errnum : errnum; + + return curl_easy_strerror(cc); +} + + #endif /* XS_IMPLEMENTATION */ #endif /* _XS_CURL_H */ diff --git a/xs_json.h b/xs_json.h @@ -4,17 +4,23 @@ #define _XS_JSON_H +#ifndef MAX_JSON_DEPTH +#define MAX_JSON_DEPTH 32 +#endif + int xs_json_dump(const xs_val *data, int indent, FILE *f); xs_str *xs_json_dumps(const xs_val *data, int indent); -xs_val *xs_json_load(FILE *f); -xs_val *xs_json_loads(const xs_str *json); +xs_val *xs_json_load_full(FILE *f, int maxdepth); +xs_val *xs_json_loads_full(const xs_str *json, int maxdepth); +#define xs_json_load(f) xs_json_load_full(f, MAX_JSON_DEPTH) +#define xs_json_loads(s) xs_json_loads_full(s, MAX_JSON_DEPTH) xstype xs_json_load_type(FILE *f); int xs_json_load_array_iter(FILE *f, xs_val **value, xstype *pt, int *c); int xs_json_load_object_iter(FILE *f, xs_str **key, xs_val **value, xstype *pt, int *c); -xs_list *xs_json_load_array(FILE *f); -xs_dict *xs_json_load_object(FILE *f); +xs_list *xs_json_load_array(FILE *f, int maxdepth); +xs_dict *xs_json_load_object(FILE *f, int maxdepth); #ifdef XS_IMPLEMENTATION @@ -371,7 +377,7 @@ int xs_json_load_array_iter(FILE *f, xs_val **value, xstype *pt, int *c) } -xs_list *xs_json_load_array(FILE *f) +xs_list *xs_json_load_array(FILE *f, int maxdepth) /* loads a full JSON array (after the initial OBRACK) */ { xstype t; @@ -387,12 +393,12 @@ xs_list *xs_json_load_array(FILE *f) if (r == 1) { /* partial load? */ - if (v == NULL) { + if (v == NULL && maxdepth != 0) { if (t == XSTYPE_LIST) - v = xs_json_load_array(f); + v = xs_json_load_array(f, maxdepth - 1); else if (t == XSTYPE_DICT) - v = xs_json_load_object(f); + v = xs_json_load_object(f, maxdepth - 1); } /* still null? fail */ @@ -459,7 +465,7 @@ int xs_json_load_object_iter(FILE *f, xs_str **key, xs_val **value, xstype *pt, } -xs_dict *xs_json_load_object(FILE *f) +xs_dict *xs_json_load_object(FILE *f, int maxdepth) /* loads a full JSON object (after the initial OCURLY) */ { xstype t; @@ -476,12 +482,12 @@ xs_dict *xs_json_load_object(FILE *f) if (r == 1) { /* partial load? */ - if (v == NULL) { + if (v == NULL && maxdepth != 0) { if (t == XSTYPE_LIST) - v = xs_json_load_array(f); + v = xs_json_load_array(f, maxdepth - 1); else if (t == XSTYPE_DICT) - v = xs_json_load_object(f); + v = xs_json_load_object(f, maxdepth - 1); } /* still null? fail */ @@ -500,14 +506,14 @@ xs_dict *xs_json_load_object(FILE *f) } -xs_val *xs_json_loads(const xs_str *json) +xs_val *xs_json_loads_full(const xs_str *json, int maxdepth) /* loads a string in JSON format and converts to a multiple data */ { FILE *f; xs_val *v = NULL; if ((f = fmemopen((char *)json, strlen(json), "r")) != NULL) { - v = xs_json_load(f); + v = xs_json_load_full(f, maxdepth); fclose(f); } @@ -533,17 +539,17 @@ xstype xs_json_load_type(FILE *f) } -xs_val *xs_json_load(FILE *f) +xs_val *xs_json_load_full(FILE *f, int maxdepth) /* loads a JSON file */ { xs_val *v = NULL; xstype t = xs_json_load_type(f); if (t == XSTYPE_LIST) - v = xs_json_load_array(f); + v = xs_json_load_array(f, maxdepth); else if (t == XSTYPE_DICT) - v = xs_json_load_object(f); + v = xs_json_load_object(f, maxdepth); return v; } diff --git a/xs_po.h b/xs_po.h @@ -0,0 +1,86 @@ +/* copyright (c) 2025 grunfink et al. / MIT license */ + +#ifndef _XS_PO_H + +#define _XS_PO_H + +xs_dict *xs_po_to_dict(const char *fn); + +#ifdef XS_IMPLEMENTATION + +xs_dict *xs_po_to_dict(const char *fn) +/* converts a PO file to a dict */ +{ + xs_dict *d = NULL; + FILE *f; + + if ((f = fopen(fn, "r")) != NULL) { + d = xs_dict_new(); + + xs *k = NULL; + xs *v = NULL; + enum { IN_NONE, IN_K, IN_V } mode = IN_NONE; + + while (!feof(f)) { + xs *l = xs_strip_i(xs_readline(f)); + + /* discard empty lines and comments */ + if (*l == '\0' || *l == '#') + continue; + + if (xs_startswith(l, "msgid ")) { + if (mode == IN_V) { + /* flush */ + if (xs_is_string(k) && xs_is_string(v) && *v) + d = xs_dict_set(d, k, v); + + k = xs_free(k); + v = xs_free(v); + } + + l = xs_replace_i(l, "msgid ", ""); + mode = IN_K; + + k = xs_str_new(NULL); + } + else + if (xs_startswith(l, "msgstr ")) { + if (mode != IN_K) + break; + + l = xs_replace_i(l, "msgstr ", ""); + mode = IN_V; + + v = xs_str_new(NULL); + } + + l = xs_replace_i(l, "\\n", "\n"); + l = xs_strip_chars_i(l, "\""); + + switch (mode) { + case IN_K: + k = xs_str_cat(k, l); + break; + + case IN_V: + v = xs_str_cat(v, l); + break; + + case IN_NONE: + break; + } + } + + /* final flush */ + if (xs_is_string(k) && xs_is_string(v) && *v) + d = xs_dict_set(d, k, v); + + fclose(f); + } + + return d; +} + +#endif /* XS_IMPLEMENTATION */ + +#endif /* XS_PO_H */ diff --git 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) while (count > 0 && !regexec(&re, (p = str + offset), 1, &rm, offset > 0 ? REG_NOTBOL : 0)) { /* add first the leading part of the string */ xs *s1 = xs_str_new_sz(p, rm.rm_so); - list = xs_list_append(list, s1); + + list = xs_list_append(list, xs_is_string(s1) ? s1 : ""); /* add now the matched text as the separator */ xs *s2 = xs_str_new_sz(p + rm.rm_so, rm.rm_eo - rm.rm_so); - list = xs_list_append(list, s2); + + list = xs_list_append(list, xs_is_string(s2) ? s2 : ""); /* move forward */ offset += rm.rm_eo; diff --git 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 */