commit 85ed0eb

violette  ·  2025-12-18 06:58:24 +0000 UTC
parent e66446a
Added emoji reactions (contributed by violette).
9 files changed,  +786, -31
M data.c
M html.c
M main.c
M snac.h
+156, -5
  1@@ -4,6 +4,7 @@
  2 #include "xs.h"
  3 #include "xs_json.h"
  4 #include "xs_curl.h"
  5+#include "xs_url.h"
  6 #include "xs_mime.h"
  7 #include "xs_openssl.h"
  8 #include "xs_regex.h"
  9@@ -1530,11 +1531,24 @@ xs_dict *msg_update(snac *snac, const xs_dict *object)
 10 
 11 
 12 xs_dict *msg_admiration(snac *snac, const char *object, const char *type)
 13-/* creates a Like or Announce message */
 14+/* creates a Like, Announce or EmojiReact message */
 15 {
 16     xs *a_msg    = NULL;
 17     xs_dict *msg = NULL;
 18     xs *wrk      = NULL;
 19+    char t       = 0;
 20+
 21+    switch (*type) {
 22+        case 'L':
 23+            t = 'l';
 24+            break;
 25+        case 'A':
 26+            t = 'a';
 27+            break;
 28+        case 'E':
 29+            t = 'e';
 30+            break;
 31+    }
 32 
 33     /* call the object */
 34     timeline_request(snac, &object, &wrk, 0);
 35@@ -1542,7 +1556,7 @@ xs_dict *msg_admiration(snac *snac, const char *object, const char *type)
 36     if (valid_status(object_get(object, &a_msg))) {
 37         xs *rcpts = xs_list_new();
 38         xs *o_md5 = xs_md5_hex(object, strlen(object));
 39-        xs *id    = xs_fmt("%s/%s/%s", snac->actor, *type == 'L' ? "l" : "a", o_md5);
 40+        xs *id    = xs_fmt("%s/%c/%s", snac->actor, t, o_md5);
 41 
 42         msg = msg_base(snac, type, id, snac->actor, "@now", object);
 43 
 44@@ -1586,6 +1600,113 @@ xs_dict *msg_repulsion(snac *user, const char *id, const char *type)
 45     return msg;
 46 }
 47 
 48+xs_dict *msg_emoji_init(snac *snac, const char *mid, const char *eid)
 49+/* creates an emoji reaction from a local user */
 50+{
 51+    xs_dict *n_msg = msg_admiration(snac, mid, "EmojiReact");
 52+
 53+    eid = xs_strip_chars_i(xs_dup(eid), ":");
 54+    xs *content = NULL;
 55+    xs *tag = xs_list_new();
 56+    xs *dict = xs_dict_new();
 57+    xs *icon = xs_dict_new();
 58+    xs *accounts = xs_list_new();
 59+
 60+    /* may be a default emoji */
 61+    xs *eidd = xs_dup(eid);
 62+    const char *eidda = eid;
 63+
 64+    if (xs_is_emoji(xs_utf8_dec(&eidda)))
 65+        content = xs_dup(eid);
 66+
 67+    else if (*eid == '%') {
 68+        content = xs_url_dec_emoji(xs_dup(eid));
 69+        if (content == NULL) {
 70+            return NULL;
 71+        }
 72+    }
 73+    else if (xs_dict_get(emojis(), xs_fmt(":%s:", eid)) == NULL)
 74+        return NULL;
 75+    else {
 76+        content = xs_fmt(":%s:", eid);
 77+        icon = xs_dict_set(icon, "type", "Image");
 78+        icon = xs_dict_set(icon, "url", xs_fmt("%s/s/%s.png", snac->actor, eid));
 79+        dict = xs_dict_set(dict, "icon", icon);
 80+
 81+        dict = xs_dict_set(dict, "id", xs_fmt("%s/s/%s.png", snac->actor, eid));
 82+        dict = xs_dict_set(dict, "name", content);
 83+        dict = xs_dict_set(dict, "type", "Emoji");
 84+        tag = xs_list_append(tag, dict);
 85+    }
 86+
 87+    accounts = xs_list_append(accounts, snac->actor);
 88+
 89+    n_msg = xs_dict_set(n_msg, "content", content);
 90+    n_msg = xs_dict_set(n_msg, "accounts", accounts);
 91+    n_msg = xs_dict_set(n_msg, "attributedTo", xs_list_get(xs_dup(xs_dict_get(n_msg, "to")), 1));
 92+    n_msg = xs_dict_set(n_msg, "accountId", snac->uid);
 93+    n_msg = xs_dict_set(n_msg, "tag", tag);
 94+
 95+    int ret = timeline_admire(snac, xs_dict_get(n_msg, "object"), snac->actor, 1, n_msg);
 96+    if (ret == 200 || ret == 201) {
 97+        enqueue_message(snac, n_msg);
 98+        return n_msg;
 99+    }
100+
101+    return NULL;
102+}
103+
104+xs_dict *msg_emoji_unreact(snac *user, const char *mid, const char *eid)
105+/* creates an Undo + emoji reaction message */
106+{
107+    xs *a_msg    = NULL;
108+    xs_dict *msg = NULL;
109+
110+    if (valid_status(object_get(mid, &a_msg))) {
111+        /* create a clone of the original admiration message */
112+        xs *object = msg_admiration(user, mid, "EmojiReact");
113+
114+        /* delete the published date */
115+        object = xs_dict_del(object, "published");
116+
117+        /* create an undo message for this object */
118+        msg = msg_undo(user, object);
119+
120+        /* copy the 'to' field */
121+        msg = xs_dict_set(msg, "to", xs_dict_get(object, "to"));
122+    }
123+
124+    xs *emotes = object_get_emoji_reacts(mid);
125+    const char *v;
126+    int c = 0;
127+
128+    /* may be a default emoji */
129+    if (strlen(eid) == 12 && *eid == '%') {
130+        eid = xs_url_dec(eid);
131+        if (eid == NULL) {
132+            return NULL;
133+        }
134+    }
135+
136+    /* lets get all emotes for this msg, and compare it to our content */
137+    while (xs_list_next(emotes, &v, &c)) {
138+        xs_dict *e = NULL;
139+        if (valid_status(object_get_by_md5(v, &e))) {
140+            const char *content = xs_dict_get(e, "content");
141+            const char *id = xs_dict_get(e, "id");
142+            const char *actor = xs_dict_get(e, "actor");
143+            /* maybe formated as :{emoteName}: too */
144+            if (xs_str_in(eid, content) != -1)
145+                if (strcmp(user->actor, actor) == 0) {
146+                    object_rm_emoji_react(mid, id);
147+                    return msg;
148+                }
149+        }
150+    }
151+
152+    return NULL;
153+}
154+
155 
156 xs_dict *msg_actor_place(snac *user, const char *label)
157 /* creates a Place object, if the user has a location defined */
158@@ -2605,6 +2726,16 @@ int process_input_message(snac *snac, const xs_dict *msg, const xs_dict *req)
159     else
160     if (strcmp(type, "Undo") == 0) { /** **/
161         const char *id = xs_dict_get(object, "object");
162+        const char *content = xs_dict_get(object, "content");
163+        /* misskey sends emojis as like + tag */
164+        xs *cd = xs_dup(content);
165+        const char *sna = cd;
166+        const xs_dict *tag = xs_dict_get(object, "tag");
167+        unsigned int utf = xs_utf8_dec((const char **)&sna);
168+
169+        int isEmoji = 0;
170+        if (xs_is_emoji(utf) || (tag && xs_list_len(tag) > 0))
171+            isEmoji = 1;
172 
173         if (xs_type(object) != XSTYPE_DICT) {
174             snac_debug(snac, 1, xs_fmt("undo: overriding utype %s | %s | %s",
175@@ -2633,8 +2764,19 @@ int process_input_message(snac *snac, const xs_dict *msg, const xs_dict *req)
176             else
177                 snac_log(snac, xs_fmt("error deleting follower %s", actor));
178         }
179+        /* *key emojis are like w/ Emoji tag */
180+        else
181+        if ((isEmoji || strcmp(utype, "EmojiReact") == 0) &&
182+                (content && strcmp(content, "♥") != 0)) {
183+            const xs_val *mid = xs_dict_get(object, "id");
184+            int status = object_rm_emoji_react((char *)id, mid);
185+            /* ensure *key notifications type */
186+            utype = "EmojiReact";
187+
188+            snac_log(snac, xs_fmt("Undo 'EmojiReact' for %s %d", id, status));
189+        }
190         else
191-        if (strcmp(utype, "Like") == 0 || strcmp(utype, "EmojiReact") == 0) { /** **/
192+        if (strcmp(utype, "Like") == 0) { /** **/
193             int status = object_unadmire(id, actor, 1);
194 
195             snac_log(snac, xs_fmt("Undo '%s' for %s %d", utype, id, status));
196@@ -2771,13 +2913,22 @@ int process_input_message(snac *snac, const xs_dict *msg, const xs_dict *req)
197     }
198     else
199     if (strcmp(type, "Like") == 0 || strcmp(type, "EmojiReact") == 0) { /** **/
200+        /* misskey sends emojis as Like + tag.
201+         * It is easier to handle them both at the same time. */
202+        const char *sna = xs_dict_get(msg, "content");
203+        const xs_dict *tag = xs_dict_get(msg, "tag");
204+        unsigned int utf = xs_utf8_dec((const char **)&sna);
205+
206+        if (xs_is_emoji(utf) || (tag && xs_list_len(tag) > 0))
207+            type = "EmojiReact";
208+
209         if (xs_type(object) == XSTYPE_DICT)
210             object = xs_dict_get(object, "id");
211 
212         if (xs_is_null(object))
213             snac_log(snac, xs_fmt("malformed message: no 'id' field"));
214         else
215-        if (timeline_admire(snac, object, actor, 1) == HTTP_STATUS_CREATED)
216+        if (timeline_admire(snac, object, actor, 1, xs_dup(msg)) == HTTP_STATUS_CREATED)
217             snac_log(snac, xs_fmt("new '%s' %s %s", type, actor, object));
218         else
219             snac_log(snac, xs_fmt("repeated '%s' from %s to %s", type, actor, object));
220@@ -2818,7 +2969,7 @@ int process_input_message(snac *snac, const xs_dict *msg, const xs_dict *req)
221                         xs *this_relay = xs_fmt("%s/relay", srv_baseurl);
222 
223                         if (strcmp(actor, this_relay) != 0) {
224-                            if (valid_status(timeline_admire(snac, object, actor, 0)))
225+                            if (valid_status(timeline_admire(snac, object, actor, 0, a_msg)))
226                                 snac_log(snac, xs_fmt("new 'Announce' %s %s", actor, object));
227                             else
228                                 snac_log(snac, xs_fmt("repeated 'Announce' from %s to %s",
M data.c
+116, -8
  1@@ -1043,6 +1043,14 @@ xs_list *object_children(const char *id)
  2 }
  3 
  4 
  5+xs_list *object_get_emoji_reacts(const char *id)
  6+/* returns the list of an object's emoji reactions */
  7+{
  8+    xs *fn = _object_index_fn(id, "_e.idx");
  9+    return index_list(fn, XS_ALL);
 10+}
 11+
 12+
 13 xs_list *object_likes(const char *id)
 14 {
 15     xs *fn = _object_index_fn(id, "_l.idx");
 16@@ -1086,12 +1094,26 @@ int object_admire(const char *id, const char *actor, int like)
 17 
 18 
 19 int object_unadmire(const char *id, const char *actor, int like)
 20-/* actor no longer likes or announces this object */
 21+/* actor retrives their likes, announces or emojis this object */
 22 {
 23+    switch (like) {
 24+    case 0:
 25+        like = 'a';
 26+        break;
 27+    case 1:
 28+        like = 'l';
 29+        break;
 30+    case 2:
 31+        like = 'e';
 32+        break;
 33+    }
 34     int status;
 35     xs *fn = _object_fn(id);
 36 
 37-    fn = xs_replace_i(fn, ".json", like ? "_l.idx" : "_a.idx");
 38+    char sfx[7] = "_x.idx";
 39+    sfx[1] = like;
 40+
 41+    fn = xs_replace_i(fn, ".json", sfx);
 42 
 43     status = index_del(fn, actor);
 44 
 45@@ -1099,7 +1121,46 @@ int object_unadmire(const char *id, const char *actor, int like)
 46         index_gc(fn);
 47 
 48     srv_debug(0,
 49-        xs_fmt("object_unadmire (%s) %s %s %d", like ? "Like" : "Announce", actor, fn, status));
 50+        xs_fmt("object_unadmire (%s) %s %s %d", like >= 'e' ?
 51+            (like == 'l' ? "Like" : "EmojiReact") : "Announce" , actor, fn, status));
 52+
 53+    return status;
 54+}
 55+
 56+int object_emoji_react(const char *mid, const char *eid)
 57+/* actor reacts w/ an emoji */
 58+{
 59+    int status = HTTP_STATUS_OK;
 60+    xs *fn     = _object_fn(mid);
 61+
 62+    fn = xs_replace_i(fn, ".json", "_e.idx");
 63+
 64+    if (!index_in(fn, eid)) {
 65+        status = index_add(fn, eid);
 66+
 67+        srv_debug(1, xs_fmt("object_emoji_react (%s) added %s to %s", "EmojiReact", eid, fn));
 68+    }
 69+
 70+    return status;
 71+}
 72+
 73+
 74+int object_rm_emoji_react(const char *mid, const char *eid)
 75+/* actor retrives their emoji reaction */
 76+{
 77+    int status;
 78+    xs *fn = _object_fn(mid);
 79+
 80+    fn = xs_replace_i(fn, ".json", "_e.idx");
 81+
 82+    status = index_del(fn, eid);
 83+    object_del(eid);
 84+
 85+    if (valid_status(status))
 86+        index_gc(fn);
 87+
 88+    srv_debug(0,
 89+        xs_fmt("object_unadmire (EmojiReact) %s %s %d", eid, fn, status));
 90 
 91     return status;
 92 }
 93@@ -1506,19 +1567,47 @@ int timeline_add(snac *snac, const char *id, const xs_dict *o_msg)
 94 }
 95 
 96 
 97-int timeline_admire(snac *snac, const char *id, const char *admirer, int like)
 98-/* updates a timeline entry with a new admiration */
 99+int timeline_emoji_react(const char *act, const char *id, xs_dict *msg)
100+/* adds an emoji reaction to a message */
101+{
102+    msg = xs_dict_append(msg, "attributedTo", act);
103+    msg = xs_dict_set(msg, "type", "EmojiReact");
104+    const char *emote_id = xs_dict_get(msg, "id");
105+
106+    int ret = object_add(emote_id, msg);
107+    if (ret == HTTP_STATUS_OK || ret == HTTP_STATUS_CREATED)
108+        ret = object_emoji_react(id, emote_id);
109+
110+    return ret;
111+}
112+
113+
114+int timeline_admire(snac *snac, const char *id,
115+                    const char *admirer, int like, xs_dict *msg)
116+/* updates a timeline entry with a new admiration or emoji reaction */
117 {
118+    int ret;
119+    const char *content = xs_dict_get_path(msg, "content");
120+    const char *type = xs_dict_get_path(msg, "type");
121+
122     /* if we are admiring this, add to both timelines */
123     if (!like && strcmp(admirer, snac->actor) == 0) {
124         object_user_cache_add(snac, id, "public");
125         object_user_cache_add(snac, id, "private");
126     }
127 
128-    int ret = object_admire(id, admirer, like);
129+    /* use utf <3 as a like, as it is ugly */
130+    if (type && xs_match(type, "Like|EmojiReact|Emoji") &&
131+            content && strcmp(content, "❤") != 0) {
132+        ret = timeline_emoji_react(xs_dup(snac->actor), id, xs_dup(msg));
133+        snac_debug(snac, 1, xs_fmt("timeline_emoji_react %s", id));
134+    }
135 
136-    snac_debug(snac, 1, xs_fmt("timeline_admire (%s) %s %s",
137-            like ? "Like" : "Announce", id, admirer));
138+    else {
139+        ret = object_admire(id, admirer, like);
140+        snac_debug(snac, 1, xs_fmt("timeline_admire (%s) %s %s",
141+                like ? "Like" : "Announce", id, admirer));
142+    }
143 
144     return ret;
145 }
146@@ -1867,6 +1956,25 @@ xs_list *muted_list(snac *user)
147     return l;
148 }
149 
150+/** emojis react **/
151+
152+const xs_str *emoji_reacted(snac *user, const char *id)
153+/* returns the emoji an user reacted to a message */
154+{
155+    xs *emojis = object_get_emoji_reacts(id);
156+    int c = 0;
157+    const char *v;
158+    xs_dict *msg;
159+
160+    while (xs_list_next(emojis, &v, &c)) {
161+        if (object_get_by_md5(v, &msg)) {
162+            const xs_val *act = xs_dict_get(msg, "actor");
163+            if (act && strcmp(act, user->actor) == 0)
164+                return xs_dict_get(msg, "content");
165+        }
166+    }
167+    return NULL;
168+}
169 
170 /** bookmarking **/
171 
+3, -0
 1@@ -459,6 +459,9 @@ xs_str *sanitize(const char *content)
 2     char *p;
 3     const char *v;
 4 
 5+    if (!content)
 6+        return NULL;
 7+
 8     sl = xs_regex_split(content, "</?[^>]+>");
 9 
10     p = sl;
M html.c
+305, -12
  1@@ -54,9 +54,10 @@ int login(snac *user, const xs_dict *headers)
  2     return logged_in;
  3 }
  4 
  5-
  6-xs_str *replace_shortnames(xs_str *s, const xs_list *tag, int ems, const char *proxy)
  7-/* replaces all the :shortnames: with the emojis in tag */
  8+xs_str *_replace_shortnames(xs_str *s, const xs_list *tag, int ems,
  9+                            const char *proxy, const xs_list *cl, const char *act)
 10+/* replace but also adds a class list and an actor in its alt text.
 11+ * Used for emoji reactions */
 12 {
 13     if (!xs_is_null(tag)) {
 14         xs *tag_list = NULL;
 15@@ -69,11 +70,15 @@ xs_str *replace_shortnames(xs_str *s, const xs_list *tag, int ems, const char *p
 16             tag_list = xs_dup(tag);
 17         }
 18 
 19-        xs *style = xs_fmt("height: %dem; width: %dem; vertical-align: middle;", ems, ems);
 20+        xs *style = xs_fmt("max-height: %dem; max-width: %dem;", ems, ems);
 21         xs *class = xs_fmt("snac-emoji snac-emoji-%d-em", ems);
 22+        if (cl)
 23+            class = xs_str_cat(class, " ", xs_join(cl, " "));
 24 
 25-        const xs_dict *v;
 26         int c = 0;
 27+        const xs_val *v;
 28+
 29+        c = 0;
 30 
 31         xs_set rep_emoji;
 32         xs_set_init(&rep_emoji);
 33@@ -100,6 +105,8 @@ xs_str *replace_shortnames(xs_str *s, const xs_list *tag, int ems, const char *p
 34                         if (!xs_is_string(mt))
 35                             mt = xs_mime_by_ext(u);
 36 
 37+                        act = act ? xs_fmt("%s\n%s", n, act) : xs_fmt("%s", n);
 38+
 39                         if (strcmp(mt, "image/svg+xml") == 0 && !xs_is_true(xs_dict_get(srv_config, "enable_svg")))
 40                             s = xs_replace_i(s, n, "");
 41                         else {
 42@@ -108,8 +115,8 @@ xs_str *replace_shortnames(xs_str *s, const xs_list *tag, int ems, const char *p
 43                             xs_html *img = xs_html_sctag("img",
 44                                 xs_html_attr("loading", "lazy"),
 45                                 xs_html_attr("src", url),
 46-                                xs_html_attr("alt", n),
 47-                                xs_html_attr("title", n),
 48+                                xs_html_attr("alt", act),
 49+                                xs_html_attr("title", act),
 50                                 xs_html_attr("class", class),
 51                                 xs_html_attr("style", style));
 52 
 53@@ -130,6 +137,13 @@ xs_str *replace_shortnames(xs_str *s, const xs_list *tag, int ems, const char *p
 54 }
 55 
 56 
 57+xs_str *replace_shortnames(xs_str *s, const xs_list *tag, int ems, const char *proxy)
 58+/* replaces all the :shortnames: with the emojis in tag */
 59+{
 60+    return _replace_shortnames(s, tag, ems, proxy, NULL, NULL);
 61+}
 62+
 63+
 64 xs_str *actor_name(xs_dict *actor, const char *proxy)
 65 /* gets the actor name */
 66 {
 67@@ -430,6 +444,52 @@ void html_note_render_visibility(snac* user, xs_html *form, const int scope)
 68     xs_html_add(form, paragraph);
 69 }
 70 
 71+/* html_note but moddled for emoji's needs. here and not bellow, since the
 72+ * other one is already so complex. */
 73+xs_html *html_emoji(snac *user, const char *summary,
 74+                   const char *div_id, const char *form_id,
 75+                   const char* placeholder, const char *post_id,
 76+                   const char* eid)
 77+{
 78+    xs *action = xs_fmt("%s/admin/action", user->actor);
 79+
 80+    xs_html *form;
 81+    const int react = eid == NULL ? 0 : 1;
 82+
 83+    xs_html *note = xs_html_tag("div",
 84+        xs_html_tag("details",
 85+            xs_html_tag("summary",
 86+                xs_html_text(summary)),
 87+                xs_html_tag("p", NULL),
 88+                xs_html_tag("div",
 89+                    xs_html_attr("class", "snac-note"),
 90+                    xs_html_attr("id",    div_id),
 91+                    form = xs_html_tag("form",
 92+                        xs_html_attr("autocomplete", "off"),
 93+                        xs_html_attr("method",       "post"),
 94+                        xs_html_attr("action",       action),
 95+                        xs_html_attr("enctype",      "multipart/form-data"),
 96+                        xs_html_attr("id",           form_id),
 97+                        xs_html_sctag("input",
 98+                            xs_html_attr("type",     "hidden"),
 99+                            xs_html_attr("name",     "id"),
100+                            xs_html_attr("value",    post_id)),
101+                        xs_html_sctag("input",
102+                            xs_html_attr("type",     react ? "hidden" : "text"),
103+                            xs_html_attr("name",     "eid"),
104+                            xs_html_attr(react ? "value" : "placeholder", react ? eid : placeholder)),
105+                        xs_html_text(" "),
106+                        xs_html_sctag("input",
107+                            xs_html_attr("type",    "submit"),
108+                            xs_html_attr("name",    "action"),
109+                            xs_html_attr("eid",    "action"),
110+                            xs_html_attr("value",   react ? L("EmojiUnreact") : L("EmojiReact"))),
111+                        xs_html_text(" "),
112+                    xs_html_tag("p", NULL)))));
113+
114+    return note;
115+}
116+
117 xs_html *html_note(snac *user, const char *summary,
118                    const char *div_id, const char *form_id,
119                    const char *ta_plh, const char *ta_content,
120@@ -1356,6 +1416,28 @@ xs_html *html_top_controls(snac *user)
121                     xs_html_attr("value",   L("Like"))),
122                 xs_html_text(" "),
123                 xs_html_text(L("(by URL)"))),
124+            xs_html_tag("form",
125+                xs_html_attr("autocomplete", "off"),
126+                xs_html_attr("method",       "post"),
127+                xs_html_attr("action",       ops_action),
128+                xs_html_sctag("input",
129+                    xs_html_attr("type",     "text"),
130+                    xs_html_attr("name",     "eid"),
131+                    xs_html_attr("required", "required"),
132+                    xs_html_attr("placeholder", ":neocat:")),
133+                xs_html_text(" "),
134+                xs_html_sctag("input",
135+                    xs_html_attr("type",     "text"),
136+                    xs_html_attr("name",     "id"),
137+                    xs_html_attr("required", "required"),
138+                    xs_html_attr("placeholder", "https:/" "/fedi.example.com/bob/...")),
139+                xs_html_text(" "),
140+                xs_html_sctag("input",
141+                    xs_html_attr("type",    "submit"),
142+                    xs_html_attr("name",    "action"),
143+                    xs_html_attr("value",   L("EmojiReact"))),
144+                xs_html_text(" "),
145+                xs_html_text(L("(by URL)"))),
146             xs_html_tag("p", NULL)));
147 
148     /** user settings **/
149@@ -2019,6 +2101,21 @@ xs_html *html_entry_controls(snac *user, const char *actor,
150             xs_html_tag("p", NULL));
151     }
152 
153+    { /** emoji react **/
154+        /* the post textarea */
155+        xs *div_id  = xs_fmt("%s_reply", md5);
156+        xs *form_id = xs_fmt("%s_reply_form", md5);
157+
158+        xs_html_add(controls, xs_html_tag("div",
159+            xs_html_tag("p", NULL),
160+            html_emoji(
161+                user, L("Emoji react"),
162+                div_id, form_id,
163+                ":neocat:", id,
164+                emoji_reacted(user, id))),
165+            xs_html_tag("p", NULL));
166+    }
167+
168     { /** reply **/
169         /* the post textarea */
170         xs *ct      = build_mentions(user, msg);
171@@ -2345,6 +2442,168 @@ xs_html *html_entry(snac *user, xs_dict *msg, int read_only,
172     xs_html_add(snac_content_wrap,
173         snac_content);
174 
175+    /* add all emoji reacts */
176+    int is_emoji = 0;
177+    {
178+        int c = 0;
179+        const xs_dict *k;
180+        xs *ls = xs_list_new();
181+        xs *sfrl = xs_dict_new();
182+        xs *rl = object_get_emoji_reacts(id);
183+
184+        xs_dict *m = NULL;
185+        while (xs_list_next(rl, &v, &c)) {
186+            if (valid_status(object_get_by_md5(v, &m))) {
187+                const char *content = xs_dict_get(m, "content");
188+                const char *actor = xs_dict_get(m, "actor");
189+                const xs_list *contentl = xs_dict_get(sfrl, content);
190+                xs *actors = xs_list_new();
191+                actors = xs_list_append(actors, actor);
192+                char me = actor && user && strcmp(actor, user->actor) == 0;
193+                int count = 1;
194+
195+                if (contentl) {
196+                    count = atoi(xs_list_get(contentl, 0)) + 1;
197+                    const xs_list *actorsc = xs_list_get(contentl, 1);
198+                    if (strncmp(xs_list_get(contentl, 2), "1", 1) == 0)
199+                        me = 1;
200+
201+                    if (xs_list_in(actorsc, actor) != -1) {
202+                        xs_free(actors);
203+                        actors = xs_dup(actorsc);
204+                    }
205+                    else
206+                        actors = xs_list_cat(actors, actorsc);
207+                }
208+
209+                xs *fl = xs_list_new();
210+                fl = xs_list_append(fl, xs_fmt("%d", count), actors, xs_fmt("%d", me));
211+                sfrl = xs_dict_append(sfrl, content, fl);
212+            }
213+        }
214+
215+        c = 0;
216+
217+        while (xs_list_next(rl, &k, &c)) {
218+            if (valid_status(object_get_by_md5(k, &m))) {
219+                const xs_dict *tag = xs_dict_get(m, "tag");
220+                const xs_dict *ide = xs_dict_get(m, "id");
221+
222+                const char *content = xs_dict_get(m, "content");
223+                const char *shortname;
224+                shortname = xs_dict_get(m, "content");
225+
226+                const xs_list *items = xs_dict_get(sfrl, content);
227+                const char *nb = xs_list_get(items, 0);
228+                const xs_list *actors = xs_list_get(items, 1);
229+                const char me = *xs_list_get(items, 2) == '1';
230+
231+                if (!xs_is_null(nb)) {
232+                    is_emoji = 1;
233+
234+                    const char *act = atoi(nb) > 1 ?
235+                        xs_fmt("%d different actors \n\t%s", atoi(nb), xs_join(actors, ",\n\t")) :
236+                        xs_dict_get(m, "actor");
237+
238+                    xs *class = xs_list_new();
239+                    class = xs_list_append(class, "snac-reaction");
240+
241+                    xs_html *ret = NULL;
242+                    if (tag && shortname) {
243+                        xs *cl = xs_list_new();
244+                        cl = xs_list_append(cl, "snac-reaction-image");
245+                        xs *emoji = _replace_shortnames(xs_dup(shortname), tag, 2, proxy, cl, act);
246+
247+                        if (me)
248+                            class = xs_list_append(class, "snac-reacted");
249+
250+                        ret = xs_html_tag("button",
251+                                xs_html_attr("type", "submit"),
252+                                xs_html_attr("name", "action"),
253+                                xs_html_attr("value", me ? L("EmojiReact") : L("EmojiUnreact")),
254+                                xs_html_raw(emoji),
255+                                xs_html_tag("span",
256+                                    xs_html_raw(nb),
257+                                    xs_html_attr("style", "padding-left: 5px;")),
258+                                xs_html_attr("title", act),
259+                                xs_html_attr("class", xs_join(class, " ")));
260+
261+                        if (!(ide && xs_startswith(ide, srv_baseurl)))
262+                            xs_html_add(ret, xs_html_attr("disabled", "true"));
263+                    }
264+                    else if (shortname) {
265+                        xs *sn = xs_dup(shortname);
266+                        const char *sna = sn;
267+                        unsigned int utf = xs_utf8_dec((const char **)&sna);
268+
269+                        if (xs_is_emoji(utf)) {
270+                            const char *style = "font-size: large;";
271+                            if (me)
272+                                class = xs_list_append(class, "snac-reacted");
273+                            ret = xs_html_tag("button",
274+                                    xs_html_attr("type", "submit"),
275+                                    xs_html_attr("name", "action"),
276+                                    xs_html_attr("value", me ? L("EmojiUnreact") : L("EmojiReact")),
277+                                    xs_html_raw(xs_fmt("&#%d", utf)),
278+                                    xs_html_tag("span",
279+                                        xs_html_raw(nb),
280+                                        xs_html_attr("style", "font-size: initial; padding-left: 5px;")),
281+                                    xs_html_attr("title", act),
282+                                    xs_html_attr("class", xs_join(class, " ")),
283+                                    xs_html_attr("style", style));
284+                        }
285+                    }
286+                    if (ret) {
287+                        xs *s1;
288+                        if (user) {
289+                            xs *action = xs_fmt("%s/admin/action", user->actor);
290+                            xs *form_id = xs_fmt("%s_reply_form", md5);
291+
292+                            xs_html *form =
293+                                xs_html_tag("form",
294+                                xs_html_attr("autocomplete", "off"),
295+                                xs_html_attr("method",       "post"),
296+                                xs_html_attr("action",       action),
297+                                xs_html_attr("enctype",      "multipart/form-data"),
298+                                xs_html_attr("style",      "display: inline-flex;"
299+                                    "vertical-align: middle;"),
300+                                xs_html_attr("id",           form_id),
301+                                xs_html_sctag("input",
302+                                    xs_html_attr("type",  "hidden"),
303+                                    xs_html_attr("name",  "id"),
304+                                    xs_html_attr("value", id)),
305+                                xs_html_sctag("input",
306+                                    xs_html_attr("type",  "hidden"),
307+                                    xs_html_attr("name",  "eid"),
308+                                    xs_html_attr("value", shortname)),
309+                                ret);
310+                            s1 = xs_html_render(form);
311+                        }
312+                        else
313+                            s1 = xs_html_render(ret);
314+
315+                        ls = xs_list_append(ls, s1);
316+                        sfrl = xs_dict_del(sfrl, content);
317+                    }
318+                }
319+            }
320+        }
321+
322+        c = 0;
323+
324+        xs_html *emoji_div;
325+        if (xs_list_len(ls) > 0) {
326+                emoji_div = xs_html_tag("div", xs_html_text(L("Emoji reactions: ")),
327+                        xs_html_attr("class", "snac-reaction-div"));
328+
329+            while (ls != NULL && xs_list_next(ls, &k, &c))
330+                xs_html_add(emoji_div, xs_html_raw(k));
331+
332+            xs_html_add(snac_content_wrap, emoji_div);
333+        }
334+
335+    }
336+
337     {
338         /** build the content string **/
339         const char *content = xs_dict_get(msg, "content");
340@@ -2371,7 +2630,8 @@ xs_html *html_entry(snac *user, xs_dict *msg, int read_only,
341 
342         c = xs_replace_i(c, "<br><br>", "<p>");
343 
344-        c = xs_str_cat(c, "<p>");
345+        if (is_emoji == 0)
346+            c = xs_str_cat(c, "<p>");
347 
348         /* replace the :shortnames: */
349         c = replace_shortnames(c, xs_dict_get(msg, "tag"), 2, proxy);
350@@ -3686,9 +3946,18 @@ xs_str *html_notifications(snac *user, int skip, int show)
351         if (strcmp(type, "EmojiReact") == 0 || strcmp(type, "Like") == 0) {
352             const char *content = xs_dict_get_path(noti, "msg.content");
353 
354+            xs *cd = xs_dup(content);
355+            const char *sna = cd;
356+            const xs_dict *tag = xs_dict_get_path(noti, "msg.tag");
357+            unsigned int utf = xs_utf8_dec((const char **)&sna);
358+
359+            int isEmoji = 0;
360+            if (xs_is_emoji(utf) || (tag && xs_list_len(tag) > 0))
361+                isEmoji = 1;
362+
363             if (xs_type(content) == XSTYPE_STRING) {
364                 xs *emoji = replace_shortnames(xs_dup(content), xs_dict_get_path(noti, "msg.tag"), 1, proxy);
365-                wrk = xs_fmt("%s (%s&#xFE0F;)", type, emoji);
366+                wrk = xs_fmt("%s (%s&#xFE0F;)", isEmoji ? "EmojiReact" : "Like", emoji);
367                 label = wrk;
368             }
369         }
370@@ -4583,8 +4852,8 @@ int html_get_handler(const xs_dict *req, const char *q_path,
371                     xs *msg = msg_admiration(&snac, id, *action == 'L' ? "Like" : "Announce");
372 
373                     if (msg != NULL) {
374+                        timeline_admire(&snac, xs_dict_get(msg, "object"), snac.actor, *action == 'L' ? 1 : 0, msg);
375                         enqueue_message(&snac, msg);
376-                        timeline_admire(&snac, xs_dict_get(msg, "object"), snac.actor, *action == 'L' ? 1 : 0);
377 
378                         status = HTTP_STATUS_SEE_OTHER;
379                     }
380@@ -4892,12 +5161,36 @@ int html_post_handler(const xs_dict *req, const char *q_path,
381 
382         status = HTTP_STATUS_SEE_OTHER;
383 
384+        if (strcmp(action, L("EmojiUnreact")) == 0) { /** **/
385+            const char *eid = xs_dict_get(p_vars, "eid");
386+
387+            if (eid != NULL) {
388+                xs *n_msg = msg_emoji_unreact(&snac, id, eid);
389+
390+                if (n_msg != NULL)
391+                    enqueue_message(&snac, n_msg);
392+            }
393+        }
394+        else
395+        if (strcmp(action, L("EmojiReact")) == 0) { /** **/
396+            const char *eid = xs_dict_get(p_vars, "eid");
397+
398+            eid = xs_strip_chars_i(xs_dup(eid), ":");
399+
400+            const xs_dict *ret = msg_emoji_init(&snac, id, eid);
401+            /* fails if either invalid or already reacted */
402+            if (!ret)
403+                ret = msg_emoji_unreact(&snac, id, eid);
404+            if (!ret)
405+                status = HTTP_STATUS_NOT_FOUND;
406+        }
407+        else
408         if (strcmp(action, L("Like")) == 0) { /** **/
409             xs *msg = msg_admiration(&snac, id, "Like");
410 
411             if (msg != NULL) {
412+                timeline_admire(&snac, xs_dict_get(msg, "object"), snac.actor, 1, msg);
413                 enqueue_message(&snac, msg);
414-                timeline_admire(&snac, xs_dict_get(msg, "object"), snac.actor, 1);
415             }
416         }
417         else
418@@ -4905,8 +5198,8 @@ int html_post_handler(const xs_dict *req, const char *q_path,
419             xs *msg = msg_admiration(&snac, id, "Announce");
420 
421             if (msg != NULL) {
422+                timeline_admire(&snac, xs_dict_get(msg, "object"), snac.actor, 0, msg);
423                 enqueue_message(&snac, msg);
424-                timeline_admire(&snac, xs_dict_get(msg, "object"), snac.actor, 0);
425             }
426         }
427         else
M main.c
+1, -1
1@@ -498,7 +498,7 @@ int main(int argc, char *argv[])
2 
3         if (msg != NULL) {
4             enqueue_message(&snac, msg);
5-            timeline_admire(&snac, xs_dict_get(msg, "object"), snac.actor, 0);
6+            timeline_admire(&snac, xs_dict_get(msg, "object"), snac.actor, 0, "");
7 
8             if (dbglevel) {
9                 xs_json_dump(msg, 4, stdout);
+156, -4
  1@@ -1158,6 +1158,97 @@ xs_dict *mastoapi_status(snac *snac, const xs_dict *msg)
  2         st = xs_dict_append(st, "tags",     htl);
  3         st = xs_dict_append(st, "emojis",   eml);
  4     }
  5+    {
  6+        xs *rl   = object_get_emoji_reacts(id);
  7+        xs *frl  = xs_list_new(); /* final */
  8+        xs *sfrl = xs_dict_new(); /* seen */
  9+        int c = 0;
 10+        const char *v;
 11+
 12+        xs_dict *msg = NULL;
 13+        while (xs_list_next(rl, &v, &c)) {
 14+            if (valid_status(object_get_by_md5(v, &msg))) {
 15+                const char *content = xs_dict_get(msg, "content");
 16+                const char *actor = xs_dict_get(msg, "actor");
 17+                const xs_list *contentl = xs_dict_get(sfrl, content);
 18+                /* NOTE: idk when there are no actor, but i encountered that bug.
 19+                 * Probably because of one of my previous attempts.
 20+                 * Keeping this just in case, can remove later */
 21+                const char *me = actor && strcmp(actor, snac->actor) == 0 ?
 22+                    xs_stock(XSTYPE_TRUE) : xs_stock(XSTYPE_FALSE);
 23+                int count = 1;
 24+
 25+                if (contentl) {
 26+                    count = atoi(xs_list_get(contentl, 0)) + 1;
 27+                    if (strncmp(xs_list_get(contentl, 1), xs_stock(XSTYPE_TRUE), 1) == 0)
 28+                        me = xs_stock(XSTYPE_TRUE);
 29+                }
 30+
 31+                xs *fl = xs_list_new();
 32+                fl = xs_list_append(fl, xs_fmt("%d", count), me);
 33+                sfrl = xs_dict_append(sfrl, content, fl);
 34+            }
 35+        }
 36+
 37+        c = 0;
 38+
 39+        while (xs_list_next(rl, &v, &c)) {
 40+            if (valid_status(object_get_by_md5(v, &msg))) {
 41+                xs *d1 = xs_dict_new();
 42+
 43+                const xs_dict *icon = xs_dict_get(xs_list_get(xs_dict_get(msg, "tag"), 0), "icon");
 44+                const char *o_url = xs_dict_get(icon, "url");
 45+                const char *name = xs_dict_get(msg, "content");
 46+                const char *actor = xs_dict_get(msg, "actor");
 47+
 48+                xs *nm = xs_dup(name);
 49+                xs *url = NULL;
 50+
 51+                if (!xs_is_null(o_url)) {
 52+                    if (actor && snac && !strcmp(actor, snac->actor))
 53+                        url = make_url(o_url, NULL, 1);
 54+                    else
 55+                        url = xs_dup(o_url);
 56+                }
 57+
 58+                xs *accounts = xs_list_new();
 59+                if (actor) {
 60+                    xs *d2 = xs_dict_new();
 61+                    object_get(actor, &d2);
 62+                    xs *e_acct = mastoapi_account(snac, d2);
 63+                    accounts = xs_list_append(accounts, e_acct);
 64+                }
 65+
 66+                const xs_list *item = xs_dict_get(sfrl, nm);
 67+                const xs_str *nb = xs_list_get(item, 0);
 68+                const xs_val *me = xs_list_get(item, 1);
 69+                if (item == NULL)
 70+                    continue;
 71+
 72+                if (nm && strcmp(nm, "")) {
 73+                    if (url && strcmp(url, "")) {
 74+                        d1 = xs_dict_append(d1, "name",              nm);
 75+                        d1 = xs_dict_append(d1, "shortcode",         nm);
 76+                        d1 = xs_dict_append(d1, "accounts",          accounts);
 77+                        d1 = xs_dict_append(d1, "me",                me);
 78+                        d1 = xs_dict_append(d1, "url",               url);
 79+                        d1 = xs_dict_append(d1, "static_url",        url);
 80+                        d1 = xs_dict_append(d1, "visible_in_picker", xs_stock(XSTYPE_TRUE));
 81+                        d1 = xs_dict_append(d1, "count", nb);
 82+                    } else {
 83+                        d1 = xs_dict_append(d1, "name",              nm);
 84+                        d1 = xs_dict_append(d1, "count",             nb);
 85+                        d1 = xs_dict_append(d1, "me",                me);
 86+                        d1 = xs_dict_append(d1, "visible_in_picker", xs_stock(XSTYPE_TRUE));
 87+                    }
 88+                    sfrl = xs_dict_del(sfrl, nm);
 89+                    frl = xs_list_append(frl, d1);
 90+                }
 91+            }
 92+        }
 93+
 94+        st = xs_dict_append(st, "reactions", frl);
 95+    }
 96 
 97     xs_free(idx);
 98     xs_free(ixc);
 99@@ -2202,15 +2293,24 @@ int mastoapi_get_handler(const xs_dict *req, const char *q_path,
100                 if (noti == NULL)
101                     continue;
102 
103+                const xs_dict *tag = xs_list_get(xs_dict_get_path(noti, "msg.tag"), 0);
104+
105                 const char *type  = xs_dict_get(noti, "type");
106                 const char *utype = xs_dict_get(noti, "utype");
107                 const char *objid = xs_dict_get(noti, "objid");
108                 const char *id    = xs_dict_get(noti, "id");
109                 const char *actid = xs_dict_get(noti, "actor");
110+
111+                int isEmoji = 0;
112+
113                 xs *fid = xs_replace(id, ".", "");
114                 xs *actor = NULL;
115                 xs *entry = NULL;
116 
117+                if (tag) {
118+                    isEmoji = strcmp(xs_dict_get(tag, "type"), "Emoji") ? 0 : 1;
119+                }
120+
121                 if (!valid_status(actor_get(actid, &actor)))
122                     continue;
123 
124@@ -2234,9 +2334,12 @@ int mastoapi_get_handler(const xs_dict *req, const char *q_path,
125                 }
126 
127                 /* convert the type */
128-                if (strcmp(type, "Like") == 0 || strcmp(type, "EmojiReact") == 0)
129+                if (strcmp(type, "Like") == 0 && !isEmoji)
130                     type = "favourite";
131                 else
132+                if (isEmoji || strcmp(type, "EmojiReact") == 0)
133+                    type = "reaction";
134+                else
135                 if (strcmp(type, "Announce") == 0)
136                     type = "reblog";
137                 else
138@@ -2277,8 +2380,29 @@ int mastoapi_get_handler(const xs_dict *req, const char *q_path,
139                 if (strcmp(type, "follow") != 0 && !xs_is_null(objid)) {
140                     xs *st = mastoapi_status(&snac1, entry);
141 
142-                    if (st)
143+                    if (st) {
144                         mn = xs_dict_append(mn, "status", st);
145+
146+                        if (strcmp(type, "reaction") == 0 && !xs_is_null(objid)) {
147+                            const char *eid = NULL;
148+                            const char *url = NULL;
149+                            int utf = 0;
150+
151+                            const xs_dict *tag = xs_list_get(xs_dict_get_path(noti, "msg.tag"), 0);
152+                            const char *content = xs_dict_get_path(noti, "msg.content");
153+
154+                            url = xs_dict_get(xs_dict_get(tag, "icon"), "url");
155+                            eid = xs_dict_get(tag, "name");
156+
157+                            if (eid && url) {
158+                                mn = xs_dict_append(mn, "emoji", eid);
159+                                mn = xs_dict_append(mn, "emoji_url", url);
160+                            }
161+
162+                            if (xs_is_emoji((utf = xs_utf8_dec(&content))))
163+                                mn = xs_dict_append(mn, "name", xs_fmt("&#%d;", utf));
164+                        }
165+                    }
166                 }
167 
168                 out = xs_list_append(out, mn);
169@@ -2594,6 +2718,11 @@ int mastoapi_get_handler(const xs_dict *req, const char *q_path,
170                 "\"max_expiration\":2629746,"
171                 "\"max_options\":8,\"min_expiration\":300}");
172             cfg = xs_dict_append(cfg, "polls", d14);
173+
174+
175+            xs *d15 = xs_json_loads("{\"max_reactions\":50}");
176+                cfg = xs_dict_append(cfg, "reactions", d15);
177+
178         }
179 
180         ins = xs_dict_append(ins, "configuration", cfg);
181@@ -3219,7 +3348,7 @@ int mastoapi_post_handler(const xs_dict *req, const char *q_path,
182 
183                         if (n_msg != NULL) {
184                             enqueue_message(&snac, n_msg);
185-                            timeline_admire(&snac, xs_dict_get(n_msg, "object"), snac.actor, 1);
186+                            timeline_admire(&snac, xs_dict_get(n_msg, "object"), snac.actor, 1, msg);
187 
188                             out = mastoapi_status(&snac, msg);
189                         }
190@@ -3234,13 +3363,36 @@ int mastoapi_post_handler(const xs_dict *req, const char *q_path,
191                             out = mastoapi_status(&snac, msg);
192                         }
193                     }
194+                    else
195+                    if (strcmp(op, "react") == 0) { /** **/
196+                        const char *eid = xs_list_get(l, 5);
197+                        xs *n_msg = msg_emoji_init(&snac, id, eid);
198+                        if (n_msg)
199+                            out = mastoapi_status(&snac, n_msg);
200+                    }
201+                    else
202+                    if (strcmp(op, "unreact") == 0) { /** **/
203+                        const char *eid = xs_list_get(l, 5);
204+                        xs *content = xs_fmt("%s", eid);
205+
206+                        if (eid) {
207+                            xs *n_msg = msg_emoji_unreact(&snac, id, content);
208+
209+                            if (n_msg != NULL) {
210+                                enqueue_message(&snac, n_msg);
211+
212+                                out = mastoapi_status(&snac, msg);
213+                            }
214+                        }
215+                    }
216+
217                     else
218                     if (strcmp(op, "reblog") == 0) { /** **/
219                         xs *n_msg = msg_admiration(&snac, id, "Announce");
220 
221                         if (n_msg != NULL) {
222                             enqueue_message(&snac, n_msg);
223-                            timeline_admire(&snac, xs_dict_get(n_msg, "object"), snac.actor, 0);
224+                            timeline_admire(&snac, xs_dict_get(n_msg, "object"), snac.actor, 0, msg);
225 
226                             out = mastoapi_status(&snac, msg);
227                         }
M snac.h
+9, -1
 1@@ -146,12 +146,15 @@ void object_touch(const char *id);
 2 int object_admire(const char *id, const char *actor, int like);
 3 int object_unadmire(const char *id, const char *actor, int like);
 4 
 5+int object_emoji_react(const char *mid, const char *eid);
 6+int object_rm_emoji_react(const char *mid, const char *eid);
 7 int object_likes_len(const char *id);
 8 int object_announces_len(const char *id);
 9 
10 xs_list *object_children(const char *id);
11 xs_list *object_likes(const char *id);
12 xs_list *object_announces(const char *id);
13+xs_list *object_get_emoji_reacts(const char *id);
14 int object_parent(const char *md5, char parent[MD5_HEX_SIZE]);
15 
16 int object_user_cache_add(snac *snac, const char *id, const char *cachedir);
17@@ -180,7 +183,8 @@ xs_str *user_index_fn(snac *user, const char *idx_name);
18 xs_list *timeline_simple_list(snac *user, const char *idx_name, int skip, int show, int *more);
19 xs_list *timeline_list(snac *snac, const char *idx_name, int skip, int show, int *more);
20 int timeline_add(snac *snac, const char *id, const xs_dict *o_msg);
21-int timeline_admire(snac *snac, const char *id, const char *admirer, int like);
22+int timeline_admire(snac *snac, const char *id, const char *admirer, int like, xs_dict *msg);
23+int timeline_emoji_react(const char *atto, const char *id, xs_dict *o_msg);
24 
25 xs_list *timeline_top_level(snac *snac, const xs_list *list);
26 void timeline_add_mark(snac *user);
27@@ -201,6 +205,8 @@ void unmute(snac *snac, const char *actor);
28 int is_muted(snac *snac, const char *actor);
29 xs_list *muted_list(snac *user);
30 
31+const xs_str *emoji_reacted(snac *user, const char *id);
32+
33 int is_bookmarked(snac *user, const char *id);
34 int bookmark(snac *user, const char *id);
35 int unbookmark(snac *user, const char *id);
36@@ -358,6 +364,8 @@ xs_list *get_attachments(const xs_dict *msg);
37 
38 xs_dict *msg_admiration(snac *snac, const char *object, const char *type);
39 xs_dict *msg_repulsion(snac *user, const char *id, const char *type);
40+xs_dict *msg_emoji_init(snac *user, const char *mid, const char *eid);
41+xs_dict *msg_emoji_unreact(snac *user, const char *id, const char *type);
42 xs_dict *msg_create(snac *snac, const xs_dict *object);
43 xs_dict *msg_follow(snac *snac, const char *actor);
44 
+7, -0
 1@@ -22,6 +22,7 @@
 2  int xs_unicode_nfc(unsigned int base, unsigned int diac, unsigned int *cpoint);
 3  int xs_unicode_is_alpha(unsigned int cpoint);
 4  int xs_unicode_is_right_to_left(unsigned int cpoint);
 5+ int xs_is_emoji(unsigned int cpoint);
 6 
 7 #ifdef _XS_H
 8  xs_str *xs_utf8_insert(xs_str *str, unsigned int cpoint, int *offset);
 9@@ -134,6 +135,12 @@ static unsigned int xs_unicode_width_table[] = {
10     0x20000,    0x2fffd,    2       /* more CJK */
11 };
12 
13+/* magic number from https://en.wikipedia.org/wiki/Emoji#Unicode_blocks */
14+int xs_is_emoji(unsigned int cpoint) {
15+/* returns wether the input is an utf8 emoji */
16+    return cpoint > 0x00A9 && cpoint < 0x1ffff;
17+}
18+
19 int xs_unicode_width(unsigned int cpoint)
20 /* returns the width in columns of a Unicode codepoint (somewhat simplified) */
21 {
+33, -0
 1@@ -6,6 +6,7 @@
 2 
 3 xs_str *xs_url_dec(const char *str);
 4 xs_str *xs_url_enc(const char *str);
 5+xs_str *xs_url_dec_emoji(const char *str);
 6 xs_dict *xs_url_vars(const char *str);
 7 xs_dict *xs_multipart_form_data(const char *payload, int p_size, const char *header);
 8 
 9@@ -79,6 +80,38 @@ xs_str *xs_url_dec(const char *str)
10 }
11 
12 
13+xs_str *xs_url_dec_emoji(const char *str)
14+/* decodes an URL, returns NULL if not every char is an encoded hex */
15+{
16+    xs_str *s = xs_str_new(NULL);
17+
18+    while (*str) {
19+        if (!xs_is_string(str))
20+            break;
21+
22+        if (*str == '%') {
23+            unsigned int i;
24+
25+            if (sscanf(str + 1, "%02x", &i) == 1) {
26+                unsigned char uc = i;
27+
28+                if (!xs_is_string((char *)&uc))
29+                    break;
30+
31+                s = xs_append_m(s, (char *)&uc, 1);
32+                str += 2;
33+            }
34+        }
35+        else
36+            return NULL;
37+
38+        str++;
39+    }
40+
41+    return s;
42+}
43+
44+
45 xs_str *xs_url_enc(const char *str)
46 /* URL-encodes a string (RFC 3986) */
47 {