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