]>
jfr.im git - z_archive/twitter.git/blob - twitter/cmdline.py
4 twitter [action] [options]
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 search search twitter (Beware: octothorpe, escape it)
14 set set your twitter status
15 shell login the twitter shell
19 -e --email <email> your email to login to twitter
20 -p --password <password> your twitter password
21 -r --refresh run this command forever, polling every once
22 in a while (default: every 5 minutes)
23 -R --refresh-rate <rate> set the refresh rate (in seconds)
24 -f --format <format> specify the output format for status updates
25 -c --config <filename> read username and password from given config
26 file (default ~/.twitter)
27 -l --length <count> specify number of status updates shown
28 (default: 20, max: 200)
29 -t --timestamp show time before status lines
30 -d --datestamp shoe date before status lines
31 --no-ssl use HTTP instead of more secure HTTPS
33 FORMATS for the --format option
35 default one line per status
36 verbose multiple lines per status, more verbose status info
38 ansi ansi colour (rainbow mode)
42 The config file should contain a [twitter] header, and all the desired options
43 you wish to set, like so:
48 format: <desired_default_format_for_output>
49 prompt: <twitter_shell_prompt e.g. '[cyan]twitter[R]> '>
54 from getopt
import gnu_getopt
as getopt
, GetoptError
55 from getpass
import getpass
58 from ConfigParser
import SafeConfigParser
60 from urllib
import quote
62 from api
import Twitter
, TwitterError
65 # Please don't change this, it was provided by the fine folks at Twitter.
66 # If you change it, it will not work.
67 AGENT_STR
= "twittercommandlinetoolpy"
76 'prompt': '[cyan]twitter[R]> ',
77 'config_filename': os
.environ
.get('HOME', '') + os
.sep
+ '.twitter',
85 def parse_args(args
, options
):
86 long_opts
= ['email', 'password', 'help', 'format', 'refresh',
87 'refresh-rate', 'config', 'length', 'timestamp',
88 'datestamp', 'no-ssl']
89 short_opts
= "e:p:f:h?rR:c:l:td"
90 opts
, extra_args
= getopt(args
, short_opts
, long_opts
)
93 if opt
in ('-e', '--email'):
94 options
['email'] = arg
95 elif opt
in ('-p', '--password'):
96 options
['password'] = arg
97 elif opt
in ('-f', '--format'):
98 options
['format'] = arg
99 elif opt
in ('-r', '--refresh'):
100 options
['refresh'] = True
101 elif opt
in ('-R', '--refresh-rate'):
102 options
['refresh_rate'] = int(arg
)
103 elif opt
in ('-l', '--length'):
104 options
["length"] = int(arg
)
105 elif opt
in ('-t', '--timestamp'):
106 options
["timestamp"] = True
107 elif opt
in ('-d', '--datestamp'):
108 options
["datestamp"] = True
109 elif opt
in ('-?', '-h', '--help'):
110 options
['action'] = 'help'
111 elif opt
in ('-c', '--config'):
112 options
['config_filename'] = arg
113 elif opt
== '--no-ssl':
114 options
['secure'] = False
116 if extra_args
and not ('action' in options
and options
['action'] == 'help'):
117 options
['action'] = extra_args
[0]
118 options
['extra_args'] = extra_args
[1:]
120 def get_time_string(status
, options
, format
="%a %b %d %H:%M:%S +0000 %Y"):
121 timestamp
= options
["timestamp"]
122 datestamp
= options
["datestamp"]
123 t
= time
.strptime(status
['created_at'], format
)
124 i_hate_timezones
= time
.timezone
126 i_hate_timezones
= time
.altzone
127 dt
= datetime
.datetime(*t
[:-3]) - datetime
.timedelta(
128 seconds
=i_hate_timezones
)
130 if timestamp
and datestamp
:
131 return time
.strftime("%Y-%m-%d %H:%M:%S ", t
)
133 return time
.strftime("%H:%M:%S ", t
)
135 return time
.strftime("%Y-%m-%d ", t
)
138 class StatusFormatter(object):
139 def __call__(self
, status
, options
):
140 return (u
"%s%s %s" %(
141 get_time_string(status
, options
),
142 status
['user']['screen_name'], status
['text']))
144 class AnsiStatusFormatter(object):
146 self
._colourMap
= ansi
.ColourMap()
148 def __call__(self
, status
, options
):
149 colour
= self
._colourMap
.colourFor(status
['user']['screen_name'])
150 return (u
"%s%s%s%s %s" %(
151 get_time_string(status
, options
),
152 ansi
.cmdColour(colour
), status
['user']['screen_name'],
153 ansi
.cmdReset(), status
['text']))
155 class VerboseStatusFormatter(object):
156 def __call__(self
, status
, options
):
157 return (u
"-- %s (%s) on %s\n%s\n" %(
158 status
['user']['screen_name'],
159 status
['user']['location'],
160 status
['created_at'],
163 class URLStatusFormatter(object):
164 urlmatch
= re
.compile(r
'https?://\S+')
165 def __call__(self
, status
, options
):
166 urls
= self
.urlmatch
.findall(status
['text'])
167 return u
'\n'.join(urls
) if urls
else ""
169 class AdminFormatter(object):
170 def __call__(self
, action
, user
):
171 user_str
= u
"%s (%s)" %(user
['screen_name'], user
['name'])
172 if action
== "follow":
173 return u
"You are now following %s.\n" %(user_str)
175 return u
"You are no longer following %s.\n" %(user_str)
177 class VerboseAdminFormatter(object):
178 def __call__(self
, action
, user
):
179 return(u
"-- %s: %s (%s): %s" % (
180 "Following" if action
== "follow" else "Leaving",
185 class SearchFormatter(object):
186 def __call__(self
, result
, options
):
188 get_time_string(result
, options
, "%a, %d %b %Y %H:%M:%S +0000"),
189 result
['from_user'], result
['text']))
191 class VerboseSearchFormatter(SearchFormatter
):
192 pass #Default to the regular one
194 class URLSearchFormatter(object):
195 urlmatch
= re
.compile(r
'https?://\S+')
196 def __call__(self
, result
, options
):
197 urls
= self
.urlmatch
.findall(result
['text'])
198 return u
'\n'.join(urls
) if urls
else ""
200 class AnsiSearchFormatter(object):
202 self
._colourMap
= ansi
.ColourMap()
204 def __call__(self
, result
, options
):
205 colour
= self
._colourMap
.colourFor(result
['from_user'])
206 return (u
"%s%s%s%s %s" %(
207 get_time_string(result
, options
, "%a, %d %b %Y %H:%M:%S +0000"),
208 ansi
.cmdColour(colour
), result
['from_user'],
209 ansi
.cmdReset(), result
['text']))
212 status_formatters
= {
213 'default': StatusFormatter
,
214 'verbose': VerboseStatusFormatter
,
215 'urls': URLStatusFormatter
,
216 'ansi': AnsiStatusFormatter
218 formatters
['status'] = status_formatters
221 'default': AdminFormatter
,
222 'verbose': VerboseAdminFormatter
,
223 'urls': AdminFormatter
,
224 'ansi': AdminFormatter
226 formatters
['admin'] = admin_formatters
228 search_formatters
= {
229 'default': SearchFormatter
,
230 'verbose': VerboseSearchFormatter
,
231 'urls': URLSearchFormatter
,
232 'ansi': AnsiSearchFormatter
234 formatters
['search'] = search_formatters
236 def get_formatter(action_type
, options
):
237 formatters_dict
= formatters
.get(action_type
)
238 if (not formatters_dict
):
240 "There was an error finding a class of formatters for your type (%s)"
242 f
= formatters_dict
.get(options
['format'])
245 "Unknown formatter '%s' for status actions" %(options
['format']))
248 class Action(object):
250 def ask(self
, subject
='perform this action', careful
=False):
252 Requests fromt he user using `raw_input` if `subject` should be
253 performed. When `careful`, the default answer is NO, otherwise YES.
254 Returns the user answer in the form `True` or `False`.
260 prompt
= 'You really want to %s %s? ' %(subject
, sample
)
262 answer
= raw_input(prompt
).lower()
264 return answer
in ('yes', 'y')
266 return answer
not in ('no', 'n')
268 print >>sys
.stderr
# Put Newline since Enter was never pressed
270 # Figure out why on OS X the raw_input keeps raising
271 # EOFError and is never able to reset and get more input
272 # Hint: Look at how IPython implements their console
278 def __call__(self
, twitter
, options
):
279 action
= actions
.get(options
['action'], NoSuchAction
)()
281 doAction
= lambda : action(twitter
, options
)
282 if (options
['refresh'] and isinstance(action
, StatusAction
)):
285 time
.sleep(options
['refresh_rate'])
288 except KeyboardInterrupt:
289 print >>sys
.stderr
, '\n[Keyboard Interrupt]'
292 class NoSuchActionError(Exception):
295 class NoSuchAction(Action
):
296 def __call__(self
, twitter
, options
):
297 raise NoSuchActionError("No such action: %s" %(options
['action']))
299 def printNicely(string
):
300 if sys
.stdout
.encoding
:
301 print string
.encode(sys
.stdout
.encoding
, 'replace')
303 print string
.encode('utf-8')
305 class StatusAction(Action
):
306 def __call__(self
, twitter
, options
):
307 statuses
= self
.getStatuses(twitter
, options
)
308 sf
= get_formatter('status', options
)
309 for status
in statuses
:
310 statusStr
= sf(status
, options
)
311 if statusStr
.strip():
312 printNicely(statusStr
)
314 class SearchAction(Action
):
315 def __call__(self
, twitter
, options
):
316 # We need to be pointing at search.twitter.com to work, and it is less
317 # tangly to do it here than in the main()
318 twitter
.domain
="search.twitter.com"
319 # We need to bypass the TwitterCall parameter encoding, so we
320 # don't encode the plus sign, so we have to encode it ourselves
321 query_string
= "+".join([quote(term
) for term
in options
['extra_args']])
322 twitter
.encoded_args
= "q=%s" %(query_string)
324 results
= twitter
.search()['results']
325 f
= get_formatter('search', options
)
326 for result
in results
:
327 resultStr
= f(result
, options
)
328 if resultStr
.strip():
329 printNicely(resultStr
)
331 class AdminAction(Action
):
332 def __call__(self
, twitter
, options
):
333 if not (options
['extra_args'] and options
['extra_args'][0]):
334 raise TwitterError("You need to specify a user (screen name)")
335 af
= get_formatter('admin', options
)
337 user
= self
.getUser(twitter
, options
['extra_args'][0])
338 except TwitterError
, e
:
339 print "There was a problem following or leaving the specified user."
340 print "You may be trying to follow a user you are already following;"
341 print "Leaving a user you are not currently following;"
342 print "Or the user may not exist."
347 printNicely(af(options
['action'], user
))
349 class FriendsAction(StatusAction
):
350 def getStatuses(self
, twitter
, options
):
351 return reversed(twitter
.statuses
.friends_timeline(count
=options
["length"]))
353 class PublicAction(StatusAction
):
354 def getStatuses(self
, twitter
, options
):
355 return reversed(twitter
.statuses
.public_timeline(count
=options
["length"]))
357 class RepliesAction(StatusAction
):
358 def getStatuses(self
, twitter
, options
):
359 return reversed(twitter
.statuses
.replies(count
=options
["length"]))
361 class FollowAction(AdminAction
):
362 def getUser(self
, twitter
, user
):
363 return twitter
.friendships
.create(id=user
)
365 class LeaveAction(AdminAction
):
366 def getUser(self
, twitter
, user
):
367 return twitter
.friendships
.destroy(id=user
)
369 class SetStatusAction(Action
):
370 def __call__(self
, twitter
, options
):
371 statusTxt
= (u
" ".join(options
['extra_args'])
372 if options
['extra_args']
373 else unicode(raw_input("message: ")))
374 status
= (statusTxt
.encode('utf8', 'replace'))
375 twitter
.statuses
.update(status
=status
)
377 class TwitterShell(Action
):
379 def render_prompt(self
, prompt
):
380 '''Parses the `prompt` string and returns the rendered version'''
381 prompt
= prompt
.strip("'").replace("\\'","'")
382 for colour
in ansi
.COLOURS_NAMED
:
383 if '[%s]' %(colour) in prompt
:
384 prompt
= prompt
.replace(
385 '[%s]' %(colour), ansi
.cmdColourNamed(colour
))
386 prompt
= prompt
.replace('[R]', ansi
.cmdReset())
389 def __call__(self
, twitter
, options
):
390 prompt
= self
.render_prompt(options
.get('prompt', 'twitter> '))
392 options
['action'] = ""
394 args
= raw_input(prompt
).split()
395 parse_args(args
, options
)
396 if not options
['action']:
398 elif options
['action'] == 'exit':
400 elif options
['action'] == 'shell':
401 print >>sys
.stderr
, 'Sorry Xzibit does not work here!'
403 elif options
['action'] == 'help':
404 print >>sys
.stderr
, '''\ntwitter> `action`\n
405 The Shell Accepts all the command line actions along with:
407 exit Leave the twitter shell (^D may also be used)
409 Full CMD Line help is appended below for your convinience.'''
410 Action()(twitter
, options
)
411 options
['action'] = ''
412 except NoSuchActionError
, e
:
413 print >>sys
.stderr
, e
414 except KeyboardInterrupt:
415 print >>sys
.stderr
, '\n[Keyboard Interrupt]'
418 leaving
= self
.ask(subject
='Leave')
420 print >>sys
.stderr
, 'Excellent!'
424 class HelpAction(Action
):
425 def __call__(self
, twitter
, options
):
429 'follow' : FollowAction
,
430 'friends' : FriendsAction
,
432 'leave' : LeaveAction
,
433 'public' : PublicAction
,
434 'replies' : RepliesAction
,
435 'search' : SearchAction
,
436 'set' : SetStatusAction
,
437 'shell' : TwitterShell
,
440 def loadConfig(filename
):
441 options
= dict(OPTIONS
)
442 if os
.path
.exists(filename
):
443 cp
= SafeConfigParser()
445 for option
in ('email', 'password', 'format', 'prompt'):
446 if cp
.has_option('twitter', option
):
447 options
[option
] = cp
.get('twitter', option
)
450 def main(args
=sys
.argv
[1:]):
453 parse_args(args
, arg_options
)
454 except GetoptError
, e
:
455 print >> sys
.stderr
, "I can't do that, %s." %(e)
459 config_options
= loadConfig(
460 arg_options
.get('config_filename') or OPTIONS
.get('config_filename'))
462 # Apply the various options in order, the most important applied last.
463 # Defaults first, then what's read from config file, then command-line
465 options
= dict(OPTIONS
)
466 for d
in config_options
, arg_options
:
467 for k
,v
in d
.items():
470 if options
['refresh'] and options
['action'] not in (
471 'friends', 'public', 'replies'):
472 print >> sys
.stderr
, "You can only refresh the friends, public, or replies actions."
473 print >> sys
.stderr
, "Use 'twitter -h' for help."
476 if options
['email'] and not options
['password']:
477 options
['password'] = getpass("Twitter password: ")
480 options
['email'], options
['password'], agent
=AGENT_STR
,
481 secure
=options
['secure'])
483 Action()(twitter
, options
)
484 except NoSuchActionError
, e
:
485 print >>sys
.stderr
, e
487 except TwitterError
, e
:
488 print >> sys
.stderr
, e
.args
[0]
489 print >> sys
.stderr
, "Use 'twitter -h' for help."