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