]> jfr.im git - z_archive/twitter.git/blame - twitter/cmdline.py
Merge pull request #178 from dkanygin/master
[z_archive/twitter.git] / twitter / cmdline.py
CommitLineData
3756d82e 1# encoding: utf-8
7364ea65 2"""
5251ea48 3USAGE:
7364ea65 4
5251ea48 5 twitter [action] [options]
6
086fc282 7
5251ea48 8ACTIONS:
086fc282 9 authorize authorize the command-line tool to interact with Twitter
527c550f 10 follow follow a user
5251ea48 11 friends get latest tweets from your friends (default action)
45688301 12 help print this help text that you are currently reading
527c550f
MV
13 leave stop following a user
14 list get list of a user's lists; give a list name to get
15 tweets from that list
16 mylist get list of your lists; give a list name to get tweets
17 from that list
7227ce91
MV
18 pyprompt start a Python prompt for interacting with the twitter
19 object directly
527c550f 20 replies get latest replies to you
6af9fa5f 21 search search twitter (Beware: octothorpe, escape it)
5251ea48 22 set set your twitter status
527c550f 23 shell login to the twitter shell
4f8b9215 24 rate get your current rate limit status (remaining API reqs)
5251ea48 25
086fc282 26
5251ea48 27OPTIONS:
28
0ea01db7 29 -r --refresh run this command forever, polling every once
30 in a while (default: every 5 minutes)
31 -R --refresh-rate <rate> set the refresh rate (in seconds)
32 -f --format <format> specify the output format for status updates
21e3bd23 33 -c --config <filename> read username and password from given config
39a6f562
MV
34 file (default ~/.twitter)
35 -l --length <count> specify number of status updates shown
36 (default: 20, max: 200)
37 -t --timestamp show time before status lines
527c550f
MV
38 -d --datestamp show date before status lines
39 --no-ssl use less-secure HTTP instead of HTTPS
7f1dd286 40 --oauth <filename> filename to read/store oauth credentials to
086fc282 41
0ea01db7 42FORMATS for the --format option
43
44 default one line per status
45 verbose multiple lines per status, more verbose status info
327e556b
MV
46 urls nothing but URLs
47 ansi ansi colour (rainbow mode)
05b85831 48
086fc282 49
21e3bd23 50CONFIG FILES
51
086fc282
MV
52 The config file should be placed in your home directory and be named .twitter.
53 It must contain a [twitter] header, and all the desired options you wish to
54 set, like so:
21e3bd23 55
56[twitter]
327e556b 57format: <desired_default_format_for_output>
05b85831 58prompt: <twitter_shell_prompt e.g. '[cyan]twitter[R]> '>
086fc282
MV
59
60 OAuth authentication tokens are stored in the file .twitter_oauth in your
61 home directory.
7364ea65 62"""
63
3930cc7b
MV
64from __future__ import print_function
65
77001ea7
MV
66try:
67 input = __builtins__['raw_input']
6990ead7 68except (AttributeError, KeyError):
77001ea7
MV
69 pass
70
71
192f2893
CC
72CONSUMER_KEY = 'uS6hO2sV6tDKIOeVjhnFnQ'
73CONSUMER_SECRET = 'MEYTOS97VvlHX7K1rwHPEqVpTSqZ71HtvoK4sVuYk'
6c527e72 74
5251ea48 75import sys
0ea01db7 76import time
f2a7ce46 77from getopt import gnu_getopt as getopt, GetoptError
f068ff42 78from getpass import getpass
0ea01db7 79import re
21e3bd23 80import os.path
4101b926 81import locale
6b3587a8 82import string
4101b926 83
3930cc7b
MV
84try:
85 from ConfigParser import SafeConfigParser
86except ImportError:
87 from configparser import ConfigParser as SafeConfigParser
a4b5e65b 88import datetime
3930cc7b
MV
89try:
90 from urllib.parse import quote
91except ImportError:
92 from urllib2 import quote
192f2893
CC
93try:
94 import HTMLParser
95except ImportError:
96 import html.parser as HTMLParser
97
6c527e72 98import webbrowser
5251ea48 99
f7e63802
MV
100from .api import Twitter, TwitterError
101from .oauth import OAuth, write_token_file, read_token_file
102from .oauth_dance import oauth_dance
103from . import ansi
098660ce 104from .util import smrt_input, printNicely
5251ea48 105
327e556b 106OPTIONS = {
5251ea48 107 'action': 'friends',
0ea01db7 108 'refresh': False,
109 'refresh_rate': 600,
110 'format': 'default',
05b85831 111 'prompt': '[cyan]twitter[R]> ',
faf29ede
AM
112 'config_filename': os.environ.get('HOME', os.environ.get('USERPROFILE', '')) + os.sep + '.twitter',
113 'oauth_filename': os.environ.get('HOME', os.environ.get('USERPROFILE', '')) + os.sep + '.twitter_oauth',
39a6f562
MV
114 'length': 20,
115 'timestamp': False,
116 'datestamp': False,
9a148ed1 117 'extra_args': [],
6c527e72 118 'secure': True,
8ec08295 119 'invert_split': False,
192f2893 120 'force-ansi': False,
5251ea48 121}
122
192f2893
CC
123gHtmlParser = HTMLParser.HTMLParser()
124hashtagRe = re.compile(r'(?P<hashtag>#\S+)')
125profileRe = re.compile(r'(?P<profile>\@\S+)')
126ansiFormatter = ansi.AnsiCmd(False)
127
5251ea48 128def parse_args(args, options):
7f1dd286 129 long_opts = ['help', 'format=', 'refresh', 'oauth=',
8ddd8500 130 'refresh-rate=', 'config=', 'length=', 'timestamp',
192f2893 131 'datestamp', 'no-ssl', 'force-ansi']
39a6f562 132 short_opts = "e:p:f:h?rR:c:l:td"
8ddd8500 133 opts, extra_args = getopt(args, short_opts, long_opts)
d2f3bdc9
MV
134 if extra_args and hasattr(extra_args[0], 'decode'):
135 extra_args = [arg.decode(locale.getpreferredencoding())
136 for arg in extra_args]
efa0ba89 137
5251ea48 138 for opt, arg in opts:
086fc282 139 if opt in ('-f', '--format'):
0ea01db7 140 options['format'] = arg
141 elif opt in ('-r', '--refresh'):
142 options['refresh'] = True
143 elif opt in ('-R', '--refresh-rate'):
144 options['refresh_rate'] = int(arg)
39a6f562
MV
145 elif opt in ('-l', '--length'):
146 options["length"] = int(arg)
147 elif opt in ('-t', '--timestamp'):
148 options["timestamp"] = True
149 elif opt in ('-d', '--datestamp'):
150 options["datestamp"] = True
5251ea48 151 elif opt in ('-?', '-h', '--help'):
05b85831 152 options['action'] = 'help'
21e3bd23 153 elif opt in ('-c', '--config'):
154 options['config_filename'] = arg
9a148ed1
MV
155 elif opt == '--no-ssl':
156 options['secure'] = False
7f1dd286
MV
157 elif opt == '--oauth':
158 options['oauth_filename'] = arg
192f2893
CC
159 elif opt == '--force-ansi':
160 options['force-ansi'] = True
efa0ba89 161
05b85831 162 if extra_args and not ('action' in options and options['action'] == 'help'):
ae1d86aa 163 options['action'] = extra_args[0]
164 options['extra_args'] = extra_args[1:]
a8b5ad3e 165
87be041f 166def get_time_string(status, options, format="%a %b %d %H:%M:%S +0000 %Y"):
39a6f562
MV
167 timestamp = options["timestamp"]
168 datestamp = options["datestamp"]
87be041f 169 t = time.strptime(status['created_at'], format)
a4b5e65b
MV
170 i_hate_timezones = time.timezone
171 if (time.daylight):
172 i_hate_timezones = time.altzone
173 dt = datetime.datetime(*t[:-3]) - datetime.timedelta(
174 seconds=i_hate_timezones)
175 t = dt.timetuple()
39a6f562
MV
176 if timestamp and datestamp:
177 return time.strftime("%Y-%m-%d %H:%M:%S ", t)
178 elif timestamp:
179 return time.strftime("%H:%M:%S ", t)
180 elif datestamp:
181 return time.strftime("%Y-%m-%d ", t)
8ddd8500 182 return ""
5251ea48 183
192f2893
CC
184def reRepl(m):
185 ansiTypes = {
186 'clear': ansiFormatter.cmdReset(),
187 'hashtag': ansiFormatter.cmdBold(),
188 'profile': ansiFormatter.cmdUnderline(),
189 }
190
191 s = None
192 try:
193 mkey = m.lastgroup
194 if m.group(mkey):
195 s = '%s%s%s' % (ansiTypes[mkey], m.group(mkey), ansiTypes['clear'])
196 except IndexError:
197 pass
198 return s
199
200def replaceInStatus(status):
201 txt = gHtmlParser.unescape(status)
202 txt = re.sub(hashtagRe, reRepl, txt)
203 txt = re.sub(profileRe, reRepl, txt)
204 return txt
205
5251ea48 206class StatusFormatter(object):
a55c0ac8 207 def __call__(self, status, options):
192f2893 208 return ("%s%s %s" % (
39a6f562 209 get_time_string(status, options),
192f2893 210 status['user']['screen_name'], gHtmlParser.unescape(status['text'])))
5251ea48 211
0b9960a3
MV
212class AnsiStatusFormatter(object):
213 def __init__(self):
214 self._colourMap = ansi.ColourMap()
a8b5ad3e 215
39a6f562 216 def __call__(self, status, options):
0b9960a3 217 colour = self._colourMap.colourFor(status['user']['screen_name'])
192f2893 218 return ("%s%s%s%s %s" % (
39a6f562 219 get_time_string(status, options),
192f2893
CC
220 ansiFormatter.cmdColour(colour), status['user']['screen_name'],
221 ansiFormatter.cmdReset(), replaceInStatus(status['text'])))
05b85831 222
f068ff42 223class VerboseStatusFormatter(object):
39a6f562 224 def __call__(self, status, options):
192f2893 225 return ("-- %s (%s) on %s\n%s\n" % (
f068ff42 226 status['user']['screen_name'],
227 status['user']['location'],
228 status['created_at'],
192f2893 229 gHtmlParser.unescape(status['text'])))
f068ff42 230
0ea01db7 231class URLStatusFormatter(object):
232 urlmatch = re.compile(r'https?://\S+')
39a6f562 233 def __call__(self, status, options):
0ea01db7 234 urls = self.urlmatch.findall(status['text'])
f7e63802 235 return '\n'.join(urls) if urls else ""
0ea01db7 236
3c2b5e3e 237
69851f4c
AL
238class ListsFormatter(object):
239 def __call__(self, list):
240 if list['description']:
f7e63802 241 list_str = "%-30s (%s)" % (list['name'], list['description'])
69851f4c 242 else:
f7e63802
MV
243 list_str = "%-30s" % (list['name'])
244 return "%s\n" % list_str
69851f4c
AL
245
246class ListsVerboseFormatter(object):
247 def __call__(self, list):
f7e63802 248 list_str = "%-30s\n description: %s\n members: %s\n mode:%s\n" % (list['name'], list['description'], list['member_count'], list['mode'])
69851f4c
AL
249 return list_str
250
251class AnsiListsFormatter(object):
252 def __init__(self):
253 self._colourMap = ansi.ColourMap()
254
255 def __call__(self, list):
256 colour = self._colourMap.colourFor(list['name'])
192f2893
CC
257 return ("%s%-15s%s %s" % (
258 ansiFormatter.cmdColour(colour), list['name'],
259 ansiFormatter.cmdReset(), list['description']))
69851f4c
AL
260
261
1c11e6d7 262class AdminFormatter(object):
efa0ba89 263 def __call__(self, action, user):
192f2893 264 user_str = "%s (%s)" % (user['screen_name'], user['name'])
da45d039 265 if action == "follow":
192f2893 266 return "You are now following %s.\n" % (user_str)
da45d039 267 else:
192f2893 268 return "You are no longer following %s.\n" % (user_str)
efa0ba89 269
1c11e6d7 270class VerboseAdminFormatter(object):
efa0ba89 271 def __call__(self, action, user):
f7e63802 272 return("-- %s: %s (%s): %s" % (
05b85831
HN
273 "Following" if action == "follow" else "Leaving",
274 user['screen_name'],
efa0ba89
MV
275 user['name'],
276 user['url']))
277
87be041f
WD
278class SearchFormatter(object):
279 def __call__(self, result, options):
192f2893 280 return("%s%s %s" % (
87be041f
WD
281 get_time_string(result, options, "%a, %d %b %Y %H:%M:%S +0000"),
282 result['from_user'], result['text']))
283
284class VerboseSearchFormatter(SearchFormatter):
192f2893 285 pass # Default to the regular one
a8b5ad3e 286
87be041f
WD
287class URLSearchFormatter(object):
288 urlmatch = re.compile(r'https?://\S+')
289 def __call__(self, result, options):
290 urls = self.urlmatch.findall(result['text'])
f7e63802 291 return '\n'.join(urls) if urls else ""
87be041f
WD
292
293class AnsiSearchFormatter(object):
294 def __init__(self):
295 self._colourMap = ansi.ColourMap()
a8b5ad3e 296
87be041f
WD
297 def __call__(self, result, options):
298 colour = self._colourMap.colourFor(result['from_user'])
192f2893 299 return ("%s%s%s%s %s" % (
87be041f 300 get_time_string(result, options, "%a, %d %b %Y %H:%M:%S +0000"),
192f2893
CC
301 ansiFormatter.cmdColour(colour), result['from_user'],
302 ansiFormatter.cmdReset(), result['text']))
87be041f 303
5a77e17a
MV
304_term_encoding = None
305def get_term_encoding():
306 global _term_encoding
307 if not _term_encoding:
308 lang = os.getenv('LANG', 'unknown.UTF-8').split('.')
309 if lang[1:]:
310 _term_encoding = lang[1]
311 else:
312 _term_encoding = 'UTF-8'
313 return _term_encoding
314
87be041f 315formatters = {}
1c11e6d7 316status_formatters = {
0ea01db7 317 'default': StatusFormatter,
318 'verbose': VerboseStatusFormatter,
0b9960a3
MV
319 'urls': URLStatusFormatter,
320 'ansi': AnsiStatusFormatter
05b85831 321}
87be041f 322formatters['status'] = status_formatters
1c11e6d7
WD
323
324admin_formatters = {
efa0ba89
MV
325 'default': AdminFormatter,
326 'verbose': VerboseAdminFormatter,
327e556b
MV
327 'urls': AdminFormatter,
328 'ansi': AdminFormatter
1c11e6d7 329}
87be041f 330formatters['admin'] = admin_formatters
efa0ba89 331
87be041f
WD
332search_formatters = {
333 'default': SearchFormatter,
334 'verbose': VerboseSearchFormatter,
335 'urls': URLSearchFormatter,
336 'ansi': AnsiSearchFormatter
337}
338formatters['search'] = search_formatters
339
69851f4c
AL
340lists_formatters = {
341 'default': ListsFormatter,
342 'verbose': ListsVerboseFormatter,
343 'urls': None,
344 'ansi': AnsiListsFormatter
345}
346formatters['lists'] = lists_formatters
347
87be041f
WD
348def get_formatter(action_type, options):
349 formatters_dict = formatters.get(action_type)
350 if (not formatters_dict):
a8b5ad3e 351 raise TwitterError(
87be041f 352 "There was an error finding a class of formatters for your type (%s)"
192f2893 353 % (action_type))
87be041f
WD
354 f = formatters_dict.get(options['format'])
355 if (not f):
efa0ba89 356 raise TwitterError(
192f2893 357 "Unknown formatter '%s' for status actions" % (options['format']))
87be041f 358 return f()
efa0ba89 359
0ea01db7 360class Action(object):
ec894371
MV
361
362 def ask(self, subject='perform this action', careful=False):
05b85831
HN
363 '''
364 Requests fromt he user using `raw_input` if `subject` should be
365 performed. When `careful`, the default answer is NO, otherwise YES.
366 Returns the user answer in the form `True` or `False`.
367 '''
f47ab046
MV
368 sample = '(y/N)'
369 if not careful:
370 sample = '(Y/n)'
a8b5ad3e 371
192f2893 372 prompt = 'You really want to %s %s? ' % (subject, sample)
05b85831 373 try:
f7e63802 374 answer = input(prompt).lower()
05b85831 375 if careful:
f47ab046 376 return answer in ('yes', 'y')
05b85831 377 else:
f47ab046 378 return answer not in ('no', 'n')
05b85831 379 except EOFError:
192f2893 380 print(file=sys.stderr) # Put Newline since Enter was never pressed
05b85831
HN
381 # TODO:
382 # Figure out why on OS X the raw_input keeps raising
383 # EOFError and is never able to reset and get more input
384 # Hint: Look at how IPython implements their console
f47ab046
MV
385 default = True
386 if careful:
387 default = False
05b85831 388 return default
a8b5ad3e 389
05b85831
HN
390 def __call__(self, twitter, options):
391 action = actions.get(options['action'], NoSuchAction)()
392 try:
393 doAction = lambda : action(twitter, options)
394 if (options['refresh'] and isinstance(action, StatusAction)):
395 while True:
396 doAction()
a2396a3d 397 sys.stdout.flush()
05b85831
HN
398 time.sleep(options['refresh_rate'])
399 else:
400 doAction()
401 except KeyboardInterrupt:
f7e63802 402 print('\n[Keyboard Interrupt]', file=sys.stderr)
05b85831
HN
403 pass
404
405class NoSuchActionError(Exception):
0ea01db7 406 pass
407
408class NoSuchAction(Action):
409 def __call__(self, twitter, options):
192f2893 410 raise NoSuchActionError("No such action: %s" % (options['action']))
0ea01db7 411
412class StatusAction(Action):
413 def __call__(self, twitter, options):
39a6f562 414 statuses = self.getStatuses(twitter, options)
87be041f 415 sf = get_formatter('status', options)
0ea01db7 416 for status in statuses:
39a6f562 417 statusStr = sf(status, options)
0ea01db7 418 if statusStr.strip():
862cce81 419 printNicely(statusStr)
1c11e6d7 420
87be041f
WD
421class SearchAction(Action):
422 def __call__(self, twitter, options):
423 # We need to be pointing at search.twitter.com to work, and it is less
424 # tangly to do it here than in the main()
192f2893
CC
425 twitter.domain = "search.twitter.com"
426 twitter.uriparts = ()
fd2bc885
WD
427 # We need to bypass the TwitterCall parameter encoding, so we
428 # don't encode the plus sign, so we have to encode it ourselves
5a77e17a 429 query_string = "+".join(
7fd7f08a 430 [quote(term)
5a77e17a 431 for term in options['extra_args']])
fd2bc885 432
e15fbef3 433 results = twitter.search(q=query_string)['results']
87be041f
WD
434 f = get_formatter('search', options)
435 for result in results:
436 resultStr = f(result, options)
437 if resultStr.strip():
438 printNicely(resultStr)
a8b5ad3e 439
1c11e6d7 440class AdminAction(Action):
efa0ba89 441 def __call__(self, twitter, options):
ec894371 442 if not (options['extra_args'] and options['extra_args'][0]):
e02facc9 443 raise TwitterError("You need to specify a user (screen name)")
87be041f 444 af = get_formatter('admin', options)
e02facc9
MV
445 try:
446 user = self.getUser(twitter, options['extra_args'][0])
f7e63802
MV
447 except TwitterError as e:
448 print("There was a problem following or leaving the specified user.")
449 print("You may be trying to follow a user you are already following;")
450 print("Leaving a user you are not currently following;")
451 print("Or the user may not exist.")
452 print("Sorry.")
453 print()
454 print(e)
e02facc9 455 else:
862cce81 456 printNicely(af(options['action'], user))
efa0ba89 457
69851f4c
AL
458class ListsAction(StatusAction):
459 def getStatuses(self, twitter, options):
3c2b5e3e
MV
460 if not options['extra_args']:
461 raise TwitterError("Please provide a user to query for lists")
462
463 screen_name = options['extra_args'][0]
464
465 if not options['extra_args'][1:]:
0a24ba9e 466 lists = twitter.lists.list(screen_name=screen_name)
3c2b5e3e
MV
467 if not lists:
468 printNicely("This user has no lists.")
469 for list in lists:
69851f4c
AL
470 lf = get_formatter('lists', options)
471 printNicely(lf(list))
3c2b5e3e
MV
472 return []
473 else:
474 return reversed(twitter.user.lists.list.statuses(
475 user=screen_name, list=options['extra_args'][1]))
476
477
478class MyListsAction(ListsAction):
479 def getStatuses(self, twitter, options):
480 screen_name = twitter.account.verify_credentials()['screen_name']
481 options['extra_args'].insert(0, screen_name)
482 return ListsAction.getStatuses(self, twitter, options)
483
69851f4c 484
0ea01db7 485class FriendsAction(StatusAction):
39a6f562 486 def getStatuses(self, twitter, options):
0a24ba9e 487 return reversed(twitter.statuses.home_timeline(count=options["length"]))
0ea01db7 488
9a9f7ae7 489class RepliesAction(StatusAction):
39a6f562 490 def getStatuses(self, twitter, options):
0a24ba9e 491 return reversed(twitter.statuses.mentions_timeline(count=options["length"]))
9a9f7ae7 492
1c11e6d7 493class FollowAction(AdminAction):
efa0ba89 494 def getUser(self, twitter, user):
70955aae 495 return twitter.friendships.create(id=user)
efa0ba89 496
1c11e6d7 497class LeaveAction(AdminAction):
efa0ba89 498 def getUser(self, twitter, user):
70955aae 499 return twitter.friendships.destroy(id=user)
1c11e6d7 500
0ea01db7 501class SetStatusAction(Action):
502 def __call__(self, twitter, options):
a87e8a6c 503 statusTxt = (" ".join(options['extra_args'])
05b85831 504 if options['extra_args']
f7e63802 505 else str(input("message: ")))
efa432c7
TN
506 replies = []
507 ptr = re.compile("@[\w_]+")
508 while statusTxt:
509 s = ptr.match(statusTxt)
510 if s and s.start() == 0:
511 replies.append(statusTxt[s.start():s.end()])
192f2893 512 statusTxt = statusTxt[s.end() + 1:]
efa432c7
TN
513 else:
514 break
515 replies = " ".join(replies)
516 if len(replies) >= 140:
517 # just go back
518 statusTxt = replies
519 replies = ""
520
6b3587a8
TN
521 splitted = []
522 while statusTxt:
efa432c7
TN
523 limit = 140 - len(replies)
524 if len(statusTxt) > limit:
525 end = string.rfind(statusTxt, ' ', 0, limit)
6b3587a8 526 else:
efa432c7 527 end = limit
192f2893 528 splitted.append(" ".join((replies, statusTxt[:end])))
6b3587a8
TN
529 statusTxt = statusTxt[end:]
530
8ec08295
TN
531 if options['invert_split']:
532 splitted.reverse()
6b3587a8
TN
533 for status in splitted:
534 twitter.statuses.update(status=status)
5251ea48 535
05b85831 536class TwitterShell(Action):
ec894371
MV
537
538 def render_prompt(self, prompt):
05b85831 539 '''Parses the `prompt` string and returns the rendered version'''
192f2893 540 prompt = prompt.strip("'").replace("\\'", "'")
05b85831 541 for colour in ansi.COLOURS_NAMED:
192f2893 542 if '[%s]' % (colour) in prompt:
05b85831 543 prompt = prompt.replace(
192f2893
CC
544 '[%s]' % (colour), ansiFormatter.cmdColourNamed(colour))
545 prompt = prompt.replace('[R]', ansiFormatter.cmdReset())
05b85831 546 return prompt
a8b5ad3e 547
05b85831
HN
548 def __call__(self, twitter, options):
549 prompt = self.render_prompt(options.get('prompt', 'twitter> '))
550 while True:
ec894371 551 options['action'] = ""
05b85831 552 try:
f7e63802 553 args = input(prompt).split()
05b85831
HN
554 parse_args(args, options)
555 if not options['action']:
556 continue
557 elif options['action'] == 'exit':
558 raise SystemExit(0)
559 elif options['action'] == 'shell':
f7e63802 560 print('Sorry Xzibit does not work here!', file=sys.stderr)
05b85831
HN
561 continue
562 elif options['action'] == 'help':
f7e63802 563 print('''\ntwitter> `action`\n
a8b5ad3e 564 The Shell Accepts all the command line actions along with:
05b85831 565
a8b5ad3e 566 exit Leave the twitter shell (^D may also be used)
05b85831 567
f7e63802 568 Full CMD Line help is appended below for your convinience.''', file=sys.stderr)
05b85831
HN
569 Action()(twitter, options)
570 options['action'] = ''
f7e63802
MV
571 except NoSuchActionError as e:
572 print(e, file=sys.stderr)
05b85831 573 except KeyboardInterrupt:
f7e63802 574 print('\n[Keyboard Interrupt]', file=sys.stderr)
05b85831 575 except EOFError:
f7e63802 576 print(file=sys.stderr)
05b85831
HN
577 leaving = self.ask(subject='Leave')
578 if not leaving:
f7e63802 579 print('Excellent!', file=sys.stderr)
05b85831
HN
580 else:
581 raise SystemExit(0)
582
a5e40197
MV
583class PythonPromptAction(Action):
584 def __call__(self, twitter, options):
585 try:
586 while True:
587 smrt_input(globals(), locals())
588 except EOFError:
589 pass
590
45688301
MV
591class HelpAction(Action):
592 def __call__(self, twitter, options):
f7e63802 593 print(__doc__)
45688301 594
086fc282
MV
595class DoNothingAction(Action):
596 def __call__(self, twitter, options):
597 pass
598
4f8b9215
MV
599class RateLimitStatus(Action):
600 def __call__(self, twitter, options):
601 rate = twitter.account.rate_limit_status()
602 print("Remaining API requests: %s / %s (hourly limit)" % (rate['remaining_hits'], rate['hourly_limit']))
192f2893 603 print("Next reset in %ss (%s)" % (int(rate['reset_time_in_seconds'] - time.time()),
4f8b9215
MV
604 time.asctime(time.localtime(rate['reset_time_in_seconds']))))
605
5251ea48 606actions = {
086fc282 607 'authorize' : DoNothingAction,
05b85831
HN
608 'follow' : FollowAction,
609 'friends' : FriendsAction,
69851f4c 610 'list' : ListsAction,
3c2b5e3e 611 'mylist' : MyListsAction,
05b85831
HN
612 'help' : HelpAction,
613 'leave' : LeaveAction,
a5e40197 614 'pyprompt' : PythonPromptAction,
05b85831 615 'replies' : RepliesAction,
87be041f 616 'search' : SearchAction,
05b85831
HN
617 'set' : SetStatusAction,
618 'shell' : TwitterShell,
4f8b9215 619 'rate' : RateLimitStatus,
5251ea48 620}
621
21e3bd23 622def loadConfig(filename):
327e556b 623 options = dict(OPTIONS)
21e3bd23 624 if os.path.exists(filename):
625 cp = SafeConfigParser()
626 cp.read([filename])
086fc282 627 for option in ('format', 'prompt'):
327e556b
MV
628 if cp.has_option('twitter', option):
629 options[option] = cp.get('twitter', option)
8ec08295
TN
630 # process booleans
631 for option in ('invert_split',):
192f2893 632 if cp.has_option('twitter', option):
8ec08295 633 options[option] = cp.getboolean('twitter', option)
327e556b 634 return options
ae1d86aa 635
327e556b
MV
636def main(args=sys.argv[1:]):
637 arg_options = {}
44405280 638 try:
327e556b 639 parse_args(args, arg_options)
f7e63802 640 except GetoptError as e:
192f2893 641 print("I can't do that, %s." % (e), file=sys.stderr)
f7e63802 642 print(file=sys.stderr)
05b85831 643 raise SystemExit(1)
21e3bd23 644
7f22c021 645 config_path = os.path.expanduser(
327e556b 646 arg_options.get('config_filename') or OPTIONS.get('config_filename'))
7f22c021 647 config_options = loadConfig(config_path)
efa0ba89 648
327e556b
MV
649 # Apply the various options in order, the most important applied last.
650 # Defaults first, then what's read from config file, then command-line
651 # arguments.
652 options = dict(OPTIONS)
653 for d in config_options, arg_options:
192f2893 654 for k, v in list(d.items()):
327e556b 655 if v: options[k] = v
05b85831 656
e02facc9 657 if options['refresh'] and options['action'] not in (
0a24ba9e
MV
658 'friends', 'replies'):
659 print("You can only refresh the friends or replies actions.", file=sys.stderr)
f7e63802 660 print("Use 'twitter -h' for help.", file=sys.stderr)
086fc282 661 return 1
0a24ba9e 662
7f22c021
MV
663 oauth_filename = os.path.expanduser(options['oauth_filename'])
664
086fc282 665 if (options['action'] == 'authorize'
7f22c021 666 or not os.path.exists(oauth_filename)):
1b31d642
MV
667 oauth_dance(
668 "the Command-Line Tool", CONSUMER_KEY, CONSUMER_SECRET,
669 options['oauth_filename'])
0a24ba9e 670
192f2893
CC
671 global ansiFormatter
672 ansiFormatter = ansi.AnsiCmd(options["force-ansi"])
6c527e72 673
7f22c021 674 oauth_token, oauth_token_secret = read_token_file(oauth_filename)
8ddd8500 675
9a148ed1 676 twitter = Twitter(
086fc282
MV
677 auth=OAuth(
678 oauth_token, oauth_token_secret, CONSUMER_KEY, CONSUMER_SECRET),
1cc9ab0b 679 secure=options['secure'],
0a24ba9e 680 api_version='1.1',
9c07e547 681 domain='api.twitter.com')
086fc282 682
5251ea48 683 try:
05b85831 684 Action()(twitter, options)
f7e63802
MV
685 except NoSuchActionError as e:
686 print(e, file=sys.stderr)
05b85831 687 raise SystemExit(1)
f7e63802
MV
688 except TwitterError as e:
689 print(str(e), file=sys.stderr)
690 print("Use 'twitter -h' for help.", file=sys.stderr)
05b85831 691 raise SystemExit(1)