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