]> jfr.im git - z_archive/twitter.git/blame_incremental - twitter/cmdline.py
Misc cleanup
[z_archive/twitter.git] / twitter / cmdline.py
... / ...
CommitLineData
1"""
2USAGE:
3
4 twitter [action] [options]
5
6ACTIONS:
7 follow add the specified user to your follow list
8 friends get latest tweets from your friends (default action)
9 help print this help text that you are currently reading
10 leave remove the specified user from your following list
11 public get latest public tweets
12 replies get latest replies
13 set set your twitter status
14 shell login the twitter shell
15
16OPTIONS:
17
18 -e --email <email> your email to login to twitter
19 -p --password <password> your twitter password
20 -r --refresh run this command forever, polling every once
21 in a while (default: every 5 minutes)
22 -R --refresh-rate <rate> set the refresh rate (in seconds)
23 -f --format <format> specify the output format for status updates
24 -c --config <filename> read username and password from given config
25 file (default ~/.twitter)
26 -l --length <count> specify number of status updates shown
27 (default: 20, max: 200)
28 -t --timestamp show time before status lines
29 -d --datestamp shoe date before status lines
30
31FORMATS for the --format option
32
33 default one line per status
34 verbose multiple lines per status, more verbose status info
35 urls nothing but URLs
36 ansi ansi colour (rainbow mode)
37
38CONFIG FILES
39
40 The config file should contain a [twitter] header, and all the desired options
41 you wish to set, like so:
42
43[twitter]
44email: <username>
45password: <password>
46format: <desired_default_format_for_output>
47prompt: <twitter_shell_prompt e.g. '[cyan]twitter[R]> '>
48"""
49
50import sys
51import time
52from getopt import gnu_getopt as getopt, GetoptError
53from getpass import getpass
54import re
55import os.path
56from ConfigParser import SafeConfigParser
57import datetime
58
59from api import Twitter, TwitterError
60import ansi
61
62# Please don't change this, it was provided by the fine folks at Twitter.
63# If you change it, it will not work.
64AGENT_STR = "twittercommandlinetoolpy"
65
66OPTIONS = {
67 'email': None,
68 'password': None,
69 'action': 'friends',
70 'refresh': False,
71 'refresh_rate': 600,
72 'format': 'default',
73 'prompt': '[cyan]twitter[R]> ',
74 'config_filename': os.environ.get('HOME', '') + os.sep + '.twitter',
75 'length': 20,
76 'timestamp': False,
77 'datestamp': False,
78 'extra_args': []
79}
80
81def parse_args(args, options):
82 long_opts = ['email', 'password', 'help', 'format', 'refresh',
83 'refresh-rate', 'config', 'length', 'timestamp', 'datestamp']
84 short_opts = "e:p:f:h?rR:c:l:td"
85 opts, extra_args = getopt(args, short_opts, long_opts)
86
87 for opt, arg in opts:
88 if opt in ('-e', '--email'):
89 options['email'] = arg
90 elif opt in ('-p', '--password'):
91 options['password'] = arg
92 elif opt in ('-f', '--format'):
93 options['format'] = arg
94 elif opt in ('-r', '--refresh'):
95 options['refresh'] = True
96 elif opt in ('-R', '--refresh-rate'):
97 options['refresh_rate'] = int(arg)
98 elif opt in ('-l', '--length'):
99 options["length"] = int(arg)
100 elif opt in ('-t', '--timestamp'):
101 options["timestamp"] = True
102 elif opt in ('-d', '--datestamp'):
103 options["datestamp"] = True
104 elif opt in ('-?', '-h', '--help'):
105 options['action'] = 'help'
106 elif opt in ('-c', '--config'):
107 options['config_filename'] = arg
108
109 if extra_args and not ('action' in options and options['action'] == 'help'):
110 options['action'] = extra_args[0]
111 options['extra_args'] = extra_args[1:]
112
113def get_time_string(status, options):
114 timestamp = options["timestamp"]
115 datestamp = options["datestamp"]
116 t = time.strptime(status['created_at'], "%a %b %d %H:%M:%S +0000 %Y")
117 i_hate_timezones = time.timezone
118 if (time.daylight):
119 i_hate_timezones = time.altzone
120 dt = datetime.datetime(*t[:-3]) - datetime.timedelta(
121 seconds=i_hate_timezones)
122 t = dt.timetuple()
123 if timestamp and datestamp:
124 return time.strftime("%Y-%m-%d %H:%M:%S ", t)
125 elif timestamp:
126 return time.strftime("%H:%M:%S ", t)
127 elif datestamp:
128 return time.strftime("%Y-%m-%d ", t)
129 return ""
130
131class StatusFormatter(object):
132 def __call__(self, status):
133 return (u"%S%s %s" %(
134 get_time_string(status, options),
135 status['user']['screen_name'], status['text']))
136
137class AnsiStatusFormatter(object):
138 def __init__(self):
139 self._colourMap = ansi.ColourMap()
140
141 def __call__(self, status, options):
142 colour = self._colourMap.colourFor(status['user']['screen_name'])
143 return (u"%s%s%s%s %s" %(
144 get_time_string(status, options),
145 ansi.cmdColour(colour), status['user']['screen_name'],
146 ansi.cmdReset(), status['text']))
147
148class VerboseStatusFormatter(object):
149 def __call__(self, status, options):
150 return (u"-- %s (%s) on %s\n%s\n" %(
151 status['user']['screen_name'],
152 status['user']['location'],
153 status['created_at'],
154 status['text']))
155
156class URLStatusFormatter(object):
157 urlmatch = re.compile(r'https?://\S+')
158 def __call__(self, status, options):
159 urls = self.urlmatch.findall(status['text'])
160 return u'\n'.join(urls) if urls else ""
161
162class AdminFormatter(object):
163 def __call__(self, action, user):
164 user_str = u"%s (%s)" %(user['screen_name'], user['name'])
165 if action == "follow":
166 return u"You are now following %s.\n" %(user_str)
167 else:
168 return u"You are no longer following %s.\n" %(user_str)
169
170class VerboseAdminFormatter(object):
171 def __call__(self, action, user):
172 return(u"-- %s: %s (%s): %s" % (
173 "Following" if action == "follow" else "Leaving",
174 user['screen_name'],
175 user['name'],
176 user['url']))
177
178status_formatters = {
179 'default': StatusFormatter,
180 'verbose': VerboseStatusFormatter,
181 'urls': URLStatusFormatter,
182 'ansi': AnsiStatusFormatter
183}
184
185admin_formatters = {
186 'default': AdminFormatter,
187 'verbose': VerboseAdminFormatter,
188 'urls': AdminFormatter,
189 'ansi': AdminFormatter
190}
191
192def get_status_formatter(options):
193 sf = status_formatters.get(options['format'])
194 if (not sf):
195 raise TwitterError(
196 "Unknown formatter '%s'" %(options['format']))
197 return sf()
198
199def get_admin_formatter(options):
200 sf = admin_formatters.get(options['format'])
201 if (not sf):
202 raise TwitterError(
203 "Unknown formatter '%s'" %(options['format']))
204 return sf()
205
206class Action(object):
207 @staticmethod
208 def ask(subject='perform this action', careful=False):
209 '''
210 Requests fromt he user using `raw_input` if `subject` should be
211 performed. When `careful`, the default answer is NO, otherwise YES.
212 Returns the user answer in the form `True` or `False`.
213 '''
214 sample = '(y/N)'
215 if not careful:
216 sample = '(Y/n)'
217
218 prompt = 'You really want to %s %s? ' %(subject, sample)
219 try:
220 answer = raw_input(prompt).lower()
221 if careful:
222 return answer in ('yes', 'y')
223 else:
224 return answer not in ('no', 'n')
225 except EOFError:
226 print >>sys.stderr # Put Newline since Enter was never pressed
227 # TODO:
228 # Figure out why on OS X the raw_input keeps raising
229 # EOFError and is never able to reset and get more input
230 # Hint: Look at how IPython implements their console
231 default = True
232 if careful:
233 default = False
234 return default
235
236 def __call__(self, twitter, options):
237 action = actions.get(options['action'], NoSuchAction)()
238 try:
239 doAction = lambda : action(twitter, options)
240 if (options['refresh'] and isinstance(action, StatusAction)):
241 while True:
242 doAction()
243 time.sleep(options['refresh_rate'])
244 else:
245 doAction()
246 except KeyboardInterrupt:
247 print >>sys.stderr, '\n[Keyboard Interrupt]'
248 pass
249
250class NoSuchActionError(Exception):
251 pass
252
253class NoSuchAction(Action):
254 def __call__(self, twitter, options):
255 raise NoSuchActionError("No such action: %s" %(options['action']))
256
257def printNicely(string):
258 if sys.stdout.encoding:
259 print string.encode(sys.stdout.encoding, 'replace')
260 else:
261 print string.encode('utf-8')
262
263class StatusAction(Action):
264 def __call__(self, twitter, options):
265 statuses = self.getStatuses(twitter, options)
266 sf = get_status_formatter(options)
267 for status in statuses:
268 statusStr = sf(status, options)
269 if statusStr.strip():
270 printNicely(statusStr)
271
272class AdminAction(Action):
273 def __call__(self, twitter, options):
274 if not options['extra_args'] or options['extra_args'][0]:
275 raise TwitterError("You need to specify a user (screen name)")
276 af = get_admin_formatter(options)
277 try:
278 user = self.getUser(twitter, options['extra_args'][0])
279 except TwitterError, e:
280 print "There was a problem following or leaving the specified user."
281 print "You may be trying to follow a user you are already following;"
282 print "Leaving a user you are not currently following;"
283 print "Or the user may not exist."
284 print "Sorry."
285 print
286 print e
287 else:
288 printNicely(af(options['action'], user))
289
290class FriendsAction(StatusAction):
291 def getStatuses(self, twitter, options):
292 return reversed(twitter.statuses.friends_timeline(count=options["length"]))
293
294class PublicAction(StatusAction):
295 def getStatuses(self, twitter, options):
296 return reversed(twitter.statuses.public_timeline(count=options["length"]))
297
298class RepliesAction(StatusAction):
299 def getStatuses(self, twitter, options):
300 return reversed(twitter.statuses.replies(count=options["length"]))
301
302class FollowAction(AdminAction):
303 def getUser(self, twitter, user):
304 return twitter.friendships.create(id=user)
305
306class LeaveAction(AdminAction):
307 def getUser(self, twitter, user):
308 return twitter.friendships.destroy(id=user)
309
310class SetStatusAction(Action):
311 def __call__(self, twitter, options):
312 statusTxt = (u" ".join(options['extra_args'])
313 if options['extra_args']
314 else unicode(raw_input("message: ")))
315 status = (statusTxt.encode('utf8', 'replace'))
316 twitter.statuses.update(status=status)
317
318class TwitterShell(Action):
319 @staticmethod
320 def render_prompt(prompt):
321 '''Parses the `prompt` string and returns the rendered version'''
322 prompt = prompt.strip("'").replace("\\'","'")
323 for colour in ansi.COLOURS_NAMED:
324 if '[%s]' %(colour) in prompt:
325 prompt = prompt.replace(
326 '[%s]' %(colour), ansi.cmdColourNamed(colour))
327 prompt = prompt.replace('[R]', ansi.cmdReset())
328 return prompt
329 def __call__(self, twitter, options):
330 prompt = self.render_prompt(options.get('prompt', 'twitter> '))
331 while True:
332 try:
333 args = raw_input(prompt).split()
334 parse_args(args, options)
335 if not options['action']:
336 continue
337 elif options['action'] == 'exit':
338 raise SystemExit(0)
339 elif options['action'] == 'shell':
340 print >>sys.stderr, 'Sorry Xzibit does not work here!'
341 continue
342 elif options['action'] == 'help':
343 print >>sys.stderr, '''\ntwitter> `action`\n
344 The Shell Accepts all the command line actions along with:
345
346 exit Leave the twitter shell (^D may also be used)
347
348 Full CMD Line help is appended below for your convinience.'''
349 Action()(twitter, options)
350 options['action'] = ''
351 except NoSuchActionError, e:
352 print >>sys.stderr, e
353 except KeyboardInterrupt:
354 print >>sys.stderr, '\n[Keyboard Interrupt]'
355 except EOFError:
356 print >>sys.stderr
357 leaving = self.ask(subject='Leave')
358 if not leaving:
359 print >>sys.stderr, 'Excellent!'
360 else:
361 raise SystemExit(0)
362
363class HelpAction(Action):
364 def __call__(self, twitter, options):
365 print __doc__
366
367actions = {
368 'follow' : FollowAction,
369 'friends' : FriendsAction,
370 'help' : HelpAction,
371 'leave' : LeaveAction,
372 'public' : PublicAction,
373 'replies' : RepliesAction,
374 'set' : SetStatusAction,
375 'shell' : TwitterShell,
376}
377
378def loadConfig(filename):
379 options = dict(OPTIONS)
380 if os.path.exists(filename):
381 cp = SafeConfigParser()
382 cp.read([filename])
383 for option in ('email', 'password', 'format', 'prompt'):
384 if cp.has_option('twitter', option):
385 options[option] = cp.get('twitter', option)
386 return options
387
388def main(args=sys.argv[1:]):
389 arg_options = {}
390 try:
391 parse_args(args, arg_options)
392 except GetoptError, e:
393 print >> sys.stderr, "I can't do that, %s." %(e)
394 print >> sys.stderr
395 raise SystemExit(1)
396
397 config_options = loadConfig(
398 arg_options.get('config_filename') or OPTIONS.get('config_filename'))
399
400 # Apply the various options in order, the most important applied last.
401 # Defaults first, then what's read from config file, then command-line
402 # arguments.
403 options = dict(OPTIONS)
404 for d in config_options, arg_options:
405 for k,v in d.items():
406 if v: options[k] = v
407
408 if options['refresh'] and options['action'] not in (
409 'friends', 'public', 'replies'):
410 print >> sys.stderr, "You can only refresh the friends, public, or replies actions."
411 print >> sys.stderr, "Use 'twitter -h' for help."
412 raise SystemExit(1)
413
414 if options['email'] and not options['password']:
415 options['password'] = getpass("Twitter password: ")
416
417 twitter = Twitter(options['email'], options['password'], agent=AGENT_STR)
418 try:
419 Action()(twitter, options)
420 except NoSuchActionError, e:
421 print >>sys.stderr, e
422 raise SystemExit(1)
423 except TwitterError, e:
424 print >> sys.stderr, e.args[0]
425 print >> sys.stderr, "Use 'twitter -h' for help."
426 raise SystemExit(1)