Browse Source

Added emoji reactions (contributed by violette).

violette 7 months ago
parent
commit
85ed0eb0d5
9 changed files with 786 additions and 31 deletions
  1. 156 5
      activitypub.c
  2. 116 8
      data.c
  3. 3 0
      format.c
  4. 305 12
      html.c
  5. 1 1
      main.c
  6. 156 4
      mastoapi.c
  7. 9 1
      snac.h
  8. 7 0
      xs_unicode.h
  9. 33 0
      xs_url.h

+ 156 - 5
activitypub.c

@@ -4,6 +4,7 @@
 #include "xs.h"
 #include "xs_json.h"
 #include "xs_curl.h"
+#include "xs_url.h"
 #include "xs_mime.h"
 #include "xs_openssl.h"
 #include "xs_regex.h"
@@ -1530,11 +1531,24 @@ xs_dict *msg_update(snac *snac, const xs_dict *object)
 
 
 xs_dict *msg_admiration(snac *snac, const char *object, const char *type)
-/* creates a Like or Announce message */
+/* creates a Like, Announce or EmojiReact message */
 {
     xs *a_msg    = NULL;
     xs_dict *msg = NULL;
     xs *wrk      = NULL;
+    char t       = 0;
+
+    switch (*type) {
+        case 'L':
+            t = 'l';
+            break;
+        case 'A':
+            t = 'a';
+            break;
+        case 'E':
+            t = 'e';
+            break;
+    }
 
     /* call the object */
     timeline_request(snac, &object, &wrk, 0);
@@ -1542,7 +1556,7 @@ xs_dict *msg_admiration(snac *snac, const char *object, const char *type)
     if (valid_status(object_get(object, &a_msg))) {
         xs *rcpts = xs_list_new();
         xs *o_md5 = xs_md5_hex(object, strlen(object));
-        xs *id    = xs_fmt("%s/%s/%s", snac->actor, *type == 'L' ? "l" : "a", o_md5);
+        xs *id    = xs_fmt("%s/%c/%s", snac->actor, t, o_md5);
 
         msg = msg_base(snac, type, id, snac->actor, "@now", object);
 
@@ -1586,6 +1600,113 @@ xs_dict *msg_repulsion(snac *user, const char *id, const char *type)
     return msg;
 }
 
+xs_dict *msg_emoji_init(snac *snac, const char *mid, const char *eid)
+/* creates an emoji reaction from a local user */
+{
+    xs_dict *n_msg = msg_admiration(snac, mid, "EmojiReact");
+
+    eid = xs_strip_chars_i(xs_dup(eid), ":");
+    xs *content = NULL;
+    xs *tag = xs_list_new();
+    xs *dict = xs_dict_new();
+    xs *icon = xs_dict_new();
+    xs *accounts = xs_list_new();
+
+    /* may be a default emoji */
+    xs *eidd = xs_dup(eid);
+    const char *eidda = eid;
+
+    if (xs_is_emoji(xs_utf8_dec(&eidda)))
+        content = xs_dup(eid);
+
+    else if (*eid == '%') {
+        content = xs_url_dec_emoji(xs_dup(eid));
+        if (content == NULL) {
+            return NULL;
+        }
+    }
+    else if (xs_dict_get(emojis(), xs_fmt(":%s:", eid)) == NULL)
+        return NULL;
+    else {
+        content = xs_fmt(":%s:", eid);
+        icon = xs_dict_set(icon, "type", "Image");
+        icon = xs_dict_set(icon, "url", xs_fmt("%s/s/%s.png", snac->actor, eid));
+        dict = xs_dict_set(dict, "icon", icon);
+
+        dict = xs_dict_set(dict, "id", xs_fmt("%s/s/%s.png", snac->actor, eid));
+        dict = xs_dict_set(dict, "name", content);
+        dict = xs_dict_set(dict, "type", "Emoji");
+        tag = xs_list_append(tag, dict);
+    }
+
+    accounts = xs_list_append(accounts, snac->actor);
+
+    n_msg = xs_dict_set(n_msg, "content", content);
+    n_msg = xs_dict_set(n_msg, "accounts", accounts);
+    n_msg = xs_dict_set(n_msg, "attributedTo", xs_list_get(xs_dup(xs_dict_get(n_msg, "to")), 1));
+    n_msg = xs_dict_set(n_msg, "accountId", snac->uid);
+    n_msg = xs_dict_set(n_msg, "tag", tag);
+
+    int ret = timeline_admire(snac, xs_dict_get(n_msg, "object"), snac->actor, 1, n_msg);
+    if (ret == 200 || ret == 201) {
+        enqueue_message(snac, n_msg);
+        return n_msg;
+    }
+
+    return NULL;
+}
+
+xs_dict *msg_emoji_unreact(snac *user, const char *mid, const char *eid)
+/* creates an Undo + emoji reaction message */
+{
+    xs *a_msg    = NULL;
+    xs_dict *msg = NULL;
+
+    if (valid_status(object_get(mid, &a_msg))) {
+        /* create a clone of the original admiration message */
+        xs *object = msg_admiration(user, mid, "EmojiReact");
+
+        /* delete the published date */
+        object = xs_dict_del(object, "published");
+
+        /* create an undo message for this object */
+        msg = msg_undo(user, object);
+
+        /* copy the 'to' field */
+        msg = xs_dict_set(msg, "to", xs_dict_get(object, "to"));
+    }
+
+    xs *emotes = object_get_emoji_reacts(mid);
+    const char *v;
+    int c = 0;
+
+    /* may be a default emoji */
+    if (strlen(eid) == 12 && *eid == '%') {
+        eid = xs_url_dec(eid);
+        if (eid == NULL) {
+            return NULL;
+        }
+    }
+
+    /* lets get all emotes for this msg, and compare it to our content */
+    while (xs_list_next(emotes, &v, &c)) {
+        xs_dict *e = NULL;
+        if (valid_status(object_get_by_md5(v, &e))) {
+            const char *content = xs_dict_get(e, "content");
+            const char *id = xs_dict_get(e, "id");
+            const char *actor = xs_dict_get(e, "actor");
+            /* maybe formated as :{emoteName}: too */
+            if (xs_str_in(eid, content) != -1)
+                if (strcmp(user->actor, actor) == 0) {
+                    object_rm_emoji_react(mid, id);
+                    return msg;
+                }
+        }
+    }
+
+    return NULL;
+}
+
 
 xs_dict *msg_actor_place(snac *user, const char *label)
 /* creates a Place object, if the user has a location defined */
@@ -2605,6 +2726,16 @@ int process_input_message(snac *snac, const xs_dict *msg, const xs_dict *req)
     else
     if (strcmp(type, "Undo") == 0) { /** **/
         const char *id = xs_dict_get(object, "object");
+        const char *content = xs_dict_get(object, "content");
+        /* misskey sends emojis as like + tag */
+        xs *cd = xs_dup(content);
+        const char *sna = cd;
+        const xs_dict *tag = xs_dict_get(object, "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(object) != XSTYPE_DICT) {
             snac_debug(snac, 1, xs_fmt("undo: overriding utype %s | %s | %s",
@@ -2633,8 +2764,19 @@ int process_input_message(snac *snac, const xs_dict *msg, const xs_dict *req)
             else
                 snac_log(snac, xs_fmt("error deleting follower %s", actor));
         }
+        /* *key emojis are like w/ Emoji tag */
+        else
+        if ((isEmoji || strcmp(utype, "EmojiReact") == 0) &&
+                (content && strcmp(content, "♥") != 0)) {
+            const xs_val *mid = xs_dict_get(object, "id");
+            int status = object_rm_emoji_react((char *)id, mid);
+            /* ensure *key notifications type */
+            utype = "EmojiReact";
+
+            snac_log(snac, xs_fmt("Undo 'EmojiReact' for %s %d", id, status));
+        }
         else
-        if (strcmp(utype, "Like") == 0 || strcmp(utype, "EmojiReact") == 0) { /** **/
+        if (strcmp(utype, "Like") == 0) { /** **/
             int status = object_unadmire(id, actor, 1);
 
             snac_log(snac, xs_fmt("Undo '%s' for %s %d", utype, id, status));
@@ -2771,13 +2913,22 @@ int process_input_message(snac *snac, const xs_dict *msg, const xs_dict *req)
     }
     else
     if (strcmp(type, "Like") == 0 || strcmp(type, "EmojiReact") == 0) { /** **/
+        /* misskey sends emojis as Like + tag.
+         * It is easier to handle them both at the same time. */
+        const char *sna = xs_dict_get(msg, "content");
+        const xs_dict *tag = xs_dict_get(msg, "tag");
+        unsigned int utf = xs_utf8_dec((const char **)&sna);
+
+        if (xs_is_emoji(utf) || (tag && xs_list_len(tag) > 0))
+            type = "EmojiReact";
+
         if (xs_type(object) == XSTYPE_DICT)
             object = xs_dict_get(object, "id");
 
         if (xs_is_null(object))
             snac_log(snac, xs_fmt("malformed message: no 'id' field"));
         else
-        if (timeline_admire(snac, object, actor, 1) == HTTP_STATUS_CREATED)
+        if (timeline_admire(snac, object, actor, 1, xs_dup(msg)) == HTTP_STATUS_CREATED)
             snac_log(snac, xs_fmt("new '%s' %s %s", type, actor, object));
         else
             snac_log(snac, xs_fmt("repeated '%s' from %s to %s", type, actor, object));
@@ -2818,7 +2969,7 @@ int process_input_message(snac *snac, const xs_dict *msg, const xs_dict *req)
                         xs *this_relay = xs_fmt("%s/relay", srv_baseurl);
 
                         if (strcmp(actor, this_relay) != 0) {
-                            if (valid_status(timeline_admire(snac, object, actor, 0)))
+                            if (valid_status(timeline_admire(snac, object, actor, 0, a_msg)))
                                 snac_log(snac, xs_fmt("new 'Announce' %s %s", actor, object));
                             else
                                 snac_log(snac, xs_fmt("repeated 'Announce' from %s to %s",

+ 116 - 8
data.c

@@ -1043,6 +1043,14 @@ xs_list *object_children(const char *id)
 }
 
 
+xs_list *object_get_emoji_reacts(const char *id)
+/* returns the list of an object's emoji reactions */
+{
+    xs *fn = _object_index_fn(id, "_e.idx");
+    return index_list(fn, XS_ALL);
+}
+
+
 xs_list *object_likes(const char *id)
 {
     xs *fn = _object_index_fn(id, "_l.idx");
@@ -1086,12 +1094,26 @@ int object_admire(const char *id, const char *actor, int like)
 
 
 int object_unadmire(const char *id, const char *actor, int like)
-/* actor no longer likes or announces this object */
+/* actor retrives their likes, announces or emojis this object */
 {
+    switch (like) {
+    case 0:
+        like = 'a';
+        break;
+    case 1:
+        like = 'l';
+        break;
+    case 2:
+        like = 'e';
+        break;
+    }
     int status;
     xs *fn = _object_fn(id);
 
-    fn = xs_replace_i(fn, ".json", like ? "_l.idx" : "_a.idx");
+    char sfx[7] = "_x.idx";
+    sfx[1] = like;
+
+    fn = xs_replace_i(fn, ".json", sfx);
 
     status = index_del(fn, actor);
 
@@ -1099,7 +1121,46 @@ int object_unadmire(const char *id, const char *actor, int like)
         index_gc(fn);
 
     srv_debug(0,
-        xs_fmt("object_unadmire (%s) %s %s %d", like ? "Like" : "Announce", actor, fn, status));
+        xs_fmt("object_unadmire (%s) %s %s %d", like >= 'e' ?
+            (like == 'l' ? "Like" : "EmojiReact") : "Announce" , actor, fn, status));
+
+    return status;
+}
+
+int object_emoji_react(const char *mid, const char *eid)
+/* actor reacts w/ an emoji */
+{
+    int status = HTTP_STATUS_OK;
+    xs *fn     = _object_fn(mid);
+
+    fn = xs_replace_i(fn, ".json", "_e.idx");
+
+    if (!index_in(fn, eid)) {
+        status = index_add(fn, eid);
+
+        srv_debug(1, xs_fmt("object_emoji_react (%s) added %s to %s", "EmojiReact", eid, fn));
+    }
+
+    return status;
+}
+
+
+int object_rm_emoji_react(const char *mid, const char *eid)
+/* actor retrives their emoji reaction */
+{
+    int status;
+    xs *fn = _object_fn(mid);
+
+    fn = xs_replace_i(fn, ".json", "_e.idx");
+
+    status = index_del(fn, eid);
+    object_del(eid);
+
+    if (valid_status(status))
+        index_gc(fn);
+
+    srv_debug(0,
+        xs_fmt("object_unadmire (EmojiReact) %s %s %d", eid, fn, status));
 
     return status;
 }
@@ -1506,19 +1567,47 @@ int timeline_add(snac *snac, const char *id, const xs_dict *o_msg)
 }
 
 
-int timeline_admire(snac *snac, const char *id, const char *admirer, int like)
-/* updates a timeline entry with a new admiration */
+int timeline_emoji_react(const char *act, const char *id, xs_dict *msg)
+/* adds an emoji reaction to a message */
+{
+    msg = xs_dict_append(msg, "attributedTo", act);
+    msg = xs_dict_set(msg, "type", "EmojiReact");
+    const char *emote_id = xs_dict_get(msg, "id");
+
+    int ret = object_add(emote_id, msg);
+    if (ret == HTTP_STATUS_OK || ret == HTTP_STATUS_CREATED)
+        ret = object_emoji_react(id, emote_id);
+
+    return ret;
+}
+
+
+int timeline_admire(snac *snac, const char *id,
+                    const char *admirer, int like, xs_dict *msg)
+/* updates a timeline entry with a new admiration or emoji reaction */
 {
+    int ret;
+    const char *content = xs_dict_get_path(msg, "content");
+    const char *type = xs_dict_get_path(msg, "type");
+
     /* if we are admiring this, add to both timelines */
     if (!like && strcmp(admirer, snac->actor) == 0) {
         object_user_cache_add(snac, id, "public");
         object_user_cache_add(snac, id, "private");
     }
 
-    int ret = object_admire(id, admirer, like);
+    /* use utf <3 as a like, as it is ugly */
+    if (type && xs_match(type, "Like|EmojiReact|Emoji") &&
+            content && strcmp(content, "❤") != 0) {
+        ret = timeline_emoji_react(xs_dup(snac->actor), id, xs_dup(msg));
+        snac_debug(snac, 1, xs_fmt("timeline_emoji_react %s", id));
+    }
 
-    snac_debug(snac, 1, xs_fmt("timeline_admire (%s) %s %s",
-            like ? "Like" : "Announce", id, admirer));
+    else {
+        ret = object_admire(id, admirer, like);
+        snac_debug(snac, 1, xs_fmt("timeline_admire (%s) %s %s",
+                like ? "Like" : "Announce", id, admirer));
+    }
 
     return ret;
 }
@@ -1867,6 +1956,25 @@ xs_list *muted_list(snac *user)
     return l;
 }
 
+/** emojis react **/
+
+const xs_str *emoji_reacted(snac *user, const char *id)
+/* returns the emoji an user reacted to a message */
+{
+    xs *emojis = object_get_emoji_reacts(id);
+    int c = 0;
+    const char *v;
+    xs_dict *msg;
+
+    while (xs_list_next(emojis, &v, &c)) {
+        if (object_get_by_md5(v, &msg)) {
+            const xs_val *act = xs_dict_get(msg, "actor");
+            if (act && strcmp(act, user->actor) == 0)
+                return xs_dict_get(msg, "content");
+        }
+    }
+    return NULL;
+}
 
 /** bookmarking **/
 

+ 3 - 0
format.c

@@ -459,6 +459,9 @@ xs_str *sanitize(const char *content)
     char *p;
     const char *v;
 
+    if (!content)
+        return NULL;
+
     sl = xs_regex_split(content, "</?[^>]+>");
 
     p = sl;

+ 305 - 12
html.c

@@ -54,9 +54,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)
+/* 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 +70,15 @@ 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)
+            class = xs_str_cat(class, " ", xs_join(cl, " "));
 
-        const xs_dict *v;
         int c = 0;
+        const xs_val *v;
+
+        c = 0;
 
         xs_set rep_emoji;
         xs_set_init(&rep_emoji);
@@ -100,6 +105,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);
 
+                        act = act ? xs_fmt("%s\n%s", n, act) : 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 +115,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 +137,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 */
 {
@@ -430,6 +444,52 @@ void html_note_render_visibility(snac* user, xs_html *form, const int scope)
     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,
                    const char *ta_plh, const char *ta_content,
@@ -1356,6 +1416,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 **/
@@ -2019,6 +2101,21 @@ xs_html *html_entry_controls(snac *user, const char *actor,
             xs_html_tag("p", NULL));
     }
 
+    { /** emoji react **/
+        /* the post textarea */
+        xs *div_id  = xs_fmt("%s_reply", md5);
+        xs *form_id = xs_fmt("%s_reply_form", md5);
+
+        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,
+                emoji_reacted(user, id))),
+            xs_html_tag("p", NULL));
+    }
+
     { /** reply **/
         /* the post textarea */
         xs *ct      = build_mentions(user, msg);
@@ -2345,6 +2442,168 @@ 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;
+    {
+        int c = 0;
+        const xs_dict *k;
+        xs *ls = xs_list_new();
+        xs *sfrl = xs_dict_new();
+        xs *rl = object_get_emoji_reacts(id);
+
+        xs_dict *m = NULL;
+        while (xs_list_next(rl, &v, &c)) {
+            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);
+                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();
+                fl = xs_list_append(fl, xs_fmt("%d", count), actors, xs_fmt("%d", me));
+                sfrl = xs_dict_append(sfrl, content, fl);
+            }
+        }
+
+        c = 0;
+
+        while (xs_list_next(rl, &k, &c)) {
+            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);
+                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';
+
+                if (!xs_is_null(nb)) {
+                    is_emoji = 1;
+
+                    const char *act = atoi(nb) > 1 ?
+                        xs_fmt("%d different actors \n\t%s", atoi(nb), xs_join(actors, ",\n\t")) :
+                        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);
+
+                        if (me)
+                            class = xs_list_append(class, "snac-reacted");
+
+                        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", xs_join(class, " ")));
+
+                        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");
+                            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(xs_fmt("&#%d", utf)),
+                                    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", xs_join(class, " ")),
+                                    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");
@@ -2371,7 +2630,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);
@@ -3686,9 +3946,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;
             }
         }
@@ -4583,8 +4852,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;
                     }
@@ -4892,12 +5161,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) { /** **/
+            const char *eid = xs_dict_get(p_vars, "eid");
+
+            eid = xs_strip_chars_i(xs_dup(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
@@ -4905,8 +5198,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

+ 1 - 1
main.c

@@ -498,7 +498,7 @@ int main(int argc, char *argv[])
 
         if (msg != NULL) {
             enqueue_message(&snac, msg);
-            timeline_admire(&snac, xs_dict_get(msg, "object"), snac.actor, 0);
+            timeline_admire(&snac, xs_dict_get(msg, "object"), snac.actor, 0, "");
 
             if (dbglevel) {
                 xs_json_dump(msg, 4, stdout);

+ 156 - 4
mastoapi.c

@@ -1158,6 +1158,97 @@ xs_dict *mastoapi_status(snac *snac, const xs_dict *msg)
         st = xs_dict_append(st, "tags",     htl);
         st = xs_dict_append(st, "emojis",   eml);
     }
+    {
+        xs *rl   = object_get_emoji_reacts(id);
+        xs *frl  = xs_list_new(); /* final */
+        xs *sfrl = xs_dict_new(); /* seen */
+        int c = 0;
+        const char *v;
+
+        xs_dict *msg = NULL;
+        while (xs_list_next(rl, &v, &c)) {
+            if (valid_status(object_get_by_md5(v, &msg))) {
+                const char *content = xs_dict_get(msg, "content");
+                const char *actor = xs_dict_get(msg, "actor");
+                const xs_list *contentl = xs_dict_get(sfrl, content);
+                /* NOTE: idk when there are no actor, but i encountered that bug.
+                 * Probably because of one of my previous attempts.
+                 * Keeping this just in case, can remove later */
+                const char *me = actor && strcmp(actor, snac->actor) == 0 ?
+                    xs_stock(XSTYPE_TRUE) : xs_stock(XSTYPE_FALSE);
+                int count = 1;
+
+                if (contentl) {
+                    count = atoi(xs_list_get(contentl, 0)) + 1;
+                    if (strncmp(xs_list_get(contentl, 1), xs_stock(XSTYPE_TRUE), 1) == 0)
+                        me = xs_stock(XSTYPE_TRUE);
+                }
+
+                xs *fl = xs_list_new();
+                fl = xs_list_append(fl, xs_fmt("%d", count), me);
+                sfrl = xs_dict_append(sfrl, content, fl);
+            }
+        }
+
+        c = 0;
+
+        while (xs_list_next(rl, &v, &c)) {
+            if (valid_status(object_get_by_md5(v, &msg))) {
+                xs *d1 = xs_dict_new();
+
+                const xs_dict *icon = xs_dict_get(xs_list_get(xs_dict_get(msg, "tag"), 0), "icon");
+                const char *o_url = xs_dict_get(icon, "url");
+                const char *name = xs_dict_get(msg, "content");
+                const char *actor = xs_dict_get(msg, "actor");
+
+                xs *nm = xs_dup(name);
+                xs *url = NULL;
+
+                if (!xs_is_null(o_url)) {
+                    if (actor && snac && !strcmp(actor, snac->actor))
+                        url = make_url(o_url, NULL, 1);
+                    else
+                        url = xs_dup(o_url);
+                }
+
+                xs *accounts = xs_list_new();
+                if (actor) {
+                    xs *d2 = xs_dict_new();
+                    object_get(actor, &d2);
+                    xs *e_acct = mastoapi_account(snac, d2);
+                    accounts = xs_list_append(accounts, e_acct);
+                }
+
+                const xs_list *item = xs_dict_get(sfrl, nm);
+                const xs_str *nb = xs_list_get(item, 0);
+                const xs_val *me = xs_list_get(item, 1);
+                if (item == NULL)
+                    continue;
+
+                if (nm && strcmp(nm, "")) {
+                    if (url && strcmp(url, "")) {
+                        d1 = xs_dict_append(d1, "name",              nm);
+                        d1 = xs_dict_append(d1, "shortcode",         nm);
+                        d1 = xs_dict_append(d1, "accounts",          accounts);
+                        d1 = xs_dict_append(d1, "me",                me);
+                        d1 = xs_dict_append(d1, "url",               url);
+                        d1 = xs_dict_append(d1, "static_url",        url);
+                        d1 = xs_dict_append(d1, "visible_in_picker", xs_stock(XSTYPE_TRUE));
+                        d1 = xs_dict_append(d1, "count", nb);
+                    } else {
+                        d1 = xs_dict_append(d1, "name",              nm);
+                        d1 = xs_dict_append(d1, "count",             nb);
+                        d1 = xs_dict_append(d1, "me",                me);
+                        d1 = xs_dict_append(d1, "visible_in_picker", xs_stock(XSTYPE_TRUE));
+                    }
+                    sfrl = xs_dict_del(sfrl, nm);
+                    frl = xs_list_append(frl, d1);
+                }
+            }
+        }
+
+        st = xs_dict_append(st, "reactions", frl);
+    }
 
     xs_free(idx);
     xs_free(ixc);
@@ -2202,15 +2293,24 @@ int mastoapi_get_handler(const xs_dict *req, const char *q_path,
                 if (noti == NULL)
                     continue;
 
+                const xs_dict *tag = xs_list_get(xs_dict_get_path(noti, "msg.tag"), 0);
+
                 const char *type  = xs_dict_get(noti, "type");
                 const char *utype = xs_dict_get(noti, "utype");
                 const char *objid = xs_dict_get(noti, "objid");
                 const char *id    = xs_dict_get(noti, "id");
                 const char *actid = xs_dict_get(noti, "actor");
+
+                int isEmoji = 0;
+
                 xs *fid = xs_replace(id, ".", "");
                 xs *actor = NULL;
                 xs *entry = NULL;
 
+                if (tag) {
+                    isEmoji = strcmp(xs_dict_get(tag, "type"), "Emoji") ? 0 : 1;
+                }
+
                 if (!valid_status(actor_get(actid, &actor)))
                     continue;
 
@@ -2234,9 +2334,12 @@ int mastoapi_get_handler(const xs_dict *req, const char *q_path,
                 }
 
                 /* convert the type */
-                if (strcmp(type, "Like") == 0 || strcmp(type, "EmojiReact") == 0)
+                if (strcmp(type, "Like") == 0 && !isEmoji)
                     type = "favourite";
                 else
+                if (isEmoji || strcmp(type, "EmojiReact") == 0)
+                    type = "reaction";
+                else
                 if (strcmp(type, "Announce") == 0)
                     type = "reblog";
                 else
@@ -2277,8 +2380,29 @@ int mastoapi_get_handler(const xs_dict *req, const char *q_path,
                 if (strcmp(type, "follow") != 0 && !xs_is_null(objid)) {
                     xs *st = mastoapi_status(&snac1, entry);
 
-                    if (st)
+                    if (st) {
                         mn = xs_dict_append(mn, "status", st);
+
+                        if (strcmp(type, "reaction") == 0 && !xs_is_null(objid)) {
+                            const char *eid = NULL;
+                            const char *url = NULL;
+                            int utf = 0;
+
+                            const xs_dict *tag = xs_list_get(xs_dict_get_path(noti, "msg.tag"), 0);
+                            const char *content = xs_dict_get_path(noti, "msg.content");
+
+                            url = xs_dict_get(xs_dict_get(tag, "icon"), "url");
+                            eid = xs_dict_get(tag, "name");
+
+                            if (eid && url) {
+                                mn = xs_dict_append(mn, "emoji", eid);
+                                mn = xs_dict_append(mn, "emoji_url", url);
+                            }
+
+                            if (xs_is_emoji((utf = xs_utf8_dec(&content))))
+                                mn = xs_dict_append(mn, "name", xs_fmt("&#%d;", utf));
+                        }
+                    }
                 }
 
                 out = xs_list_append(out, mn);
@@ -2594,6 +2718,11 @@ int mastoapi_get_handler(const xs_dict *req, const char *q_path,
                 "\"max_expiration\":2629746,"
                 "\"max_options\":8,\"min_expiration\":300}");
             cfg = xs_dict_append(cfg, "polls", d14);
+
+
+            xs *d15 = xs_json_loads("{\"max_reactions\":50}");
+                cfg = xs_dict_append(cfg, "reactions", d15);
+
         }
 
         ins = xs_dict_append(ins, "configuration", cfg);
@@ -3219,7 +3348,7 @@ int mastoapi_post_handler(const xs_dict *req, const char *q_path,
 
                         if (n_msg != NULL) {
                             enqueue_message(&snac, n_msg);
-                            timeline_admire(&snac, xs_dict_get(n_msg, "object"), snac.actor, 1);
+                            timeline_admire(&snac, xs_dict_get(n_msg, "object"), snac.actor, 1, msg);
 
                             out = mastoapi_status(&snac, msg);
                         }
@@ -3234,13 +3363,36 @@ int mastoapi_post_handler(const xs_dict *req, const char *q_path,
                             out = mastoapi_status(&snac, msg);
                         }
                     }
+                    else
+                    if (strcmp(op, "react") == 0) { /** **/
+                        const char *eid = xs_list_get(l, 5);
+                        xs *n_msg = msg_emoji_init(&snac, id, eid);
+                        if (n_msg)
+                            out = mastoapi_status(&snac, n_msg);
+                    }
+                    else
+                    if (strcmp(op, "unreact") == 0) { /** **/
+                        const char *eid = xs_list_get(l, 5);
+                        xs *content = xs_fmt("%s", eid);
+
+                        if (eid) {
+                            xs *n_msg = msg_emoji_unreact(&snac, id, content);
+
+                            if (n_msg != NULL) {
+                                enqueue_message(&snac, n_msg);
+
+                                out = mastoapi_status(&snac, msg);
+                            }
+                        }
+                    }
+
                     else
                     if (strcmp(op, "reblog") == 0) { /** **/
                         xs *n_msg = msg_admiration(&snac, id, "Announce");
 
                         if (n_msg != NULL) {
                             enqueue_message(&snac, n_msg);
-                            timeline_admire(&snac, xs_dict_get(n_msg, "object"), snac.actor, 0);
+                            timeline_admire(&snac, xs_dict_get(n_msg, "object"), snac.actor, 0, msg);
 
                             out = mastoapi_status(&snac, msg);
                         }

+ 9 - 1
snac.h

@@ -146,12 +146,15 @@ void object_touch(const char *id);
 int object_admire(const char *id, const char *actor, int like);
 int object_unadmire(const char *id, const char *actor, int like);
 
+int object_emoji_react(const char *mid, const char *eid);
+int object_rm_emoji_react(const char *mid, const char *eid);
 int object_likes_len(const char *id);
 int object_announces_len(const char *id);
 
 xs_list *object_children(const char *id);
 xs_list *object_likes(const char *id);
 xs_list *object_announces(const char *id);
+xs_list *object_get_emoji_reacts(const char *id);
 int object_parent(const char *md5, char parent[MD5_HEX_SIZE]);
 
 int object_user_cache_add(snac *snac, const char *id, const char *cachedir);
@@ -180,7 +183,8 @@ xs_str *user_index_fn(snac *user, const char *idx_name);
 xs_list *timeline_simple_list(snac *user, const char *idx_name, int skip, int show, int *more);
 xs_list *timeline_list(snac *snac, const char *idx_name, int skip, int show, int *more);
 int timeline_add(snac *snac, const char *id, const xs_dict *o_msg);
-int timeline_admire(snac *snac, const char *id, const char *admirer, int like);
+int timeline_admire(snac *snac, const char *id, const char *admirer, int like, xs_dict *msg);
+int timeline_emoji_react(const char *atto, const char *id, xs_dict *o_msg);
 
 xs_list *timeline_top_level(snac *snac, const xs_list *list);
 void timeline_add_mark(snac *user);
@@ -201,6 +205,8 @@ void unmute(snac *snac, const char *actor);
 int is_muted(snac *snac, const char *actor);
 xs_list *muted_list(snac *user);
 
+const xs_str *emoji_reacted(snac *user, const char *id);
+
 int is_bookmarked(snac *user, const char *id);
 int bookmark(snac *user, const char *id);
 int unbookmark(snac *user, const char *id);
@@ -358,6 +364,8 @@ xs_list *get_attachments(const xs_dict *msg);
 
 xs_dict *msg_admiration(snac *snac, const char *object, const char *type);
 xs_dict *msg_repulsion(snac *user, const char *id, const char *type);
+xs_dict *msg_emoji_init(snac *user, const char *mid, const char *eid);
+xs_dict *msg_emoji_unreact(snac *user, const char *id, const char *type);
 xs_dict *msg_create(snac *snac, const xs_dict *object);
 xs_dict *msg_follow(snac *snac, const char *actor);
 

+ 7 - 0
xs_unicode.h

@@ -22,6 +22,7 @@
  int xs_unicode_nfc(unsigned int base, unsigned int diac, unsigned int *cpoint);
  int xs_unicode_is_alpha(unsigned int cpoint);
  int xs_unicode_is_right_to_left(unsigned int cpoint);
+ int xs_is_emoji(unsigned int cpoint);
 
 #ifdef _XS_H
  xs_str *xs_utf8_insert(xs_str *str, unsigned int cpoint, int *offset);
@@ -134,6 +135,12 @@ static unsigned int xs_unicode_width_table[] = {
     0x20000,    0x2fffd,    2       /* more CJK */
 };
 
+/* magic number from https://en.wikipedia.org/wiki/Emoji#Unicode_blocks */
+int xs_is_emoji(unsigned int cpoint) {
+/* returns wether the input is an utf8 emoji */
+    return cpoint > 0x00A9 && cpoint < 0x1ffff;
+}
+
 int xs_unicode_width(unsigned int cpoint)
 /* returns the width in columns of a Unicode codepoint (somewhat simplified) */
 {

+ 33 - 0
xs_url.h

@@ -6,6 +6,7 @@
 
 xs_str *xs_url_dec(const char *str);
 xs_str *xs_url_enc(const char *str);
+xs_str *xs_url_dec_emoji(const char *str);
 xs_dict *xs_url_vars(const char *str);
 xs_dict *xs_multipart_form_data(const char *payload, int p_size, const char *header);
 
@@ -79,6 +80,38 @@ xs_str *xs_url_dec(const char *str)
 }
 
 
+xs_str *xs_url_dec_emoji(const char *str)
+/* decodes an URL, returns NULL if not every char is an encoded hex */
+{
+    xs_str *s = xs_str_new(NULL);
+
+    while (*str) {
+        if (!xs_is_string(str))
+            break;
+
+        if (*str == '%') {
+            unsigned int i;
+
+            if (sscanf(str + 1, "%02x", &i) == 1) {
+                unsigned char uc = i;
+
+                if (!xs_is_string((char *)&uc))
+                    break;
+
+                s = xs_append_m(s, (char *)&uc, 1);
+                str += 2;
+            }
+        }
+        else
+            return NULL;
+
+        str++;
+    }
+
+    return s;
+}
+
+
 xs_str *xs_url_enc(const char *str)
 /* URL-encodes a string (RFC 3986) */
 {