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