]>
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 | ||
13 | AUTHENTICATION | |
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 | ||
19 | from __future__ import print_function | |
20 | ||
21 | import os, sys, time, calendar, urllib2, httplib | |
22 | from getopt import gnu_getopt as getopt, GetoptError | |
23 | ||
24 | # T-Follow (Twitter-Follow) application registered by @stalkr_ | |
25 | CONSUMER_KEY='USRZQfvFFjB6UvZIN2Edww' | |
26 | CONSUMER_SECRET='AwGAaSzZa5r0TDL8RKCDtffnI9H9mooZUdOa95nw8' | |
27 | ||
28 | from .api import Twitter, TwitterError | |
29 | from .oauth import OAuth, read_token_file | |
30 | from .oauth_dance import oauth_dance | |
31 | from .auth import NoAuth | |
32 | from .util import Fail, err | |
33 | ||
34 | def 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 | ||
55 | def 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 | ||
63 | def 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 | ||
107 | def 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 | ||
116 | def 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 | ||
170 | def 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 | ||
179 | def 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))) |