Skip to content

Instantly share code, notes, and snippets.

@janlay
Last active June 27, 2022 15:27
Show Gist options
  • Star 6 You must be signed in to star a gist
  • Fork 1 You must be signed in to fork a gist
  • Save janlay/a6ca31f85c8f650dee338b409e7ecfd5 to your computer and use it in GitHub Desktop.
Save janlay/a6ca31f85c8f650dee338b409e7ecfd5 to your computer and use it in GitHub Desktop.
Delete and unfavorite tweets

Delete and unfavorite tweets

This util deletes your historical tweets from a downloaded archive file, unfavorites tweets from your public profile.

No guarantee, no support, use it at your own risk.

Preparations

  1. Apply to become a Twitter developer first.
    Once your Twitter developer account application is approved, You can create a new app and get its Consumer API keys and generate Access token. These keys are used to configure tweet.sh.
  2. Obtain archived tweets by requesting to download your Twitter data.
    You can extract tweet.js file from the downloaded .zip file.
  3. Download tweet.sh from the forked repo.
    Configure it following its README. tweet.sh requires curl, jq, nkf, openssl.
  4. Download delete-tweets.sh and unfav-tweets.sh from this gist.
    chmod +x delete-tweets.sh unfav-tweets.sh is recommended.
  5. Install GNU date by running brew install coreutils if you decide to run this util on macOS.

Usage

Run command in your terminal:
delete-tweets.sh /path/to/tweet.js
This will delete tweets saved in tweet.js.

unfav-tweets.sh
This will unfavorite tweets. It doesn't require the archived file tweet.js.

Whenever you quit the task, or it crashes accidentally, run again and it should resume the task from last working entry.

Configuration

  • Change variable KEEP_DAYS to 365 in the scripts, if you want to keep tweets posted within recent one year.
  • Delete the file declared by LAST_TWEET_FILE, if you want to start over.
#!/bin/bash
# author: janlay@gmail.com
set -e
KEEP_DAYS=90
LAST_TWEET_FILE="$HOME/.delete-tweets"
TWEET_UTIL="./tweet.sh"
function delete_tweet {
# empty buffer before calling tweet.sh
echo -n '' | "$TWEET_UTIL" delete $1 2>&1 || true
}
function undo_retweet {
echo -n '' | "$TWEET_UTIL" unretweet $1 2>&1 || true
}
function raise_error {
echo "${1:-Error occurred.}" >&2
exit ${2:-1}
}
function main {
echo -n "Checking... "
[ -f "$1" ] || raise_error "Missing archived tweets file: tweet.js."
[ -f "$TWEET_UTIL" ] || raise_error "tweet.sh is required." 2
local nickname="$("$TWEET_UTIL" whoami)"
[ "$nickname" == "null" ] && raise_error "tweet.sh is not configured correctly." 3
local date_cmd=date
if [ "$(uname)" == "Darwin" ]; then
which gdate > /dev/null || raise_error "gdate, the GNU date utility is required." 4
date_cmd=gdate
fi
echo -n 'OK.'
sleep 0.5
local timestamp=$($date_cmd -d "$($date_cmd +%Y-%m-%d) -$(($KEEP_DAYS-1)) days" +%s)
printf "\e[0E\e[K\rThis utility will delete @%s's tweets posted before %s." $nickname $($date_cmd -d @$timestamp +%D)
echo
echo -n "Loading tweets from $1... "
local items=$(sed '1 s/.*/[/' "$1" | jq -ecr '.[] | map([.created_at, .id_str, ((.full_text // "") | startswith("RT @") | tostring)]) | .[] | join(",")')
local count=$(echo "$items" | wc -l | tr -d ' ')
echo "$count tweets."
local started=1
local last_deleted
if [ -f "$LAST_TWEET_FILE" ]; then
last_deleted=$(cat "$LAST_TWEET_FILE")
echo -n "Resuming... "
started=0
fi
local index=0
echo "$items" | while IFS=, read -r tweet_date tweet_id retweeted; do
index=$(($index + 1))
# resuming from last working entry
if [ $started -eq 0 ]; then
if [ "$tweet_id" == "$last_deleted" ]; then
echo Found.
started=1
fi
continue
fi
# compare dates
if [ $($date_cmd -d "$tweet_date" +%s) -ge $timestamp ]; then
continue
fi
# print prograss and current entry
printf "\e[0E\e[K\r[%.2f%%] " $(echo "scale = 4; $index * 100 / $count" | bc)
[ "$retweeted" == "true" ] && echo -n "Unretweeting" || echo -n "Deleting"
echo -n " tweet $tweet_id posted on $($date_cmd -d "$tweet_date" +%D)... "
# execute
RESULT="$(([ "$retweeted" == "true" ] && undo_retweet $tweet_id || delete_tweet $tweet_id) | jq -r '.errors | first // empty')"
if [ -n "$RESULT" ]; then
if [ "$(echo "$RESULT" | jq '.code')" -eq 144 ]; then
echo "Already deleted."
else
echo -n "ERROR: "
echo "$RESULT" | jq -r '.message'
fi
fi
# save working entry
echo $tweet_id > "$LAST_TWEET_FILE"
done
}
main "$@"
#!/bin/bash
# author: janlay@gmail.com
set -e
KEEP_DAYS=90
LAST_TWEET_FILE="$HOME/.unfav-tweets"
TWEET_UTIL="./tweet.sh"
function unfav_tweet {
# empty buffer before calling tweet.sh
echo -n '' | "$TWEET_UTIL" unfavorite $1 2>&1 || true
}
function raise_error {
echo "${1:-Error occurred.}" >&2
exit ${2:-1}
}
function main {
echo -n "Checking... "
[ -f "$TWEET_UTIL" ] || raise_error "tweet.sh is required." 2
local nickname="$("$TWEET_UTIL" whoami)"
[ "$nickname" == "null" ] && raise_error "tweet.sh is not configured correctly." 3
local date_cmd=date
if [ "$(uname)" == "Darwin" ]; then
which gdate > /dev/null || raise_error "gdate, the GNU date utility is required." 4
date_cmd=gdate
fi
echo -n 'OK.'
sleep 0.5
local timestamp=$($date_cmd -d "$($date_cmd +%Y-%m-%d) -$(($KEEP_DAYS-1)) days" +%s)
printf "\e[0E\e[K\rThis utility will unfavorite @%s's tweets created before %s." $nickname $($date_cmd -d @$timestamp +%D)
echo
local count=$("$TWEET_UTIL" showme | jq '.favourites_count')
if [ $count -eq 0 ]; then
echo 'No favorites found.'
exit 0
fi
local started=1
local last_entry
if [ -f "$LAST_TWEET_FILE" ]; then
last_entry=$(cat "$LAST_TWEET_FILE")
echo -n "Resuming... "
started=0
fi
index=0
max_id=$("$TWEET_UTIL" fetch-favorites -c 1 | jq -r '.[] | .id_str')
for (( ; ; )); do
# printf "\e[0E\e[K\r[%.2f%%] " $(echo "scale = 4; $index * 100 / $count" | bc)
# echo -n "Querying more favorites... "
local items="$("$TWEET_UTIL" fetch-favorites -c 100 -m $max_id | jq -r 'map([.created_at, .id_str] | join(",")) | .[]')"
# echo OK.
# skip the first fav
[ $(echo "$items" | wc -l) -eq 1 ] && break
while IFS=, read -r tweet_date tweet_id; do
index=$(($index + 1))
max_id=$tweet_id
# resuming from last working entry
if [ $started -eq 0 ]; then
if [ "$tweet_id" == "$last_entry" ]; then
echo Found.
started=1
fi
continue
fi
# print prograss and current entry
printf "\e[0E\e[K\r[%.2f%%] " $(echo "scale = 4; $index * 100 / $count" | bc)
echo -n "Unfavoriting tweet $tweet_id created on $($date_cmd -d "$tweet_date" +%D)... "
# compare dates
if [ $($date_cmd -d "$tweet_date" +%s) -ge $timestamp ]; then
continue
fi
# execute
RESULT="$(unfav_tweet $tweet_id | jq -r '.errors | first // empty')"
if [ -n "$RESULT" ]; then
if [ "$(echo "$RESULT" | jq '.code')" -eq 144 ]; then
echo "Already unfavorited."
else
echo -n "ERROR: "
echo "$RESULT" | jq -r '.message'
fi
fi
# save working entry
echo $tweet_id > "$LAST_TWEET_FILE"
done <<< "$(echo "$items" | tail -n +2)"
done
}
main "$@"
@feuvan
Copy link

feuvan commented Nov 23, 2019

tips:
看到类似 [1.23%] Unretweeting tweet NNNNNN posted on mm/dd/yy... parse error: Invalid numeric literal at line 1, column 19 错误的网友可以酌情把 TWEET_UTIL="tweet.sh" 改成 TWEET_UTIL="./tweet.sh"

@janlay
Copy link
Author

janlay commented Nov 25, 2019

@feuvan 👍🏿

@quchao
Copy link

quchao commented Jun 27, 2022

One should apply Elevated API access first, or you will always get 403 Forbidden errors.
Thanks.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment