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