]>
jfr.im git - z_archive/twitter.git/blob - twitter/cmdline.py
5 twitter [action] [options]
9 authorize authorize the command-line tool to interact with Twitter
11 friends get latest tweets from your friends (default action)
12 help print this help text that you are currently reading
13 leave stop following a user
14 list get list of a user's lists; give a list name to get
16 mylist get list of your lists; give a list name to get tweets
18 public get latest public tweets
19 replies get latest replies to you
20 search search twitter (Beware: octothorpe, escape it)
21 set set your twitter status
22 shell login to the twitter shell
27 -r --refresh run this command forever, polling every once
28 in a while (default: every 5 minutes)
29 -R --refresh-rate <rate> set the refresh rate (in seconds)
30 -f --format <format> specify the output format for status updates
31 -c --config <filename> read username and password from given config
32 file (default ~/.twitter)
33 -l --length <count> specify number of status updates shown
34 (default: 20, max: 200)
35 -t --timestamp show time before status lines
36 -d --datestamp show date before status lines
37 --no-ssl use less-secure HTTP instead of HTTPS
38 --oauth <filename> filename to read/store oauth credentials to
40 FORMATS for the --format option
42 default one line per status
43 verbose multiple lines per status, more verbose status info
45 ansi ansi colour (rainbow mode)
50 The config file should be placed in your home directory and be named .twitter.
51 It must contain a [twitter] header, and all the desired options you wish to
55 format: <desired_default_format_for_output>
56 prompt: <twitter_shell_prompt e.g. '[cyan]twitter[R]> '>
58 OAuth authentication tokens are stored in the file .twitter_oauth in your
62 CONSUMER_KEY
='uS6hO2sV6tDKIOeVjhnFnQ'
63 CONSUMER_SECRET
='MEYTOS97VvlHX7K1rwHPEqVpTSqZ71HtvoK4sVuYk'
67 from getopt
import gnu_getopt
as getopt
, GetoptError
68 from getpass
import getpass
71 from ConfigParser
import SafeConfigParser
73 from urllib
import quote
76 from api
import Twitter
, TwitterError
77 from oauth
import OAuth
, write_token_file
, read_token_file
78 from oauth_dance
import oauth_dance
86 'prompt': '[cyan]twitter[R]> ',
87 'config_filename': os
.environ
.get('HOME', '') + os
.sep
+ '.twitter',
88 'oauth_filename': os
.environ
.get('HOME', '') + os
.sep
+ '.twitter_oauth',
96 def parse_args(args
, options
):
97 long_opts
= ['help', 'format=', 'refresh', 'oauth=',
98 'refresh-rate=', 'config=', 'length=', 'timestamp',
99 'datestamp', 'no-ssl']
100 short_opts
= "e:p:f:h?rR:c:l:td"
101 opts
, extra_args
= getopt(args
, short_opts
, long_opts
)
103 for opt
, arg
in opts
:
104 if opt
in ('-f', '--format'):
105 options
['format'] = arg
106 elif opt
in ('-r', '--refresh'):
107 options
['refresh'] = True
108 elif opt
in ('-R', '--refresh-rate'):
109 options
['refresh_rate'] = int(arg
)
110 elif opt
in ('-l', '--length'):
111 options
["length"] = int(arg
)
112 elif opt
in ('-t', '--timestamp'):
113 options
["timestamp"] = True
114 elif opt
in ('-d', '--datestamp'):
115 options
["datestamp"] = True
116 elif opt
in ('-?', '-h', '--help'):
117 options
['action'] = 'help'
118 elif opt
in ('-c', '--config'):
119 options
['config_filename'] = arg
120 elif opt
== '--no-ssl':
121 options
['secure'] = False
122 elif opt
== '--oauth':
123 options
['oauth_filename'] = arg
125 if extra_args
and not ('action' in options
and options
['action'] == 'help'):
126 options
['action'] = extra_args
[0]
127 options
['extra_args'] = extra_args
[1:]
129 def get_time_string(status
, options
, format
="%a %b %d %H:%M:%S +0000 %Y"):
130 timestamp
= options
["timestamp"]
131 datestamp
= options
["datestamp"]
132 t
= time
.strptime(status
['created_at'], format
)
133 i_hate_timezones
= time
.timezone
135 i_hate_timezones
= time
.altzone
136 dt
= datetime
.datetime(*t
[:-3]) - datetime
.timedelta(
137 seconds
=i_hate_timezones
)
139 if timestamp
and datestamp
:
140 return time
.strftime("%Y-%m-%d %H:%M:%S ", t
)
142 return time
.strftime("%H:%M:%S ", t
)
144 return time
.strftime("%Y-%m-%d ", t
)
147 class StatusFormatter(object):
148 def __call__(self
, status
, options
):
149 return (u
"%s%s %s" %(
150 get_time_string(status
, options
),
151 status
['user']['screen_name'], status
['text']))
153 class AnsiStatusFormatter(object):
155 self
._colourMap
= ansi
.ColourMap()
157 def __call__(self
, status
, options
):
158 colour
= self
._colourMap
.colourFor(status
['user']['screen_name'])
159 return (u
"%s%s%s%s %s" %(
160 get_time_string(status
, options
),
161 ansi
.cmdColour(colour
), status
['user']['screen_name'],
162 ansi
.cmdReset(), status
['text']))
164 class VerboseStatusFormatter(object):
165 def __call__(self
, status
, options
):
166 return (u
"-- %s (%s) on %s\n%s\n" %(
167 status
['user']['screen_name'],
168 status
['user']['location'],
169 status
['created_at'],
172 class URLStatusFormatter(object):
173 urlmatch
= re
.compile(r
'https?://\S+')
174 def __call__(self
, status
, options
):
175 urls
= self
.urlmatch
.findall(status
['text'])
176 return u
'\n'.join(urls
) if urls
else ""
179 class ListsFormatter(object):
180 def __call__(self
, list):
181 if list['description']:
182 list_str
= u
"%-30s (%s)" % (list['name'], list['description'])
184 list_str
= u
"%-30s" % (list['name'])
185 return u
"%s\n" % list_str
187 class ListsVerboseFormatter(object):
188 def __call__(self
, list):
189 list_str
= u
"%-30s\n description: %s\n members: %s\n mode:%s\n" % (list['name'], list['description'], list['member_count'], list['mode'])
192 class AnsiListsFormatter(object):
194 self
._colourMap
= ansi
.ColourMap()
196 def __call__(self
, list):
197 colour
= self
._colourMap
.colourFor(list['name'])
198 return (u
"%s%-15s%s %s" %(
199 ansi
.cmdColour(colour
), list['name'],
200 ansi
.cmdReset(), list['description']))
203 class AdminFormatter(object):
204 def __call__(self
, action
, user
):
205 user_str
= u
"%s (%s)" %(user
['screen_name'], user
['name'])
206 if action
== "follow":
207 return u
"You are now following %s.\n" %(user_str)
209 return u
"You are no longer following %s.\n" %(user_str)
211 class VerboseAdminFormatter(object):
212 def __call__(self
, action
, user
):
213 return(u
"-- %s: %s (%s): %s" % (
214 "Following" if action
== "follow" else "Leaving",
219 class SearchFormatter(object):
220 def __call__(self
, result
, options
):
222 get_time_string(result
, options
, "%a, %d %b %Y %H:%M:%S +0000"),
223 result
['from_user'], result
['text']))
225 class VerboseSearchFormatter(SearchFormatter
):
226 pass #Default to the regular one
228 class URLSearchFormatter(object):
229 urlmatch
= re
.compile(r
'https?://\S+')
230 def __call__(self
, result
, options
):
231 urls
= self
.urlmatch
.findall(result
['text'])
232 return u
'\n'.join(urls
) if urls
else ""
234 class AnsiSearchFormatter(object):
236 self
._colourMap
= ansi
.ColourMap()
238 def __call__(self
, result
, options
):
239 colour
= self
._colourMap
.colourFor(result
['from_user'])
240 return (u
"%s%s%s%s %s" %(
241 get_time_string(result
, options
, "%a, %d %b %Y %H:%M:%S +0000"),
242 ansi
.cmdColour(colour
), result
['from_user'],
243 ansi
.cmdReset(), result
['text']))
245 _term_encoding
= None
246 def get_term_encoding():
247 global _term_encoding
248 if not _term_encoding
:
249 lang
= os
.getenv('LANG', 'unknown.UTF-8').split('.')
251 _term_encoding
= lang
[1]
253 _term_encoding
= 'UTF-8'
254 return _term_encoding
257 status_formatters
= {
258 'default': StatusFormatter
,
259 'verbose': VerboseStatusFormatter
,
260 'urls': URLStatusFormatter
,
261 'ansi': AnsiStatusFormatter
263 formatters
['status'] = status_formatters
266 'default': AdminFormatter
,
267 'verbose': VerboseAdminFormatter
,
268 'urls': AdminFormatter
,
269 'ansi': AdminFormatter
271 formatters
['admin'] = admin_formatters
273 search_formatters
= {
274 'default': SearchFormatter
,
275 'verbose': VerboseSearchFormatter
,
276 'urls': URLSearchFormatter
,
277 'ansi': AnsiSearchFormatter
279 formatters
['search'] = search_formatters
282 'default': ListsFormatter
,
283 'verbose': ListsVerboseFormatter
,
285 'ansi': AnsiListsFormatter
287 formatters
['lists'] = lists_formatters
289 def get_formatter(action_type
, options
):
290 formatters_dict
= formatters
.get(action_type
)
291 if (not formatters_dict
):
293 "There was an error finding a class of formatters for your type (%s)"
295 f
= formatters_dict
.get(options
['format'])
298 "Unknown formatter '%s' for status actions" %(options
['format']))
301 class Action(object):
303 def ask(self
, subject
='perform this action', careful
=False):
305 Requests fromt he user using `raw_input` if `subject` should be
306 performed. When `careful`, the default answer is NO, otherwise YES.
307 Returns the user answer in the form `True` or `False`.
313 prompt
= 'You really want to %s %s? ' %(subject
, sample
)
315 answer
= raw_input(prompt
).lower()
317 return answer
in ('yes', 'y')
319 return answer
not in ('no', 'n')
321 print >>sys
.stderr
# Put Newline since Enter was never pressed
323 # Figure out why on OS X the raw_input keeps raising
324 # EOFError and is never able to reset and get more input
325 # Hint: Look at how IPython implements their console
331 def __call__(self
, twitter
, options
):
332 action
= actions
.get(options
['action'], NoSuchAction
)()
334 doAction
= lambda : action(twitter
, options
)
335 if (options
['refresh'] and isinstance(action
, StatusAction
)):
338 time
.sleep(options
['refresh_rate'])
341 except KeyboardInterrupt:
342 print >>sys
.stderr
, '\n[Keyboard Interrupt]'
345 class NoSuchActionError(Exception):
348 class NoSuchAction(Action
):
349 def __call__(self
, twitter
, options
):
350 raise NoSuchActionError("No such action: %s" %(options
['action']))
352 def printNicely(string
):
353 if sys
.stdout
.encoding
:
354 print string
.encode(sys
.stdout
.encoding
, 'replace')
356 print string
.encode('utf-8')
358 class StatusAction(Action
):
359 def __call__(self
, twitter
, options
):
360 statuses
= self
.getStatuses(twitter
, options
)
361 sf
= get_formatter('status', options
)
362 for status
in statuses
:
363 statusStr
= sf(status
, options
)
364 if statusStr
.strip():
365 printNicely(statusStr
)
367 class SearchAction(Action
):
368 def __call__(self
, twitter
, options
):
369 # We need to be pointing at search.twitter.com to work, and it is less
370 # tangly to do it here than in the main()
371 twitter
.domain
="search.twitter.com"
373 # We need to bypass the TwitterCall parameter encoding, so we
374 # don't encode the plus sign, so we have to encode it ourselves
375 query_string
= "+".join(
376 [quote(term
.decode(get_term_encoding()))
377 for term
in options
['extra_args']])
379 results
= twitter
.search(q
=query_string
)['results']
380 f
= get_formatter('search', options
)
381 for result
in results
:
382 resultStr
= f(result
, options
)
383 if resultStr
.strip():
384 printNicely(resultStr
)
386 class AdminAction(Action
):
387 def __call__(self
, twitter
, options
):
388 if not (options
['extra_args'] and options
['extra_args'][0]):
389 raise TwitterError("You need to specify a user (screen name)")
390 af
= get_formatter('admin', options
)
392 user
= self
.getUser(twitter
, options
['extra_args'][0])
393 except TwitterError
, e
:
394 print "There was a problem following or leaving the specified user."
395 print "You may be trying to follow a user you are already following;"
396 print "Leaving a user you are not currently following;"
397 print "Or the user may not exist."
402 printNicely(af(options
['action'], user
))
404 class ListsAction(StatusAction
):
405 def getStatuses(self
, twitter
, options
):
406 if not options
['extra_args']:
407 raise TwitterError("Please provide a user to query for lists")
409 screen_name
= options
['extra_args'][0]
411 if not options
['extra_args'][1:]:
412 lists
= twitter
.user
.lists(user
=screen_name
)['lists']
414 printNicely("This user has no lists.")
416 lf
= get_formatter('lists', options
)
417 printNicely(lf(list))
420 return reversed(twitter
.user
.lists
.list.statuses(
421 user
=screen_name
, list=options
['extra_args'][1]))
424 class MyListsAction(ListsAction
):
425 def getStatuses(self
, twitter
, options
):
426 screen_name
= twitter
.account
.verify_credentials()['screen_name']
427 options
['extra_args'].insert(0, screen_name
)
428 return ListsAction
.getStatuses(self
, twitter
, options
)
431 class FriendsAction(StatusAction
):
432 def getStatuses(self
, twitter
, options
):
433 return reversed(twitter
.statuses
.friends_timeline(count
=options
["length"]))
435 class PublicAction(StatusAction
):
436 def getStatuses(self
, twitter
, options
):
437 return reversed(twitter
.statuses
.public_timeline(count
=options
["length"]))
439 class RepliesAction(StatusAction
):
440 def getStatuses(self
, twitter
, options
):
441 return reversed(twitter
.statuses
.replies(count
=options
["length"]))
443 class FollowAction(AdminAction
):
444 def getUser(self
, twitter
, user
):
445 return twitter
.friendships
.create(id=user
)
447 class LeaveAction(AdminAction
):
448 def getUser(self
, twitter
, user
):
449 return twitter
.friendships
.destroy(id=user
)
451 class SetStatusAction(Action
):
452 def __call__(self
, twitter
, options
):
453 statusTxt
= (" ".join(options
['extra_args']).decode(get_term_encoding())
454 if options
['extra_args']
455 else unicode(raw_input("message: ")))
456 status
= (statusTxt
.encode('utf8', 'replace'))
457 twitter
.statuses
.update(status
=status
)
459 class TwitterShell(Action
):
461 def render_prompt(self
, prompt
):
462 '''Parses the `prompt` string and returns the rendered version'''
463 prompt
= prompt
.strip("'").replace("\\'","'")
464 for colour
in ansi
.COLOURS_NAMED
:
465 if '[%s]' %(colour) in prompt
:
466 prompt
= prompt
.replace(
467 '[%s]' %(colour), ansi
.cmdColourNamed(colour
))
468 prompt
= prompt
.replace('[R]', ansi
.cmdReset())
471 def __call__(self
, twitter
, options
):
472 prompt
= self
.render_prompt(options
.get('prompt', 'twitter> '))
474 options
['action'] = ""
476 args
= raw_input(prompt
).split()
477 parse_args(args
, options
)
478 if not options
['action']:
480 elif options
['action'] == 'exit':
482 elif options
['action'] == 'shell':
483 print >>sys
.stderr
, 'Sorry Xzibit does not work here!'
485 elif options
['action'] == 'help':
486 print >>sys
.stderr
, '''\ntwitter> `action`\n
487 The Shell Accepts all the command line actions along with:
489 exit Leave the twitter shell (^D may also be used)
491 Full CMD Line help is appended below for your convinience.'''
492 Action()(twitter
, options
)
493 options
['action'] = ''
494 except NoSuchActionError
, e
:
495 print >>sys
.stderr
, e
496 except KeyboardInterrupt:
497 print >>sys
.stderr
, '\n[Keyboard Interrupt]'
500 leaving
= self
.ask(subject
='Leave')
502 print >>sys
.stderr
, 'Excellent!'
506 class HelpAction(Action
):
507 def __call__(self
, twitter
, options
):
510 class DoNothingAction(Action
):
511 def __call__(self
, twitter
, options
):
515 'authorize' : DoNothingAction
,
516 'follow' : FollowAction
,
517 'friends' : FriendsAction
,
518 'list' : ListsAction
,
519 'mylist' : MyListsAction
,
521 'leave' : LeaveAction
,
522 'public' : PublicAction
,
523 'replies' : RepliesAction
,
524 'search' : SearchAction
,
525 'set' : SetStatusAction
,
526 'shell' : TwitterShell
,
529 def loadConfig(filename
):
530 options
= dict(OPTIONS
)
531 if os
.path
.exists(filename
):
532 cp
= SafeConfigParser()
534 for option
in ('format', 'prompt'):
535 if cp
.has_option('twitter', option
):
536 options
[option
] = cp
.get('twitter', option
)
539 def main(args
=sys
.argv
[1:]):
542 parse_args(args
, arg_options
)
543 except GetoptError
, e
:
544 print >> sys
.stderr
, "I can't do that, %s." %(e)
548 config_path
= os
.path
.expanduser(
549 arg_options
.get('config_filename') or OPTIONS
.get('config_filename'))
550 config_options
= loadConfig(config_path
)
552 # Apply the various options in order, the most important applied last.
553 # Defaults first, then what's read from config file, then command-line
555 options
= dict(OPTIONS
)
556 for d
in config_options
, arg_options
:
557 for k
,v
in d
.items():
560 if options
['refresh'] and options
['action'] not in (
561 'friends', 'public', 'replies'):
562 print >> sys
.stderr
, "You can only refresh the friends, public, or replies actions."
563 print >> sys
.stderr
, "Use 'twitter -h' for help."
566 oauth_filename
= os
.path
.expanduser(options
['oauth_filename'])
568 if (options
['action'] == 'authorize'
569 or not os
.path
.exists(oauth_filename
)):
571 "the Command-Line Tool", CONSUMER_KEY
, CONSUMER_SECRET
,
572 options
['oauth_filename'])
574 oauth_token
, oauth_token_secret
= read_token_file(oauth_filename
)
578 oauth_token
, oauth_token_secret
, CONSUMER_KEY
, CONSUMER_SECRET
),
579 secure
=options
['secure'],
581 domain
='api.twitter.com')
584 Action()(twitter
, options
)
585 except NoSuchActionError
, e
:
586 print >>sys
.stderr
, e
588 except TwitterError
, e
:
589 print >> sys
.stderr
, str(e
)
590 print >> sys
.stderr
, "Use 'twitter -h' for help."