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