Pins across servers, both ways

An account elsewhere that pins or unpins one of its posts (Add or Remove on its `featured`) now shows those pins on its
profile here (`pinned=true`), in its order and its public posts only; a community's announce of a moderator's Add does
the same for the community. The `featured` collection itself is read with the account's counts, at most once a day, so
pins made before PrivaPub ever saw an account show too. Any other target (Smithereen's wall, a community's moderators)
is dropped. A persona's pin and unpin go to the post's audience as Add and Remove on /trophies, as Mastodon sends them.

Checked live against Mastodon (scenarios/pins.sh, 8 checks). The town's checker learnt three peer rules from the
village: Misskey and Sharkey keep a forwarded reply only with its author's LD signature (only Mastodon signs), they
count no renote by a bot, and Mastodon never sees a Lemmy vote on a post in a community. The village of 2026-10-05
checks clean, 2454 of 2454.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LsXgEaXee4GCU1hwYgPJXw
This commit is contained in:
thepraandClaude Opus 5.5 committed 2026-10-05 22:48:57 +02:00
1 parent 5f150b7a59
commit 1c7d1ece9c
23 files changed
+510 -19

No files matched your search

+54
View File
@@ -0,0 +1,54 @@
# Pinned posts across servers (featured, FEP-e232's Add and Remove): a Mastodon account pins and unpins while alice
# follows it; alice pins and unpins while it follows her; an account that pinned before PrivaPub ever saw it shows its
# pins once resolved (its featured collection, read with its counts). Needs the mastodon peer.
M=https://mastodon.test:6443
mcurl() { curl -sk --resolve mastodon.test:6443:127.0.0.1 "$@"; }
. "$here/peers/mastodon.sh"
p_pinned() { curl -s -H "$PH" "$P/api/v1/accounts/$1/statuses?pinned=true" | j "print(' '.join(s['uri'] for s in d))"; }
m_pinned() { mcurl -H "$MH" "$M/api/v1/accounts/$1/statuses?pinned=true" | j "print(' '.join(s['uri'] for s in d))"; }
echo "pins"
PT=$(privapub_token alice_pins)
PH="Authorization: Bearer $PT"
[ -n "$PT" ] && ok "PrivaPub token for alice_pins" || { ko "PrivaPub token for alice_pins"; return 1; }
run=$(date +%s)
mastodon_user pinner
MT=$(mastodon_token pinner)
MH="Authorization: Bearer $MT"
[ -n "$MT" ] && ok "Mastodon token for pinner" || { ko "Mastodon token for pinner"; return 1; }
echo " following each other"
pinner_on_p=$(curl -s -H "$PH" "$P/api/v2/search?q=pinner@mastodon.test&resolve=true&type=accounts" | j "print(d['accounts'][0]['id'])")
curl -s -o /dev/null -X POST -H "$PH" "$P/api/v1/accounts/$pinner_on_p/follow"
alice_on_m=$(mcurl -H "$MH" "$M/api/v2/search?q=@alice_pins@privapub.test&resolve=true&type=accounts" | j "print(d['accounts'][0]['id'])")
mcurl -o /dev/null -X POST -H "$MH" "$M/api/v1/accounts/$alice_on_m/follow"
until_true 45 '[ "$(curl -s -H "$PH" "$P/api/v1/accounts/relationships?id[]=$pinner_on_p" | j "print(d[0][\"following\"] and d[0][\"followed_by\"])")" = "True" ]' \
&& ok "alice and pinner follow each other" || ko "the follows between alice and pinner never took"
echo " pinner pins"
m_post=$(mcurl -X POST -H "$MH" "$M/api/v1/statuses" -d "status=a Mastodon post worth pinning $run&visibility=public")
m_post_id=$(echo "$m_post" | j "print(d['id'])"); m_post_uri=$(echo "$m_post" | j "print(d['uri'])")
mcurl -o /dev/null -X POST -H "$MH" "$M/api/v1/statuses/$m_post_id/pin"
until_true 45 '[ "$(p_pinned $pinner_on_p)" = "$m_post_uri" ]' && ok "pinner's pin shows on its profile here" || ko "pinner's pin never arrived ($(p_pinned $pinner_on_p))"
mcurl -o /dev/null -X POST -H "$MH" "$M/api/v1/statuses/$m_post_id/unpin"
until_true 45 '[ -z "$(p_pinned $pinner_on_p)" ]' && ok "and its unpin takes it off" || ko "pinner's unpin never arrived"
echo " alice pins"
p_post=$(curl -s -X POST -H "$PH" "$P/api/v1/statuses" -d "status=a PrivaPub post worth pinning $run&visibility=public")
p_post_id=$(echo "$p_post" | j "print(d['id'])"); p_post_uri=$(echo "$p_post" | j "print(d['uri'])")
until_true 45 '[ "$(mcurl -H "$MH" "$M/api/v1/accounts/$alice_on_m/statuses?limit=5" | j "print(any(s[\"uri\"] == \"$p_post_uri\" for s in d))")" = "True" ]'
curl -s -o /dev/null -X POST -H "$PH" "$P/api/v1/statuses/$p_post_id/pin"
until_true 45 '[ "$(m_pinned $alice_on_m)" = "$p_post_uri" ]' && ok "alice's pin shows on her profile on Mastodon" || ko "alice's pin never reached Mastodon ($(m_pinned $alice_on_m))"
curl -s -o /dev/null -X POST -H "$PH" "$P/api/v1/statuses/$p_post_id/unpin"
until_true 45 '[ -z "$(m_pinned $alice_on_m)" ]' && ok "and her unpin takes it off" || ko "alice's unpin never reached Mastodon"
echo " pins made before"
# a fresh account pins first; PrivaPub reads its featured collection when it first fetches it
early="early$run"
mastodon_user "$early"
ET=$(mastodon_token "$early")
e_post=$(mcurl -X POST -H "Authorization: Bearer $ET" "$M/api/v1/statuses" -d "status=pinned before anyone looked $run&visibility=public")
mcurl -o /dev/null -X POST -H "Authorization: Bearer $ET" "$M/api/v1/statuses/$(echo "$e_post" | j "print(d['id'])")/pin"
early_on_p=$(curl -s -H "$PH" "$P/api/v2/search?q=$early@mastodon.test&resolve=true&type=accounts" | j "print(d['accounts'][0]['id'])")
until_true 45 '[ "$(p_pinned $early_on_p)" = "$(echo "$e_post" | j "print(d[\"uri\"])")" ]' \
&& ok "an account's earlier pin shows once it is resolved" || ko "the earlier pin never showed ($(p_pinned $early_on_p))"
+19 -7
View File
@@ -37,10 +37,17 @@ NEEDS_PICTURE = {"pixelfed"}
# others there (nobody relays a followers-only reply), nor anything under it
THREAD_BOUND = {"friendica"}
# Mastodon takes an activity another server passes on only with its author's LD signature (or a FEP-8b32 proof) and
# drops it otherwise (ActivityPub::ProcessActivityService); of the platforms here only Mastodon and Misskey and its forks
# sign their Creates that way, so another's reply PrivaPub passes on never lands there
NEEDS_LD_SIGNATURE = {"mastodon"}
LD_SIGNS = {"mastodon", "misskey", "sharkey"}
# drops it otherwise (ActivityPub::ProcessActivityService); Misskey and Sharkey skip it the same way ("http-signature
# verification failed and no LD-Signature"). Of the platforms here only Mastodon signs its Creates that way: Misskey
# 2026.10's carry no signature (seen in the village of 2026-10-05), so another's reply PrivaPub passes on lands there
# only when Mastodon wrote it
NEEDS_LD_SIGNATURE = {"mastodon", "misskey", "sharkey"}
LD_SIGNS = {"mastodon"}
# Misskey and Sharkey count no renote by a bot (NoteCreateService: `!user.isBot` before incRenoteCount)
BOT_RENOTES_UNCOUNTED = {"misskey", "sharkey"}
# a Lemmy account's vote goes to the community alone, which announces it; Mastodon takes no Announce of a Like, so a
# Lemmy vote on a Mastodon post in a community never counts there
GROUP_VOTES_UNSEEN = {"mastodon"}
class World:
@@ -256,6 +263,7 @@ class Sweep:
# -- counts on the object's own server, and PrivaPub's exact counts
def counts(self, stored):
likes, reacts, boosts, votes = defaultdict(int), defaultdict(lambda: defaultdict(int)), defaultdict(int), defaultdict(lambda: defaultdict(int))
bot_boosts = defaultdict(int)
for f in self.w.interactions:
ref = f["ref"]
if f["verb"] == "like":
@@ -269,6 +277,8 @@ class Sweep:
reacts[ref]["❤️" if f["emoji"] in HEARTS else f["emoji"]] += 1
elif f["verb"] == "boost":
boosts[ref] += 1
if self.w.planner.accounts[f["actor"]]["bot"]:
bot_boosts[ref] += 1
elif f["verb"] == "vote":
for c in f["choices"]:
votes[ref][c] += 1
@@ -293,12 +303,14 @@ class Sweep:
self.add(f"count.vote.{v}", "privapub", "privapub", row.votes == want, ref, want, row.votes)
elif origin != "lemmy":
# elsewhere: at least the plain likes that every platform sends as Like
plain = sum(1 for f in self.w.interactions if f["ref"] == ref and f["verb"] == "like")
plain = sum(1 for f in self.w.interactions if f["ref"] == ref and f["verb"] == "like"
and not (origin in GROUP_VOTES_UNSEEN and o.get("group") and self.w.platform(f["actor"]) == "lemmy"))
if plain:
total = (row.likes or 0)
self.add(f"count.like.{v}", "*", origin, total >= plain, ref, f">={plain}", total)
if boosts[ref]:
self.add(f"count.boost.{v}", "*", origin, (row.boosts or 0) >= boosts[ref], ref, f">={boosts[ref]}", row.boosts)
counted = boosts[ref] - (bot_boosts[ref] if origin in BOT_RENOTES_UNCOUNTED else 0)
if counted:
self.add(f"count.boost.{v}", "*", origin, (row.boosts or 0) >= counted, ref, f">={counted}", row.boosts)
if votes[ref]:
want = sum(votes[ref].values())
got = sum(row.votes or [])