aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorviolette <vi@violette.town>2025-12-18 07:58:24 +0100
committergrunfink <grunfink@comam.es>2025-12-18 07:58:24 +0100
commit85ed0eb0d535700a5df837c37f51848811e461a0 (patch)
tree818c80cc48c445b2950796230946c63eca3a3242
parente66446a57d79623227353e57cd553cbb42e4ca93 (diff)
Added emoji reactions (contributed by violette).
-rw-r--r--activitypub.c161
-rw-r--r--data.c124
-rw-r--r--format.c3
-rw-r--r--html.c317
-rw-r--r--main.c2
-rw-r--r--mastoapi.c160
-rw-r--r--snac.h10
-rw-r--r--xs_unicode.h7
-rw-r--r--xs_url.h33
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 (strcmp(utype, "Like") == 0 || strcmp(utype, "EmojiReact") == 0) { /** **/
+ 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) { /** **/
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, "<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
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);
}
@@ -3235,12 +3364,35 @@ int mastoapi_post_handler(const xs_dict *req, const char *q_path,
}
}
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) */
{