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