diff options
| author | ltning <ltning@noreply.codeberg.org> | 2025-01-27 18:07:00 +0000 |
|---|---|---|
| committer | ltning <ltning@noreply.codeberg.org> | 2025-01-27 18:07:00 +0000 |
| commit | f6044d3aa0241a832b0ad1d2c394c0a1b814dbe3 (patch) | |
| tree | 72334e7a24b997957d201490681552b6b1ad2e2f /html.c | |
| parent | 4528029dabb7d79cddefaa5677ae314e16ab51f4 (diff) | |
| parent | 5d267968cb0d2670f8a4bd7587e505f812dfa3bc (diff) | |
Merge branch 'master' into master
Diffstat (limited to 'html.c')
| -rw-r--r-- | html.c | 393 |
1 files changed, 374 insertions, 19 deletions
@@ -1,5 +1,5 @@ /* snac - A simple, minimalistic ActivityPub instance */ -/* copyright (c) 2022 - 2024 grunfink et al. / MIT license */ +/* copyright (c) 2022 - 2025 grunfink et al. / MIT license */ #include "xs.h" #include "xs_io.h" @@ -12,6 +12,7 @@ #include "xs_match.h" #include "xs_html.h" #include "xs_curl.h" +#include "xs_unicode.h" #include "snac.h" @@ -113,7 +114,8 @@ xs_str *actor_name(xs_dict *actor, const char *proxy) xs_html *html_actor_icon(snac *user, xs_dict *actor, const char *date, - const char *udate, const char *url, int priv, int in_people, const char *proxy) + const char *udate, const char *url, int priv, + int in_people, const char *proxy, const char *lang) { xs_html *actor_icon = xs_html_tag("p", NULL); @@ -219,6 +221,9 @@ xs_html *html_actor_icon(snac *user, xs_dict *actor, const char *date, date_title = xs_str_cat(date_title, " / ", udate); } + if (xs_is_string(lang)) + date_title = xs_str_cat(date_title, " (", lang, ")"); + xs_html_add(actor_icon, xs_html_text(" "), xs_html_tag("time", @@ -265,6 +270,7 @@ xs_html *html_msg_icon(snac *user, const char *actor_id, const xs_dict *msg, con const char *date = NULL; const char *udate = NULL; const char *url = NULL; + const char *lang = NULL; int priv = 0; const char *type = xs_dict_get(msg, "type"); @@ -276,7 +282,17 @@ xs_html *html_msg_icon(snac *user, const char *actor_id, const xs_dict *msg, con date = xs_dict_get(msg, "published"); udate = xs_dict_get(msg, "updated"); - actor_icon = html_actor_icon(user, actor, date, udate, url, priv, 0, proxy); + lang = xs_dict_get(msg, "contentMap"); + if (xs_is_dict(lang)) { + const char *v; + int c = 0; + + xs_dict_next(lang, &lang, &v, &c); + } + else + lang = NULL; + + actor_icon = html_actor_icon(user, actor, date, udate, url, priv, 0, proxy, lang); } return actor_icon; @@ -933,6 +949,7 @@ static xs_html *html_user_body(snac *user, int read_only) xs_html_raw("✔ "), xs_html_tag("a", xs_html_attr("rel", "me"), + xs_html_attr("target", "_blank"), xs_html_attr("href", v), xs_html_text(v))); } @@ -969,6 +986,23 @@ static xs_html *html_user_body(snac *user, int read_only) snac_metadata); } + const char *latitude = xs_dict_get_def(user->config, "latitude", ""); + 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"), + latitude, longitude); + + xs_html_add(top_user, + xs_html_tag("p", + xs_html_text(L("Location: ")), + xs_html_tag("a", + xs_html_attr("href", url), + xs_html_attr("target", "_blank"), + xs_html_text(label)))); + } + if (xs_is_true(xs_dict_get(user->config, "show_contact_metrics"))) { xs *fwers = follower_list(user); xs *fwing = following_list(user); @@ -1110,6 +1144,8 @@ xs_html *html_top_controls(snac *snac) 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", ""); xs *metadata = NULL; const xs_dict *md = xs_dict_get(snac->config, "metadata"); @@ -1300,6 +1336,20 @@ xs_html *html_top_controls(snac *snac) xs_html_attr("for", "show_contact_metrics"), xs_html_text(L("Publish follower and following metrics")))), xs_html_tag("p", + xs_html_text(L("Current location:")), + xs_html_sctag("br", NULL), + xs_html_sctag("input", + xs_html_attr("type", "text"), + xs_html_attr("name", "latitude"), + xs_html_attr("value", latitude), + xs_html_attr("placeholder", "latitude")), + xs_html_text(" "), + xs_html_sctag("input", + xs_html_attr("type", "text"), + xs_html_attr("name", "longitude"), + xs_html_attr("value", longitude), + xs_html_attr("placeholder", "longitude"))), + xs_html_tag("p", xs_html_text(L("Profile metadata (key=value pairs in each line):")), xs_html_sctag("br", NULL), xs_html_tag("textarea", @@ -1328,7 +1378,41 @@ xs_html *html_top_controls(snac *snac) xs_html_sctag("input", xs_html_attr("type", "submit"), xs_html_attr("class", "button"), - xs_html_attr("value", L("Update user info"))))))); + xs_html_attr("value", L("Update user info"))), + + 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, + "followed_hashtags", xs_stock(XSTYPE_LIST)), "\n"); + + xs_html_add(top_controls, + xs_html_tag("details", + xs_html_tag("summary", + xs_html_text(L("Followed hashtags..."))), + xs_html_tag("p", + xs_html_text(L("One hashtag per line"))), + xs_html_tag("div", + xs_html_attr("class", "snac-followed-hashtags"), + xs_html_tag("form", + xs_html_attr("autocomplete", "off"), + xs_html_attr("method", "post"), + xs_html_attr("action", followed_hashtags_action), + xs_html_attr("enctype", "multipart/form-data"), + + xs_html_tag("textarea", + xs_html_attr("name", "followed_hashtags"), + xs_html_attr("cols", "40"), + xs_html_attr("rows", "4"), + xs_html_attr("placeholder", "#cats\n#windowfriday\n#classicalmusic"), + xs_html_text(followed_hashtags)), + + xs_html_tag("br", NULL), + + xs_html_sctag("input", + xs_html_attr("type", "submit"), + xs_html_attr("class", "button"), + xs_html_attr("value", L("Update hashtags"))))))); return top_controls; } @@ -1781,13 +1865,15 @@ xs_html *html_entry(snac *user, xs_dict *msg, int read_only, } } } - else - if (strcmp(type, "Note") == 0) { - if (level == 0) { - /* is the parent not here? */ - const char *parent = get_in_reply_to(msg); - if (user && !xs_is_null(parent) && *parent && !timeline_here(user, parent)) { + if (user && strcmp(type, "Note") == 0) { + /* is the parent not here? */ + const char *parent = get_in_reply_to(msg); + + if (!xs_is_null(parent) && *parent) { + xs *md5 = xs_md5_hex(parent, strlen(parent)); + + if (!timeline_here(user, md5)) { xs_html_add(post_header, xs_html_tag("div", xs_html_attr("class", "snac-origin"), @@ -2199,6 +2285,135 @@ xs_html *html_entry(snac *user, xs_dict *msg, int read_only, au_tag); } + /* does it have a location? */ + const xs_dict *location = xs_dict_get(msg, "location"); + if (xs_type(location) == XSTYPE_DICT) { + const xs_number *latitude = xs_dict_get(location, "latitude"); + const xs_number *longitude = xs_dict_get(location, "longitude"); + const char *name = xs_dict_get(location, "name"); + const char *address = xs_dict_get(location, "address"); + xs *label_list = xs_list_new(); + + if (xs_type(name) == XSTYPE_STRING) + label_list = xs_list_append(label_list, name); + if (xs_type(address) == XSTYPE_STRING) + label_list = xs_list_append(label_list, address); + + if (xs_list_len(label_list)) { + const char *url = xs_dict_get(location, "url"); + xs *label = xs_join(label_list, ", "); + + if (xs_type(url) == XSTYPE_STRING) { + xs_html_add(snac_content_wrap, + xs_html_tag("p", + xs_html_text(L("Location: ")), + xs_html_tag("a", + xs_html_attr("href", url), + xs_html_attr("target", "_blank"), + xs_html_text(label)))); + } + else + if (!xs_is_null(latitude) && !xs_is_null(longitude)) { + xs *url = xs_fmt("https://openstreetmap.org/search/?query=%s,%s", + xs_number_str(latitude), xs_number_str(longitude)); + + xs_html_add(snac_content_wrap, + xs_html_tag("p", + xs_html_text(L("Location: ")), + xs_html_tag("a", + xs_html_attr("href", url), + xs_html_attr("target", "_blank"), + xs_html_text(label)))); + } + else + xs_html_add(snac_content_wrap, + xs_html_tag("p", + xs_html_text(L("Location: ")), + xs_html_text(label))); + } + } + + if (strcmp(type, "Event") == 0) { /** Event start and end times **/ + const char *s_time = xs_dict_get(msg, "startTime"); + + if (xs_is_string(s_time) && strlen(s_time) > 20) { + const char *e_time = xs_dict_get(msg, "endTime"); + const char *tz = xs_dict_get(msg, "timezone"); + + xs *s = xs_replace_i(xs_dup(s_time), "T", " "); + xs *e = NULL; + + if (xs_is_string(e_time) && strlen(e_time) > 20) + e = xs_replace_i(xs_dup(e_time), "T", " "); + + /* if the event has a timezone, crop the offsets */ + if (xs_is_string(tz)) { + s = xs_crop_i(s, 0, 19); + + if (e) + e = xs_crop_i(e, 0, 19); + } + else + tz = ""; + + /* if start and end share the same day, crop it from the end */ + if (e && memcmp(s, e, 11) == 0) + e = xs_crop_i(e, 11, 0); + + if (e) + s = xs_str_cat(s, " / ", e); + + if (*tz) + s = xs_str_cat(s, " (", tz, ")"); + + /* replace ugly decimals */ + s = xs_replace_i(s, ".000", ""); + + xs_html_add(snac_content_wrap, + xs_html_tag("p", + xs_html_text(L("Time: ")), + xs_html_text(s))); + } + } + + /* show all hashtags that has not been shown previously in the content */ + const xs_list *tags = xs_dict_get(msg, "tag"); + const char *o_content = xs_dict_get_def(msg, "content", ""); + + if (xs_is_string(o_content) && xs_is_list(tags) && xs_list_len(tags)) { + xs *content = xs_utf8_to_lower(o_content); + const xs_dict *tag; + + xs_html *add_hashtags = xs_html_tag("ul", + xs_html_attr("class", "snac-more-hashtags")); + + xs_list_foreach(tags, tag) { + const char *type = xs_dict_get(tag, "type"); + + if (xs_is_string(type) && strcmp(type, "Hashtag") == 0) { + const char *o_href = xs_dict_get(tag, "href"); + const char *name = xs_dict_get(tag, "name"); + + if (xs_is_string(o_href) && xs_is_string(name)) { + xs *href = xs_utf8_to_lower(o_href); + + if (xs_str_in(content, href) == -1 && xs_str_in(content, name) == -1) { + /* not in the content: add here */ + xs_html_add(add_hashtags, + xs_html_tag("li", + xs_html_tag("a", + xs_html_attr("href", href), + xs_html_text(name), + xs_html_text(" ")))); + } + } + } + } + + xs_html_add(snac_content_wrap, + add_hashtags); + } + /** controls **/ if (!read_only && user) { @@ -2583,7 +2798,7 @@ xs_html *html_people_list(snac *snac, xs_list *list, char *header, char *t, cons xs_html_tag("div", xs_html_attr("class", "snac-post-header"), html_actor_icon(snac, actor, xs_dict_get(actor, "published"), - NULL, NULL, 0, 1, proxy))); + NULL, NULL, 0, 1, proxy, NULL))); /* content (user bio) */ const char *c = xs_dict_get(actor, "summary"); @@ -2762,9 +2977,15 @@ xs_str *html_notifications(snac *user, int skip, int show) xs_html_attr("class", "snac-posts")); xs_html_add(body, posts); - xs_list *p = n_list; + xs_set rep; + xs_set_init(&rep); + + /* dict to store previous notification labels */ + xs *admiration_labels = xs_dict_new(); + const xs_str *v; - while (xs_list_iter(&p, &v)) { + + xs_list_foreach(n_list, v) { xs *noti = notify_get(user, v); if (noti == NULL) @@ -2775,6 +2996,7 @@ xs_str *html_notifications(snac *user, int skip, int show) const char *utype = xs_dict_get(noti, "utype"); const char *id = xs_dict_get(noti, "objid"); const char *date = xs_dict_get(noti, "date"); + const char *id2 = xs_dict_get_path(noti, "msg.id"); xs *wrk = NULL; if (xs_is_null(id)) @@ -2783,8 +3005,16 @@ xs_str *html_notifications(snac *user, int skip, int show) if (is_hidden(user, id)) continue; + if (xs_is_string(id2) && xs_set_add(&rep, id2) != 1) + continue; + object_get(id, &obj); + const char *msg_id = NULL; + + if (xs_is_dict(obj)) + msg_id = xs_dict_get(obj, "id"); + const char *actor_id = xs_dict_get(noti, "actor"); xs *actor = NULL; @@ -2817,9 +3047,7 @@ xs_str *html_notifications(snac *user, int skip, int show) xs *s_date = xs_crop_i(xs_dup(date), 0, 10); - xs_html *entry = xs_html_tag("div", - xs_html_attr("class", "snac-post-with-desc"), - xs_html_tag("p", + xs_html *this_html_label = xs_html_container( xs_html_tag("b", xs_html_text(label), xs_html_text(" by "), @@ -2830,13 +3058,45 @@ xs_str *html_notifications(snac *user, int skip, int show) xs_html_tag("time", xs_html_attr("class", "dt-published snac-pubdate"), xs_html_attr("title", date), - xs_html_text(s_date)))); + xs_html_text(s_date))); + + xs_html *html_label = NULL; + + if (xs_is_string(msg_id)) { + const xs_val *prev_label = xs_dict_get(admiration_labels, msg_id); + + if (xs_type(prev_label) == XSTYPE_DATA) { + /* there is a previous list of admiration labels! */ + xs_data_get(&html_label, prev_label); + + xs_html_add(html_label, + xs_html_sctag("br", NULL), + this_html_label); + + continue; + } + } + + xs_html *entry = NULL; + + html_label = xs_html_tag("p", + this_html_label); + + /* store in the admiration labels dict */ + xs *pl = xs_data_new(&html_label, sizeof(html_label)); + + if (xs_is_string(msg_id)) + admiration_labels = xs_dict_set(admiration_labels, msg_id, pl); + + entry = xs_html_tag("div", + xs_html_attr("class", "snac-post-with-desc"), + html_label); if (strcmp(type, "Follow") == 0 || strcmp(utype, "Follow") == 0 || strcmp(type, "Block") == 0) { xs_html_add(entry, xs_html_tag("div", xs_html_attr("class", "snac-post"), - html_actor_icon(user, actor, NULL, NULL, NULL, 0, 0, proxy))); + html_actor_icon(user, actor, NULL, NULL, NULL, 0, 0, proxy, NULL))); } else if (strcmp(type, "Move") == 0) { @@ -2850,7 +3110,7 @@ xs_str *html_notifications(snac *user, int skip, int show) xs_html_add(entry, xs_html_tag("div", xs_html_attr("class", "snac-post"), - html_actor_icon(user, old_actor, NULL, NULL, NULL, 0, 0, proxy))); + html_actor_icon(user, old_actor, NULL, NULL, NULL, 0, 0, proxy, NULL))); } } } @@ -2917,6 +3177,8 @@ xs_str *html_notifications(snac *user, int skip, int show) xs_html_text(L("More..."))))); } + xs_set_free(&rep); + xs_html_add(body, html_footer()); @@ -2970,6 +3232,21 @@ int html_get_handler(const xs_dict *req, const char *q_path, else return HTTP_STATUS_NOT_FOUND; } + else + if (strcmp(v, "auth-int-bridge") == 0) { + const char *login = xs_dict_get(q_vars, "login"); + const char *id = xs_dict_get(q_vars, "id"); + const char *action = xs_dict_get(q_vars, "action"); + + if (xs_is_string(login) && xs_is_string(id) && xs_is_string(action)) { + *body = xs_fmt("%s/%s/authorize_interaction?action=%s&id=%s", + srv_baseurl, login, action, id); + + return HTTP_STATUS_SEE_OTHER; + } + else + return HTTP_STATUS_NOT_FOUND; + } uid = xs_dup(v); @@ -3542,6 +3819,52 @@ int html_get_handler(const xs_dict *req, const char *q_path, } } else + if (strcmp(p_path, "authorize_interaction") == 0) { /** follow, like or boost from Mastodon **/ + if (!login(&snac, req)) { + *body = xs_dup(uid); + status = HTTP_STATUS_UNAUTHORIZED; + } + else { + status = HTTP_STATUS_NOT_FOUND; + + const char *id = xs_dict_get(q_vars, "id"); + const char *action = xs_dict_get(q_vars, "action"); + + if (xs_is_string(id) && xs_is_string(action)) { + if (strcmp(action, "Follow") == 0) { + xs *msg = msg_follow(&snac, id); + + if (msg != NULL) { + const char *actor = xs_dict_get(msg, "object"); + + following_add(&snac, actor, msg); + + enqueue_output_by_actor(&snac, msg, actor, 0); + + status = HTTP_STATUS_SEE_OTHER; + } + } + else + if (xs_match(action, "Like|Boost|Announce")) { + /* bring the post */ + xs *msg = msg_admiration(&snac, id, *action == 'L' ? "Like" : "Announce"); + + if (msg != NULL) { + enqueue_message(&snac, msg); + timeline_admire(&snac, xs_dict_get(msg, "object"), snac.actor, *action == 'L' ? 1 : 0); + + status = HTTP_STATUS_SEE_OTHER; + } + } + } + + if (status == HTTP_STATUS_SEE_OTHER) { + *body = xs_fmt("%s/admin", snac.actor); + *b_size = strlen(*body); + } + } + } + else status = HTTP_STATUS_NOT_FOUND; user_free(&snac); @@ -4024,6 +4347,9 @@ int html_post_handler(const xs_dict *req, const char *q_path, else snac.config = xs_dict_set(snac.config, "show_contact_metrics", xs_stock(XSTYPE_FALSE)); + 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", "")); + if ((v = xs_dict_get(p_vars, "metadata")) != NULL) snac.config = xs_dict_set(snac.config, "metadata", v); @@ -4140,6 +4466,35 @@ int html_post_handler(const xs_dict *req, const char *q_path, status = HTTP_STATUS_SEE_OTHER; } + else + if (p_path && strcmp(p_path, "admin/followed-hashtags") == 0) { /** **/ + const char *followed_hashtags = xs_dict_get(p_vars, "followed_hashtags"); + + if (xs_is_string(followed_hashtags)) { + xs *new_hashtags = xs_list_new(); + xs *l = xs_split(followed_hashtags, "\n"); + const char *v; + + xs_list_foreach(l, v) { + xs *s1 = xs_strip_i(xs_dup(v)); + s1 = xs_replace_i(s1, " ", ""); + + if (*s1 == '\0') + continue; + + xs *s2 = xs_utf8_to_lower(s1); + if (*s2 != '#') + s2 = xs_str_prepend_i(s2, "#"); + + new_hashtags = xs_list_append(new_hashtags, s2); + } + + snac.config = xs_dict_set(snac.config, "followed_hashtags", new_hashtags); + user_persist(&snac, 0); + } + + status = HTTP_STATUS_SEE_OTHER; + } if (status == HTTP_STATUS_SEE_OTHER) { const char *redir = xs_dict_get(p_vars, "redir"); |