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