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