]>
jfr.im git - z_archive/twitter.git/blob - twitter/cmdline.py
d722a6ace1b0bd845f49db70529854a3c879abb8
4 twitter [action] [options]
8 authorize authorize the command-line tool to interact with Twitter
9 follow add the specified user to your follow list
10 friends get latest tweets from your friends (default action)
11 help print this help text that you are currently reading
12 leave remove the specified user from your following list
13 public get latest public tweets
14 replies get latest replies
15 search search twitter (Beware: octothorpe, escape it)
16 set set your twitter status
17 shell login the twitter shell
22 -r --refresh run this command forever, polling every once
23 in a while (default: every 5 minutes)
24 -R --refresh-rate <rate> set the refresh rate (in seconds)
25 -f --format <format> specify the output format for status updates
26 -c --config <filename> read username and password from given config
27 file (default ~/.twitter)
28 -l --length <count> specify number of status updates shown
29 (default: 20, max: 200)
30 -t --timestamp show time before status lines
31 -d --datestamp shoe date before status lines
32 --no-ssl use HTTP instead of more secure HTTPS
33 --oauth <filename> filename to read/store oauth credentials to
35 FORMATS for the --format option
37 default one line per status
38 verbose multiple lines per status, more verbose status info
40 ansi ansi colour (rainbow mode)
45 The config file should be placed in your home directory and be named .twitter.
46 It must contain a [twitter] header, and all the desired options you wish to
50 format: <desired_default_format_for_output>
51 prompt: <twitter_shell_prompt e.g. '[cyan]twitter[R]> '>
53 OAuth authentication tokens are stored in the file .twitter_oauth in your
57 CONSUMER_KEY
='uS6hO2sV6tDKIOeVjhnFnQ'
58 CONSUMER_SECRET
='MEYTOS97VvlHX7K1rwHPEqVpTSqZ71HtvoK4sVuYk'
62 from getopt
import gnu_getopt
as getopt
, GetoptError
63 from getpass
import getpass
66 from ConfigParser
import SafeConfigParser
68 from urllib
import quote
71 from api
import Twitter
, TwitterError
72 from oauth
import OAuth
, write_token_file
, read_token_file
73 from oauth_dance
import oauth_dance
81 'prompt': '[cyan]twitter[R]> ',
82 'config_filename': os
.environ
.get('HOME', '') + os
.sep
+ '.twitter',
83 'oauth_filename': os
.environ
.get('HOME', '') + os
.sep
+ '.twitter_oauth',
91 def parse_args(args
, options
):
92 long_opts
= ['help', 'format=', 'refresh', 'oauth=',
93 'refresh-rate=', 'config=', 'length=', 'timestamp',
94 'datestamp', 'no-ssl']
95 short_opts
= "e:p:f:h?rR:c:l:td"
96 opts
, extra_args
= getopt(args
, short_opts
, long_opts
)
99 if opt
in ('-f', '--format'):
100 options
['format'] = arg
101 elif opt
in ('-r', '--refresh'):
102 options
['refresh'] = True
103 elif opt
in ('-R', '--refresh-rate'):
104 options
['refresh_rate'] = int(arg
)
105 elif opt
in ('-l', '--length'):
106 options
["length"] = int(arg
)
107 elif opt
in ('-t', '--timestamp'):
108 options
["timestamp"] = True
109 elif opt
in ('-d', '--datestamp'):
110 options
["datestamp"] = True
111 elif opt
in ('-?', '-h', '--help'):
112 options
['action'] = 'help'
113 elif opt
in ('-c', '--config'):
114 options
['config_filename'] = arg
115 elif opt
== '--no-ssl':
116 options
['secure'] = False
117 elif opt
== '--oauth':
118 options
['oauth_filename'] = arg
120 if extra_args
and not ('action' in options
and options
['action'] == 'help'):
121 options
['action'] = extra_args
[0]
122 options
['extra_args'] = extra_args
[1:]
124 def get_time_string(status
, options
, format
="%a %b %d %H:%M:%S +0000 %Y"):
125 timestamp
= options
["timestamp"]
126 datestamp
= options
["datestamp"]
127 t
= time
.strptime(status
['created_at'], format
)
128 i_hate_timezones
= time
.timezone
130 i_hate_timezones
= time
.altzone
131 dt
= datetime
.datetime(*t
[:-3]) - datetime
.timedelta(
132 seconds
=i_hate_timezones
)
134 if timestamp
and datestamp
:
135 return time
.strftime("%Y-%m-%d %H:%M:%S ", t
)
137 return time
.strftime("%H:%M:%S ", t
)
139 return time
.strftime("%Y-%m-%d ", t
)
142 class StatusFormatter(object):
143 def __call__(self
, status
, options
):
144 return (u
"%s%s %s" %(
145 get_time_string(status
, options
),
146 status
['user']['screen_name'], status
['text']))
148 class AnsiStatusFormatter(object):
150 self
._colourMap
= ansi
.ColourMap()
152 def __call__(self
, status
, options
):
153 colour
= self
._colourMap
.colourFor(status
['user']['screen_name'])
154 return (u
"%s%s%s%s %s" %(
155 get_time_string(status
, options
),
156 ansi
.cmdColour(colour
), status
['user']['screen_name'],
157 ansi
.cmdReset(), status
['text']))
159 class VerboseStatusFormatter(object):
160 def __call__(self
, status
, options
):
161 return (u
"-- %s (%s) on %s\n%s\n" %(
162 status
['user']['screen_name'],
163 status
['user']['location'],
164 status
['created_at'],
167 class URLStatusFormatter(object):
168 urlmatch
= re
.compile(r
'https?://\S+')
169 def __call__(self
, status
, options
):
170 urls
= self
.urlmatch
.findall(status
['text'])
171 return u
'\n'.join(urls
) if urls
else ""
173 class AdminFormatter(object):
174 def __call__(self
, action
, user
):
175 user_str
= u
"%s (%s)" %(user
['screen_name'], user
['name'])
176 if action
== "follow":
177 return u
"You are now following %s.\n" %(user_str)
179 return u
"You are no longer following %s.\n" %(user_str)
181 class VerboseAdminFormatter(object):
182 def __call__(self
, action
, user
):
183 return(u
"-- %s: %s (%s): %s" % (
184 "Following" if action
== "follow" else "Leaving",
189 class SearchFormatter(object):
190 def __call__(self
, result
, options
):
192 get_time_string(result
, options
, "%a, %d %b %Y %H:%M:%S +0000"),
193 result
['from_user'], result
['text']))
195 class VerboseSearchFormatter(SearchFormatter
):
196 pass #Default to the regular one
198 class URLSearchFormatter(object):
199 urlmatch
= re
.compile(r
'https?://\S+')
200 def __call__(self
, result
, options
):
201 urls
= self
.urlmatch
.findall(result
['text'])
202 return u
'\n'.join(urls
) if urls
else ""
204 class AnsiSearchFormatter(object):
206 self
._colourMap
= ansi
.ColourMap()
208 def __call__(self
, result
, options
):
209 colour
= self
._colourMap
.colourFor(result
['from_user'])
210 return (u
"%s%s%s%s %s" %(
211 get_time_string(result
, options
, "%a, %d %b %Y %H:%M:%S +0000"),
212 ansi
.cmdColour(colour
), result
['from_user'],
213 ansi
.cmdReset(), result
['text']))
215 _term_encoding
= None
216 def get_term_encoding():
217 global _term_encoding
218 if not _term_encoding
:
219 lang
= os
.getenv('LANG', 'unknown.UTF-8').split('.')
221 _term_encoding
= lang
[1]
223 _term_encoding
= 'UTF-8'
224 return _term_encoding
227 status_formatters
= {
228 'default': StatusFormatter
,
229 'verbose': VerboseStatusFormatter
,
230 'urls': URLStatusFormatter
,
231 'ansi': AnsiStatusFormatter
233 formatters
['status'] = status_formatters
236 'default': AdminFormatter
,
237 'verbose': VerboseAdminFormatter
,
238 'urls': AdminFormatter
,
239 'ansi': AdminFormatter
241 formatters
['admin'] = admin_formatters
243 search_formatters
= {
244 'default': SearchFormatter
,
245 'verbose': VerboseSearchFormatter
,
246 'urls': URLSearchFormatter
,
247 'ansi': AnsiSearchFormatter
249 formatters
['search'] = search_formatters
251 def get_formatter(action_type
, options
):
252 formatters_dict
= formatters
.get(action_type
)
253 if (not formatters_dict
):
255 "There was an error finding a class of formatters for your type (%s)"
257 f
= formatters_dict
.get(options
['format'])
260 "Unknown formatter '%s' for status actions" %(options
['format']))
263 class Action(object):
265 def ask(self
, subject
='perform this action', careful
=False):
267 Requests fromt he user using `raw_input` if `subject` should be
268 performed. When `careful`, the default answer is NO, otherwise YES.
269 Returns the user answer in the form `True` or `False`.
275 prompt
= 'You really want to %s %s? ' %(subject
, sample
)
277 answer
= raw_input(prompt
).lower()
279 return answer
in ('yes', 'y')
281 return answer
not in ('no', 'n')
283 print >>sys
.stderr
# Put Newline since Enter was never pressed
285 # Figure out why on OS X the raw_input keeps raising
286 # EOFError and is never able to reset and get more input
287 # Hint: Look at how IPython implements their console
293 def __call__(self
, twitter
, options
):
294 action
= actions
.get(options
['action'], NoSuchAction
)()
296 doAction
= lambda : action(twitter
, options
)
297 if (options
['refresh'] and isinstance(action
, StatusAction
)):
300 time
.sleep(options
['refresh_rate'])
303 except KeyboardInterrupt:
304 print >>sys
.stderr
, '\n[Keyboard Interrupt]'
307 class NoSuchActionError(Exception):
310 class NoSuchAction(Action
):
311 def __call__(self
, twitter
, options
):
312 raise NoSuchActionError("No such action: %s" %(options
['action']))
314 def printNicely(string
):
315 if sys
.stdout
.encoding
:
316 print string
.encode(sys
.stdout
.encoding
, 'replace')
318 print string
.encode('utf-8')
320 class StatusAction(Action
):
321 def __call__(self
, twitter
, options
):
322 statuses
= self
.getStatuses(twitter
, options
)
323 sf
= get_formatter('status', options
)
324 for status
in statuses
:
325 statusStr
= sf(status
, options
)
326 if statusStr
.strip():
327 printNicely(statusStr
)
329 class SearchAction(Action
):
330 def __call__(self
, twitter
, options
):
331 # We need to be pointing at search.twitter.com to work, and it is less
332 # tangly to do it here than in the main()
333 twitter
.domain
="search.twitter.com"
335 # We need to bypass the TwitterCall parameter encoding, so we
336 # don't encode the plus sign, so we have to encode it ourselves
337 query_string
= "+".join(
338 [quote(term
.decode(get_term_encoding()))
339 for term
in options
['extra_args']])
340 twitter
.encoded_args
= "q=%s" %(query_string)
342 results
= twitter
.search()['results']
343 f
= get_formatter('search', options
)
344 for result
in results
:
345 resultStr
= f(result
, options
)
346 if resultStr
.strip():
347 printNicely(resultStr
)
349 class AdminAction(Action
):
350 def __call__(self
, twitter
, options
):
351 if not (options
['extra_args'] and options
['extra_args'][0]):
352 raise TwitterError("You need to specify a user (screen name)")
353 af
= get_formatter('admin', options
)
355 user
= self
.getUser(twitter
, options
['extra_args'][0])
356 except TwitterError
, e
:
357 print "There was a problem following or leaving the specified user."
358 print "You may be trying to follow a user you are already following;"
359 print "Leaving a user you are not currently following;"
360 print "Or the user may not exist."
365 printNicely(af(options
['action'], user
))
367 class FriendsAction(StatusAction
):
368 def getStatuses(self
, twitter
, options
):
369 return reversed(twitter
.statuses
.friends_timeline(count
=options
["length"]))
371 class PublicAction(StatusAction
):
372 def getStatuses(self
, twitter
, options
):
373 return reversed(twitter
.statuses
.public_timeline(count
=options
["length"]))
375 class RepliesAction(StatusAction
):
376 def getStatuses(self
, twitter
, options
):
377 return reversed(twitter
.statuses
.replies(count
=options
["length"]))
379 class FollowAction(AdminAction
):
380 def getUser(self
, twitter
, user
):
381 return twitter
.friendships
.create(id=user
)
383 class LeaveAction(AdminAction
):
384 def getUser(self
, twitter
, user
):
385 return twitter
.friendships
.destroy(id=user
)
387 class SetStatusAction(Action
):
388 def __call__(self
, twitter
, options
):
389 statusTxt
= (" ".join(options
['extra_args']).decode(get_term_encoding())
390 if options
['extra_args']
391 else unicode(raw_input("message: ")))
392 status
= (statusTxt
.encode('utf8', 'replace'))
393 twitter
.statuses
.update(status
=status
)
395 class TwitterShell(Action
):
397 def render_prompt(self
, prompt
):
398 '''Parses the `prompt` string and returns the rendered version'''
399 prompt
= prompt
.strip("'").replace("\\'","'")
400 for colour
in ansi
.COLOURS_NAMED
:
401 if '[%s]' %(colour) in prompt
:
402 prompt
= prompt
.replace(
403 '[%s]' %(colour), ansi
.cmdColourNamed(colour
))
404 prompt
= prompt
.replace('[R]', ansi
.cmdReset())
407 def __call__(self
, twitter
, options
):
408 prompt
= self
.render_prompt(options
.get('prompt', 'twitter> '))
410 options
['action'] = ""
412 args
= raw_input(prompt
).split()
413 parse_args(args
, options
)
414 if not options
['action']:
416 elif options
['action'] == 'exit':
418 elif options
['action'] == 'shell':
419 print >>sys
.stderr
, 'Sorry Xzibit does not work here!'
421 elif options
['action'] == 'help':
422 print >>sys
.stderr
, '''\ntwitter> `action`\n
423 The Shell Accepts all the command line actions along with:
425 exit Leave the twitter shell (^D may also be used)
427 Full CMD Line help is appended below for your convinience.'''
428 Action()(twitter
, options
)
429 options
['action'] = ''
430 except NoSuchActionError
, e
:
431 print >>sys
.stderr
, e
432 except KeyboardInterrupt:
433 print >>sys
.stderr
, '\n[Keyboard Interrupt]'
436 leaving
= self
.ask(subject
='Leave')
438 print >>sys
.stderr
, 'Excellent!'
442 class HelpAction(Action
):
443 def __call__(self
, twitter
, options
):
446 class DoNothingAction(Action
):
447 def __call__(self
, twitter
, options
):
451 'authorize' : DoNothingAction
,
452 'follow' : FollowAction
,
453 'friends' : FriendsAction
,
455 'leave' : LeaveAction
,
456 'public' : PublicAction
,
457 'replies' : RepliesAction
,
458 'search' : SearchAction
,
459 'set' : SetStatusAction
,
460 'shell' : TwitterShell
,
463 def loadConfig(filename
):
464 options
= dict(OPTIONS
)
465 if os
.path
.exists(filename
):
466 cp
= SafeConfigParser()
468 for option
in ('format', 'prompt'):
469 if cp
.has_option('twitter', option
):
470 options
[option
] = cp
.get('twitter', option
)
473 def main(args
=sys
.argv
[1:]):
476 parse_args(args
, arg_options
)
477 except GetoptError
, e
:
478 print >> sys
.stderr
, "I can't do that, %s." %(e)
482 config_options
= loadConfig(
483 arg_options
.get('config_filename') or OPTIONS
.get('config_filename'))
485 # Apply the various options in order, the most important applied last.
486 # Defaults first, then what's read from config file, then command-line
488 options
= dict(OPTIONS
)
489 for d
in config_options
, arg_options
:
490 for k
,v
in d
.items():
493 if options
['refresh'] and options
['action'] not in (
494 'friends', 'public', 'replies'):
495 print >> sys
.stderr
, "You can only refresh the friends, public, or replies actions."
496 print >> sys
.stderr
, "Use 'twitter -h' for help."
499 if (options
['action'] == 'authorize'
500 or not os
.path
.exists(options
['oauth_filename'])):
502 "the Command-Line Tool", CONSUMER_KEY
, CONSUMER_SECRET
,
503 options
['oauth_filename'])
505 oauth_token
, oauth_token_secret
= read_token_file(options
['oauth_filename'])
509 oauth_token
, oauth_token_secret
, CONSUMER_KEY
, CONSUMER_SECRET
),
510 secure
=options
['secure'],
512 domain
='api.twitter.com')
515 Action()(twitter
, options
)
516 except NoSuchActionError
, e
:
517 print >>sys
.stderr
, e
519 except TwitterError
, e
:
520 print >> sys
.stderr
, str(e
)
521 print >> sys
.stderr
, "Use 'twitter -h' for help."