From 85ed0eb0d535700a5df837c37f51848811e461a0 Mon Sep 17 00:00:00 2001
From: violette
Date: Thu, 18 Dec 2025 07:58:24 +0100
Subject: Added emoji reactions (contributed by violette).
---
activitypub.c | 161 ++++++++++++++++++++++++++++-
data.c | 124 +++++++++++++++++++++--
format.c | 3 +
html.c | 317 +++++++++++++++++++++++++++++++++++++++++++++++++++++++---
main.c | 2 +-
mastoapi.c | 160 ++++++++++++++++++++++++++++-
snac.h | 10 +-
xs_unicode.h | 7 ++
xs_url.h | 33 ++++++
9 files changed, 786 insertions(+), 31 deletions(-)
diff --git a/activitypub.c b/activitypub.c
index 90230d8..19f0cc6 100644
--- a/activitypub.c
+++ b/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",
diff --git a/data.c b/data.c
index 688d2e3..07eea3c 100644
--- a/data.c
+++ b/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 **/
diff --git a/format.c b/format.c
index 84c634d..4f93b7b 100644
--- a/format.c
+++ b/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;
diff --git a/html.c b/html.c
index 8f7c4a9..8cc0067 100644
--- a/html.c
+++ b/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, "
", "
"); - c = xs_str_cat(c, "
"); + if (is_emoji == 0) + c = xs_str_cat(c, "
"); /* 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️)", type, emoji); + wrk = xs_fmt("%s (%s️)", 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 diff --git a/main.c b/main.c index 6915bd0..4b0463e 100644 --- a/main.c +++ b/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); diff --git a/mastoapi.c b/mastoapi.c index 9503447..1d99cd5 100644 --- a/mastoapi.c +++ b/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); } diff --git a/snac.h b/snac.h index e1d25f2..3d98aac 100644 --- a/snac.h +++ b/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); diff --git a/xs_unicode.h b/xs_unicode.h index 67b3827..0b4de1c 100644 --- a/xs_unicode.h +++ b/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) */ { diff --git a/xs_url.h b/xs_url.h index 7bdff49..222771f 100644 --- a/xs_url.h +++ b/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) */ { -- cgit v1.2.3