Files
thepraandClaude Opus 5.5 3d7d406834 A persona's archive: exported in Mastodon's layout, imported without telling anyone
A root exports one of its personas (a job) as Mastodon's account archive, so other servers' importers read it: the
actor with its public key only, its own posts and boosts with their media, likes, bookmarks and Mastodon's CSV files,
plus PrivaPub's filters, followed hashtags, notification policy, pins, scheduled and located posts; nothing of its root,
its siblings, its keys or anyone's token. A ticket link downloads it, for a week.

An archive (PrivaPub's or Mastodon's) uploaded in pieces is imported into a persona in a job, the parts the root picks,
with progress and a stop. SafeArchive refuses links, escaping paths, duplicates, bombs and oversized items, and reads the
outbox one item at a time. Imported posts are delivered to no one, put in no home and notify nobody, yet show on the
profile, outbox, hashtags and search; back home a post keeps its id, from another actor it gets one of its date and
ImportedFromURI, so importing twice changes nothing. Relationships go through the existing services; located, scheduled
and likes only when asked; followers never.

tools/pasture/scenarios/persona-archive.sh imports mastouser's real Mastodon archive (156 posts, 24 pictures) into a
persona Mastodon follows: Mastodon receives none of it, and a second import changes nothing.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LsXgEaXee4GCU1hwYgPJXw
2026-10-07 12:43:12 +02:00

71 lines
5.0 KiB
Bash

# A persona's archive (owner decision 2026-10-07), with a real one: mastouser's own account archive, made by Mastodon's
# backup service, is imported into mover_archive, a PrivaPub persona mastouser follows. Its posts arrive, with their
# media, on the persona's profile, and nobody is told: Mastodon, which would get anything delivered to the persona's
# followers, holds none of the copies. Importing it again changes nothing. Then the persona's own archive is exported and
# downloaded through its link, in Mastodon's layout. 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; }
echo "persona-archive"
JWT=$(privapub_root)
RH="Authorization: Bearer $JWT"
AT=$(privapub_token mover_archive)
AH="Authorization: Bearer $AT"
[ -n "$AT" ] && ok "PrivaPub token for mover_archive" || { ko "PrivaPub token for mover_archive"; return 1; }
me=$(curl -s -H "$AH" "$P/api/v1/accounts/verify_credentials")
me_id=$(echo "$me" | j "print(d['id'])")
me_uri=$(echo "$me" | j "print(d['url'])" | sed 's|/@|/peasants/|')
A="$P/clientapi/persona/$me_id/archive"
# each run starts from a persona that has imported nothing
podman exec pasture-mongo mongosh --quiet PrivaPub --eval "db.Post.deleteMany({GroupUserId:'$me_id', ImportedFromURI:{\$exists:true, \$ne:null}}); db.MediaAttachment.deleteMany({OwnerAvatarId:'$me_id'})" >/dev/null
m_follows() { m_rails "puts Follow.exists?(account: Account.find_local(\"mastouser\"), target_account: Account.find_by(uri: \"$me_uri\")) ? \"True\" : \"False\""; }
if [ "$(m_follows)" != "True" ]; then
on_m=$(mcurl -H "Authorization: Bearer $(mastodon_token)" "$M/api/v2/search?q=@mover_archive@privapub.test&resolve=true&type=accounts" | j "print(d['accounts'][0]['id'])")
mcurl -o /dev/null -X POST -H "Authorization: Bearer $(mastodon_token)" "$M/api/v1/accounts/$on_m/follow"
fi
until_true 45 '[ "$(m_follows)" = "True" ]' && ok "mastouser follows mover_archive" || ko "mastouser never followed mover_archive"
dump=$(m_rails 'u = Account.find_local("mastouser").user; b = u.backups.create!; BackupService.new.call(b); puts b.reload.dump.path')
podman cp "pasture-mastodon:$dump" "$work/mastodon.zip" 2>/dev/null
[ -s "$work/mastodon.zip" ] && ok "Mastodon made mastouser's archive" || { ko "Mastodon made no archive"; return 1; }
import() {
local upload
upload=$(curl -s -X POST -H "$RH" "$P/clientapi/persona/archive/uploads" | j "print(d['id'])")
curl -s -o /dev/null -X PUT -H "$RH" -H "Content-Type: application/octet-stream" --data-binary "@$work/mastodon.zip" "$P/clientapi/persona/archive/uploads/$upload?offset=0"
curl -s -o /dev/null -w '%{http_code}' -X POST -H "$RH" -H 'Content-Type: application/json' "$A/import" -d "{\"uploadId\":\"$upload\",\"parts\":[\"posts\"]}"
}
state() { curl -s -H "$RH" "$A" | j "print(d['$1'])"; }
count() { curl -s -H "$RH" "$A" | j "print(d['importCounts'].get('$1', 0))"; }
[ "$(import)" = "202" ] && ok "the import is asked for" || { ko "the import was refused"; return 1; }
until_true 150 '[ "$(state importState)" = "done" ]' && ok "the import is done" || { ko "the import is $(state importState): $(state importError)"; return 1; }
posts=$(count posts)
[ "$posts" -gt 0 ] && ok "$posts of mastouser's posts imported ($(count 'boosts skipped') boosts and $(count 'direct or circle posts skipped') private posts left out)" \
|| ko "no post was imported"
[ "$(count media)" -gt 0 ] && ok "$(count media) pictures came with them" || ko "no picture came with them"
copies=$(curl -s -H "$AH" "$P/api/v1/accounts/$me_id/statuses?limit=40")
[ "$(echo "$copies" | j "print(len(d))")" -gt 0 ] && ok "they are on mover_archive's profile" || ko "the profile shows none of them"
# nobody told: what PrivaPub would have delivered would be at Mastodon by now
sleep 20
uris=$(echo "$copies" | j "print(' '.join(s['uri'] for s in d[:5]))")
held=$("$here/town.sh" stored mastodon $uris | j "print(sum(1 for v in d.values() if v['exists']))")
[ "$held" = "0" ] && ok "Mastodon holds none of the copies" || ko "Mastodon holds $held of the copies"
[ "$(import)" = "202" ] && until_true 150 '[ "$(state importState)" = "done" ]' \
&& [ "$(count posts)" = "0" ] && [ "$(count 'posts already here')" = "$posts" ] \
&& ok "importing it again changes nothing" || ko "the second import: $(count posts) posts, $(count 'posts already here') already here"
echo " exported"
[ "$(curl -s -o /dev/null -w '%{http_code}' -X POST -H "$RH" "$A")" = "202" ] && ok "the persona's archive is asked for" || ko "the archive was refused"
until_true 90 '[ "$(state exportState)" = "ready" ]' && ok "the archive is ready" || ko "the archive is $(state exportState): $(state exportError)"
link=$(curl -s -X POST -H "$RH" "$A/ticket" | j "print(d['url'])")
curl -s -o "$work/mover.zip" "$P$link"
names=$(unzip -Z1 "$work/mover.zip" 2>/dev/null)
grep -qx outbox.json <<< "$names" && grep -qx actor.json <<< "$names" \
&& ok "it downloads, in Mastodon's layout" || ko "the download is not an archive"