commit f5db6ddb21b6d7eb542c20e0c98bf8abe5332093 Author: Jaidyn Ann <10477760+JadedCtrl@users.noreply.github.com> Date: Sat May 6 11:27:09 2023 -0500 Init diff --git a/README.md b/README.md new file mode 100644 index 0000000..c68ef01 --- /dev/null +++ b/README.md @@ -0,0 +1,8 @@ +# PLEROMA-MIGRATOR + +A horrific group of scripts that can help you copy your fedi posts to a new account. If the server's Pleroma and you have direct access to the database, you can even preserve post dates. + +See invididual script's --help messages, with pleroma-migrator.sh being the one that combines them all for you. + + +License is CC0, ofc. diff --git a/fedi-archive.sh b/fedi-archive.sh new file mode 100644 index 0000000..80d539e --- /dev/null +++ b/fedi-archive.sh @@ -0,0 +1,113 @@ +#!/bin/sh +#――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――― +# Name: fedi-archive.sh +# Desc: Downloads (most) posts from a fedi account in parseable format. +# Reqs: jq, curl +# Date: 2023-05-06 +# Auth: @jadedctrl@jam.xwx.moe +#――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――― + + +# Given a JSON file containing /api/v1/accounts/$user/statuses output, +# output a post at the given index from the file like so: +# FAVOURITE_COUNT DATE_POSTED POST_ID POST_URL +# [media: URL DESCRIPTION] +# [media: URL DESCRIPTION] +# [CONTENT…] +# [] meaning "optional". There might be an arbitrary amount of Media: lines. +output_post_of_index() { + local index="$1" + local file="$2" + jq -r --arg INDEX "$index" \ +'.[$INDEX|tonumber] | "\(.favourites_count) \(.created_at) \(.id) \(.url) +\(.media_attachments[] | "media: " + .url + " " + .description) +\(.content)"' \ + < "$file" +} + + +# Fetch a list of a user's statuses, given their server and username. +# `max_id` can be passed to return only messages older than said message. +fetch_page() { + local server="$1"; local user="$2"; local max_id="$3" + local url="https://$server/api/v1/accounts/$user/statuses?exclude_replies=true&exclude_reblogs=true&limit=40" + if test -n "$max_id"; then + url="${url}&max_id=${max_id}" + fi + curl "$url" +} + + +# Given a JSON file containing /api/v1/accounts/$user/statuses output, +# output each status into an individual file of the format of +# output_post_of_index(); see its comment for more information. +# Prints the ID of the last post of the file. +archive_posts() { + local json_file="$1" + local prefix="$2" + + local post_file="$prefix-$i" + local last_post_file="" + local i="0" + + local output_ret=0 + while test "$output_ret" -eq 0; do + post_file="$prefix-$i" + echo "$post_file" 1>&2 + output_post_of_index "$i" "$json_file" \ + > "$post_file" + output_ret="$?" + + if test -e "$post_file" -a -n "$(cat "$post_file")"; then + last_post_file="$post_file" + elif test -e "$post_file"; then + rm "$post_file" + fi + i="$(echo "$i + 1" | bc)" + done + + head -1 "$last_post_file" \ + | awk '{print $3}' +} + + +# Fetch all posts for the given user at given server. +archive_all_posts() { + local server="$1" + local username="$2" + local temp="$(mktemp)" + + fetch_page "$server" "$username" \ + > "$temp" + + local page="1" + local next_id="$(archive_posts "$temp" "$page")" + while test -n "$next_id"; do + page="$(echo "$page + 1" | bc)" + echo "$next_id - $page…" + fetch_page "$server" "$username" "$next_id" \ + > "$temp" + next_id="$(archive_posts "$temp" "$page")" + done + + rm "$temp" +} + + +usage() { + echo "usage: $(basename $0) username server" 1>&2 + echo "" 1>&2 + echo "$(basename $0) is a script that fetches all of a user's Mastodon/Pleroma" 1>&2 + echo "posts for archival purposes." 1>&2 + echo "Mainly for use with fedi-post.sh or pleroma-migrate.sh." 1>&2 + exit 2; +} + + +USERNAME="$1" +SERVER="$2" +if test -z "$USERNAME" -o -z "$SERVER" -o "$1" = "-h" -o "$1" = "--help"; then + usage +fi + +archive_all_posts "$SERVER" "$USERNAME" diff --git a/fedi-post.sh b/fedi-post.sh new file mode 100644 index 0000000..a412422 --- /dev/null +++ b/fedi-post.sh @@ -0,0 +1,129 @@ +#!/bin/sh +#――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――― +# Name: fedi-post.sh +# Desc: Makes a new post using an post archived with fedi-archive.sh. +# Reqs: curl, jq +# Date: 2023-05-06 +# Auth: @jadedctrl@jam.xwx.moe +#――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――― + + +# Outputs only the status text of a user's post, with a link to the original +# appended to the bottom. +file_status() { + local file="$1" + local id="$(head -1 "$file" | awk '{print $3}')" + local url="$(head -1 "$file" | awk '{print $4}')" + tail +2 "$file" \ + | grep -v ^media: \ + | grep -v "$id" \ + | sed 's%\\%\\\\%g' \ + | sed 's%"%\\"%g' \ + | sed -z 's%\n%\\n%g' + if test -n "$url"; then + printf '
[Originala afiŝo]\\n' "$url" + fi +} + + +# Turns a space-delimited list of uploaded media-IDs into a JSON array. +media_json() { + local ids="$1" + echo "$ids" \ + | sed 's%^ %%' \ + | sed 's% $%%' \ + | sed 's%^%["%' \ + | sed 's% %","%g' \ + | sed 's%$%"]%' +} + + +# Takes a post message and JSON-array of uploaded media-IDs, outputting +# the post's appropriate JSON. +post_json() { + local message="$1" + local media_ids="$2" + printf '{ "content_type": "text/html", "visibility": "unlisted"' + if test -n "$media_ids"; then + printf ', "media_ids": %s, ' "$media_ids" + fi + printf '"status": "%s" }\n' "$message" +} + + +# Upload a file to the fedi server with the given description. +post_media() { + local media_file="$1" + local description="$2" + + curl --request POST \ + --header "Authorization: Bearer $FEDI_AUTH" \ + --header "Content-Type: multipart/form-data" \ + --form "file=@$media_file" \ + --form "description=$description" \ + "https://jam.xwx.moe/api/v1/media" +} + + +# Post a status of the given message and JSON-array of uploaded media-IDs. +post_status() { + local message="$1" + local media_ids="$2" + + curl --request POST \ + --header "Authorization: Bearer $FEDI_AUTH" \ + --header "Content-Type: application/json" \ + --data "$(post_json "$message" "$media_ids")" \ + "https://jam.xwx.moe/api/v1/statuses" +} + + +# Take a post file generated by fedi-archive.sh, and post it. +# Just *do it*. Why not? What're you scared of? Huh, huh? Huh?! +post_archived_post() { + local file="$1" + IFS=" +" + local ids="" + for media in $(grep "^media: " "$file"); do + local url="$(echo "$media" | awk '{print $2}')" + local desc="$(echo "$media" | awk '{ $1=$2=""; print}' | sed 's%^ %%')" + + curl -o "$(basename "$url")" "$url" + ids="$ids $(post_media "$(basename "$url")" "$desc" | jq -r '.id')" + rm "$(basename "$url")" + done + + printf '%s ' "$(head -1 "$file" | awk '{print $1, $2}')" + post_status "$(file_status "$file")" "$(media_json "$ids")" \ + | jq -r .uri +} + + +usage() { + echo "usage: $(basename "$0") ARCHIVED-POST" 1>&2 + echo "" 1>&2 + echo "Will post a new status with the same text and attachments as one" 1>&2 + echo "from an archived post (in fedi-archive.sh format)." 1>&2 + echo "Your authorization key must be borrowed from your web-browser and" 1>&2 + echo 'placed in the $FEDI_AUTH environment variable.' 1>&2 + exit 2 +} + + +if test -z "$FEDI_AUTH"; then + echo 'You need to set the environment variable $FEDI_AUTH!' 1>&2 + echo 'You can find your auth key by examining the "Authentication: Bearer" header' 1>&2 + echo "used in requests by your server's web-client." 1>&2 + echo 'In Firefox, F12→Network.' 1>&2 + echo "" 1>&2 + usage +fi + +FILE="$1" +if test -z "$FILE" -o "$1" = "-h" -o "$1" = "--help"; then + usage +fi + + +post_archived_post "$FILE" diff --git a/pleroma-migrate.sh b/pleroma-migrate.sh new file mode 100755 index 0000000..db83048 --- /dev/null +++ b/pleroma-migrate.sh @@ -0,0 +1,54 @@ +#!/bin/sh +#――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――― +# Name: pleroma-migrate.sh +# Desc: Downloads posts from a pleroma account on a foreign server, to post them +# on your new server. Then directly edits Pleroma's database to match the +# new posts' dates with the original posts' dates. +# Reqs: fedi-post.sh, fedi-archive.sh, pleroma-redate.sh +# Date: 2023-05-06 +# Auth: @jadedctrl@jam.xwx.moe +#――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――― + +usage() { + echo "usage: $(basename "$0") USERNAME OLD-SERVER" 1>&2 + echo "" 1>&2 + echo "Will archive all posts from your old account (fedi-archive.sh)," 1>&2 + echo "post them to your new one (fedi-post.sh), and then modify Pleroma's" 1>&2 + echo "database to match the copy-posts' creation dates with the original posts." 1>&2 + echo 'The env variable $FEDI_AUTH must contain your authentication key from your browser.' 1>&2 + exit 2 +} + + +USERNAME="$1" +SERVER="$2" +if test -z "$USERNAME" -o -z "$SERVER" -o "$1" = "-h" -o "$1" = "--help"; then + usage +fi + + +mkdir archive +cd archive/ +sh ../fedi-archive.sh "$USERNAME" "$SERVER" + + +for file in ./*; do + sh ../fedi-post.sh "$file" \ + >> imports-data.txt +done + + +IFS=" +" + +echo "It's time to re-date your posts!" +echo "Are you suuuuuuuuuuuuure you wanna risk your database? Do a backup, first!" +echo "^C now, before you risk it! Hit ENTER, if you're sure." +read +sleep 5 +echo "Alright, then, you brave fellow! I'm touched you trust me so much, though :o" + +for line in $(cat imports-data.txt); do + sh ../pleroma-redate.sh "$(echo "$line" | awk '{print $3}')" \ + "$(echo "$line" | awk '{print $2}')" +done diff --git a/pleroma-redate.sh b/pleroma-redate.sh new file mode 100644 index 0000000..c4dd10d --- /dev/null +++ b/pleroma-redate.sh @@ -0,0 +1,32 @@ +#!/bin/sh +#――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――― +# Name: pleroma-redate.sh +# Desc: Changes the creation-date of a post in Pleroma's database. +# Reqs: psql, sudo +# Date: 2023-05-06 +# Auth: @jadedctrl@jam.xwx.moe +#――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――― + +usage() { + echo "usage: $(basename "$0") URL NEW-DATE" 1>&2 + echo "" 1>&2 + echo "Will change the stored date of an archived fedi post" 1>&2 + echo "in fedi-archive.sh format, by editing directly Pleroma's database." 1>&2 + echo "URL ought be the direct /object/ URL, and NEW-DATE ought be ISO-8601" 1>&2 + echo "up to the milisecond." 1>&2 + echo "Assumes you can sudo as 'postgres' and the database is called 'pleroma'." 1>&2 + exit 2 +} + + +URL="$1" +NEWDATE="$2" +if test -z "$URL" -o -z "$NEWDATE" -o "$1" = "-h" -o "$1" = "--help"; then + usage +fi + + +sudo -u postgres psql --dbname=pleroma \ + -c "UPDATE objects +SET data = jsonb_set(data, '{published}', '\"$NEWDATE\"'::jsonb, false) +WHERE CAST(data::json->'id' AS TEXT) = '\"$URL\"' ;"