Created
November 15, 2024 15:20
-
-
Save marysaka/bc6037e4715eba961a3c440e82703c28 to your computer and use it in GitHub Desktop.
Some Python script to delete all your tweets (or until a certain cutoff date) based on Twitter Archive (tweet.js)
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import requests | |
import json | |
import sys | |
import time | |
from requests.models import Response | |
# Common Twitter WebApp Bearer | |
USER_AUTH_TOKEN = "AAAAAAAAAAAAAAAAAAAAANRILgAAAAAAnNwIzUejRCOuH5E6I8xnZz4puTs%3D1Zv7ttfk8LF81IUq16cHjhLTvJu4FA33AGWWjCpTnA" | |
# Credentials | |
# Copy of the cookie passed in requests (usually start with "night_mode=") | |
USER_COOKIE = "<secret>" | |
# Copy of the header value of "x-csrf-token" | |
USER_CSRF = "<secret>" | |
RETRY_BASE_SECONDS = 5 | |
DELETE_GRAPHQL_METHOD_ID = "VaenaVgh5q5ih7kvyVjgtg" | |
DELETE_ENDPOINT_URL = ( | |
f"https://x.com/i/api/graphql/{DELETE_GRAPHQL_METHOD_ID}/DeleteTweet" | |
) | |
def delete_tweet_by_id( | |
tweet_id: str, auth_token: str, cookie: str, csrf: str | |
) -> Response: | |
if "ct0=" not in cookie: | |
cookie = cookie + "; ct0=" + csrf | |
headers = { | |
"User-Agent": "Mozilla/5.0 (X11; Linux x86_64; rv:132.0) Gecko/20100101 Firefox/132.0", | |
"Content-Type": "application/json", | |
"x-twitter-auth-type": "OAuth2Session", | |
"x-twitter-client-language": "en-GB", | |
"x-twitter-active-user": "yes", | |
"Origin": "https://x.com", | |
"Referer": "https://x.com", | |
"Cookie": cookie, | |
"x-csrf-token": csrf, | |
"authorization": "Bearer " + auth_token, | |
} | |
data = { | |
"variables": json.dumps({"tweet_id": tweet_id, "dark_request": False}), | |
"queryId": DELETE_GRAPHQL_METHOD_ID, | |
} | |
return requests.post(DELETE_ENDPOINT_URL, headers=headers, json=data) | |
if len(sys.argv) < 2: | |
print("Usage <tweet.js> [cutoff_date]") | |
print("example cutoff_date: Mon Oct 20 01:44:42 +0000 2025") | |
sys.exit(1) | |
raw_tweets = open(sys.argv[1], "rb").read().decode("utf-8") | |
cutoff_date = None | |
if len(sys.argv) >= 3: | |
cutoff_date = time.strptime(sys.argv[2], "%a %b %d %H:%M:%S %z %Y") | |
json_start_index = raw_tweets.find("[") | |
if json_start_index == -1: | |
print("Start of tweet.js data not found (malformed file?s)") | |
sys.exit(1) | |
tweets = json.loads(raw_tweets[json_start_index:]) | |
for data in tweets: | |
tweet: str = data["tweet"] | |
created_at_date = time.strptime(tweet["created_at"], "%a %b %d %H:%M:%S %z %Y") | |
if not cutoff_date or created_at_date < cutoff_date: | |
should_retry = True | |
retry_seconds = RETRY_BASE_SECONDS | |
while should_retry: | |
res = delete_tweet_by_id( | |
tweet["id_str"], | |
auth_token=USER_AUTH_TOKEN, | |
cookie=USER_COOKIE, | |
csrf=USER_CSRF, | |
) | |
print(f"{tweet['id_str']} {res.text}") | |
should_retry = res.status_code == 429 | |
if should_retry: | |
error_message = res.json()["errors"][0]["message"] | |
# Twitter GraphQL API will tell you you are rate limited to 500 but it will actually perform deletion | |
# This behaviour seems to change when you hit the limit 1000 marker resulting in global account rate limiting. | |
# XXX: November 2024 update: Rate limits seem completely gone on Twitter now?! | |
if ( | |
error_message | |
== "Rate limit exceeded for feature graphql_operation_mutation with limit 500" | |
): | |
break | |
print(f"{tweet['id_str']} RETRYING IN {retry_seconds} seconds") | |
time.sleep(retry_seconds) | |
retry_seconds += RETRY_BASE_SECONDS |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment