FEP-8fcf, received. When a delivery's Collection-Synchronization header digests the sender's followers on PrivaPub
otherwise than the personas following it, a job reads the list the header names (on the sender's origin, signed by the
instance actor): a follow the list leaves out ends, only when the list is the one the digest describes; a request it
lists is taken as accepted; a persona it lists that follows nothing there sends Undo{Follow}, as Mastodon does. Each
claiming delivery is compared once.
Checked live (scenarios/followsync.sh, now 15 checks): PrivaPub ends a follow Mastodon lost and undoes one only
Mastodon remembered.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LsXgEaXee4GCU1hwYgPJXw
80 lines
6.1 KiB
Bash
80 lines
6.1 KiB
Bash
# Followers synchronisation (FEP-8fcf, owner decision 2026-10-06), both ways. Sent: alice's followers-only post tells
|
|
# Mastodon, in a signed header, a digest of her followers there; when Mastodon's view differs it reads her roll-call and
|
|
# mends itself: a follow PrivaPub lost (Mastodon drops it), and a follow Mastodon lost (it sends the Undo PrivaPub
|
|
# applies). The roll-call answers only a signed request. Received: mastouser's followers-only post tells PrivaPub a digest
|
|
# of its followers here (alice and bob); PrivaPub reads Mastodon's list and ends a follow Mastodon lost, and undoes one
|
|
# only Mastodon remembers. Needs the mastodon peer.
|
|
M=https://mastodon.test:6443
|
|
mcurl() { curl -sk --resolve mastodon.test:6443:127.0.0.1 "$@"; }
|
|
. "$here/peers/mastodon.sh"
|
|
m_rails() { podman exec pasture-mastodon bin/rails runner "$1" 2>/dev/null | tail -1; }
|
|
p_mongo() { podman exec pasture-mongo mongosh --quiet PrivaPub --eval "$1"; }
|
|
|
|
echo "followsync"
|
|
AT=$(privapub_token alice_sync)
|
|
AH="Authorization: Bearer $AT"
|
|
[ -n "$AT" ] && ok "PrivaPub token for alice_sync" || { ko "PrivaPub token for alice_sync"; return 1; }
|
|
MT=$(mastodon_token)
|
|
MH="Authorization: Bearer $MT"
|
|
run=$(date +%s)
|
|
alice_acct=$(curl -s -H "$AH" "$P/api/v1/accounts/verify_credentials")
|
|
alice_id=$(echo "$alice_acct" | j "print(d['id'])")
|
|
alice_uri=$(echo "$alice_acct" | j "print(d['url'])" | sed 's|/@|/peasants/|')
|
|
alice_followers() { curl -s -H "$AH" "$P/api/v1/accounts/verify_credentials" | j "print(d['followers_count'])"; }
|
|
alice_on_m=$(mcurl -H "$MH" "$M/api/v2/search?q=@alice_sync@privapub.test&resolve=true&type=accounts" | j "print(d['accounts'][0]['id'])")
|
|
# (from Mastodon's database: its API caches relationships a day, and the scenario changes them behind its back)
|
|
m_follows() { m_rails "puts Follow.exists?(account: Account.find_local(\"mastouser\"), target_account: Account.find_by(uri: \"$alice_uri\")) ? \"True\" : \"False\""; }
|
|
mastouser_uri=$(m_rails 'puts ActivityPub::TagManager.instance.uri_for(Account.find_local("mastouser"))')
|
|
follow_from_m() {
|
|
mcurl -o /dev/null -X POST -H "$MH" "$M/api/v1/accounts/$alice_on_m/follow"
|
|
until_true 45 '[ "$(m_follows)" = "True" ] && [ "$(alice_followers)" = "1" ]'
|
|
}
|
|
quiet_post() { curl -s -o /dev/null -X POST -H "$AH" "$P/api/v1/statuses" -d "status=$1&visibility=private"; }
|
|
|
|
[ "$(m_follows)" = "True" ] || follow_from_m
|
|
[ "$(m_follows)" = "True" ] && [ "$(alice_followers)" = "1" ] && ok "mastouser follows alice" || ko "mastouser never followed alice"
|
|
|
|
echo " the roll-call"
|
|
[ "$(pfetch -s -o /dev/null -w '%{http_code}' -H 'Accept: application/activity+json' "$alice_uri/groupies/roll-call")" = "401" ] \
|
|
&& ok "an unsigned roll-call is refused" || ko "the roll-call answered an unsigned request"
|
|
|
|
echo " a follow PrivaPub lost"
|
|
p_mongo "db.Follower.deleteMany({LocalActorId:'$alice_id', ActorURI:'$mastouser_uri'})" >/dev/null
|
|
[ "$(alice_followers)" = "0" ] && ok "PrivaPub forgets mastouser" || ko "PrivaPub still counts mastouser"
|
|
# (nothing goes to Mastodon for a follower PrivaPub does not know of: alice names mastouser, so her post still goes there)
|
|
quiet_post "@mastouser@mastodon.test for my followers $run"
|
|
until_true 60 '[ "$(m_follows)" = "False" ]' && ok "Mastodon reads alice's roll-call and drops the follow" || ko "Mastodon still has mastouser following alice"
|
|
|
|
echo " a follow Mastodon lost"
|
|
follow_from_m && ok "mastouser follows alice again" || ko "mastouser could not follow alice again"
|
|
m_rails "a = Account.find_local(\"mastouser\"); t = Account.find_by(uri: \"$alice_uri\"); Follow.where(account: a, target_account: t).destroy_all" >/dev/null
|
|
[ "$(m_follows)" = "False" ] && ok "Mastodon forgets the follow" || ko "Mastodon still has the follow"
|
|
quiet_post "for my followers again $run"
|
|
until_true 60 '[ "$(alice_followers)" = "0" ]' && ok "Mastodon reads the roll-call and undoes the follow PrivaPub had" || ko "PrivaPub still counts mastouser"
|
|
|
|
echo " received: mastouser's followers here"
|
|
BT=$(privapub_token bob_sync)
|
|
BH="Authorization: Bearer $BT"
|
|
m_on_p=$(curl -s -H "$AH" "$P/api/v2/search?q=mastouser@mastodon.test&resolve=true&type=accounts" | j "print(d['accounts'][0]['id'])")
|
|
p_follows() { curl -s -H "$1" "$P/api/v1/accounts/relationships?id[]=$m_on_p" | j "print(d[0]['following'])"; }
|
|
m_followed_by() { m_rails "puts Follow.exists?(account: Account.find_by(uri: \"${P_BASE:-https://privapub.test}/peasants/$1\"), target_account: Account.find_local(\"mastouser\")) ? \"True\" : \"False\""; }
|
|
m_quiet() { mcurl -s -o /dev/null -X POST -H "$MH" "$M/api/v1/statuses" -d "status=$1&visibility=private"; }
|
|
for who in "$AH" "$BH"; do
|
|
[ "$(p_follows "$who")" = "True" ] || curl -s -o /dev/null -X POST -H "$who" "$P/api/v1/accounts/$m_on_p/follow"
|
|
done
|
|
until_true 45 '[ "$(p_follows "$AH")" = "True" ] && [ "$(p_follows "$BH")" = "True" ] && [ "$(m_followed_by alice_sync)" = "True" ] && [ "$(m_followed_by bob_sync)" = "True" ]' \
|
|
&& ok "alice and bob follow mastouser" || ko "alice and bob never both followed mastouser"
|
|
|
|
m_rails "Follow.where(account: Account.find_by(uri: \"https://privapub.test/peasants/alice_sync\"), target_account: Account.find_local(\"mastouser\")).destroy_all" >/dev/null
|
|
[ "$(m_followed_by alice_sync)" = "False" ] && ok "Mastodon forgets alice's follow" || ko "Mastodon still has alice's follow"
|
|
m_quiet "for my followers $run"
|
|
until_true 60 '[ "$(p_follows "$AH")" = "False" ]' && ok "PrivaPub reads mastouser's list and ends alice's follow" || ko "alice still follows mastouser on PrivaPub"
|
|
[ "$(p_follows "$BH")" = "True" ] && ok "bob's follow stays" || ko "bob's follow ended too"
|
|
|
|
curl -s -o /dev/null -X POST -H "$AH" "$P/api/v1/accounts/$m_on_p/follow"
|
|
until_true 45 '[ "$(p_follows "$AH")" = "True" ] && [ "$(m_followed_by alice_sync)" = "True" ]' && ok "alice follows mastouser again" || ko "alice could not follow mastouser again"
|
|
p_mongo "db.Following.deleteMany({AvatarId:'$alice_id', TargetActorURI:'$mastouser_uri'})" >/dev/null
|
|
[ "$(p_follows "$AH")" = "False" ] && ok "PrivaPub forgets alice's follow" || ko "PrivaPub still has alice's follow"
|
|
m_quiet "for my followers again $run"
|
|
until_true 60 '[ "$(m_followed_by alice_sync)" = "False" ]' && ok "PrivaPub undoes the follow only Mastodon remembered" || ko "Mastodon still has alice following mastouser"
|