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