aboutsummaryrefslogtreecommitdiff
path: root/html.c
diff options
context:
space:
mode:
Diffstat (limited to 'html.c')
-rw-r--r--html.c781
1 files changed, 693 insertions, 88 deletions
diff --git a/html.c b/html.c
index df5b508..3f5435c 100644
--- a/html.c
+++ b/html.c
@@ -1,5 +1,5 @@
/* snac - A simple, minimalistic ActivityPub instance */
-/* copyright (c) 2022 - 2025 grunfink et al. / MIT license */
+/* copyright (c) 2022 - 2026 grunfink et al. / MIT license */
#include "xs.h"
#include "xs_io.h"
@@ -16,6 +16,7 @@
#include "xs_url.h"
#include "xs_random.h"
#include "xs_http.h"
+#include "xs_list_tools.h"
#include "snac.h"
@@ -54,9 +55,10 @@ int login(snac *user, const xs_dict *headers)
return logged_in;
}
-
-xs_str *replace_shortnames(xs_str *s, const xs_list *tag, int ems, const char *proxy)
-/* replaces all the :shortnames: with the emojis in tag */
+xs_str *_replace_shortnames(xs_str *s, const xs_list *tag, int ems,
+ const char *proxy, const xs_list *cl, const char *act_o)
+/* replace but also adds a class list and an actor in its alt text.
+ * Used for emoji reactions */
{
if (!xs_is_null(tag)) {
xs *tag_list = NULL;
@@ -69,11 +71,17 @@ xs_str *replace_shortnames(xs_str *s, const xs_list *tag, int ems, const char *p
tag_list = xs_dup(tag);
}
- xs *style = xs_fmt("height: %dem; width: %dem; vertical-align: middle;", ems, ems);
+ xs *style = xs_fmt("max-height: %dem; max-width: %dem;", ems, ems);
xs *class = xs_fmt("snac-emoji snac-emoji-%d-em", ems);
+ if (cl) {
+ xs *l = xs_join(cl, " ");
+ class = xs_str_cat(class, " ", l);
+ }
- const xs_dict *v;
int c = 0;
+ const xs_val *v;
+
+ c = 0;
xs_set rep_emoji;
xs_set_init(&rep_emoji);
@@ -100,6 +108,8 @@ xs_str *replace_shortnames(xs_str *s, const xs_list *tag, int ems, const char *p
if (!xs_is_string(mt))
mt = xs_mime_by_ext(u);
+ xs *act = act_o ? xs_fmt("%s\n%s", n, act_o) : xs_fmt("%s", n);
+
if (strcmp(mt, "image/svg+xml") == 0 && !xs_is_true(xs_dict_get(srv_config, "enable_svg")))
s = xs_replace_i(s, n, "");
else {
@@ -108,8 +118,8 @@ xs_str *replace_shortnames(xs_str *s, const xs_list *tag, int ems, const char *p
xs_html *img = xs_html_sctag("img",
xs_html_attr("loading", "lazy"),
xs_html_attr("src", url),
- xs_html_attr("alt", n),
- xs_html_attr("title", n),
+ xs_html_attr("alt", act),
+ xs_html_attr("title", act),
xs_html_attr("class", class),
xs_html_attr("style", style));
@@ -130,6 +140,13 @@ xs_str *replace_shortnames(xs_str *s, const xs_list *tag, int ems, const char *p
}
+xs_str *replace_shortnames(xs_str *s, const xs_list *tag, int ems, const char *proxy)
+/* replaces all the :shortnames: with the emojis in tag */
+{
+ return _replace_shortnames(s, tag, ems, proxy, NULL, NULL);
+}
+
+
xs_str *actor_name(xs_dict *actor, const char *proxy)
/* gets the actor name */
{
@@ -145,6 +162,34 @@ xs_str *actor_name(xs_dict *actor, const char *proxy)
}
+xs_str *actor_pronouns(xs_dict *actor)
+/* gets the actor name */
+{
+ const xs_list *attachment;
+ const xs_dict *d;
+ const char *v;
+ const char *pronouns = "";
+ xs_str *ret;
+
+ if (xs_is_list((attachment = xs_dict_get(actor, "attachment")))) {
+ xs_list_foreach(attachment, d) {
+ xs *prop = xs_utf8_to_lower(xs_dict_get(d, "name"));
+ /* make sure that we are reading the correct metadata */
+ if (strlen(prop) == 8 && strcmp(prop, "pronouns") == 0) {
+ /* safeguard from NULL values */
+ v = xs_dict_get(d, "value");
+ pronouns = v ? v : pronouns;
+ break;
+ }
+ }
+ }
+
+ /* strip all HTML tags */
+ ret = xs_regex_replace(pronouns, "</?[^>]+>", "");
+ return ret;
+}
+
+
xs_str *format_text_with_emoji(snac *user, const char *text, int ems, const char *proxy)
/* needed when we have local text with no tags attached */
{
@@ -178,6 +223,7 @@ xs_html *html_actor_icon(snac *user, xs_dict *actor, const char *date,
int fwer = 0;
xs *name = actor_name(actor, proxy);
+ xs *pronouns = actor_pronouns(actor);
/* get the avatar */
if ((v = xs_dict_get(actor, "icon")) != NULL) {
@@ -205,23 +251,35 @@ xs_html *html_actor_icon(snac *user, xs_dict *actor, const char *date,
anchored link to the people page instead of the actor url */
if (fwer || fwing) {
xs *md5 = xs_md5_hex(actor_id, strlen(actor_id));
- href = xs_fmt("%s/people#%s", user->actor, md5);
+ href = xs_fmt("%s/people/%s", user->actor, md5);
}
}
if (href == NULL)
href = xs_dup(actor_id);
+ xs_html *name_link = xs_html_tag("a",
+ xs_html_attr("href", href),
+ xs_html_attr("class", "p-author h-card snac-author"),
+ xs_html_raw(name)); /* name is already html-escaped */
+
+ if (*pronouns) {
+ xs_html_add(name_link,
+ xs_html_text(" ["),
+ xs_html_tag("span",
+ xs_html_attr("class", "snac-pronouns"),
+ xs_html_attr("title", "user's pronouns"),
+ xs_html_raw(pronouns)),
+ xs_html_text("]"));
+ }
+
xs_html_add(actor_icon,
xs_html_sctag("img",
xs_html_attr("loading", "lazy"),
xs_html_attr("class", "snac-avatar"),
xs_html_attr("src", avatar),
xs_html_attr("alt", "[?]")),
- xs_html_tag("a",
- xs_html_attr("href", href),
- xs_html_attr("class", "p-author h-card snac-author"),
- xs_html_raw(name))); /* name is already html-escaped */
+ name_link);
if (!xs_is_null(url)) {
xs *md5 = xs_md5_hex(url, strlen(url));
@@ -234,6 +292,7 @@ xs_html *html_actor_icon(snac *user, xs_dict *actor, const char *date,
xs_html_text("ยป")));
}
+
if (strcmp(xs_dict_get(actor, "type"), "Service") == 0) {
xs_html_add(actor_icon,
xs_html_text(" "),
@@ -398,6 +457,83 @@ xs_html *html_msg_icon(snac *user, const char *actor_id, const xs_dict *msg,
return actor_icon;
}
+void html_note_render_visibility(snac* user, xs_html *form, const int scope)
+{
+ // scopes aren't sorted by value unfortunately, so simple math won't work to limit them. using map-like thing here
+ static const int public_scopes[5] = {SCOPE_PUBLIC, SCOPE_UNLISTED, SCOPE_FOLLOWERS, SCOPE_MENTIONED, -1};
+ static const int unlisted_scopes[4] = {SCOPE_UNLISTED, SCOPE_FOLLOWERS, SCOPE_MENTIONED, -1};
+ static const int followers_scopes[3] = {SCOPE_FOLLOWERS, SCOPE_MENTIONED, -1};
+ static const int mentioned_scopes[2] = {SCOPE_MENTIONED, -1};
+ static const int * const scopes[4] = { public_scopes, mentioned_scopes, unlisted_scopes, followers_scopes};
+ static const char * const scopes_tags[4] = { "public", "mentioned", "unlisted", "followers"};
+ static const char * const scopes_names[4] = { "Public", "Direct Message", "Unlisted", "Followers-only"};
+
+ xs_html *paragraph = xs_html_tag("p", xs_html_text(L("Visibility: ")));
+ const int* to_render = scopes[scope];
+ for( int i = 0; to_render[i] != -1; i++ ){
+ const int scope_i = to_render[i];
+ const char* value = scopes_tags[scope_i];
+ const char* name = scopes_names[scope_i];
+ xs_html_add(paragraph,
+ xs_html_tag("label",
+ xs_html_sctag("input",
+ xs_html_attr("type", "radio"),
+ xs_html_attr("name", "visibility"),
+ xs_html_attr("value", value),
+ xs_html_attr(scope == scope_i ? "checked" : "", NULL)),
+ xs_html_text(" "),
+ xs_html_text(L(name)),
+ xs_html_text(" "))
+ );
+ }
+ xs_html_add(form, paragraph);
+}
+
+/* html_note but moddled for emoji's needs. here and not bellow, since the
+ * other one is already so complex. */
+xs_html *html_emoji(snac *user, const char *summary,
+ const char *div_id, const char *form_id,
+ const char* placeholder, const char *post_id,
+ const char* eid)
+{
+ xs *action = xs_fmt("%s/admin/action", user->actor);
+
+ xs_html *form;
+ const int react = eid == NULL ? 0 : 1;
+
+ xs_html *note = xs_html_tag("div",
+ xs_html_tag("details",
+ xs_html_tag("summary",
+ xs_html_text(summary)),
+ xs_html_tag("p", NULL),
+ xs_html_tag("div",
+ xs_html_attr("class", "snac-note"),
+ xs_html_attr("id", div_id),
+ form = xs_html_tag("form",
+ xs_html_attr("autocomplete", "off"),
+ xs_html_attr("method", "post"),
+ xs_html_attr("action", action),
+ xs_html_attr("enctype", "multipart/form-data"),
+ xs_html_attr("id", form_id),
+ xs_html_sctag("input",
+ xs_html_attr("type", "hidden"),
+ xs_html_attr("name", "id"),
+ xs_html_attr("value", post_id)),
+ xs_html_sctag("input",
+ xs_html_attr("type", react ? "hidden" : "text"),
+ xs_html_attr("name", "eid"),
+ xs_html_attr(react ? "value" : "placeholder", react ? eid : placeholder)),
+ xs_html_text(" "),
+ xs_html_sctag("input",
+ xs_html_attr("type", "submit"),
+ xs_html_attr("name", "action"),
+ xs_html_attr("eid", "action"),
+ xs_html_attr("value", react ? L("EmojiUnreact") : L("EmojiReact"))),
+ xs_html_text(" "),
+ xs_html_tag("p", NULL)))));
+
+ return note;
+}
xs_html *html_note(snac *user, const char *summary,
const char *div_id, const char *form_id,
@@ -455,46 +591,8 @@ xs_html *html_note(snac *user, const char *summary,
xs_html_attr("type", "hidden"),
xs_html_attr("name", "to"),
xs_html_attr("value", actor_id)));
- else {
- xs_html_add(form,
- xs_html_tag("p",
- xs_html_text(L("Visibility: ")),
- xs_html_tag("label",
- xs_html_sctag("input",
- xs_html_attr("type", "radio"),
- xs_html_attr("name", "visibility"),
- xs_html_attr("value", "public"),
- xs_html_attr(scope == SCOPE_PUBLIC ? "checked" : "", NULL)),
- xs_html_text(" "),
- xs_html_text(L("Public"))),
- xs_html_text(" "),
- xs_html_tag("label",
- xs_html_sctag("input",
- xs_html_attr("type", "radio"),
- xs_html_attr("name", "visibility"),
- xs_html_attr("value", "unlisted"),
- xs_html_attr(scope == SCOPE_UNLISTED ? "checked" : "", NULL)),
- xs_html_text(" "),
- xs_html_text(L("Unlisted"))),
- xs_html_text(" "),
- xs_html_tag("label",
- xs_html_sctag("input",
- xs_html_attr("type", "radio"),
- xs_html_attr("name", "visibility"),
- xs_html_attr("value", "followers"),
- xs_html_attr(scope == SCOPE_FOLLOWERS ? "checked" : "", NULL)),
- xs_html_text(" "),
- xs_html_text(L("Followers-only"))),
- xs_html_text(" "),
- xs_html_tag("label",
- xs_html_sctag("input",
- xs_html_attr("type", "radio"),
- xs_html_attr("name", "visibility"),
- xs_html_attr("value", "mentioned"),
- xs_html_attr(scope == SCOPE_MENTIONED ? "checked" : "", NULL)),
- xs_html_text(" "),
- xs_html_text(L("Direct Message")))));
- }
+ if (edit_id == NULL)
+ html_note_render_visibility(user, form, scope);
if (redir)
xs_html_add(form,
@@ -1363,6 +1461,28 @@ xs_html *html_top_controls(snac *user)
xs_html_attr("value", L("Like"))),
xs_html_text(" "),
xs_html_text(L("(by URL)"))),
+ xs_html_tag("form",
+ xs_html_attr("autocomplete", "off"),
+ xs_html_attr("method", "post"),
+ xs_html_attr("action", ops_action),
+ xs_html_sctag("input",
+ xs_html_attr("type", "text"),
+ xs_html_attr("name", "eid"),
+ xs_html_attr("required", "required"),
+ xs_html_attr("placeholder", ":neocat:")),
+ xs_html_text(" "),
+ xs_html_sctag("input",
+ xs_html_attr("type", "text"),
+ xs_html_attr("name", "id"),
+ xs_html_attr("required", "required"),
+ xs_html_attr("placeholder", "https:/" "/fedi.example.com/bob/...")),
+ xs_html_text(" "),
+ xs_html_sctag("input",
+ xs_html_attr("type", "submit"),
+ xs_html_attr("name", "action"),
+ xs_html_attr("value", L("EmojiReact"))),
+ xs_html_text(" "),
+ xs_html_text(L("(by URL)"))),
xs_html_tag("p", NULL)));
/** user settings **/
@@ -1787,6 +1907,38 @@ xs_html *html_top_controls(snac *user)
xs_html_attr("class", "button"),
xs_html_attr("value", L("Update hashtags")))))));
+ xs *muted_words_action = xs_fmt("%s/admin/muted-words", user->actor);
+ xs *muted_words = xs_join(xs_dict_get_def(user->config,
+ "muted_words", xs_stock(XSTYPE_LIST)), "\n");
+
+ xs_html_add(top_controls,
+ xs_html_tag("details",
+ xs_html_tag("summary",
+ xs_html_text(L("Muted words..."))),
+ xs_html_tag("p",
+ xs_html_text(L("One word per line, partial matches count"))),
+ xs_html_tag("div",
+ xs_html_attr("class", "snac-muted-words"),
+ xs_html_tag("form",
+ xs_html_attr("autocomplete", "off"),
+ xs_html_attr("method", "post"),
+ xs_html_attr("action", muted_words_action),
+ xs_html_attr("enctype", "multipart/form-data"),
+
+ xs_html_tag("textarea",
+ xs_html_attr("name", "muted_words"),
+ xs_html_attr("cols", "40"),
+ xs_html_attr("rows", "4"),
+ xs_html_attr("placeholder", "nascar\nsuperbowl\nFIFA"),
+ xs_html_text(muted_words)),
+
+ 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 muted words")))))));
+
return top_controls;
}
@@ -1898,7 +2050,7 @@ xs_html *html_entry_controls(snac *user, const char *actor,
xs_html_attr("name", "redir"),
xs_html_attr("value", redir))));
- if (!xs_startswith(id, user->actor)) {
+ if (!is_msg_mine(user, id)) {
if (xs_list_in(likes, user->md5) == -1) {
/* not already liked; add button */
xs_html_add(form,
@@ -2026,6 +2178,22 @@ xs_html *html_entry_controls(snac *user, const char *actor,
xs_html_tag("p", NULL));
}
+ if (!xs_is_true(xs_dict_get(srv_config, "disable_emojireact"))) { /** emoji react **/
+ /* the post textarea */
+ xs *div_id = xs_fmt("%s_reply", md5);
+ xs *form_id = xs_fmt("%s_reply_form", md5);
+ xs *e_react = emoji_reacted(user, id);
+
+ xs_html_add(controls, xs_html_tag("div",
+ xs_html_tag("p", NULL),
+ html_emoji(
+ user, L("Emoji react..."),
+ div_id, form_id,
+ ":neocat:", id,
+ e_react)),
+ xs_html_tag("p", NULL));
+ }
+
{ /** reply **/
/* the post textarea */
xs *ct = build_mentions(user, msg);
@@ -2051,6 +2219,30 @@ xs_html *html_entry_controls(snac *user, const char *actor,
}
+static const xs_str* words_in_content(const xs_list *words, const xs_val *content)
+/* returns a word that matches any of the words in content */
+{
+ if (!xs_is_list(words) || !xs_is_string(content)) {
+ return NULL;
+ }
+ xs *c = xs_split(content, " ");
+ xs *sc = xs_list_sort(c, NULL);
+
+ const xs_str *wv;
+ const xs_str *cv;
+ xs_list_foreach(words, wv) {
+ xs_list_foreach(sc, cv) {
+ xs_tolower_i((xs_str*)cv);
+ if(xs_str_in(cv, wv) != -1){
+ return wv;
+ }
+ }
+ }
+
+ return NULL;
+}
+
+
xs_html *html_entry(snac *user, xs_dict *msg, int read_only,
int level, const char *md5, int hide_children)
{
@@ -2262,7 +2454,7 @@ xs_html *html_entry(snac *user, xs_dict *msg, int read_only,
}
if (!read_only && (fwers || fwing))
- href = xs_fmt("%s/people#%s", user->actor, p);
+ href = xs_fmt("%s/people/%s", user->actor, p);
else
href = xs_dup(id);
@@ -2345,6 +2537,17 @@ xs_html *html_entry(snac *user, xs_dict *msg, int read_only,
xs_html_text(v),
xs_html_text(L(" [SENSITIVE CONTENT]"))));
}
+ else
+ if (user &&
+ /* muted_words is all lowercase and sorted for performance */
+ (v = words_in_content(xs_dict_get(user->config, "muted_words"),
+ xs_dict_get(msg, "content"))) != NULL) {
+ snac_debug(user, 1, xs_fmt("word %s muted by user preferences: %s", v, id));
+ snac_content = xs_html_tag("details",
+ xs_html_tag("summary",
+ xs_html_text(L("Muted: ")),
+ xs_html_text(v)));
+ }
else {
snac_content = xs_html_tag("div", NULL);
}
@@ -2352,6 +2555,182 @@ xs_html *html_entry(snac *user, xs_dict *msg, int read_only,
xs_html_add(snac_content_wrap,
snac_content);
+ /* add all emoji reacts */
+ int is_emoji = 0;
+ if (!xs_is_true(xs_dict_get(srv_config, "disable_emojireact"))) {
+ int c = 0;
+ const xs_dict *k;
+ xs *ls = xs_list_new();
+ xs *sfrl = xs_dict_new();
+ xs *rl = object_get_emoji_reacts(id);
+
+ while (xs_list_next(rl, &v, &c)) {
+ xs *m = NULL;
+ if (valid_status(object_get_by_md5(v, &m))) {
+ const char *content = xs_dict_get(m, "content");
+ const char *actor = xs_dict_get(m, "actor");
+ const xs_list *contentl = xs_dict_get(sfrl, content);
+
+ if ((user && is_muted(user, actor)) || is_instance_blocked(actor))
+ continue;
+
+ xs *actors = xs_list_new();
+ actors = xs_list_append(actors, actor);
+ char me = actor && user && strcmp(actor, user->actor) == 0;
+ int count = 1;
+
+ if (contentl) {
+ count = atoi(xs_list_get(contentl, 0)) + 1;
+ const xs_list *actorsc = xs_list_get(contentl, 1);
+ if (strncmp(xs_list_get(contentl, 2), "1", 1) == 0)
+ me = 1;
+
+ if (xs_list_in(actorsc, actor) != -1) {
+ xs_free(actors);
+ actors = xs_dup(actorsc);
+ }
+ else
+ actors = xs_list_cat(actors, actorsc);
+ }
+
+ xs *fl = xs_list_new();
+ xs *c1 = xs_fmt("%d", count);
+ xs *c2 = xs_fmt("%d", me);
+ fl = xs_list_append(fl, c1, actors, c2);
+ sfrl = xs_dict_append(sfrl, content, fl);
+ }
+ }
+
+ c = 0;
+
+ while (xs_list_next(rl, &k, &c)) {
+ xs *m = NULL;
+ if (valid_status(object_get_by_md5(k, &m))) {
+ const xs_dict *tag = xs_dict_get(m, "tag");
+ const xs_dict *ide = xs_dict_get(m, "id");
+
+ const char *content = xs_dict_get(m, "content");
+ const char *shortname;
+ shortname = xs_dict_get(m, "content");
+
+ const xs_list *items = xs_dict_get(sfrl, content);
+
+ if (!xs_is_null(items)) {
+ const char *nb = xs_list_get(items, 0);
+ const xs_list *actors = xs_list_get(items, 1);
+ const char me = *xs_list_get(items, 2) == '1';
+
+ is_emoji = 1;
+
+ xs *al = xs_join(actors, ",\n\t");
+ xs *act = atoi(nb) > 1 ?
+ xs_fmt("%d different actors \n\t%s", atoi(nb), al) :
+ xs_dup(xs_dict_get(m, "actor"));
+
+ xs *class = xs_list_new();
+ class = xs_list_append(class, "snac-reaction");
+
+ xs_html *ret = NULL;
+ if (tag && shortname) {
+ xs *cl = xs_list_new();
+ cl = xs_list_append(cl, "snac-reaction-image");
+ xs *emoji = _replace_shortnames(xs_dup(shortname), tag, 2, proxy, cl, act);
+
+ emoji = xs_strip_chars_i(emoji, ":");
+
+ if (me)
+ class = xs_list_append(class, "snac-reacted");
+
+ xs *l1 = xs_join(class, " ");
+ ret = xs_html_tag("button",
+ xs_html_attr("type", "submit"),
+ xs_html_attr("name", "action"),
+ xs_html_attr("value", me ? L("EmojiReact") : L("EmojiUnreact")),
+ xs_html_raw(emoji),
+ xs_html_tag("span",
+ xs_html_raw(nb),
+ xs_html_attr("style", "padding-left: 5px;")),
+ xs_html_attr("title", act),
+ xs_html_attr("class", l1));
+
+ if (!(ide && xs_startswith(ide, srv_baseurl)))
+ xs_html_add(ret, xs_html_attr("disabled", "true"));
+ }
+ else if (shortname) {
+ xs *sn = xs_dup(shortname);
+ const char *sna = sn;
+ unsigned int utf = xs_utf8_dec((const char **)&sna);
+
+ if (xs_is_emoji(utf)) {
+ const char *style = "font-size: large;";
+ if (me)
+ class = xs_list_append(class, "snac-reacted");
+ xs *l1 = xs_join(class, " ");
+ xs *s1 = xs_fmt("&#%d", utf);
+ ret = xs_html_tag("button",
+ xs_html_attr("type", "submit"),
+ xs_html_attr("name", "action"),
+ xs_html_attr("value", me ? L("EmojiUnreact") : L("EmojiReact")),
+ xs_html_raw(s1),
+ xs_html_tag("span",
+ xs_html_raw(nb),
+ xs_html_attr("style", "font-size: initial; padding-left: 5px;")),
+ xs_html_attr("title", act),
+ xs_html_attr("class", l1),
+ xs_html_attr("style", style));
+ }
+ }
+ if (ret) {
+ xs *s1;
+ if (user) {
+ xs *action = xs_fmt("%s/admin/action", user->actor);
+ xs *form_id = xs_fmt("%s_reply_form", md5);
+
+ xs_html *form =
+ xs_html_tag("form",
+ xs_html_attr("autocomplete", "off"),
+ xs_html_attr("method", "post"),
+ xs_html_attr("action", action),
+ xs_html_attr("enctype", "multipart/form-data"),
+ xs_html_attr("style", "display: inline-flex;"
+ "vertical-align: middle;"),
+ xs_html_attr("id", form_id),
+ xs_html_sctag("input",
+ xs_html_attr("type", "hidden"),
+ xs_html_attr("name", "id"),
+ xs_html_attr("value", id)),
+ xs_html_sctag("input",
+ xs_html_attr("type", "hidden"),
+ xs_html_attr("name", "eid"),
+ xs_html_attr("value", shortname)),
+ ret);
+ s1 = xs_html_render(form);
+ }
+ else
+ s1 = xs_html_render(ret);
+
+ ls = xs_list_append(ls, s1);
+ sfrl = xs_dict_del(sfrl, content);
+ }
+ }
+ }
+ }
+
+ c = 0;
+
+ xs_html *emoji_div;
+ if (xs_list_len(ls) > 0) {
+ emoji_div = xs_html_tag("div", xs_html_text(L("Emoji reactions: ")),
+ xs_html_attr("class", "snac-reaction-div"));
+
+ while (ls != NULL && xs_list_next(ls, &k, &c))
+ xs_html_add(emoji_div, xs_html_raw(k));
+
+ xs_html_add(snac_content_wrap, emoji_div);
+ }
+
+ }
+
{
/** build the content string **/
const char *content = xs_dict_get(msg, "content");
@@ -2378,7 +2757,8 @@ xs_html *html_entry(snac *user, xs_dict *msg, int read_only,
c = xs_replace_i(c, "<br><br>", "<p>");
- c = xs_str_cat(c, "<p>");
+ if (is_emoji == 0)
+ c = xs_str_cat(c, "<p>");
/* replace the :shortnames: */
c = replace_shortnames(c, xs_dict_get(msg, "tag"), 2, proxy);
@@ -2426,7 +2806,7 @@ xs_html *html_entry(snac *user, xs_dict *msg, int read_only,
if (read_only)
closed = 1; /* non-identified page; show as closed */
else
- if (user && xs_startswith(id, user->actor))
+ if (user && is_msg_mine(user, id))
closed = 1; /* we questioned; closed for us */
else
if (user && was_question_voted(user, id))
@@ -2634,7 +3014,7 @@ xs_html *html_entry(snac *user, xs_dict *msg, int read_only,
xs_html_attr("title", name))));
}
else
- if (xs_startswith(type, "video/")) {
+ if (xs_startswith(type, "video/") || strcmp(type, "Video") == 0) {
xs_html_add(content_attachments,
xs_html_tag("video",
xs_html_attr("preload", "none"),
@@ -3003,7 +3383,7 @@ xs_html *html_footer(const snac *user)
xs_str *html_timeline(snac *user, const xs_list *list, int read_only,
int skip, int show, int show_more,
const char *title, const char *page,
- int utl, const char *error)
+ int utl, const char *error, int terse)
/* returns the HTML for the timeline */
{
xs_list *p = (xs_list *)list;
@@ -3031,7 +3411,11 @@ xs_str *html_timeline(snac *user, const xs_list *list, int read_only,
if (user) {
head = html_user_head(user, desc, alternate);
- body = html_user_body(user, read_only);
+
+ if (terse)
+ body = xs_html_tag("body", NULL);
+ else
+ body = html_user_body(user, read_only);
}
else {
head = html_instance_head();
@@ -3251,7 +3635,7 @@ xs_str *html_timeline(snac *user, const xs_list *list, int read_only,
if (list && user && read_only) {
/** history **/
- if (xs_type(xs_dict_get(srv_config, "disable_history")) != XSTYPE_TRUE) {
+ if (xs_type(xs_dict_get(srv_config, "disable_history")) != XSTYPE_TRUE && !terse) {
xs_html *ul = xs_html_tag("ul", NULL);
xs_html *history = xs_html_tag("div",
@@ -3323,13 +3707,19 @@ xs_str *html_timeline(snac *user, const xs_list *list, int read_only,
}
-xs_html *html_people_list(snac *user, xs_list *list, const char *header, const char *t, const char *proxy)
+xs_html *html_people_list(snac *user, xs_list *list, const char *header, const char *t, const char *proxy, int do_count)
{
xs_html *snac_posts;
+ xs *header_cnt;
+ if (do_count)
+ header_cnt = xs_fmt("%s - %d\n", header, xs_list_len(list));
+ else
+ header_cnt = xs_fmt("%s\n", header);
+
xs_html *people = xs_html_tag("div",
xs_html_tag("h2",
xs_html_attr("class", "snac-header"),
- xs_html_text(header)),
+ xs_html_text(header_cnt)),
snac_posts = xs_html_tag("details",
xs_html_attr("open", NULL),
xs_html_tag("summary",
@@ -3570,12 +3960,12 @@ xs_str *html_people(snac *user)
if (xs_list_len(pending) || xs_is_true(xs_dict_get(user->config, "approve_followers"))) {
xs_html_add(lists,
- html_people_list(user, pending, L("Pending follow confirmations"), "p", proxy));
+ html_people_list(user, pending, L("Pending follow confirmations"), "p", proxy, 1));
}
xs_html_add(lists,
- html_people_list(user, wing, L("People you follow"), "i", proxy),
- html_people_list(user, wers, L("People that follow you"), "e", proxy));
+ html_people_list(user, wing, L("People you follow"), "i", proxy, 1),
+ html_people_list(user, wers, L("People that follow you"), "e", proxy, 1));
xs_html *html = xs_html_tag("html",
html_user_head(user, NULL, NULL),
@@ -3586,6 +3976,108 @@ xs_str *html_people(snac *user)
return xs_html_render_s(html, "<!DOCTYPE html>\n");
}
+/* Filter list to display only posts by actor. We'll probably show
+ fewer than show posts. Should we try harder to find some? */
+xs_str *html_people_one(snac *user, const char *actor, const xs_list *list,
+ int skip, int show, int show_more, const char *page)
+{
+ const char *proxy = NULL;
+ xs_list *p = (xs_list *)list;
+ const char *v;
+
+ if (xs_is_true(xs_dict_get(srv_config, "proxy_media")))
+ proxy = user->actor;
+
+ xs_html *body = html_user_body(user, 0);
+
+ xs_html *lists = xs_html_tag("div",
+ xs_html_attr("class", "snac-posts"));
+
+ xs *foll = xs_list_append(xs_list_new(), actor);
+
+ xs_html_add(lists,
+ html_people_list(user, foll, L("Contact's posts"), "p", proxy, 0));
+
+ xs_html_add(body, lists);
+
+ while (xs_list_iter(&p, &v)) {
+ xs *msg = NULL;
+ int status;
+
+ status = timeline_get_by_md5(user, v, &msg);
+
+ if (!valid_status(status))
+ continue;
+
+ const char *id = xs_dict_get(msg, "id");
+ const char *by = get_atto(msg);
+ xs *actor_md5 = NULL;
+ xs_list *boosts = NULL;
+ xs_list *likes = NULL;
+ xs_list *reacts = NULL;
+ /* Besides actor's posts, also show actor's boosts, and also
+ posts by user with likes or reacts by actor. I.e., any
+ actor's actions that user could have seen in the timeline
+ or in notifications. */
+ if (!(by && strcmp(actor, by) == 0) &&
+ xs_list_in((boosts = object_announces(id)),
+ (actor_md5 = xs_md5_hex(actor, strlen(actor)))) == -1 &&
+ (!(by && strcmp(user->actor, by) == 0) ||
+ (xs_list_in((likes = object_likes(id)), actor_md5) == -1 &&
+ xs_list_in((reacts = object_get_emoji_reacts(id)), actor_md5) == -1)))
+ continue;
+
+ xs_html *entry = html_entry(user, msg, 0, 0, v, 1);
+
+ if (entry != NULL)
+ xs_html_add(lists,
+ entry);
+ }
+
+ if (show_more) {
+ xs *m = NULL;
+ xs *m10 = NULL;
+ xs *ss = xs_fmt("skip=%d&show=%d", skip + show, show);
+
+ xs *url = xs_dup(user == NULL ? srv_baseurl : user->actor);
+
+ if (page != NULL)
+ url = xs_str_cat(url, page);
+
+ if (xs_str_in(url, "?") != -1)
+ m = xs_fmt("%s&%s", url, ss);
+ else
+ m = xs_fmt("%s?%s", url, ss);
+
+ m10 = xs_fmt("%s0", m);
+
+ xs_html *more_links = xs_html_tag("p",
+ xs_html_tag("a",
+ xs_html_attr("href", url),
+ xs_html_attr("name", "snac-more"),
+ xs_html_text(L("Back to top"))),
+ xs_html_text(" - "),
+ xs_html_tag("a",
+ xs_html_attr("href", m),
+ xs_html_attr("name", "snac-more"),
+ xs_html_text(L("More..."))),
+ xs_html_text(" - "),
+ xs_html_tag("a",
+ xs_html_attr("href", m10),
+ xs_html_attr("name", "snac-more"),
+ xs_html_text(L("More (x 10)..."))));
+
+ xs_html_add(body,
+ more_links);
+ }
+
+ xs_html *html = xs_html_tag("html",
+ html_user_head(user, NULL, NULL),
+ xs_html_add(body,
+ html_footer(user)));
+
+ return xs_html_render_s(html, "<!DOCTYPE html>\n");
+}
xs_str *html_notifications(snac *user, int skip, int show)
{
@@ -3654,6 +4146,9 @@ xs_str *html_notifications(snac *user, int skip, int show)
if (xs_is_string(id2) && xs_set_add(&rep, id2) != 1)
continue;
+ if (strcmp(type, "EmojiReact") == 0 && xs_is_true(xs_dict_get(srv_config, "disable_emojireact")))
+ continue;
+
object_get(id, &obj);
const char *msg_id = NULL;
@@ -3689,9 +4184,18 @@ xs_str *html_notifications(snac *user, int skip, int show)
if (strcmp(type, "EmojiReact") == 0 || strcmp(type, "Like") == 0) {
const char *content = xs_dict_get_path(noti, "msg.content");
+ xs *cd = xs_dup(content);
+ const char *sna = cd;
+ const xs_dict *tag = xs_dict_get_path(noti, "msg.tag");
+ unsigned int utf = xs_utf8_dec((const char **)&sna);
+
+ int isEmoji = 0;
+ if (xs_is_emoji(utf) || (tag && xs_list_len(tag) > 0))
+ isEmoji = 1;
+
if (xs_type(content) == XSTYPE_STRING) {
xs *emoji = replace_shortnames(xs_dup(content), xs_dict_get_path(noti, "msg.tag"), 1, proxy);
- wrk = xs_fmt("%s (%s&#xFE0F;)", type, emoji);
+ wrk = xs_fmt("%s (%s&#xFE0F;)", isEmoji ? "EmojiReact" : "Like", emoji);
label = wrk;
}
}
@@ -3909,6 +4413,7 @@ int html_get_handler(const xs_dict *req, const char *q_path,
int cache = 1;
int save = 1;
int proxy = 0;
+ int terse = 0;
const char *v;
const xs_dict *q_vars = xs_dict_get(req, "q_vars");
@@ -3921,6 +4426,9 @@ int html_get_handler(const xs_dict *req, const char *q_path,
return HTTP_STATUS_NOT_FOUND;
}
+ if (!xs_is_null(xs_dict_get(q_vars, "terse")))
+ terse = 1;
+
if (strcmp(v, "share-bridge") == 0) {
/* temporary redirect for a post */
const char *login = xs_dict_get(q_vars, "login");
@@ -3988,8 +4496,12 @@ int html_get_handler(const xs_dict *req, const char *q_path,
cache = 0;
int skip = 0;
+ const char *max_show_default = "50";
+ int max_show = xs_number_get(xs_dict_get_def(srv_config, "max_timeline_entries",
+ max_show_default));
int def_show = xs_number_get(xs_dict_get_def(srv_config, "def_timeline_entries",
- xs_dict_get_def(srv_config, "max_timeline_entries", "50")));
+ xs_dict_get_def(srv_config, "max_timeline_entries",
+ max_show_default)));
int show = def_show;
if ((v = xs_dict_get(q_vars, "skip")) != NULL)
@@ -4015,13 +4527,15 @@ int html_get_handler(const xs_dict *req, const char *q_path,
/* a show of 0 has no sense */
if (show == 0)
show = def_show;
+ if (show > max_show)
+ show = max_show;
if (p_path == NULL) { /** public timeline **/
xs *h = xs_str_localtime(0, "%Y-%m.html");
if (xs_type(xs_dict_get(snac.config, "private")) == XSTYPE_TRUE) {
/** empty public timeline for private users **/
- *body = html_timeline(&snac, NULL, 1, 0, 0, 0, NULL, "", 1, error);
+ *body = html_timeline(&snac, NULL, 1, 0, 0, 0, NULL, "", 1, error, terse);
*b_size = strlen(*body);
status = HTTP_STATUS_OK;
}
@@ -4044,7 +4558,7 @@ int html_get_handler(const xs_dict *req, const char *q_path,
xs *pins = pinned_list(&snac);
pins = xs_list_cat(pins, list);
- *body = html_timeline(&snac, pins, 1, skip, show, more, NULL, "", 1, error);
+ *body = html_timeline(&snac, pins, 1, skip, show, more, NULL, "", 1, error, terse);
*b_size = strlen(*body);
status = HTTP_STATUS_OK;
@@ -4060,9 +4574,15 @@ int html_get_handler(const xs_dict *req, const char *q_path,
status = HTTP_STATUS_UNAUTHORIZED;
}
else {
- const char *q = xs_dict_get(q_vars, "q");
+ const char *q = NULL;
+ xs *cq = xs_dup(xs_dict_get(q_vars, "q"));
xs *url_acct = NULL;
+ if (xs_is_string(cq)) {
+ cq = xs_strip_i(cq);
+ q = cq;
+ }
+
/* searching for an URL? */
if (q && xs_match(q, "https://*|http://*")) {
/* may by an actor; try a webfinger */
@@ -4126,11 +4646,11 @@ int html_get_handler(const xs_dict *req, const char *q_path,
actor_add(actor, actor_obj);
/* create a people list with only one element */
- l = xs_list_append(xs_list_new(), actor);
+ l = xs_list_append(l, actor);
xs *title = xs_fmt(L("Search results for account %s"), q);
- page = html_people_list(&snac, l, title, "wf", NULL);
+ page = html_people_list(&snac, l, title, "wf", NULL, 1);
}
}
@@ -4168,7 +4688,7 @@ int html_get_handler(const xs_dict *req, const char *q_path,
xs *title = xs_fmt(xs_list_len(tl) ?
L("Search results for tag %s") : L("Nothing found for tag %s"), q);
- *body = html_timeline(&snac, tl, 0, skip, show, more, title, page, 0, error);
+ *body = html_timeline(&snac, tl, 0, skip, show, more, title, page, 0, error, terse);
*b_size = strlen(*body);
status = HTTP_STATUS_OK;
}
@@ -4193,7 +4713,7 @@ int html_get_handler(const xs_dict *req, const char *q_path,
title = xs_fmt(L("Nothing found for '%s'"), q);
*body = html_timeline(&snac, tl, 0, skip, tl_len, to || tl_len == show,
- title, page, 0, error);
+ title, page, 0, error, terse);
*b_size = strlen(*body);
status = HTTP_STATUS_OK;
}
@@ -4220,7 +4740,7 @@ int html_get_handler(const xs_dict *req, const char *q_path,
xs *list = timeline_list(&snac, "private", skip, show, &more);
*body = html_timeline(&snac, list, 0, skip, show,
- more, NULL, "/admin", 1, error);
+ more, NULL, "/admin", 1, error, terse);
*b_size = strlen(*body);
status = HTTP_STATUS_OK;
@@ -4247,7 +4767,7 @@ int html_get_handler(const xs_dict *req, const char *q_path,
xs *list0 = xs_list_append(xs_list_new(), md5);
xs *list = timeline_top_level(&snac, list0);
- *body = html_timeline(&snac, list, 0, 0, 0, 0, NULL, "/admin", 1, error);
+ *body = html_timeline(&snac, list, 0, 0, 0, 0, NULL, "/admin", 1, error, terse);
*b_size = strlen(*body);
status = HTTP_STATUS_OK;
}
@@ -4266,6 +4786,37 @@ int html_get_handler(const xs_dict *req, const char *q_path,
}
}
else
+ if (xs_startswith(p_path, "people/")) { /** a single actor **/
+ if (!login(&snac, req)) {
+ *body = xs_dup(uid);
+ status = HTTP_STATUS_UNAUTHORIZED;
+ }
+ else {
+ xs *actor_dict = NULL;
+ const char *actor_id = NULL;
+ xs *actor = NULL;
+ xs_list *page_lst = xs_split_n(p_path, "?", 2);
+ xs *page = xs_str_cat(xs_str_new("/"), xs_list_get(page_lst, 0));
+ xs_list *l = xs_split_n(page, "/", 3);
+ const char *actor_md5 = xs_list_get(l, 2);
+
+ if (valid_status(object_get_by_md5(actor_md5, &actor_dict)) &&
+ (actor_id = xs_dict_get(actor_dict, "id")) != NULL &&
+ valid_status(actor_get(actor_id, &actor))) {
+ int more = 0;
+ xs *list = timeline_simple_list(&snac, "private", skip, show, &more);
+
+ *body = html_people_one(&snac, actor_id, list, skip, show, more, page);
+ *b_size = strlen(*body);
+ status = HTTP_STATUS_OK;
+ }
+ else {
+ *body = xs_dup(uid);
+ status = HTTP_STATUS_NOT_FOUND;
+ }
+ }
+ }
+ else
if (strcmp(p_path, "notifications") == 0) { /** the list of notifications **/
if (!login(&snac, req)) {
*body = xs_dup(uid);
@@ -4288,7 +4839,7 @@ int html_get_handler(const xs_dict *req, const char *q_path,
xs *next = timeline_instance_list(skip + show, 1);
*body = html_timeline(&snac, list, 0, skip, show,
- xs_list_len(next), L("Showing instance timeline"), "/instance", 0, error);
+ xs_list_len(next), L("Showing instance timeline"), "/instance", 0, error, terse);
*b_size = strlen(*body);
status = HTTP_STATUS_OK;
}
@@ -4303,7 +4854,7 @@ int html_get_handler(const xs_dict *req, const char *q_path,
xs *list = pinned_list(&snac);
*body = html_timeline(&snac, list, 0, skip, show,
- 0, L("Pinned posts"), "", 0, error);
+ 0, L("Pinned posts"), "", 0, error, terse);
*b_size = strlen(*body);
status = HTTP_STATUS_OK;
}
@@ -4318,7 +4869,7 @@ int html_get_handler(const xs_dict *req, const char *q_path,
xs *list = bookmark_list(&snac);
*body = html_timeline(&snac, list, 0, skip, show,
- 0, L("Bookmarked posts"), "", 0, error);
+ 0, L("Bookmarked posts"), "", 0, error, terse);
*b_size = strlen(*body);
status = HTTP_STATUS_OK;
}
@@ -4333,7 +4884,7 @@ int html_get_handler(const xs_dict *req, const char *q_path,
xs *list = draft_list(&snac);
*body = html_timeline(&snac, list, 0, skip, show,
- 0, L("Post drafts"), "", 0, error);
+ 0, L("Post drafts"), "", 0, error, terse);
*b_size = strlen(*body);
status = HTTP_STATUS_OK;
}
@@ -4348,7 +4899,7 @@ int html_get_handler(const xs_dict *req, const char *q_path,
xs *list = scheduled_list(&snac);
*body = html_timeline(&snac, list, 0, skip, show,
- 0, L("Scheduled posts"), "", 0, error);
+ 0, L("Scheduled posts"), "", 0, error, terse);
*b_size = strlen(*body);
status = HTTP_STATUS_OK;
}
@@ -4374,7 +4925,7 @@ int html_get_handler(const xs_dict *req, const char *q_path,
xs *title = xs_fmt(L("Showing timeline for list '%s'"), name);
*body = html_timeline(&snac, ttl, 0, skip, show,
- xs_list_len(next), title, base, 1, error);
+ xs_list_len(next), title, base, 1, error, terse);
*b_size = strlen(*body);
status = HTTP_STATUS_OK;
}
@@ -4394,7 +4945,7 @@ int html_get_handler(const xs_dict *req, const char *q_path,
list = xs_list_append(list, md5);
- *body = html_timeline(&snac, list, 1, 0, 0, 0, NULL, "", 1, error);
+ *body = html_timeline(&snac, list, 1, 0, 0, 0, NULL, "", 1, error, terse);
*b_size = strlen(*body);
status = HTTP_STATUS_OK;
}
@@ -4582,8 +5133,8 @@ int html_get_handler(const xs_dict *req, const char *q_path,
xs *msg = msg_admiration(&snac, id, *action == 'L' ? "Like" : "Announce");
if (msg != NULL) {
+ timeline_admire(&snac, xs_dict_get(msg, "object"), snac.actor, *action == 'L' ? 1 : 0, msg);
enqueue_message(&snac, msg);
- timeline_admire(&snac, xs_dict_get(msg, "object"), snac.actor, *action == 'L' ? 1 : 0);
status = HTTP_STATUS_SEE_OTHER;
}
@@ -4858,6 +5409,9 @@ int html_post_handler(const xs_dict *req, const char *q_path,
/* overwrite object, not updating the indexes */
object_add_ow(edit_id, msg);
+ /* index tags */
+ tag_index(edit_id, msg);
+
/* update message */
c_msg = msg_update(&snac, msg);
}
@@ -4891,12 +5445,36 @@ int html_post_handler(const xs_dict *req, const char *q_path,
status = HTTP_STATUS_SEE_OTHER;
+ if (strcmp(action, L("EmojiUnreact")) == 0) { /** **/
+ const char *eid = xs_dict_get(p_vars, "eid");
+
+ if (eid != NULL) {
+ xs *n_msg = msg_emoji_unreact(&snac, id, eid);
+
+ if (n_msg != NULL)
+ enqueue_message(&snac, n_msg);
+ }
+ }
+ else
+ if (strcmp(action, L("EmojiReact")) == 0) { /** **/
+ xs *eid = xs_dup(xs_dict_get(p_vars, "eid"));
+
+ eid = xs_strip_chars_i(eid, ":");
+
+ const xs_dict *ret = msg_emoji_init(&snac, id, eid);
+ /* fails if either invalid or already reacted */
+ if (!ret)
+ ret = msg_emoji_unreact(&snac, id, eid);
+ if (!ret)
+ status = HTTP_STATUS_NOT_FOUND;
+ }
+ else
if (strcmp(action, L("Like")) == 0) { /** **/
xs *msg = msg_admiration(&snac, id, "Like");
if (msg != NULL) {
+ timeline_admire(&snac, xs_dict_get(msg, "object"), snac.actor, 1, msg);
enqueue_message(&snac, msg);
- timeline_admire(&snac, xs_dict_get(msg, "object"), snac.actor, 1);
}
}
else
@@ -4904,8 +5482,8 @@ int html_post_handler(const xs_dict *req, const char *q_path,
xs *msg = msg_admiration(&snac, id, "Announce");
if (msg != NULL) {
+ timeline_admire(&snac, xs_dict_get(msg, "object"), snac.actor, 0, msg);
enqueue_message(&snac, msg);
- timeline_admire(&snac, xs_dict_get(msg, "object"), snac.actor, 0);
}
}
else
@@ -5022,7 +5600,7 @@ int html_post_handler(const xs_dict *req, const char *q_path,
}
else {
/* delete an entry */
- if (xs_startswith(id, snac.actor) && !is_draft(&snac, id)) {
+ if (is_msg_mine(&snac, id) && !is_draft(&snac, id)) {
/* it's a post by us: generate a delete */
xs *msg = msg_delete(&snac, id);
@@ -5351,6 +5929,33 @@ 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/muted-words") == 0) {
+ const char *words = xs_dict_get(p_vars, "muted_words");
+
+ if (xs_is_string(words)) {
+ xs *new_words = xs_list_new();
+ xs *l = xs_split(words, "\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);
+
+ new_words = xs_list_insert_sorted(new_words, s2);
+ }
+
+ snac.config = xs_dict_set(snac.config, "muted_words", new_words);
+ user_persist(&snac, 0);
+ }
+
+ status = HTTP_STATUS_SEE_OTHER;
+ }
if (status == HTTP_STATUS_SEE_OTHER) {
const char *hard_redir = xs_dict_get(p_vars, "hard-redir");