]>
jfr.im git - z_archive/twitter.git/blob - twitter/cmdline.py
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']])
341 results
= twitter
.search(q
=query_string
)['results']
342 f
= get_formatter('search', options
)
343 for result
in results
:
344 resultStr
= f(result
, options
)
345 if resultStr
.strip():
346 printNicely(resultStr
)
348 class AdminAction(Action
):
349 def __call__(self
, twitter
, options
):
350 if not (options
['extra_args'] and options
['extra_args'][0]):
351 raise TwitterError("You need to specify a user (screen name)")
352 af
= get_formatter('admin', options
)
354 user
= self
.getUser(twitter
, options
['extra_args'][0])
355 except TwitterError
, e
:
356 print "There was a problem following or leaving the specified user."
357 print "You may be trying to follow a user you are already following;"
358 print "Leaving a user you are not currently following;"
359 print "Or the user may not exist."
364 printNicely(af(options
['action'], user
))
366 class FriendsAction(StatusAction
):
367 def getStatuses(self
, twitter
, options
):
368 return reversed(twitter
.statuses
.friends_timeline(count
=options
["length"]))
370 class PublicAction(StatusAction
):
371 def getStatuses(self
, twitter
, options
):
372 return reversed(twitter
.statuses
.public_timeline(count
=options
["length"]))
374 class RepliesAction(StatusAction
):
375 def getStatuses(self
, twitter
, options
):
376 return reversed(twitter
.statuses
.replies(count
=options
["length"]))
378 class FollowAction(AdminAction
):
379 def getUser(self
, twitter
, user
):
380 return twitter
.friendships
.create(id=user
)
382 class LeaveAction(AdminAction
):
383 def getUser(self
, twitter
, user
):
384 return twitter
.friendships
.destroy(id=user
)
386 class SetStatusAction(Action
):
387 def __call__(self
, twitter
, options
):
388 statusTxt
= (" ".join(options
['extra_args']).decode(get_term_encoding())
389 if options
['extra_args']
390 else unicode(raw_input("message: ")))
391 status
= (statusTxt
.encode('utf8', 'replace'))
392 twitter
.statuses
.update(status
=status
)
394 class TwitterShell(Action
):
396 def render_prompt(self
, prompt
):
397 '''Parses the `prompt` string and returns the rendered version'''
398 prompt
= prompt
.strip("'").replace("\\'","'")
399 for colour
in ansi
.COLOURS_NAMED
:
400 if '[%s]' %(colour) in prompt
:
401 prompt
= prompt
.replace(
402 '[%s]' %(colour), ansi
.cmdColourNamed(colour
))
403 prompt
= prompt
.replace('[R]', ansi
.cmdReset())
406 def __call__(self
, twitter
, options
):
407 prompt
= self
.render_prompt(options
.get('prompt', 'twitter> '))
409 options
['action'] = ""
411 args
= raw_input(prompt
).split()
412 parse_args(args
, options
)
413 if not options
['action']:
415 elif options
['action'] == 'exit':
417 elif options
['action'] == 'shell':
418 print >>sys
.stderr
, 'Sorry Xzibit does not work here!'
420 elif options
['action'] == 'help':
421 print >>sys
.stderr
, '''\ntwitter> `action`\n
422 The Shell Accepts all the command line actions along with:
424 exit Leave the twitter shell (^D may also be used)
426 Full CMD Line help is appended below for your convinience.'''
427 Action()(twitter
, options
)
428 options
['action'] = ''
429 except NoSuchActionError
, e
:
430 print >>sys
.stderr
, e
431 except KeyboardInterrupt:
432 print >>sys
.stderr
, '\n[Keyboard Interrupt]'
435 leaving
= self
.ask(subject
='Leave')
437 print >>sys
.stderr
, 'Excellent!'
441 class HelpAction(Action
):
442 def __call__(self
, twitter
, options
):
445 class DoNothingAction(Action
):
446 def __call__(self
, twitter
, options
):
450 'authorize' : DoNothingAction
,
451 'follow' : FollowAction
,
452 'friends' : FriendsAction
,
454 'leave' : LeaveAction
,
455 'public' : PublicAction
,
456 'replies' : RepliesAction
,
457 'search' : SearchAction
,
458 'set' : SetStatusAction
,
459 'shell' : TwitterShell
,
462 def loadConfig(filename
):
463 options
= dict(OPTIONS
)
464 if os
.path
.exists(filename
):
465 cp
= SafeConfigParser()
467 for option
in ('format', 'prompt'):
468 if cp
.has_option('twitter', option
):
469 options
[option
] = cp
.get('twitter', option
)
472 def main(args
=sys
.argv
[1:]):
475 parse_args(args
, arg_options
)
476 except GetoptError
, e
:
477 print >> sys
.stderr
, "I can't do that, %s." %(e)
481 config_path
= os
.path
.expanduser(
482 arg_options
.get('config_filename') or OPTIONS
.get('config_filename'))
483 config_options
= loadConfig(config_path
)
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 oauth_filename
= os
.path
.expanduser(options
['oauth_filename'])
501 if (options
['action'] == 'authorize'
502 or not os
.path
.exists(oauth_filename
)):
504 "the Command-Line Tool", CONSUMER_KEY
, CONSUMER_SECRET
,
505 options
['oauth_filename'])
507 oauth_token
, oauth_token_secret
= read_token_file(oauth_filename
)
511 oauth_token
, oauth_token_secret
, CONSUMER_KEY
, CONSUMER_SECRET
),
512 secure
=options
['secure'],
514 domain
='api.twitter.com')
517 Action()(twitter
, options
)
518 except NoSuchActionError
, e
:
519 print >>sys
.stderr
, e
521 except TwitterError
, e
:
522 print >> sys
.stderr
, str(e
)
523 print >> sys
.stderr
, "Use 'twitter -h' for help."