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