# Followers synchronisation (FEP-8fcf, owner decision 2026-10-06): 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. Both ways: # a follow PrivaPub lost (Mastodon drops it), and a follow Mastodon lost (it sends the Undo PrivaPub then applies). The # roll-call lists Mastodon's accounts only, and only to a signed request. 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"