]> jfr.im git - z_archive/twitter.git/blob - twitter/cmdline.py
More misc cleanup
[z_archive/twitter.git] / twitter / cmdline.py
1 """
2 USAGE:
3
4 twitter [action] [options]
5
6 ACTIONS:
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
16 OPTIONS:
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
31 FORMATS 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
38 CONFIG 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]
44 email: <username>
45 password: <password>
46 format: <desired_default_format_for_output>
47 prompt: <twitter_shell_prompt e.g. '[cyan]twitter[R]> '>
48 """
49
50 import sys
51 import time
52 from getopt import gnu_getopt as getopt, GetoptError
53 from getpass import getpass
54 import re
55 import os.path
56 from ConfigParser import SafeConfigParser
57 import datetime
58
59 from api import Twitter, TwitterError
60 import 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.
64 AGENT_STR = "twittercommandlinetoolpy"
65
66 OPTIONS = {
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
81 def 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
113 def 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
131 class 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
137 class 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
148 class 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
156 class 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
162 class 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
170 class 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
178 status_formatters = {
179 'default': StatusFormatter,
180 'verbose': VerboseStatusFormatter,
181 'urls': URLStatusFormatter,
182 'ansi': AnsiStatusFormatter
183 }
184
185 admin_formatters = {
186 'default': AdminFormatter,
187 'verbose': VerboseAdminFormatter,
188 'urls': AdminFormatter,
189 'ansi': AdminFormatter
190 }
191
192 def 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
199 def 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
206 class Action(object):
207
208 def ask(self, 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
250 class NoSuchActionError(Exception):
251 pass
252
253 class NoSuchAction(Action):
254 def __call__(self, twitter, options):
255 raise NoSuchActionError("No such action: %s" %(options['action']))
256
257 def printNicely(string):
258 if sys.stdout.encoding:
259 print string.encode(sys.stdout.encoding, 'replace')
260 else:
261 print string.encode('utf-8')
262
263 class 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
272 class AdminAction(Action):
273 def __call__(self, twitter, options):
274 if not (options['extra_args'] and 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
290 class FriendsAction(StatusAction):
291 def getStatuses(self, twitter, options):
292 return reversed(twitter.statuses.friends_timeline(count=options["length"]))
293
294 class PublicAction(StatusAction):
295 def getStatuses(self, twitter, options):
296 return reversed(twitter.statuses.public_timeline(count=options["length"]))
297
298 class RepliesAction(StatusAction):
299 def getStatuses(self, twitter, options):
300 return reversed(twitter.statuses.replies(count=options["length"]))
301
302 class FollowAction(AdminAction):
303 def getUser(self, twitter, user):
304 return twitter.friendships.create(id=user)
305
306 class LeaveAction(AdminAction):
307 def getUser(self, twitter, user):
308 return twitter.friendships.destroy(id=user)
309
310 class 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
318 class TwitterShell(Action):
319
320 def render_prompt(self, 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
330 def __call__(self, twitter, options):
331 prompt = self.render_prompt(options.get('prompt', 'twitter> '))
332 while True:
333 options['action'] = ""
334 try:
335 args = raw_input(prompt).split()
336 parse_args(args, options)
337 if not options['action']:
338 continue
339 elif options['action'] == 'exit':
340 raise SystemExit(0)
341 elif options['action'] == 'shell':
342 print >>sys.stderr, 'Sorry Xzibit does not work here!'
343 continue
344 elif options['action'] == 'help':
345 print >>sys.stderr, '''\ntwitter> `action`\n
346 The Shell Accepts all the command line actions along with:
347
348 exit Leave the twitter shell (^D may also be used)
349
350 Full CMD Line help is appended below for your convinience.'''
351 Action()(twitter, options)
352 options['action'] = ''
353 except NoSuchActionError, e:
354 print >>sys.stderr, e
355 except KeyboardInterrupt:
356 print >>sys.stderr, '\n[Keyboard Interrupt]'
357 except EOFError:
358 print >>sys.stderr
359 leaving = self.ask(subject='Leave')
360 if not leaving:
361 print >>sys.stderr, 'Excellent!'
362 else:
363 raise SystemExit(0)
364
365 class HelpAction(Action):
366 def __call__(self, twitter, options):
367 print __doc__
368
369 actions = {
370 'follow' : FollowAction,
371 'friends' : FriendsAction,
372 'help' : HelpAction,
373 'leave' : LeaveAction,
374 'public' : PublicAction,
375 'replies' : RepliesAction,
376 'set' : SetStatusAction,
377 'shell' : TwitterShell,
378 }
379
380 def loadConfig(filename):
381 options = dict(OPTIONS)
382 if os.path.exists(filename):
383 cp = SafeConfigParser()
384 cp.read([filename])
385 for option in ('email', 'password', 'format', 'prompt'):
386 if cp.has_option('twitter', option):
387 options[option] = cp.get('twitter', option)
388 return options
389
390 def main(args=sys.argv[1:]):
391 arg_options = {}
392 try:
393 parse_args(args, arg_options)
394 except GetoptError, e:
395 print >> sys.stderr, "I can't do that, %s." %(e)
396 print >> sys.stderr
397 raise SystemExit(1)
398
399 config_options = loadConfig(
400 arg_options.get('config_filename') or OPTIONS.get('config_filename'))
401
402 # Apply the various options in order, the most important applied last.
403 # Defaults first, then what's read from config file, then command-line
404 # arguments.
405 options = dict(OPTIONS)
406 for d in config_options, arg_options:
407 for k,v in d.items():
408 if v: options[k] = v
409
410 if options['refresh'] and options['action'] not in (
411 'friends', 'public', 'replies'):
412 print >> sys.stderr, "You can only refresh the friends, public, or replies actions."
413 print >> sys.stderr, "Use 'twitter -h' for help."
414 raise SystemExit(1)
415
416 if options['email'] and not options['password']:
417 options['password'] = getpass("Twitter password: ")
418
419 twitter = Twitter(options['email'], options['password'], agent=AGENT_STR)
420 try:
421 Action()(twitter, options)
422 except NoSuchActionError, e:
423 print >>sys.stderr, e
424 raise SystemExit(1)
425 except TwitterError, e:
426 print >> sys.stderr, e.args[0]
427 print >> sys.stderr, "Use 'twitter -h' for help."
428 raise SystemExit(1)