├── .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
--------------------------------------------------------------------------------