]> jfr.im git - z_archive/twitter.git/blame - twitter/follow.py
twitter-archiver and twitter-follow initial import
[z_archive/twitter.git] / twitter / follow.py
CommitLineData
a7282452
S
1"""USAGE
2 twitter-follow [options] <user>
3
4DESCRIPTION
5 Display all following/followers of a user, one user per line.
6
7OPTIONS
8 -o --oauth authenticate to Twitter using OAuth (default no)
9 -r --followers display followers of the given user (default)
10 -g --following display users the given user is following
11 -a --api-rate see your current API rate limit status
12
13AUTHENTICATION
14 Authenticate to Twitter using OAuth to see following/followers of private
15 profiles and have higher API rate limits. OAuth authentication tokens
16 are stored in the file .twitter-follow_oauth in your home directory.
17"""
18
19from __future__ import print_function
20
21import os, sys, time, calendar, urllib2, httplib
22from getopt import gnu_getopt as getopt, GetoptError
23
24# T-Follow (Twitter-Follow) application registered by @stalkr_
25CONSUMER_KEY='USRZQfvFFjB6UvZIN2Edww'
26CONSUMER_SECRET='AwGAaSzZa5r0TDL8RKCDtffnI9H9mooZUdOa95nw8'
27
28from .api import Twitter, TwitterError
29from .oauth import OAuth, read_token_file
30from .oauth_dance import oauth_dance
31from .auth import NoAuth
32from .util import Fail, err
33
34def parse_args(args, options):
35 """Parse arguments from command-line to set options."""
36 long_opts = ['help', 'oauth', 'followers', 'following', 'api-rate']
37 short_opts = "horga"
38 opts, extra_args = getopt(args, short_opts, long_opts)
39
40 for opt, arg in opts:
41 if opt in ('-h', '--help'):
42 print(__doc__)
43 raise SystemExit(1)
44 elif opt in ('-o', '--oauth'):
45 options['oauth'] = True
46 elif opt in ('-r', '--followers'):
47 options['followers'] = True
48 elif opt in ('-g', '--following'):
49 options['followers'] = False
50 elif opt in ('-a', '--api-rate'):
51 options['api-rate' ] = True
52
53 options['extra_args'] = extra_args
54
55def lookup_portion(twitter, user_ids):
56 """Resolve a limited list of user ids to screen names."""
57 users = {}
58 kwargs = dict(user_id=",".join(map(str, user_ids)), skip_status=1)
59 for u in twitter.users.lookup(**kwargs):
60 users[int(u['id'])] = u['screen_name']
61 return users
62
63def lookup(twitter, user_ids):
64 """Resolve an entire list of user ids to screen names."""
65 users = {}
66 api_limit = 100
67 for i in range(0, len(user_ids), api_limit):
68 fail = Fail()
69 while True:
70 try:
71 portion = lookup_portion(twitter, user_ids[i:][:api_limit])
72 except TwitterError as e:
73 if e.e.code == 400:
74 err("Fail: %i API rate limit exceeded" % e.e.code)
75 rate = twitter.account.rate_limit_status()
76 reset = rate['reset_time_in_seconds']
77 reset = time.asctime(time.localtime(reset))
78 delay = int(rate['reset_time_in_seconds']
79 - time.time()) + 5 # avoid race
80 err("Hourly limit of %i requests reached, next reset on "
81 "%s: going to sleep for %i secs"
82 % (rate['hourly_limit'], reset, delay))
83 fail.wait(delay)
84 continue
85 elif e.e.code == 502:
86 err("Fail: %i Service currently unavailable, retrying..."
87 % e.e.code)
88 else:
89 err("Fail: %s\nRetrying..." % str(e)[:500])
90 fail.wait(3)
91 except urllib2.URLError as e:
92 err("Fail: urllib2.URLError %s - Retrying..." % str(e))
93 fail.wait(3)
94 except httplib.error as e:
95 err("Fail: httplib.error %s - Retrying..." % str(e))
96 fail.wait(3)
97 except KeyError as e:
98 err("Fail: KeyError %s - Retrying..." % str(e))
99 fail.wait(3)
100 else:
101 users.update(portion)
102 err("Resolving user ids to screen names: %i/%i"
103 % (len(users), len(user_ids)))
104 break
105 return users
106
107def follow_portion(twitter, screen_name, cursor=-1, followers=True):
108 """Get a portion of followers/following for a user."""
109 kwargs = dict(screen_name=screen_name, cursor=cursor)
110 if followers:
111 t = twitter.followers.ids(**kwargs)
112 else: # following
113 t = twitter.friends.ids(**kwargs)
114 return t['ids'], t['next_cursor']
115
116def follow(twitter, screen_name, followers=True):
117 """Get the entire list of followers/following for a user."""
118 user_ids = []
119 cursor = -1
120 fail = Fail()
121 while True:
122 try:
123 portion, cursor = follow_portion(twitter, screen_name, cursor,
124 followers)
125 except TwitterError as e:
126 if e.e.code == 401:
127 reason = ("follow%s of that user are protected"
128 % ("ers" if followers else "ing"))
129 err("Fail: %i Unauthorized (%s)" % (e.e.code, reason))
130 break
131 elif e.e.code == 400:
132 err("Fail: %i API rate limit exceeded" % e.e.code)
133 rate = twitter.account.rate_limit_status()
134 reset = rate['reset_time_in_seconds']
135 reset = time.asctime(time.localtime(reset))
136 delay = int(rate['reset_time_in_seconds']
137 - time.time()) + 5 # avoid race
138 err("Hourly limit of %i requests reached, next reset on %s: "
139 "going to sleep for %i secs" % (rate['hourly_limit'],
140 reset, delay))
141 fail.wait(delay)
142 continue
143 elif e.e.code == 502:
144 err("Fail: %i Service currently unavailable, retrying..."
145 % e.e.code)
146 else:
147 err("Fail: %s\nRetrying..." % str(e)[:500])
148 fail.wait(3)
149 except urllib2.URLError as e:
150 err("Fail: urllib2.URLError %s - Retrying..." % str(e))
151 fail.wait(3)
152 except httplib.error as e:
153 err("Fail: httplib.error %s - Retrying..." % str(e))
154 fail.wait(3)
155 except KeyError as e:
156 err("Fail: KeyError %s - Retrying..." % str(e))
157 fail.wait(3)
158 else:
159 new = -len(user_ids)
160 user_ids = list(set(user_ids + portion))
161 new += len(user_ids)
162 what = "follow%s" % ("ers" if followers else "ing")
163 err("Browsing %s %s, new: %i" % (screen_name, what, new))
164 if cursor == 0:
165 break
166 fail = Fail()
167 return user_ids
168
169
170def rate_limit_status(twitter):
171 """Print current Twitter API rate limit status."""
172 r = twitter.account.rate_limit_status()
173 print("Remaining API requests: %i/%i (hourly limit)"
174 % (r['remaining_hits'], r['hourly_limit']))
175 print("Next reset in %is (%s)"
176 % (int(r['reset_time_in_seconds'] - time.time()),
177 time.asctime(time.localtime(r['reset_time_in_seconds']))))
178
179def main(args=sys.argv[1:]):
180 options = {
181 'oauth': False,
182 'followers': True,
183 'api-rate': False
184 }
185 try:
186 parse_args(args, options)
187 except GetoptError as e:
188 err("I can't do that, %s." % e)
189 raise SystemExit(1)
190
191 # exit if no user or given, except if asking for API rate
192 if not options['extra_args'] and not options['api-rate']:
193 print(__doc__)
194 raise SystemExit(1)
195
196 # authenticate using OAuth, asking for token if necessary
197 if options['oauth']:
198 oauth_filename = (os.getenv("HOME", "") + os.sep
199 + ".twitter-follow_oauth")
200 if not os.path.exists(oauth_filename):
201 oauth_dance("Twitter-Follow", CONSUMER_KEY, CONSUMER_SECRET,
202 oauth_filename)
203 oauth_token, oauth_token_secret = read_token_file(oauth_filename)
204 auth = OAuth(oauth_token, oauth_token_secret, CONSUMER_KEY,
205 CONSUMER_SECRET)
206 else:
207 auth = NoAuth()
208
209 twitter = Twitter(auth=auth, api_version='1', domain='api.twitter.com')
210
211 if options['api-rate']:
212 rate_limit_status(twitter)
213 return
214
215 # obtain list of followers (or following) for every given user
216 for user in options['extra_args']:
217 user_ids, users = [], {}
218 try:
219 user_ids = follow(twitter, user, options['followers'])
220 users = lookup(twitter, user_ids)
221 except KeyboardInterrupt as e:
222 err()
223 err("Interrupted.")
224 raise SystemExit(1)
225
226 for uid in user_ids:
227 print(users[uid].encode("utf-8"))
228
229 # print total on stderr to separate from user list on stdout
230 if options['followers']:
231 err("Total followers for %s: %i" % (user, len(user_ids)))
232 else:
233 err("Total users %s is following: %i" % (user, len(user_ids)))