]> jfr.im git - z_archive/twitter.git/blame - twitter/cmdline.py
Misc cleanup
[z_archive/twitter.git] / twitter / cmdline.py
CommitLineData
7364ea65 1"""
5251ea48 2USAGE:
7364ea65 3
5251ea48 4 twitter [action] [options]
5
6ACTIONS:
1c11e6d7 7 follow add the specified user to your follow list
5251ea48 8 friends get latest tweets from your friends (default action)
45688301 9 help print this help text that you are currently reading
efa0ba89 10 leave remove the specified user from your following list
5251ea48 11 public get latest public tweets
9a9f7ae7 12 replies get latest replies
5251ea48 13 set set your twitter status
05b85831 14 shell login the twitter shell
5251ea48 15
16OPTIONS:
17
18 -e --email <email> your email to login to twitter
19 -p --password <password> your twitter password
0ea01db7 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
21e3bd23 24 -c --config <filename> read username and password from given config
39a6f562
MV
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
0ea01db7 30
31FORMATS for the --format option
32
33 default one line per status
34 verbose multiple lines per status, more verbose status info
327e556b
MV
35 urls nothing but URLs
36 ansi ansi colour (rainbow mode)
05b85831 37
21e3bd23 38CONFIG FILES
39
327e556b
MV
40 The config file should contain a [twitter] header, and all the desired options
41 you wish to set, like so:
21e3bd23 42
43[twitter]
44email: <username>
45password: <password>
327e556b 46format: <desired_default_format_for_output>
05b85831 47prompt: <twitter_shell_prompt e.g. '[cyan]twitter[R]> '>
7364ea65 48"""
49
5251ea48 50import sys
0ea01db7 51import time
f2a7ce46 52from getopt import gnu_getopt as getopt, GetoptError
f068ff42 53from getpass import getpass
0ea01db7 54import re
21e3bd23 55import os.path
56from ConfigParser import SafeConfigParser
a4b5e65b 57import datetime
5251ea48 58
59from api import Twitter, TwitterError
0b9960a3 60import ansi
5251ea48 61
3d17fdfc
MV
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"
45688301 65
327e556b 66OPTIONS = {
5251ea48 67 'email': None,
68 'password': None,
69 'action': 'friends',
0ea01db7 70 'refresh': False,
71 'refresh_rate': 600,
72 'format': 'default',
05b85831 73 'prompt': '[cyan]twitter[R]> ',
21e3bd23 74 'config_filename': os.environ.get('HOME', '') + os.sep + '.twitter',
39a6f562
MV
75 'length': 20,
76 'timestamp': False,
77 'datestamp': False,
5251ea48 78 'extra_args': []
79}
80
81def parse_args(args, options):
0ea01db7 82 long_opts = ['email', 'password', 'help', 'format', 'refresh',
39a6f562
MV
83 'refresh-rate', 'config', 'length', 'timestamp', 'datestamp']
84 short_opts = "e:p:f:h?rR:c:l:td"
44405280 85 opts, extra_args = getopt(args, short_opts, long_opts)
efa0ba89 86
5251ea48 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
0ea01db7 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)
39a6f562
MV
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
5251ea48 104 elif opt in ('-?', '-h', '--help'):
05b85831 105 options['action'] = 'help'
21e3bd23 106 elif opt in ('-c', '--config'):
107 options['config_filename'] = arg
efa0ba89 108
05b85831 109 if extra_args and not ('action' in options and options['action'] == 'help'):
ae1d86aa 110 options['action'] = extra_args[0]
111 options['extra_args'] = extra_args[1:]
39a6f562
MV
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")
a4b5e65b
MV
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()
39a6f562
MV
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 ""
5251ea48 130
131class StatusFormatter(object):
132 def __call__(self, status):
39a6f562
MV
133 return (u"%S%s %s" %(
134 get_time_string(status, options),
0ea01db7 135 status['user']['screen_name'], status['text']))
5251ea48 136
0b9960a3
MV
137class AnsiStatusFormatter(object):
138 def __init__(self):
139 self._colourMap = ansi.ColourMap()
140
39a6f562 141 def __call__(self, status, options):
0b9960a3 142 colour = self._colourMap.colourFor(status['user']['screen_name'])
39a6f562
MV
143 return (u"%s%s%s%s %s" %(
144 get_time_string(status, options),
0b9960a3 145 ansi.cmdColour(colour), status['user']['screen_name'],
05b85831
HN
146 ansi.cmdReset(), status['text']))
147
f068ff42 148class VerboseStatusFormatter(object):
39a6f562 149 def __call__(self, status, options):
f068ff42 150 return (u"-- %s (%s) on %s\n%s\n" %(
151 status['user']['screen_name'],
152 status['user']['location'],
153 status['created_at'],
0ea01db7 154 status['text']))
f068ff42 155
0ea01db7 156class URLStatusFormatter(object):
157 urlmatch = re.compile(r'https?://\S+')
39a6f562 158 def __call__(self, status, options):
0ea01db7 159 urls = self.urlmatch.findall(status['text'])
160 return u'\n'.join(urls) if urls else ""
161
1c11e6d7 162class AdminFormatter(object):
efa0ba89 163 def __call__(self, action, user):
da45d039
MV
164 user_str = u"%s (%s)" %(user['screen_name'], user['name'])
165 if action == "follow":
e02facc9 166 return u"You are now following %s.\n" %(user_str)
da45d039 167 else:
e02facc9 168 return u"You are no longer following %s.\n" %(user_str)
efa0ba89 169
1c11e6d7 170class VerboseAdminFormatter(object):
efa0ba89
MV
171 def __call__(self, action, user):
172 return(u"-- %s: %s (%s): %s" % (
05b85831
HN
173 "Following" if action == "follow" else "Leaving",
174 user['screen_name'],
efa0ba89
MV
175 user['name'],
176 user['url']))
177
1c11e6d7 178status_formatters = {
0ea01db7 179 'default': StatusFormatter,
180 'verbose': VerboseStatusFormatter,
0b9960a3
MV
181 'urls': URLStatusFormatter,
182 'ansi': AnsiStatusFormatter
05b85831 183}
1c11e6d7
WD
184
185admin_formatters = {
efa0ba89
MV
186 'default': AdminFormatter,
187 'verbose': VerboseAdminFormatter,
327e556b
MV
188 'urls': AdminFormatter,
189 'ansi': AdminFormatter
1c11e6d7 190}
efa0ba89 191
0ea01db7 192def get_status_formatter(options):
1c11e6d7 193 sf = status_formatters.get(options['format'])
0ea01db7 194 if (not sf):
195 raise TwitterError(
196 "Unknown formatter '%s'" %(options['format']))
197 return sf()
198
1c11e6d7 199def get_admin_formatter(options):
efa0ba89
MV
200 sf = admin_formatters.get(options['format'])
201 if (not sf):
202 raise TwitterError(
203 "Unknown formatter '%s'" %(options['format']))
204 return sf()
205
0ea01db7 206class Action(object):
05b85831
HN
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 '''
f47ab046
MV
214 sample = '(y/N)'
215 if not careful:
216 sample = '(Y/n)'
217
05b85831
HN
218 prompt = 'You really want to %s %s? ' %(subject, sample)
219 try:
220 answer = raw_input(prompt).lower()
221 if careful:
f47ab046 222 return answer in ('yes', 'y')
05b85831 223 else:
f47ab046 224 return answer not in ('no', 'n')
05b85831
HN
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
f47ab046
MV
231 default = True
232 if careful:
233 default = False
05b85831 234 return default
f47ab046 235
05b85831
HN
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):
0ea01db7 251 pass
252
253class NoSuchAction(Action):
254 def __call__(self, twitter, options):
05b85831 255 raise NoSuchActionError("No such action: %s" %(options['action']))
0ea01db7 256
862cce81
MV
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
0ea01db7 263class StatusAction(Action):
264 def __call__(self, twitter, options):
39a6f562 265 statuses = self.getStatuses(twitter, options)
0ea01db7 266 sf = get_status_formatter(options)
267 for status in statuses:
39a6f562 268 statusStr = sf(status, options)
0ea01db7 269 if statusStr.strip():
862cce81 270 printNicely(statusStr)
1c11e6d7
WD
271
272class AdminAction(Action):
efa0ba89 273 def __call__(self, twitter, options):
f47ab046 274 if not options['extra_args'] or options['extra_args'][0]:
e02facc9 275 raise TwitterError("You need to specify a user (screen name)")
efa0ba89 276 af = get_admin_formatter(options)
e02facc9
MV
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."
f47ab046
MV
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."
e02facc9 285 print
45688301 286 print e
e02facc9 287 else:
862cce81 288 printNicely(af(options['action'], user))
efa0ba89 289
0ea01db7 290class FriendsAction(StatusAction):
39a6f562
MV
291 def getStatuses(self, twitter, options):
292 return reversed(twitter.statuses.friends_timeline(count=options["length"]))
efa0ba89 293
0ea01db7 294class PublicAction(StatusAction):
39a6f562
MV
295 def getStatuses(self, twitter, options):
296 return reversed(twitter.statuses.public_timeline(count=options["length"]))
0ea01db7 297
9a9f7ae7 298class RepliesAction(StatusAction):
39a6f562
MV
299 def getStatuses(self, twitter, options):
300 return reversed(twitter.statuses.replies(count=options["length"]))
9a9f7ae7 301
1c11e6d7 302class FollowAction(AdminAction):
efa0ba89 303 def getUser(self, twitter, user):
70955aae 304 return twitter.friendships.create(id=user)
efa0ba89 305
1c11e6d7 306class LeaveAction(AdminAction):
efa0ba89 307 def getUser(self, twitter, user):
70955aae 308 return twitter.friendships.destroy(id=user)
1c11e6d7 309
0ea01db7 310class SetStatusAction(Action):
311 def __call__(self, twitter, options):
05b85831
HN
312 statusTxt = (u" ".join(options['extra_args'])
313 if options['extra_args']
772fbdd1 314 else unicode(raw_input("message: ")))
315 status = (statusTxt.encode('utf8', 'replace'))
0ea01db7 316 twitter.statuses.update(status=status)
5251ea48 317
05b85831
HN
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
45688301
MV
363class HelpAction(Action):
364 def __call__(self, twitter, options):
365 print __doc__
366
5251ea48 367actions = {
05b85831
HN
368 'follow' : FollowAction,
369 'friends' : FriendsAction,
370 'help' : HelpAction,
371 'leave' : LeaveAction,
372 'public' : PublicAction,
373 'replies' : RepliesAction,
374 'set' : SetStatusAction,
375 'shell' : TwitterShell,
5251ea48 376}
377
21e3bd23 378def loadConfig(filename):
327e556b 379 options = dict(OPTIONS)
21e3bd23 380 if os.path.exists(filename):
381 cp = SafeConfigParser()
382 cp.read([filename])
05b85831 383 for option in ('email', 'password', 'format', 'prompt'):
327e556b
MV
384 if cp.has_option('twitter', option):
385 options[option] = cp.get('twitter', option)
386 return options
ae1d86aa 387
327e556b
MV
388def main(args=sys.argv[1:]):
389 arg_options = {}
44405280 390 try:
327e556b 391 parse_args(args, arg_options)
44405280
MV
392 except GetoptError, e:
393 print >> sys.stderr, "I can't do that, %s." %(e)
394 print >> sys.stderr
05b85831 395 raise SystemExit(1)
21e3bd23 396
327e556b
MV
397 config_options = loadConfig(
398 arg_options.get('config_filename') or OPTIONS.get('config_filename'))
efa0ba89 399
327e556b
MV
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
05b85831 407
e02facc9
MV
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."
0ea01db7 411 print >> sys.stderr, "Use 'twitter -h' for help."
05b85831
HN
412 raise SystemExit(1)
413
f068ff42 414 if options['email'] and not options['password']:
415 options['password'] = getpass("Twitter password: ")
05b85831 416
45688301 417 twitter = Twitter(options['email'], options['password'], agent=AGENT_STR)
5251ea48 418 try:
05b85831
HN
419 Action()(twitter, options)
420 except NoSuchActionError, e:
421 print >>sys.stderr, e
422 raise SystemExit(1)
5251ea48 423 except TwitterError, e:
f1a8ed67 424 print >> sys.stderr, e.args[0]
5251ea48 425 print >> sys.stderr, "Use 'twitter -h' for help."
05b85831 426 raise SystemExit(1)