]> jfr.im git - z_archive/twitter.git/blame - twitter/cmdline.py
Fix list URL for twitter cmdline
[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)
0ea01db7 424 for status in statuses:
39a6f562 425 statusStr = sf(status, options)
0ea01db7 426 if statusStr.strip():
862cce81 427 printNicely(statusStr)
1c11e6d7 428
87be041f
WD
429class SearchAction(Action):
430 def __call__(self, twitter, options):
431 # We need to be pointing at search.twitter.com to work, and it is less
432 # tangly to do it here than in the main()
192f2893
CC
433 twitter.domain = "search.twitter.com"
434 twitter.uriparts = ()
fd2bc885
WD
435 # We need to bypass the TwitterCall parameter encoding, so we
436 # don't encode the plus sign, so we have to encode it ourselves
5a77e17a 437 query_string = "+".join(
7fd7f08a 438 [quote(term)
5a77e17a 439 for term in options['extra_args']])
fd2bc885 440
e15fbef3 441 results = twitter.search(q=query_string)['results']
87be041f
WD
442 f = get_formatter('search', options)
443 for result in results:
444 resultStr = f(result, options)
445 if resultStr.strip():
446 printNicely(resultStr)
a8b5ad3e 447
1c11e6d7 448class AdminAction(Action):
efa0ba89 449 def __call__(self, twitter, options):
ec894371 450 if not (options['extra_args'] and options['extra_args'][0]):
e02facc9 451 raise TwitterError("You need to specify a user (screen name)")
87be041f 452 af = get_formatter('admin', options)
e02facc9
MV
453 try:
454 user = self.getUser(twitter, options['extra_args'][0])
f7e63802
MV
455 except TwitterError as e:
456 print("There was a problem following or leaving the specified user.")
457 print("You may be trying to follow a user you are already following;")
458 print("Leaving a user you are not currently following;")
459 print("Or the user may not exist.")
460 print("Sorry.")
461 print()
462 print(e)
e02facc9 463 else:
862cce81 464 printNicely(af(options['action'], user))
efa0ba89 465
69851f4c
AL
466class ListsAction(StatusAction):
467 def getStatuses(self, twitter, options):
3c2b5e3e
MV
468 if not options['extra_args']:
469 raise TwitterError("Please provide a user to query for lists")
470
471 screen_name = options['extra_args'][0]
472
473 if not options['extra_args'][1:]:
0a24ba9e 474 lists = twitter.lists.list(screen_name=screen_name)
3c2b5e3e
MV
475 if not lists:
476 printNicely("This user has no lists.")
477 for list in lists:
69851f4c
AL
478 lf = get_formatter('lists', options)
479 printNicely(lf(list))
3c2b5e3e
MV
480 return []
481 else:
61904ece
MC
482 return reversed(twitter.lists.statuses(
483 owner_screen_name=screen_name, slug=options['extra_args'][1]))
3c2b5e3e
MV
484
485
486class MyListsAction(ListsAction):
487 def getStatuses(self, twitter, options):
488 screen_name = twitter.account.verify_credentials()['screen_name']
489 options['extra_args'].insert(0, screen_name)
490 return ListsAction.getStatuses(self, twitter, options)
491
69851f4c 492
0ea01db7 493class FriendsAction(StatusAction):
39a6f562 494 def getStatuses(self, twitter, options):
0a24ba9e 495 return reversed(twitter.statuses.home_timeline(count=options["length"]))
0ea01db7 496
9a9f7ae7 497class RepliesAction(StatusAction):
39a6f562 498 def getStatuses(self, twitter, options):
0a24ba9e 499 return reversed(twitter.statuses.mentions_timeline(count=options["length"]))
9a9f7ae7 500
1c11e6d7 501class FollowAction(AdminAction):
efa0ba89 502 def getUser(self, twitter, user):
565f59bb 503 return twitter.friendships.create(screen_name=user)
efa0ba89 504
1c11e6d7 505class LeaveAction(AdminAction):
efa0ba89 506 def getUser(self, twitter, user):
565f59bb 507 return twitter.friendships.destroy(screen_name=user)
1c11e6d7 508
0ea01db7 509class SetStatusAction(Action):
510 def __call__(self, twitter, options):
a87e8a6c 511 statusTxt = (" ".join(options['extra_args'])
05b85831 512 if options['extra_args']
f7e63802 513 else str(input("message: ")))
efa432c7
TN
514 replies = []
515 ptr = re.compile("@[\w_]+")
516 while statusTxt:
517 s = ptr.match(statusTxt)
518 if s and s.start() == 0:
519 replies.append(statusTxt[s.start():s.end()])
192f2893 520 statusTxt = statusTxt[s.end() + 1:]
efa432c7
TN
521 else:
522 break
523 replies = " ".join(replies)
524 if len(replies) >= 140:
525 # just go back
526 statusTxt = replies
527 replies = ""
528
6b3587a8
TN
529 splitted = []
530 while statusTxt:
efa432c7
TN
531 limit = 140 - len(replies)
532 if len(statusTxt) > limit:
533 end = string.rfind(statusTxt, ' ', 0, limit)
6b3587a8 534 else:
efa432c7 535 end = limit
192f2893 536 splitted.append(" ".join((replies, statusTxt[:end])))
6b3587a8
TN
537 statusTxt = statusTxt[end:]
538
8ec08295
TN
539 if options['invert_split']:
540 splitted.reverse()
6b3587a8
TN
541 for status in splitted:
542 twitter.statuses.update(status=status)
5251ea48 543
05b85831 544class TwitterShell(Action):
ec894371
MV
545
546 def render_prompt(self, prompt):
05b85831 547 '''Parses the `prompt` string and returns the rendered version'''
192f2893 548 prompt = prompt.strip("'").replace("\\'", "'")
05b85831 549 for colour in ansi.COLOURS_NAMED:
192f2893 550 if '[%s]' % (colour) in prompt:
05b85831 551 prompt = prompt.replace(
192f2893
CC
552 '[%s]' % (colour), ansiFormatter.cmdColourNamed(colour))
553 prompt = prompt.replace('[R]', ansiFormatter.cmdReset())
05b85831 554 return prompt
a8b5ad3e 555
05b85831
HN
556 def __call__(self, twitter, options):
557 prompt = self.render_prompt(options.get('prompt', 'twitter> '))
558 while True:
ec894371 559 options['action'] = ""
05b85831 560 try:
f7e63802 561 args = input(prompt).split()
05b85831
HN
562 parse_args(args, options)
563 if not options['action']:
564 continue
565 elif options['action'] == 'exit':
566 raise SystemExit(0)
567 elif options['action'] == 'shell':
f7e63802 568 print('Sorry Xzibit does not work here!', file=sys.stderr)
05b85831
HN
569 continue
570 elif options['action'] == 'help':
f7e63802 571 print('''\ntwitter> `action`\n
a8b5ad3e 572 The Shell Accepts all the command line actions along with:
05b85831 573
a8b5ad3e 574 exit Leave the twitter shell (^D may also be used)
05b85831 575
f7e63802 576 Full CMD Line help is appended below for your convinience.''', file=sys.stderr)
05b85831
HN
577 Action()(twitter, options)
578 options['action'] = ''
f7e63802
MV
579 except NoSuchActionError as e:
580 print(e, file=sys.stderr)
05b85831 581 except KeyboardInterrupt:
f7e63802 582 print('\n[Keyboard Interrupt]', file=sys.stderr)
05b85831 583 except EOFError:
f7e63802 584 print(file=sys.stderr)
05b85831
HN
585 leaving = self.ask(subject='Leave')
586 if not leaving:
f7e63802 587 print('Excellent!', file=sys.stderr)
05b85831
HN
588 else:
589 raise SystemExit(0)
590
a5e40197
MV
591class PythonPromptAction(Action):
592 def __call__(self, twitter, options):
593 try:
594 while True:
595 smrt_input(globals(), locals())
596 except EOFError:
597 pass
598
45688301
MV
599class HelpAction(Action):
600 def __call__(self, twitter, options):
f7e63802 601 print(__doc__)
45688301 602
086fc282
MV
603class DoNothingAction(Action):
604 def __call__(self, twitter, options):
605 pass
606
4f8b9215
MV
607class RateLimitStatus(Action):
608 def __call__(self, twitter, options):
9580a9df 609 rate = twitter.application.rate_limit_status()
4f8b9215 610 print("Remaining API requests: %s / %s (hourly limit)" % (rate['remaining_hits'], rate['hourly_limit']))
192f2893 611 print("Next reset in %ss (%s)" % (int(rate['reset_time_in_seconds'] - time.time()),
4f8b9215
MV
612 time.asctime(time.localtime(rate['reset_time_in_seconds']))))
613
5251ea48 614actions = {
086fc282 615 'authorize' : DoNothingAction,
05b85831
HN
616 'follow' : FollowAction,
617 'friends' : FriendsAction,
69851f4c 618 'list' : ListsAction,
3c2b5e3e 619 'mylist' : MyListsAction,
05b85831
HN
620 'help' : HelpAction,
621 'leave' : LeaveAction,
a5e40197 622 'pyprompt' : PythonPromptAction,
05b85831 623 'replies' : RepliesAction,
87be041f 624 'search' : SearchAction,
05b85831
HN
625 'set' : SetStatusAction,
626 'shell' : TwitterShell,
4f8b9215 627 'rate' : RateLimitStatus,
5251ea48 628}
629
21e3bd23 630def loadConfig(filename):
327e556b 631 options = dict(OPTIONS)
21e3bd23 632 if os.path.exists(filename):
633 cp = SafeConfigParser()
634 cp.read([filename])
086fc282 635 for option in ('format', 'prompt'):
327e556b
MV
636 if cp.has_option('twitter', option):
637 options[option] = cp.get('twitter', option)
8ec08295
TN
638 # process booleans
639 for option in ('invert_split',):
192f2893 640 if cp.has_option('twitter', option):
8ec08295 641 options[option] = cp.getboolean('twitter', option)
327e556b 642 return options
ae1d86aa 643
327e556b
MV
644def main(args=sys.argv[1:]):
645 arg_options = {}
44405280 646 try:
327e556b 647 parse_args(args, arg_options)
f7e63802 648 except GetoptError as e:
192f2893 649 print("I can't do that, %s." % (e), file=sys.stderr)
f7e63802 650 print(file=sys.stderr)
05b85831 651 raise SystemExit(1)
21e3bd23 652
7f22c021 653 config_path = os.path.expanduser(
327e556b 654 arg_options.get('config_filename') or OPTIONS.get('config_filename'))
7f22c021 655 config_options = loadConfig(config_path)
efa0ba89 656
327e556b
MV
657 # Apply the various options in order, the most important applied last.
658 # Defaults first, then what's read from config file, then command-line
659 # arguments.
660 options = dict(OPTIONS)
661 for d in config_options, arg_options:
192f2893 662 for k, v in list(d.items()):
327e556b 663 if v: options[k] = v
05b85831 664
e02facc9 665 if options['refresh'] and options['action'] not in (
0a24ba9e
MV
666 'friends', 'replies'):
667 print("You can only refresh the friends or replies actions.", file=sys.stderr)
f7e63802 668 print("Use 'twitter -h' for help.", file=sys.stderr)
086fc282 669 return 1
0a24ba9e 670
7f22c021
MV
671 oauth_filename = os.path.expanduser(options['oauth_filename'])
672
086fc282 673 if (options['action'] == 'authorize'
7f22c021 674 or not os.path.exists(oauth_filename)):
1b31d642
MV
675 oauth_dance(
676 "the Command-Line Tool", CONSUMER_KEY, CONSUMER_SECRET,
677 options['oauth_filename'])
0a24ba9e 678
192f2893
CC
679 global ansiFormatter
680 ansiFormatter = ansi.AnsiCmd(options["force-ansi"])
6c527e72 681
7f22c021 682 oauth_token, oauth_token_secret = read_token_file(oauth_filename)
8ddd8500 683
9a148ed1 684 twitter = Twitter(
086fc282
MV
685 auth=OAuth(
686 oauth_token, oauth_token_secret, CONSUMER_KEY, CONSUMER_SECRET),
1cc9ab0b 687 secure=options['secure'],
0a24ba9e 688 api_version='1.1',
9c07e547 689 domain='api.twitter.com')
086fc282 690
5251ea48 691 try:
05b85831 692 Action()(twitter, options)
f7e63802
MV
693 except NoSuchActionError as e:
694 print(e, file=sys.stderr)
05b85831 695 raise SystemExit(1)
f7e63802
MV
696 except TwitterError as e:
697 print(str(e), file=sys.stderr)
698 print("Use 'twitter -h' for help.", file=sys.stderr)
05b85831 699 raise SystemExit(1)