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