├── .gitignore ├── README.md ├── blocked_list_clear.py ├── common.py ├── export_threads.py ├── followers_clear.py ├── friends_backup.py ├── nuke_friends_followers.py ├── nuke_like_archive.py ├── nuke_tweet_archive.py ├── protected_followers_clear.py └── requirements.txt /.gitignore: -------------------------------------------------------------------------------- 1 | # Python related 2 | *.pyc 3 | 4 | # IDEA related 5 | *.iml 6 | idea/ 7 | .idea/ 8 | 9 | # VS code related 10 | .vscode/ 11 | 12 | 13 | .twitter_credentials.yml 14 | 15 | *.list 16 | *.json 17 | *.xlsx -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Usage 2 | 3 | 1. Execute `pip3 install -r requirements.txt` to install dependencies 4 | 2. Execute `followers_clear.py` and follow instructions. 5 | 6 | # Notes 7 | 8 | Consumer keys can be found [here](https://gist.github.com/mariotaku/5465786). Use these keys instead of applying yourself can get rid of rate limit. -------------------------------------------------------------------------------- /blocked_list_clear.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | import concurrent.futures 3 | from twitter import TwitterError 4 | 5 | import common 6 | from common import confirm 7 | 8 | api = common.api() 9 | with open("blocked.list") as f: 10 | blocked_ids = list(filter(lambda l: len(l) > 0, map(lambda l: l.rstrip('\n'), f.readlines()))) 11 | 12 | executor = concurrent.futures.ThreadPoolExecutor(max_workers=80) 13 | 14 | cancelled = False 15 | 16 | unblock_failed_ids = [] 17 | 18 | def do_unblock(uid): 19 | if cancelled: 20 | return 21 | try: 22 | print('unblocking %d' % uid) 23 | api.DestroyBlock(uid) 24 | except TwitterError as e: 25 | unblock_failed_ids.append(uid) 26 | 27 | try: 28 | for user_id in blocked_ids: 29 | executor.submit(do_unblock, int(user_id)) 30 | executor.shutdown(wait=True) 31 | except (KeyboardInterrupt, SystemExit): 32 | cancelled = True 33 | print('Interrupted, exiting...') -------------------------------------------------------------------------------- /common.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | import argparse 3 | import datetime 4 | import webbrowser 5 | from datetime import datetime, timedelta 6 | from email.utils import parsedate_tz 7 | 8 | import twitter 9 | import yaml 10 | from requests_oauthlib import OAuth1Session 11 | from twitter import TwitterError 12 | 13 | REQUEST_TOKEN_URL = 'https://api.twitter.com/oauth/request_token' 14 | ACCESS_TOKEN_URL = 'https://api.twitter.com/oauth/access_token' 15 | AUTHORIZATION_URL = 'https://api.twitter.com/oauth/authorize' 16 | SIGNIN_URL = 'https://api.twitter.com/oauth/authenticate' 17 | 18 | 19 | def confirm(message, default=None): 20 | if default is None: 21 | t = input(message) 22 | else: 23 | if default: 24 | default_input = 'y' 25 | else: 26 | default_input = 'n' 27 | t = input('%s [%s]' % (message, default_input)) 28 | if t == '' or t is None and default is not None: 29 | return default 30 | while t != 'y' and t != 'n': 31 | t = input('Type y or n: ') 32 | if t == 'y': 33 | return True 34 | else: 35 | return False 36 | 37 | 38 | def get_access_token(ck, cs): 39 | oauth_client = OAuth1Session(client_key=ck, client_secret=cs, callback_uri='oob') 40 | 41 | print('\nRequesting temp token from Twitter...\n') 42 | 43 | try: 44 | resp = oauth_client.fetch_request_token(REQUEST_TOKEN_URL) 45 | except ValueError as e: 46 | raise 'Invalid response from Twitter requesting temp token: {0}'.format(e) 47 | 48 | url = oauth_client.authorization_url(AUTHORIZATION_URL) 49 | 50 | print('I will try to start a browser to visit the following Twitter page ' 51 | 'if a browser will not start, copy the URL to your browser ' 52 | 'and retrieve the pincode to be used ' 53 | 'in the next step to obtaining an Authentication Token: \n' 54 | '\n\t{0}'.format(url)) 55 | 56 | webbrowser.open(url) 57 | pincode = input('\nEnter your pincode? ') 58 | 59 | print('\nGenerating and signing request for an access token...\n') 60 | 61 | oauth_client = OAuth1Session(client_key=ck, client_secret=cs, 62 | resource_owner_key=resp.get('oauth_token'), 63 | resource_owner_secret=resp.get('oauth_token_secret'), 64 | verifier=pincode) 65 | try: 66 | resp = oauth_client.fetch_access_token(ACCESS_TOKEN_URL) 67 | except ValueError as e: 68 | raise 'Invalid response from Twitter requesting temp token: {0}'.format(e) 69 | 70 | return resp.get('oauth_token'), resp.get('oauth_token_secret') 71 | 72 | 73 | def load_credentials(): 74 | try: 75 | with open('.twitter_credentials.yml') as f: 76 | c = yaml.load(f) 77 | return {'ck': c['consumer_key'], 78 | 'cs': c['consumer_secret'], 79 | 'at': c['access_token'], 80 | 'ats': c['access_token_secret']} 81 | except IOError: 82 | return None 83 | 84 | 85 | def get_credentials(crds): 86 | ck = None 87 | cs = None 88 | if crds and crds['ck'] and crds['cs']: 89 | if not confirm('Do you need to use new consumer key/secret?', default=False): 90 | ck = crds['ck'] 91 | cs = crds['cs'] 92 | if ck is None or cs is None: 93 | ck = input('Input consumer key: ') 94 | cs = input('Input consumer secret: ') 95 | 96 | if confirm('Do you have access token and secret already?', default=False): 97 | at = input('Input access token: ') 98 | ats = input('Input access token secret: ') 99 | else: 100 | at, ats = get_access_token(ck, cs) 101 | return {'ck': ck, 'cs': cs, 'at': at, 'ats': ats} 102 | 103 | 104 | def api(): 105 | a, me = api2() 106 | return a 107 | 108 | 109 | def api2(): 110 | credentials = load_credentials() 111 | 112 | if not credentials or confirm('Do you want to switch to a new user?', default=False): 113 | credentials = get_credentials(credentials) 114 | with open('.twitter_credentials.yml', 'w') as f: 115 | yaml.dump({ 116 | 'consumer_key': credentials['ck'], 117 | 'consumer_secret': credentials['cs'], 118 | 'access_token': credentials['at'], 119 | 'access_token_secret': credentials['ats'], 120 | }, f, default_flow_style=False) 121 | 122 | a = twitter.Api(consumer_key=credentials['ck'], consumer_secret=credentials['cs'], 123 | access_token_key=credentials['at'], 124 | access_token_secret=credentials['ats'], 125 | sleep_on_rate_limit=True) 126 | 127 | try: 128 | me = a.VerifyCredentials() 129 | except TwitterError: 130 | print('User logged out') 131 | exit(0) 132 | 133 | return a, me 134 | 135 | 136 | def argparse_date(date_str): 137 | try: 138 | return datetime.strptime(date_str, '%Y-%m-%d') 139 | except ValueError: 140 | raise argparse.ArgumentTypeError(f'Unrecognized date {date_str}') 141 | 142 | 143 | def to_datetime(datestring): 144 | time_tuple = parsedate_tz(datestring.strip()) 145 | dt = datetime(*time_tuple[:6]) 146 | return dt - timedelta(seconds=time_tuple[-1]) 147 | -------------------------------------------------------------------------------- /export_threads.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | import html 3 | import json 4 | import sys 5 | from datetime import datetime 6 | from pathlib import Path 7 | 8 | tweets = {} 9 | 10 | data_dir = Path(sys.argv[1], 'data') 11 | 12 | if not data_dir.is_dir(): 13 | print('Usage: export_threads.py /path/to/extracted/archive', file=sys.stderr) 14 | exit(1) 15 | 16 | for entry in data_dir.iterdir(): 17 | if entry.name == 'tweets.js' or entry.name.startswith('tweets-part'): 18 | with entry.open(encoding='utf-8') as f: 19 | js = f.read() 20 | idx = js.index('= [') + 2 21 | for tweet in json.loads(js[idx:]): 22 | if 'tweet' in tweet: 23 | tweet = tweet['tweet'] 24 | tweets[tweet['id']] = tweet 25 | 26 | for tweet in tweets.values(): 27 | if 'in_reply_to_status_id' in tweet: 28 | in_reply_to_status_id = tweet['in_reply_to_status_id'] 29 | if in_reply_to_status_id in tweets: 30 | in_reply_to_tweet = tweets[in_reply_to_status_id] 31 | tweet['in_reply_to_tweet'] = in_reply_to_tweet 32 | if 'thread_replies' not in in_reply_to_tweet: 33 | in_reply_to_tweet['thread_replies'] = [] 34 | in_reply_to_tweet['thread_replies'].append(tweet) 35 | 36 | 37 | def tweet_created_at(t): 38 | return datetime.strptime(t['created_at'], '%a %b %d %H:%M:%S %z %Y') 39 | 40 | 41 | def is_thread_head(t): 42 | return 'thread_replies' in t and 'in_reply_to_status_id' not in t 43 | 44 | 45 | threads = sorted(filter(is_thread_head, tweets.values()), key=tweet_created_at) 46 | 47 | with Path(data_dir.parent, 'Your threads.html').open(mode='w', encoding='utf-8') as f: 48 | print('', file=f) 49 | print(""" 50 | 51 | Your threads 52 | 71 | 72 | """, file=f) 73 | print('', file=f) 74 | for thread in threads: 75 | print('
', file=f) 76 | created_at = tweet_created_at(thread).strftime('%Y-%m-%d') 77 | print(f'

Thread posted at {created_at}

', file=f) 78 | 79 | 80 | def print_tweet(t): 81 | print(f'

{html.escape(html.unescape(t["full_text"]))}

'.replace('\n', '
'), file=f) 82 | if 'extended_entities' in t: 83 | entities = t['extended_entities'] 84 | if 'media' in entities: 85 | for m in entities['media']: 86 | if m['type'] == 'photo': 87 | media_url: str = m["media_url_https"] 88 | file_name = f'{t["id"]}-{media_url.rsplit("/", 1)[-1]}' 89 | print(f'
', file=f) 90 | elif m['type'] == 'video': 91 | print('
', file=f) 98 | if 'thread_replies' in t: 99 | for st in t['thread_replies']: 100 | print_tweet(st) 101 | 102 | 103 | print_tweet(thread) 104 | print('
', file=f) 105 | 106 | print('
', file=f) 107 | print('', file=f) 108 | -------------------------------------------------------------------------------- /followers_clear.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | import concurrent.futures 3 | 4 | from twitter import TwitterError 5 | 6 | import common 7 | from common import confirm 8 | 9 | api = common.api() 10 | friend_ids_cursor = -1 11 | friend_ids = [] 12 | 13 | print('Getting following list...') 14 | while friend_ids_cursor != 0: 15 | friend_ids_cursor, _, ids = api.GetFriendIDsPaged(cursor=friend_ids_cursor) 16 | friend_ids += ids 17 | print('You have %d followings' % len(friend_ids)) 18 | 19 | follower_ids_cursor = -1 20 | follower_ids = [] 21 | 22 | print('Getting followers list') 23 | while follower_ids_cursor != 0: 24 | follower_ids_cursor, _, ids = api.GetFollowerIDsPaged(cursor=follower_ids_cursor) 25 | follower_ids += ids 26 | print('You have %d followers' % len(follower_ids)) 27 | 28 | no_mutual_followers = set(follower_ids) - set(friend_ids) 29 | 30 | print('You have %d followers you haven\'t followed.' % len(no_mutual_followers)) 31 | 32 | unblock = confirm('Unblock those users after removed from followers list?', default=True) 33 | 34 | executor = concurrent.futures.ThreadPoolExecutor(max_workers=80) 35 | 36 | cancelled = False 37 | 38 | block_failed_ids = [] 39 | unblock_failed_ids = [] 40 | 41 | 42 | def remove_follower(uid): 43 | if cancelled: 44 | return 45 | try: 46 | print('blocking %d' % uid) 47 | api.CreateBlock(uid) 48 | except TwitterError: 49 | block_failed_ids.append(uid) 50 | if unblock: 51 | try: 52 | print('unblocking %d' % uid) 53 | api.DestroyBlock(uid) 54 | except TwitterError: 55 | unblock_failed_ids.append(uid) 56 | 57 | 58 | try: 59 | for user_id in no_mutual_followers: 60 | executor.submit(remove_follower, user_id) 61 | executor.shutdown(wait=True) 62 | except (KeyboardInterrupt, SystemExit): 63 | cancelled = True 64 | print('Interrupted, exiting...') 65 | 66 | with open('block_failed_ids.list', 'w') as f: 67 | for user_id in block_failed_ids: 68 | f.write('%d\n' % user_id) 69 | 70 | with open('unblock_failed_ids.list', 'w') as f: 71 | for user_id in unblock_failed_ids: 72 | f.write('%d\n' % user_id) 73 | -------------------------------------------------------------------------------- /friends_backup.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | from openpyxl import Workbook 4 | 5 | import common 6 | 7 | api = common.api() 8 | 9 | friends_cursor = -1 10 | friends = [] 11 | 12 | print('Getting following list...') 13 | while friends_cursor != 0: 14 | friends_cursor, _, ids = api.GetFriendsPaged(cursor=friends_cursor) 15 | friends += ids 16 | print('You have %d followings' % len(friends)) 17 | 18 | wb = Workbook() 19 | ws = wb.active 20 | 21 | for user in friends: 22 | ws.append([user.id, '@%s' % user.screen_name, user.name, 'https://twitter.com/%s' % user.screen_name]) 23 | 24 | wb.save('friends.xlsx') 25 | -------------------------------------------------------------------------------- /nuke_friends_followers.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | import concurrent.futures 3 | 4 | from twitter import TwitterError 5 | 6 | import common 7 | from common import confirm 8 | 9 | api = common.api() 10 | 11 | friend_ids_cursor = -1 12 | friend_ids = [] 13 | 14 | print('Getting following list...') 15 | while friend_ids_cursor != 0: 16 | friend_ids_cursor, _, ids = api.GetFriendIDsPaged(cursor=friend_ids_cursor) 17 | friend_ids += ids 18 | print('You have %d followings' % len(friend_ids)) 19 | 20 | follower_ids_cursor = -1 21 | follower_ids = [] 22 | 23 | print('Getting followers list') 24 | while follower_ids_cursor != 0: 25 | follower_ids_cursor, _, ids = api.GetFollowerIDsPaged(cursor=follower_ids_cursor) 26 | follower_ids += ids 27 | print('You have %d followers' % len(follower_ids)) 28 | 29 | ids_to_delete = set(follower_ids + friend_ids) 30 | 31 | print('You have %d followers/friends in total.' % len(ids_to_delete)) 32 | 33 | unblock = confirm('Unblock those users after removed?', default=True) 34 | 35 | executor = concurrent.futures.ThreadPoolExecutor(max_workers=80) 36 | 37 | cancelled = False 38 | 39 | block_failed_ids = [] 40 | unblock_failed_ids = [] 41 | 42 | 43 | def remove_follower(uid): 44 | if cancelled: 45 | return 46 | try: 47 | print('blocking %d' % uid) 48 | api.CreateBlock(uid) 49 | except TwitterError: 50 | block_failed_ids.append(uid) 51 | if unblock: 52 | try: 53 | print('unblocking %d' % uid) 54 | api.DestroyBlock(uid) 55 | except TwitterError: 56 | unblock_failed_ids.append(uid) 57 | 58 | 59 | try: 60 | for user_id in ids_to_delete: 61 | executor.submit(remove_follower, user_id) 62 | executor.shutdown(wait=True) 63 | except (KeyboardInterrupt, SystemExit): 64 | cancelled = True 65 | print('Interrupted, exiting...') 66 | 67 | with open('block_failed_ids.list', 'w') as f: 68 | for user_id in block_failed_ids: 69 | f.write('%d\n' % user_id) 70 | 71 | with open('unblock_failed_ids.list', 'w') as f: 72 | for user_id in unblock_failed_ids: 73 | f.write('%d\n' % user_id) 74 | -------------------------------------------------------------------------------- /nuke_like_archive.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | import concurrent.futures 3 | import json 4 | import sys 5 | 6 | from twitter import TwitterError 7 | 8 | import common 9 | from common import confirm 10 | 11 | api = common.api() 12 | likes = [] 13 | 14 | with open(sys.argv[-1], encoding='utf-8') as f: 15 | js = f.read() 16 | idx = js.index('= [') + 2 17 | likes = json.loads(js[idx:]) 18 | 19 | confirmed = confirm('There are %d likes. Are you sure to delete all likes in this archive?' % len(likes), 20 | default=False) 21 | 22 | if not confirmed: 23 | exit(0) 24 | 25 | executor = concurrent.futures.ThreadPoolExecutor(max_workers=80) 26 | 27 | cancelled = False 28 | 29 | delete_failed_ids = [] 30 | 31 | 32 | def delete_tweet(tid): 33 | if cancelled: 34 | return 35 | try: 36 | print('deleting %s' % tid) 37 | api.DestroyFavorite(status_id=tid) 38 | except TwitterError as e: 39 | if e.message[0]['code'] == 144: 40 | return 41 | print(e) 42 | delete_failed_ids.append(tid) 43 | 44 | 45 | try: 46 | for like in likes: 47 | executor.submit(delete_tweet, like['like']['tweetId']) 48 | executor.shutdown(wait=True) 49 | except (KeyboardInterrupt, SystemExit): 50 | cancelled = True 51 | print('Interrupted, exiting...') 52 | 53 | with open('delete_failed_like_ids.list', 'w') as f: 54 | for tid in delete_failed_ids: 55 | f.write('%s\n' % tid) 56 | -------------------------------------------------------------------------------- /nuke_tweet_archive.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | import argparse 3 | import concurrent.futures 4 | import json 5 | from json import JSONDecodeError 6 | from time import sleep 7 | 8 | from twitter import TwitterError 9 | 10 | import common 11 | from common import confirm, to_datetime 12 | 13 | parser = argparse.ArgumentParser(description='Delete all tweets') 14 | parser.add_argument('tweets', metavar='tweet.js', help='Tweets data from your twitter archive') 15 | parser.add_argument('--since', dest='since', type=common.argparse_date, help='Delete all tweets since date (UTC)', 16 | required=False) 17 | parser.add_argument('--until', dest='until', type=common.argparse_date, help='Delete all tweets until date (UTC)', 18 | required=False) 19 | 20 | args = parser.parse_args() 21 | 22 | filter_since = args.since 23 | filter_until = args.until 24 | 25 | print(filter_since) 26 | 27 | api = common.api() 28 | 29 | tweets = [] 30 | 31 | 32 | def filter_by_args(tweet): 33 | created_at = to_datetime(tweet['tweet']['created_at']) 34 | if filter_since is not None and created_at < filter_since: 35 | return False 36 | if filter_until is not None and created_at > filter_until: 37 | return False 38 | return True 39 | 40 | 41 | try: 42 | with open(args.tweets, encoding='utf-8') as f: 43 | js = f.read() 44 | idx = js.index('= [') + 2 45 | tweets = list(filter(filter_by_args, json.loads(js[idx:]))) 46 | except IOError or JSONDecodeError: 47 | print('Usage: nuke_tweet_archive.py tweet.js') 48 | exit(0) 49 | 50 | confirmed = confirm('There are %d tweets. Are you sure to delete all tweets in this archive?' % len(tweets), 51 | default=False) 52 | 53 | if not confirmed: 54 | exit(0) 55 | 56 | executor = concurrent.futures.ThreadPoolExecutor(max_workers=80) 57 | 58 | cancelled = False 59 | 60 | delete_failed_ids = [] 61 | 62 | 63 | def delete_tweet(tid): 64 | if cancelled: 65 | return 66 | try: 67 | print('deleting %s' % tid) 68 | api.DestroyStatus(tid) 69 | except TwitterError as te: 70 | err_body = te.message[0] 71 | err_code = err_body['code'] 72 | if err_code != 144: 73 | print('deleting %s failed: %s' % (tid, err_body)) 74 | delete_failed_ids.append(tid) 75 | 76 | 77 | try: 78 | for tweet in tweets: 79 | executor.submit(delete_tweet, tweet['tweet']['id']) 80 | executor.shutdown(wait=True) 81 | except (KeyboardInterrupt, SystemExit): 82 | cancelled = True 83 | print('Interrupted, exiting...') 84 | 85 | with open('delete_failed_tweet_ids.list', 'w') as f: 86 | for tid in delete_failed_ids: 87 | f.write('%s\n' % tid) 88 | -------------------------------------------------------------------------------- /protected_followers_clear.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | import concurrent.futures 3 | 4 | from twitter import TwitterError 5 | 6 | import common 7 | from common import confirm 8 | 9 | api, me = common.api2() 10 | friend_ids_cursor = -1 11 | friend_ids = [] 12 | 13 | print('Getting following list...') 14 | while friend_ids_cursor != 0: 15 | friend_ids_cursor, _, ids = api.GetFriendIDsPaged(cursor=friend_ids_cursor) 16 | friend_ids += ids 17 | print('You have %d followings' % len(friend_ids)) 18 | 19 | follower_ids_cursor = -1 20 | protected_follower_ids = [] 21 | 22 | print('Getting followers list (%d total)' % me.followers_count) 23 | fetched = 0 24 | while follower_ids_cursor != 0: 25 | follower_ids_cursor, _, followers = api.GetFollowersPaged(cursor=follower_ids_cursor) 26 | fetched += len(followers) 27 | protected_batch = list(map(lambda x: x.id, filter(lambda x: x.protected, followers))) 28 | print('Fetched %d/%d (%d protected in this batch)' % (fetched, me.followers_count, len(protected_batch))) 29 | protected_follower_ids += protected_batch 30 | print('You have %d protected followers' % len(protected_follower_ids)) 31 | 32 | no_mutual_followers = set(protected_follower_ids) - set(friend_ids) 33 | 34 | print('You have %d protected followers you haven\'t followed.' % len(no_mutual_followers)) 35 | 36 | unblock = confirm('Unblock those users after removed from followers list?', default=True) 37 | 38 | executor = concurrent.futures.ThreadPoolExecutor(max_workers=80) 39 | 40 | cancelled = False 41 | 42 | block_failed_ids = [] 43 | unblock_failed_ids = [] 44 | 45 | 46 | def remove_follower(uid): 47 | if cancelled: 48 | return 49 | try: 50 | print('blocking %d' % uid) 51 | api.CreateBlock(uid) 52 | except TwitterError: 53 | block_failed_ids.append(uid) 54 | if unblock: 55 | try: 56 | print('unblocking %d' % uid) 57 | api.DestroyBlock(uid) 58 | except TwitterError: 59 | unblock_failed_ids.append(uid) 60 | 61 | 62 | try: 63 | for user_id in no_mutual_followers: 64 | executor.submit(remove_follower, user_id) 65 | executor.shutdown(wait=True) 66 | except (KeyboardInterrupt, SystemExit): 67 | cancelled = True 68 | print('Interrupted, exiting...') 69 | 70 | with open('block_failed_ids.list', 'w') as f: 71 | for user_id in block_failed_ids: 72 | f.write('%d\n' % user_id) 73 | 74 | with open('unblock_failed_ids.list', 'w') as f: 75 | for user_id in unblock_failed_ids: 76 | f.write('%d\n' % user_id) 77 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | python-twitter 2 | requests 3 | requests_oauthlib 4 | pyyaml 5 | openpyxl --------------------------------------------------------------------------------