# 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"