diff options
| -rw-r--r-- | RELEASE_NOTES.md | 12 | ||||
| -rw-r--r-- | activitypub.c | 31 | ||||
| -rw-r--r-- | data.c | 22 | ||||
| -rw-r--r-- | doc/snac.5 | 4 | ||||
| -rw-r--r-- | html.c | 153 | ||||
| -rw-r--r-- | mastoapi.c | 4 | ||||
| -rw-r--r-- | po/cs.po | 32 | ||||
| -rw-r--r-- | rss.c | 2 | ||||
| -rw-r--r-- | snac.h | 3 | ||||
| -rw-r--r-- | upgrade.c | 2 | ||||
| -rw-r--r-- | webfinger.c | 4 |
11 files changed, 224 insertions, 45 deletions
diff --git a/RELEASE_NOTES.md b/RELEASE_NOTES.md index 7dfb332..505d080 100644 --- a/RELEASE_NOTES.md +++ b/RELEASE_NOTES.md @@ -1,5 +1,17 @@ # Release Notes +## UNRELEASED + +Quoted posts are now shown. + +Added metadata to remote users in the people page (contributed by dandelions). + +Fixed memory leak (contributed by dandelions). + +Fixed user matching (contributed by rakoo). + +Fixed typo in man page (contributed by spky). + ## 2.84 Implemented more scopes to match other ActivityPub implementations (public, unlisted, followers-only and direct message) (contributed by byte). diff --git a/activitypub.c b/activitypub.c index 0368ac8..90230d8 100644 --- a/activitypub.c +++ b/activitypub.c @@ -779,7 +779,7 @@ int is_msg_for_me(snac *snac, const xs_dict *c_msg) object_get(object, &obj); /* if it's about one of our posts, accept it */ - if (xs_startswith(object, snac->actor)) + if (is_msg_mine(snac, object)) return 2; /* blocked by hashtag? */ @@ -1242,7 +1242,7 @@ void notify(snac *snac, const char *type, const char *utype, const char *actor, if (xs_match(type, "Like|Announce|EmojiReact")) { /* if it's not an admiration about something by us, done */ - if (xs_is_null(objid) || !xs_startswith(objid, snac->actor)) + if (xs_is_null(objid) || !is_msg_mine(snac, objid)) return; /* if it's an announce by our own relay, done */ @@ -1267,7 +1267,7 @@ void notify(snac *snac, const char *type, const char *utype, const char *actor, return; /* if it's not ours and we didn't vote, discard */ - if (!xs_startswith(poll_id, snac->actor) && !was_question_voted(snac, poll_id)) + if (!is_msg_mine(snac, poll_id) && !was_question_voted(snac, poll_id)) return; } @@ -2686,6 +2686,20 @@ int process_input_message(snac *snac, const xs_dict *msg, const xs_dict *req) timeline_request(snac, &in_reply_to, &wrk, 0); + const char *quoted_id = xs_or(xs_dict_get(object, "quoteUri"), xs_dict_get(object, "quoteUrl")); + + if (xs_is_string(quoted_id) && xs_match(quoted_id, "https://*|http://*")) { /** **/ + xs *quoted_post = NULL; + int status; + + if (valid_status(status = activitypub_request(snac, quoted_id, "ed_post))) { + /* got quoted post */ + object_add(quoted_id, quoted_post); + } + + snac_debug(snac, 1, xs_fmt("retrieving quoted post %s %d", quoted_id, status)); + } + if (timeline_add(snac, id, object)) { snac_log(snac, xs_fmt("new '%s' %s %s", utype, actor, id)); do_notify = 1; @@ -2778,10 +2792,10 @@ int process_input_message(snac *snac, const xs_dict *msg, const xs_dict *req) if (xs_is_null(object)) snac_log(snac, xs_fmt("malformed message: no 'id' field")); else - if (is_muted(snac, actor) && !xs_startswith(object, snac->actor)) + if (is_muted(snac, actor) && !is_msg_mine(snac, object)) snac_log(snac, xs_fmt("dropped 'Announce' from muted actor %s", actor)); else - if (is_limited(snac, actor) && !xs_startswith(object, snac->actor)) + if (is_limited(snac, actor) && !is_msg_mine(snac, object)) snac_log(snac, xs_fmt("dropped 'Announce' from limited actor %s", actor)); else { xs *a_msg = NULL; @@ -2792,6 +2806,9 @@ int process_input_message(snac *snac, const xs_dict *msg, const xs_dict *req) if (valid_status(object_get(object, &a_msg))) { const char *who = get_atto(a_msg); + /* got the admired object: instance is [back] online */ + instance_failure(object, 2); + if (who && !is_muted(snac, who)) { /* bring the actor */ xs *who_o = NULL; @@ -2886,7 +2903,7 @@ int process_input_message(snac *snac, const xs_dict *msg, const xs_dict *req) snac_log(snac, xs_fmt("malformed message: no 'id' field")); else if (object_here(object)) { - if (xs_startswith(object, srv_baseurl) && !xs_startswith(object, actor)) + if (xs_startswith(object, srv_baseurl) && !is_msg_mine(snac, object)) snac_log(snac, xs_fmt("ignored incorrect 'Delete' %s %s", actor, object)); else { timeline_del(snac, object); @@ -3699,7 +3716,7 @@ int activitypub_get_handler(const xs_dict *req, const char *q_path, const char *type = xs_dict_get(i, "type"); const char *id = xs_dict_get(i, "id"); - if (type && id && strcmp(type, "Note") == 0 && xs_startswith(id, snac.actor)) { + if (type && id && strcmp(type, "Note") == 0 && is_msg_mine(&snac, id)) { if (is_msg_public(i)) { xs *c_msg = msg_create(&snac, i); list = xs_list_append(list, c_msg); @@ -1358,6 +1358,20 @@ int pending_count(snac *user) } +int is_msg_mine(snac *user, const char *id) +/* returns true if a post id is by the given user */ +{ + int ret = 0; + + if (xs_is_string(id)) { + xs *s1 = xs_fmt("%s/", user->actor); + ret = xs_startswith(id, s1); + } + + return ret; +} + + /** timeline **/ double timeline_mtime(snac *snac) @@ -1453,7 +1467,7 @@ void timeline_update_indexes(snac *snac, const char *id) { object_user_cache_add(snac, id, "private"); - if (xs_startswith(id, snac->actor)) { + if (is_msg_mine(snac, id)) { xs *msg = NULL; if (valid_status(object_get(id, &msg))) { @@ -1913,7 +1927,7 @@ int pin(snac *user, const char *id) { int ret = -2; - if (xs_startswith(id, user->actor)) { + if (is_msg_mine(user, id)) { if (is_pinned(user, id)) ret = -3; else @@ -3513,7 +3527,7 @@ void enqueue_output(snac *snac, const xs_dict *msg, const xs_str *inbox, int retries, int p_status) /* enqueues an output message to an inbox */ { - if (xs_startswith(inbox, snac->actor)) { + if (is_msg_mine(snac, inbox)) { snac_debug(snac, 1, xs_str_new("refusing enqueue to myself")); return; } @@ -4041,7 +4055,7 @@ void delete_purged_posts(snac *user, int days) if (xs_is_dict(msg)) { const char *id = xs_dict_get(msg, "id"); - if (xs_is_string(id) && xs_startswith(id, user->actor)) { + if (xs_is_string(id) && is_msg_mine(user, id)) { xs *d_msg = msg_delete(user, id); enqueue_message(user, d_msg); @@ -143,6 +143,8 @@ in the "last_announcement" field of the .Pa user.json file. When the file is modified, the announcement will then reappear. It can contain only text and will be ignored if it has more than 2048 bytes. +.It Pa server.pid +This file stores the server PID in a single text line. .El .Pp Each user directory is a subdirectory of @@ -222,8 +224,6 @@ after executing the 'export_csv' command-line operation. .It Pa import/ Mastodon-compatible CSV files must be copied into this directory to use any of the importing functions. -.It Pa server.pid -This file stores the server PID in a single text line. .El .Sh SEE ALSO .Xr snac 1 , @@ -1688,7 +1688,7 @@ xs_html *html_top_controls(snac *user) xs_html_text(L("Languages you usually post in:")), xs_html_sctag("br", NULL), xs_html_sctag("input", - xs_html_attr("type", "next"), + xs_html_attr("type", "text"), xs_html_attr("name", "post_langs"), xs_html_attr("value", post_langs), xs_html_attr("placeholder", L("en fr es de_AT")))), @@ -1891,7 +1891,7 @@ xs_html *html_entry_controls(snac *user, const char *actor, xs_html_attr("name", "redir"), xs_html_attr("value", redir)))); - if (!xs_startswith(id, user->actor)) { + if (!is_msg_mine(user, id)) { if (xs_list_in(likes, user->md5) == -1) { /* not already liked; add button */ xs_html_add(form, @@ -2387,6 +2387,23 @@ xs_html *html_entry(snac *user, xs_dict *msg, int read_only, /* c contains sanitized HTML */ xs_html_add(snac_content, xs_html_raw(c)); + + /* quoted post */ + const char *quoted_id = xs_or(xs_dict_get(msg, "quoteUri"), xs_dict_get(msg, "quoteUrl")); + + if (xs_is_string(quoted_id) && xs_match(quoted_id, "https://*|http://*")) { /** **/ + xs *quoted_post = NULL; + + if (valid_status(object_get(quoted_id, "ed_post))) { + xs_html_add(snac_content, + xs_html_tag("blockquote", + xs_html_attr("class", "snac-quoted-post"), + html_entry(user, quoted_post, 1, 1, NULL, 1))); + } + else + if (user) + enqueue_object_request(user, quoted_id, 0); + } } if (strcmp(type, "Question") == 0) { /** question content **/ @@ -2402,7 +2419,7 @@ xs_html *html_entry(snac *user, xs_dict *msg, int read_only, if (read_only) closed = 1; /* non-identified page; show as closed */ else - if (user && xs_startswith(id, user->actor)) + if (user && is_msg_mine(user, id)) closed = 1; /* we questioned; closed for us */ else if (user && was_question_voted(user, id)) @@ -2610,7 +2627,7 @@ xs_html *html_entry(snac *user, xs_dict *msg, int read_only, xs_html_attr("title", name)))); } else - if (xs_startswith(type, "video/")) { + if (xs_startswith(type, "video/") || strcmp(type, "Video") == 0) { xs_html_add(content_attachments, xs_html_tag("video", xs_html_attr("preload", "none"), @@ -3214,7 +3231,7 @@ xs_str *html_timeline(snac *user, const xs_list *list, int read_only, continue; } /* hide non-public posts viewed from outside */ - if (read_only && scope != SCOPE_PUBLIC){ + if (read_only && (scope != SCOPE_PUBLIC && scope != SCOPE_UNLISTED)) { continue; } @@ -3331,10 +3348,11 @@ xs_html *html_people_list(snac *user, xs_list *list, const char *header, const c /* content (user bio) */ const char *c = xs_dict_get(actor, "summary"); + const xs_val *tag = xs_dict_get(actor, "tag"); if (!xs_is_null(c)) { xs *sc = sanitize(c); - sc = replace_shortnames(sc, xs_dict_get(actor, "tag"), 2, proxy); + sc = replace_shortnames(sc, tag, 2, proxy); xs_html *snac_content = xs_html_tag("div", xs_html_attr("class", "snac-content")); @@ -3350,6 +3368,93 @@ xs_html *html_people_list(snac *user, xs_list *list, const char *header, const c xs_html_add(snac_post, snac_content); } + /* add user metadata */ + xs_html *snac_metadata = xs_html_tag("div", + xs_html_attr("class", "snac-metadata")); + + int count = 0; + const xs_val *address = xs_dict_get(actor, "vcard:Address"); + if (xs_is_string(address)) { + xs_html_add(snac_metadata, + xs_html_tag("span", + xs_html_attr("class", "snac-property-name"), + xs_html_raw("📍 Location")), + xs_html_text(":"), + xs_html_raw(" "), + xs_html_tag("span", + xs_html_attr("class", "snac-property-value p-adr"), + xs_html_text(address)), + xs_html_sctag("br", NULL)); + + count++; + } + + const xs_val *birthday = xs_dict_get(actor, "vcard:bday"); + if (xs_is_string(birthday)) { + xs_html_add(snac_metadata, + xs_html_tag("span", + xs_html_attr("class", "snac-property-name"), + xs_html_raw("🎂 Birthday")), + xs_html_text(":"), + xs_html_raw(" "), + xs_html_tag("time", + xs_html_attr("class", "snac-property-value dt-bday"), + xs_html_text(birthday)), + xs_html_sctag("br", NULL)); + + count++; + } + + const xs_list *attachment = xs_dict_get(actor, "attachment"); + if (count > 0 && xs_list_len(attachment) > 0) { + xs_html_add(snac_metadata, + xs_html_sctag("hr", + xs_html_attr("class", "snac-property-divider"))); + } + + const xs_val *v; + xs_list_foreach(attachment, v) { + const char *type = xs_dict_get(v, "type"); + const char *name = xs_dict_get(v, "name"); + const char *value = xs_dict_get(v, "value"); + + if (!xs_is_null(type) && !xs_is_null(name) && + !xs_is_null(value) && strcmp(type, "PropertyValue") == 0) { + /* both the name and the value can contain emoji */ + xs *nam = sanitize(name); + nam = replace_shortnames(nam, tag, 1, proxy); + + /* todo: sometimes the value is transmitted as markdown and not html ._. */ + xs *val = sanitize(value); + val = replace_shortnames(val, tag, 1, proxy); + + /* delete <p> tags, because some software sends them */ + val = xs_replace_i(val, "<p>", ""); + val = xs_replace_i(val, "</p>", ""); + + xs_html_add(snac_metadata, + xs_html_tag("span", + xs_html_attr("class", "snac-property-name"), + xs_html_raw(nam)), + xs_html_text(":"), + xs_html_raw(" "), + xs_html_tag("span", + xs_html_attr("class", "snac-property-value"), + xs_html_raw(val)), + xs_html_sctag("br", NULL)); + + count++; + } + } + + if (count > 0) { + xs_html_add(snac_post, snac_metadata); + } + else { + /* free the html, by rendering it... */ + xs_free(xs_html_render(snac_metadata)); + } + /* buttons */ xs *btn_form_action = xs_fmt("%s/admin/action", user->actor); @@ -3555,9 +3660,13 @@ xs_str *html_notifications(snac *user, int skip, int show) if (valid_status(actor_get(actor_id, &actor))) a_name = actor_name(actor, proxy); - else + else { a_name = xs_dup(actor_id); + /* actor not here: request it */ + enqueue_actor_refresh(user, actor_id, 0); + } + xs *label_sanitized = sanitize(type); const char *label = label_sanitized; @@ -3630,7 +3739,7 @@ xs_str *html_notifications(snac *user, int skip, int show) xs_html_attr("class", "snac-post-with-desc"), html_label); - if (strcmp(type, "Follow") == 0 || strcmp(utype, "Follow") == 0 || strcmp(type, "Block") == 0) { + if (strcmp(type, "Block") == 0) { if (actor) xs_html_add(entry, xs_html_tag("div", @@ -3638,6 +3747,32 @@ xs_str *html_notifications(snac *user, int skip, int show) html_actor_icon(user, actor, NULL, NULL, NULL, -1, 0, proxy, NULL, NULL))); } else + if (strcmp(type, "Follow") == 0 || strcmp(utype, "Follow") == 0) { + if (actor) { + xs *action = xs_fmt("%s/admin/action", user->actor); + xs_html *button = NULL; + + if (following_check(user, actor_id)) + button = html_button("unfollow", L("Unfollow"), L("Stop following this user's activity")); + else + button = html_button("follow", L("Follow"), L("Start following this user's activity")); + + xs_html_add(entry, + xs_html_tag("div", + xs_html_attr("class", "snac-post"), + html_actor_icon(user, actor, NULL, NULL, NULL, -1, 0, proxy, NULL, NULL), + xs_html_tag("form", + xs_html_attr("method", "post"), + xs_html_attr("action", action), + xs_html_sctag("input", + xs_html_attr("type", "hidden"), + xs_html_attr("name", "actor"), + xs_html_attr("value", actor_id)), + button, + xs_html_sctag("br", NULL)))); + } + } + else if (strcmp(type, "Move") == 0) { const xs_dict *o_msg = xs_dict_get(noti, "msg"); const char *target; @@ -4880,7 +5015,7 @@ int html_post_handler(const xs_dict *req, const char *q_path, } else { /* delete an entry */ - if (xs_startswith(id, snac.actor) && !is_draft(&snac, id)) { + if (is_msg_mine(&snac, id) && !is_draft(&snac, id)) { /* it's a post by us: generate a delete */ xs *msg = msg_delete(&snac, id); @@ -1919,7 +1919,7 @@ int mastoapi_get_handler(const xs_dict *req, const char *q_path, /* add only posts by the author */ if (!xs_is_null(msg_id) && strcmp(xs_dict_get(msg, "type"), "Note") == 0 && - xs_startswith(xs_dict_get(msg, "id"), snac2.actor) && is_msg_public(msg)) { + is_msg_mine(&snac2, xs_dict_get(msg, "id")) && is_msg_public(msg)) { /* if max_id is set, skip entries until we find it */ if (skip_until_max) { @@ -3824,7 +3824,7 @@ int mastoapi_delete_handler(const xs_dict *req, const char *q_path, if (valid_status(object_get_by_md5(p, &obj))) { const char *id = xs_dict_get(obj, "id"); - if (xs_is_string(id) && xs_startswith(id, snac.actor)) { + if (xs_is_string(id) && is_msg_mine(&snac, id)) { xs *out = mastoapi_status(&snac, obj); xs *msg = msg_delete(&snac, id); @@ -14,7 +14,7 @@ msgstr "Citlivý obsah: " #: html.c:448 msgid "Sensitive content description" -msgstr "Varování k citlivému obsahu" +msgstr "Varování o citlivém obsahu" msgid "Only for mentioned people: " msgstr "Pouze pro zmíněné osoby:" @@ -185,11 +185,11 @@ msgstr "Vaše jméno" #: html.c:1507 msgid "Avatar: " -msgstr "Avatar: " +msgstr "Profilový obrázek: " #: html.c:1515 msgid "Delete current avatar" -msgstr "Smazat současný avatar" +msgstr "Smazat profilový obrázek" #: html.c:1517 msgid "Header image (banner): " @@ -197,7 +197,7 @@ msgstr "Obrázek v záhlaví profilu: " #: html.c:1525 msgid "Delete current header image" -msgstr "Smazat současný obrázek v záhlaví" +msgstr "Smazat obrázek v záhlaví" #: html.c:1527 msgid "Bio:" @@ -205,11 +205,11 @@ msgstr "Bio:" #: html.c:1533 msgid "Write about yourself here..." -msgstr "Napište sem něco o sobě..." +msgstr "Napište něco o sobě..." #: html.c:1542 msgid "Always show sensitive content" -msgstr "Vždy zobrazit příspěvky s varováním o citlivém obsahu" +msgstr "Rozbalit citlivé příspěvky" #: html.c:1544 msgid "Email address for notifications:" @@ -233,11 +233,11 @@ msgstr "Zahodit soukromé zprávy od lidí, které nesledujete" #: html.c:1611 msgid "This account is a bot" -msgstr "Tenhle účet je robot" +msgstr "Tento účet je robotem" #: html.c:1620 msgid "Auto-boost all mentions to this account" -msgstr "Automaticky boostovat všechny zmíňky o tomto účtu" +msgstr "Automaticky boostit všechna zmínění tohoto účtu" #: html.c:1629 msgid "This account is private (posts are not shown through the web)" @@ -246,11 +246,11 @@ msgstr "" #: html.c:1639 msgid "Collapse top threads by default" -msgstr "Zobrazovat vlákna složená" +msgstr "Složit vlákna" #: html.c:1648 msgid "Follow requests must be approved" -msgstr "Žádosti o sledování je nutno manuálně potvrdit" +msgstr "Žádosti o sledování je nutné manuálně potvrdit" #: html.c:1657 msgid "Publish follower and following metrics" @@ -274,7 +274,7 @@ msgstr "Nové heslo:" #: html.c:1710 msgid "Repeat new password:" -msgstr "Zopakujte nové heslo:" +msgstr "Nové heslo znovu:" #: html.c:1720 msgid "Update user info" @@ -330,7 +330,7 @@ msgstr "Odboostit" #: html.c:1930 msgid "I regret I boosted this" -msgstr "Boostit to byl blbej nápad" +msgstr "Boostit to byl špatný nápad" #: html.c:1936 html.c:4922 msgid "Unbookmark" @@ -438,7 +438,7 @@ msgstr "Událost" #: html.c:2246 html.c:2275 msgid "boosted" -msgstr "boostuje" +msgstr "boostí" #: html.c:2289 msgid "in reply to" @@ -478,7 +478,7 @@ msgstr "Popisek..." #: html.c:2697 msgid "Source channel or community" -msgstr "" +msgstr "Původní kanál nebo komunita" #: html.c:2791 msgid "Time: " @@ -519,7 +519,7 @@ msgstr "Rozepsané příspěvky" #: html.c:3177 msgid "No more unseen posts" -msgstr "Nic víc nového" +msgstr "Nic nového" #: html.c:3181 html.c:3290 msgid "Back to top" @@ -623,7 +623,7 @@ msgstr "Nové" #: html.c:3707 msgid "Already seen" -msgstr "Zobrazeno dříve" +msgstr "Již viděno" #: html.c:3722 msgid "None" @@ -59,7 +59,7 @@ xs_str *rss_from_timeline(snac *user, const xs_list *timeline, const char *content = xs_dict_get(msg, "content"); const char *published = xs_dict_get(msg, "published"); - if (user && !xs_startswith(id, user->actor)) + if (user && !is_msg_mine(user, id)) continue; if (!id || !content || !published) @@ -1,7 +1,7 @@ /* snac - A simple, minimalistic ActivityPub instance */ /* copyright (c) 2022 - 2025 grunfink et al. / MIT license */ -#define VERSION "2.84" +#define VERSION "2.85-dev" #define USER_AGENT "snac/" VERSION @@ -388,6 +388,7 @@ int send_to_inbox(snac *snac, const xs_str *inbox, const xs_dict *msg, xs_str *get_actor_inbox(const char *actor, int shared); int send_to_actor(snac *snac, const char *actor, const xs_dict *msg, xs_val **payload, int *p_size, int timeout); +int is_msg_mine(snac *user, const char *id); int is_msg_public(const xs_dict *msg); int is_msg_from_private_user(const xs_dict *msg); int is_msg_for_me(snac *snac, const xs_dict *msg); @@ -213,7 +213,7 @@ int snac_upgrade(xs_str **error) object_add_ow(id, o); /* if it's from us, add to public */ - if (xs_startswith(id, snac.actor)) { + if (is_msg_mine(&snac, id)) { const xs_list *p; const char *v; int c; diff --git a/webfinger.c b/webfinger.c index 12ec42c..1ce5e76 100644 --- a/webfinger.c +++ b/webfinger.c @@ -76,9 +76,9 @@ int webfinger_request_signed(snac *snac, const char *qs, xs_str **actor, xs_str xs *url = xs_fmt("%s:/" "/%s/.well-known/webfinger?resource=%s", proto, host, resource); if (snac == NULL) - xs_http_request("GET", url, headers, NULL, 0, &status, &payload, &p_size, 0); + xs_free(xs_http_request("GET", url, headers, NULL, 0, &status, &payload, &p_size, 0)); else - http_signed_request(snac, "GET", url, headers, NULL, 0, &status, &payload, &p_size, 0); + xs_free(http_signed_request(snac, "GET", url, headers, NULL, 0, &status, &payload, &p_size, 0)); } if (obj == NULL && valid_status(status) && payload) { |