summaryrefslogtreecommitdiff
path: root/activitypub.c
diff options
context:
space:
mode:
Diffstat (limited to 'activitypub.c')
-rw-r--r--activitypub.c209
1 files changed, 195 insertions, 14 deletions
diff --git a/activitypub.c b/activitypub.c
index 2c0fa2e..c34e510 100644
--- a/activitypub.c
+++ b/activitypub.c
@@ -1,9 +1,10 @@
1/* snac - A simple, minimalistic ActivityPub instance */ 1/* snac - A simple, minimalistic ActivityPub instance */
2/* copyright (c) 2022 - 2025 grunfink et al. / MIT license */ 2/* copyright (c) 2022 - 2026 grunfink et al. / MIT license */
3 3
4#include "xs.h" 4#include "xs.h"
5#include "xs_json.h" 5#include "xs_json.h"
6#include "xs_curl.h" 6#include "xs_curl.h"
7#include "xs_url.h"
7#include "xs_mime.h" 8#include "xs_mime.h"
8#include "xs_openssl.h" 9#include "xs_openssl.h"
9#include "xs_regex.h" 10#include "xs_regex.h"
@@ -779,7 +780,7 @@ int is_msg_for_me(snac *snac, const xs_dict *c_msg)
779 object_get(object, &obj); 780 object_get(object, &obj);
780 781
781 /* if it's about one of our posts, accept it */ 782 /* if it's about one of our posts, accept it */
782 if (xs_startswith(object, snac->actor)) 783 if (is_msg_mine(snac, object))
783 return 2; 784 return 2;
784 785
785 /* blocked by hashtag? */ 786 /* blocked by hashtag? */
@@ -1242,7 +1243,7 @@ void notify(snac *snac, const char *type, const char *utype, const char *actor,
1242 1243
1243 if (xs_match(type, "Like|Announce|EmojiReact")) { 1244 if (xs_match(type, "Like|Announce|EmojiReact")) {
1244 /* if it's not an admiration about something by us, done */ 1245 /* if it's not an admiration about something by us, done */
1245 if (xs_is_null(objid) || !xs_startswith(objid, snac->actor)) 1246 if (xs_is_null(objid) || !is_msg_mine(snac, objid))
1246 return; 1247 return;
1247 1248
1248 /* if it's an announce by our own relay, done */ 1249 /* if it's an announce by our own relay, done */
@@ -1267,7 +1268,7 @@ void notify(snac *snac, const char *type, const char *utype, const char *actor,
1267 return; 1268 return;
1268 1269
1269 /* if it's not ours and we didn't vote, discard */ 1270 /* if it's not ours and we didn't vote, discard */
1270 if (!xs_startswith(poll_id, snac->actor) && !was_question_voted(snac, poll_id)) 1271 if (!is_msg_mine(snac, poll_id) && !was_question_voted(snac, poll_id))
1271 return; 1272 return;
1272 } 1273 }
1273 1274
@@ -1530,11 +1531,24 @@ xs_dict *msg_update(snac *snac, const xs_dict *object)
1530 1531
1531 1532
1532xs_dict *msg_admiration(snac *snac, const char *object, const char *type) 1533xs_dict *msg_admiration(snac *snac, const char *object, const char *type)
1533/* creates a Like or Announce message */ 1534/* creates a Like, Announce or EmojiReact message */
1534{ 1535{
1535 xs *a_msg = NULL; 1536 xs *a_msg = NULL;
1536 xs_dict *msg = NULL; 1537 xs_dict *msg = NULL;
1537 xs *wrk = NULL; 1538 xs *wrk = NULL;
1539 char t = 0;
1540
1541 switch (*type) {
1542 case 'L':
1543 t = 'l';
1544 break;
1545 case 'A':
1546 t = 'a';
1547 break;
1548 case 'E':
1549 t = 'e';
1550 break;
1551 }
1538 1552
1539 /* call the object */ 1553 /* call the object */
1540 timeline_request(snac, &object, &wrk, 0); 1554 timeline_request(snac, &object, &wrk, 0);
@@ -1542,7 +1556,7 @@ xs_dict *msg_admiration(snac *snac, const char *object, const char *type)
1542 if (valid_status(object_get(object, &a_msg))) { 1556 if (valid_status(object_get(object, &a_msg))) {
1543 xs *rcpts = xs_list_new(); 1557 xs *rcpts = xs_list_new();
1544 xs *o_md5 = xs_md5_hex(object, strlen(object)); 1558 xs *o_md5 = xs_md5_hex(object, strlen(object));
1545 xs *id = xs_fmt("%s/%s/%s", snac->actor, *type == 'L' ? "l" : "a", o_md5); 1559 xs *id = xs_fmt("%s/%c/%s", snac->actor, t, o_md5);
1546 1560
1547 msg = msg_base(snac, type, id, snac->actor, "@now", object); 1561 msg = msg_base(snac, type, id, snac->actor, "@now", object);
1548 1562
@@ -1586,6 +1600,138 @@ xs_dict *msg_repulsion(snac *user, const char *id, const char *type)
1586 return msg; 1600 return msg;
1587} 1601}
1588 1602
1603xs_dict *msg_emoji_init(snac *snac, const char *mid, const char *eid_o)
1604/* creates an emoji reaction from a local user */
1605{
1606 xs_dict *n_msg = msg_admiration(snac, mid, "EmojiReact");
1607
1608 xs *eid = xs_strip_chars_i(xs_dup(eid_o), ":");
1609 xs *content = NULL;
1610 xs *tag = xs_list_new();
1611 xs *dict = xs_dict_new();
1612 xs *icon = xs_dict_new();
1613 xs *accounts = xs_list_new();
1614 xs *emjs = emojis_rm_categories();
1615
1616 /* may be a default emoji */
1617 xs *eidd = xs_dup(eid);
1618 const char *eidda = eid;
1619
1620 if (xs_is_emoji(xs_utf8_dec(&eidda)))
1621 content = xs_dup(eid);
1622
1623 else if (*eid == '%') {
1624 content = xs_url_dec_emoji(xs_dup(eid));
1625 if (content == NULL) {
1626 return NULL;
1627 }
1628 }
1629 else {
1630 content = xs_fmt(":%s:", eid);
1631 const char *emo = xs_dict_get(emjs, content);
1632
1633 if (emo == NULL)
1634 return NULL;
1635
1636 if (xs_match(emo, "https://*|http://*")) { /* emoji is an URL to an image */
1637 icon = xs_dict_set(icon, "type", "Image");
1638 icon = xs_dict_set(icon, "url", emo);
1639 dict = xs_dict_set(dict, "icon", icon);
1640
1641 dict = xs_dict_set(dict, "id", emo);
1642 dict = xs_dict_set(dict, "name", content);
1643 dict = xs_dict_set(dict, "type", "Emoji");
1644 tag = xs_list_append(tag, dict);
1645 }
1646 else
1647 if (xs_startswith(emo, "&#")) {
1648 /* snac default emoji as an HTML entity: decode */
1649 content = xs_free(content);
1650
1651 xs *s1 = xs_strip_chars_i(xs_dup(emo), "&#");
1652 unsigned int cpoint = 0;
1653 sscanf(s1, "%u;", &cpoint);
1654
1655 if (cpoint)
1656 content = xs_utf8_cat(xs_str_new(NULL), cpoint);
1657 else
1658 content = xs_dup(emo);
1659 }
1660 else {
1661 /* use as it is and hope for the best */
1662 xs_free(content);
1663 content = xs_dup(emo);
1664 }
1665 }
1666
1667 accounts = xs_list_append(accounts, snac->actor);
1668
1669 n_msg = xs_dict_set(n_msg, "content", content);
1670 n_msg = xs_dict_set(n_msg, "accounts", accounts);
1671 n_msg = xs_dict_set(n_msg, "attributedTo", xs_list_get(xs_dict_get(n_msg, "to"), 1));
1672 n_msg = xs_dict_set(n_msg, "accountId", snac->uid);
1673 n_msg = xs_dict_set(n_msg, "tag", tag);
1674
1675 int ret = timeline_admire(snac, xs_dict_get(n_msg, "object"), snac->actor, 1, n_msg);
1676 if (ret == 200 || ret == 201) {
1677 enqueue_message(snac, n_msg);
1678 return n_msg;
1679 }
1680
1681 return NULL;
1682}
1683
1684xs_dict *msg_emoji_unreact(snac *user, const char *mid, const char *eid)
1685/* creates an Undo + emoji reaction message */
1686{
1687 xs *a_msg = NULL;
1688 xs_dict *msg = NULL;
1689
1690 if (valid_status(object_get(mid, &a_msg))) {
1691 /* create a clone of the original admiration message */
1692 xs *object = msg_admiration(user, mid, "EmojiReact");
1693
1694 /* delete the published date */
1695 object = xs_dict_del(object, "published");
1696
1697 /* create an undo message for this object */
1698 msg = msg_undo(user, object);
1699
1700 /* copy the 'to' field */
1701 msg = xs_dict_set(msg, "to", xs_dict_get(object, "to"));
1702 }
1703
1704 xs *emotes = object_get_emoji_reacts(mid);
1705 const char *v;
1706 int c = 0;
1707
1708 /* may be a default emoji */
1709 if (strlen(eid) == 12 && *eid == '%') {
1710 eid = xs_url_dec(eid);
1711 if (eid == NULL) {
1712 return NULL;
1713 }
1714 }
1715
1716 /* lets get all emotes for this msg, and compare it to our content */
1717 while (xs_list_next(emotes, &v, &c)) {
1718 xs_dict *e = NULL;
1719 if (valid_status(object_get_by_md5(v, &e))) {
1720 const char *content = xs_dict_get(e, "content");
1721 const char *id = xs_dict_get(e, "id");
1722 const char *actor = xs_dict_get(e, "actor");
1723 /* maybe formated as :{emoteName}: too */
1724 if (xs_str_in(eid, content) != -1)
1725 if (strcmp(user->actor, actor) == 0) {
1726 object_rm_emoji_react(mid, id);
1727 return msg;
1728 }
1729 }
1730 }
1731
1732 return NULL;
1733}
1734
1589 1735
1590xs_dict *msg_actor_place(snac *user, const char *label) 1736xs_dict *msg_actor_place(snac *user, const char *label)
1591/* creates a Place object, if the user has a location defined */ 1737/* creates a Place object, if the user has a location defined */
@@ -2432,6 +2578,11 @@ int process_input_message(snac *snac, const xs_dict *msg, const xs_dict *req)
2432 return -1; 2578 return -1;
2433 } 2579 }
2434 2580
2581 if (strcmp(type, "EmojiReact") == 0 && xs_is_true(xs_dict_get(srv_config, "disable_emojireact"))) {
2582 srv_log(xs_fmt("Dropping EmojiReact from %s due to admin configuration", actor));
2583 return -1;
2584 }
2585
2435 const char *object, *utype; 2586 const char *object, *utype;
2436 2587
2437 object = xs_dict_get(msg, "object"); 2588 object = xs_dict_get(msg, "object");
@@ -2605,6 +2756,16 @@ int process_input_message(snac *snac, const xs_dict *msg, const xs_dict *req)
2605 else 2756 else
2606 if (strcmp(type, "Undo") == 0) { /** **/ 2757 if (strcmp(type, "Undo") == 0) { /** **/
2607 const char *id = xs_dict_get(object, "object"); 2758 const char *id = xs_dict_get(object, "object");
2759 const char *content = xs_dict_get(object, "content");
2760 /* misskey sends emojis as like + tag */
2761 xs *cd = xs_dup(content);
2762 const char *sna = cd;
2763 const xs_dict *tag = xs_dict_get(object, "tag");
2764 unsigned int utf = xs_utf8_dec((const char **)&sna);
2765
2766 int isEmoji = 0;
2767 if (xs_is_emoji(utf) || (tag && xs_list_len(tag) > 0))
2768 isEmoji = 1;
2608 2769
2609 if (xs_type(object) != XSTYPE_DICT) { 2770 if (xs_type(object) != XSTYPE_DICT) {
2610 snac_debug(snac, 1, xs_fmt("undo: overriding utype %s | %s | %s", 2771 snac_debug(snac, 1, xs_fmt("undo: overriding utype %s | %s | %s",
@@ -2633,8 +2794,19 @@ int process_input_message(snac *snac, const xs_dict *msg, const xs_dict *req)
2633 else 2794 else
2634 snac_log(snac, xs_fmt("error deleting follower %s", actor)); 2795 snac_log(snac, xs_fmt("error deleting follower %s", actor));
2635 } 2796 }
2797 /* *key emojis are like w/ Emoji tag */
2798 else
2799 if ((isEmoji || strcmp(utype, "EmojiReact") == 0) &&
2800 (content && strcmp(content, "♥") != 0)) {
2801 const xs_val *mid = xs_dict_get(object, "id");
2802 int status = object_rm_emoji_react((char *)id, mid);
2803 /* ensure *key notifications type */
2804 utype = "EmojiReact";
2805
2806 snac_log(snac, xs_fmt("Undo 'EmojiReact' for %s %d", id, status));
2807 }
2636 else 2808 else
2637 if (strcmp(utype, "Like") == 0 || strcmp(utype, "EmojiReact") == 0) { /** **/ 2809 if (strcmp(utype, "Like") == 0) { /** **/
2638 int status = object_unadmire(id, actor, 1); 2810 int status = object_unadmire(id, actor, 1);
2639 2811
2640 snac_log(snac, xs_fmt("Undo '%s' for %s %d", utype, id, status)); 2812 snac_log(snac, xs_fmt("Undo '%s' for %s %d", utype, id, status));
@@ -2771,13 +2943,22 @@ int process_input_message(snac *snac, const xs_dict *msg, const xs_dict *req)
2771 } 2943 }
2772 else 2944 else
2773 if (strcmp(type, "Like") == 0 || strcmp(type, "EmojiReact") == 0) { /** **/ 2945 if (strcmp(type, "Like") == 0 || strcmp(type, "EmojiReact") == 0) { /** **/
2946 /* misskey sends emojis as Like + tag.
2947 * It is easier to handle them both at the same time. */
2948 const char *sna = xs_dict_get(msg, "content");
2949 const xs_dict *tag = xs_dict_get(msg, "tag");
2950 unsigned int utf = xs_utf8_dec((const char **)&sna);
2951
2952 if (xs_is_emoji(utf) || (tag && xs_list_len(tag) > 0))
2953 type = "EmojiReact";
2954
2774 if (xs_type(object) == XSTYPE_DICT) 2955 if (xs_type(object) == XSTYPE_DICT)
2775 object = xs_dict_get(object, "id"); 2956 object = xs_dict_get(object, "id");
2776 2957
2777 if (xs_is_null(object)) 2958 if (xs_is_null(object))
2778 snac_log(snac, xs_fmt("malformed message: no 'id' field")); 2959 snac_log(snac, xs_fmt("malformed message: no 'id' field"));
2779 else 2960 else
2780 if (timeline_admire(snac, object, actor, 1) == HTTP_STATUS_CREATED) 2961 if (timeline_admire(snac, object, actor, 1, msg) == HTTP_STATUS_CREATED)
2781 snac_log(snac, xs_fmt("new '%s' %s %s", type, actor, object)); 2962 snac_log(snac, xs_fmt("new '%s' %s %s", type, actor, object));
2782 else 2963 else
2783 snac_log(snac, xs_fmt("repeated '%s' from %s to %s", type, actor, object)); 2964 snac_log(snac, xs_fmt("repeated '%s' from %s to %s", type, actor, object));
@@ -2792,10 +2973,10 @@ int process_input_message(snac *snac, const xs_dict *msg, const xs_dict *req)
2792 if (xs_is_null(object)) 2973 if (xs_is_null(object))
2793 snac_log(snac, xs_fmt("malformed message: no 'id' field")); 2974 snac_log(snac, xs_fmt("malformed message: no 'id' field"));
2794 else 2975 else
2795 if (is_muted(snac, actor) && !xs_startswith(object, snac->actor)) 2976 if (is_muted(snac, actor) && !is_msg_mine(snac, object))
2796 snac_log(snac, xs_fmt("dropped 'Announce' from muted actor %s", actor)); 2977 snac_log(snac, xs_fmt("dropped 'Announce' from muted actor %s", actor));
2797 else 2978 else
2798 if (is_limited(snac, actor) && !xs_startswith(object, snac->actor)) 2979 if (is_limited(snac, actor) && !is_msg_mine(snac, object))
2799 snac_log(snac, xs_fmt("dropped 'Announce' from limited actor %s", actor)); 2980 snac_log(snac, xs_fmt("dropped 'Announce' from limited actor %s", actor));
2800 else { 2981 else {
2801 xs *a_msg = NULL; 2982 xs *a_msg = NULL;
@@ -2818,7 +2999,7 @@ int process_input_message(snac *snac, const xs_dict *msg, const xs_dict *req)
2818 xs *this_relay = xs_fmt("%s/relay", srv_baseurl); 2999 xs *this_relay = xs_fmt("%s/relay", srv_baseurl);
2819 3000
2820 if (strcmp(actor, this_relay) != 0) { 3001 if (strcmp(actor, this_relay) != 0) {
2821 if (valid_status(timeline_admire(snac, object, actor, 0))) 3002 if (valid_status(timeline_admire(snac, object, actor, 0, a_msg)))
2822 snac_log(snac, xs_fmt("new 'Announce' %s %s", actor, object)); 3003 snac_log(snac, xs_fmt("new 'Announce' %s %s", actor, object));
2823 else 3004 else
2824 snac_log(snac, xs_fmt("repeated 'Announce' from %s to %s", 3005 snac_log(snac, xs_fmt("repeated 'Announce' from %s to %s",
@@ -2903,7 +3084,7 @@ int process_input_message(snac *snac, const xs_dict *msg, const xs_dict *req)
2903 snac_log(snac, xs_fmt("malformed message: no 'id' field")); 3084 snac_log(snac, xs_fmt("malformed message: no 'id' field"));
2904 else 3085 else
2905 if (object_here(object)) { 3086 if (object_here(object)) {
2906 if (xs_startswith(object, srv_baseurl) && !xs_startswith(object, actor)) 3087 if (xs_startswith(object, srv_baseurl) && !is_msg_mine(snac, object))
2907 snac_log(snac, xs_fmt("ignored incorrect 'Delete' %s %s", actor, object)); 3088 snac_log(snac, xs_fmt("ignored incorrect 'Delete' %s %s", actor, object));
2908 else { 3089 else {
2909 timeline_del(snac, object); 3090 timeline_del(snac, object);
@@ -3214,7 +3395,7 @@ void process_user_queue_item(snac *user, xs_dict *q_item)
3214 actor_add(actor, actor_o); 3395 actor_add(actor, actor_o);
3215 } 3396 }
3216 else { 3397 else {
3217 if (status == HTTP_STATUS_GONE) { 3398 if (status == HTTP_STATUS_GONE || status == HTTP_STATUS_NOT_FOUND) {
3218 actor_failure(actor, 1); 3399 actor_failure(actor, 1);
3219 snac_log(user, xs_fmt("actor_refresh marking actor %s as broken %d", actor, status)); 3400 snac_log(user, xs_fmt("actor_refresh marking actor %s as broken %d", actor, status));
3220 } 3401 }
@@ -3716,7 +3897,7 @@ int activitypub_get_handler(const xs_dict *req, const char *q_path,
3716 const char *type = xs_dict_get(i, "type"); 3897 const char *type = xs_dict_get(i, "type");
3717 const char *id = xs_dict_get(i, "id"); 3898 const char *id = xs_dict_get(i, "id");
3718 3899
3719 if (type && id && strcmp(type, "Note") == 0 && xs_startswith(id, snac.actor)) { 3900 if (type && id && strcmp(type, "Note") == 0 && is_msg_mine(&snac, id)) {
3720 if (is_msg_public(i)) { 3901 if (is_msg_public(i)) {
3721 xs *c_msg = msg_create(&snac, i); 3902 xs *c_msg = msg_create(&snac, i);
3722 list = xs_list_append(list, c_msg); 3903 list = xs_list_append(list, c_msg);