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