twurl 0.6.0

Sign up to get free protection for your applications and to get access to all the features.
data/COPYING ADDED
@@ -0,0 +1,18 @@
1
+ # Copyright (c) 2009 Marcel Molina <marcel@twitter.com>, Twitter, Inc.
2
+ #
3
+ # Permission is hereby granted, free of charge, to any person obtaining a copy of
4
+ # this software and associated documentation files (the "Software"), to deal in the
5
+ # Software without restriction, including without limitation the rights to use,
6
+ # copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
7
+ # Software, and to permit persons to whom the Software is furnished to do so,
8
+ # subject to the following conditions:
9
+ #
10
+ # The above copyright notice and this permission notice shall be included in all
11
+ # copies or substantial portions of the Software.
12
+ #
13
+ # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
14
+ # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
15
+ # FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
16
+ # COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
17
+ # AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
18
+ # WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/INSTALL ADDED
@@ -0,0 +1,18 @@
1
+ +-----------------------+
2
+ | Install with RubyGems |
3
+ +-----------------------+
4
+
5
+ sudo gem i twurl --source http://rubygems.org
6
+
7
+ +---------------------+
8
+ | Install from source |
9
+ +---------------------+
10
+
11
+ rake dist:gem
12
+ sudo gem i pkg/twurl*gem
13
+
14
+ +--------------+
15
+ | Dependencies |
16
+ +--------------+
17
+
18
+ sudo gem i oauth
data/README ADDED
@@ -0,0 +1,119 @@
1
+ +-------+
2
+ | Twurl |
3
+ +-------+
4
+
5
+ Twurl is like curl, but tailored specifically for the Twitter API.
6
+ It knows how to grant an access token to a client application for
7
+ a specified user and then sign all requests with that access token.
8
+
9
+ It also provides other development and debugging conveniences such
10
+ as defining aliases for common requests, as well as support for
11
+ multiple access tokens to easily switch between different client
12
+ applications and Twitter accounts.
13
+
14
+ +-----------------+
15
+ | Getting Started |
16
+ +-----------------+
17
+
18
+ The first thing you have to do is register an OAuth application
19
+ to get a consumer key and secret.
20
+
21
+ http://dev.twitter.com/apps/new
22
+
23
+ When you have your consumer key and its secret you authorize
24
+ your Twitter account to make API requests with your consumer key
25
+ and secret.
26
+
27
+ % twurl authorize --consumer-key the_key \
28
+ --consumer-secret the_secret
29
+
30
+ This will return an URL that you should open up in your browser.
31
+ Authenticate to Twitter, and then enter the returned PIN back into
32
+ the terminal. Assuming all that works well, you will beauthorized
33
+ to make requests with the API. Twurl will tell you as much.
34
+
35
+ If your consumer application has xAuth enabled, then you can use
36
+ a variant of the above
37
+
38
+ % twurl authorize -u username -p password \
39
+ --consumer-key the_key \
40
+ --consumer-secret the_secret
41
+
42
+ And, again assuming your username, password, key and secret is
43
+ correct, will authorize you in one step.
44
+
45
+ +-----------------+
46
+ | Making Requests |
47
+ +-----------------+
48
+
49
+ The simplest request just requires that you specify the path you
50
+ want to request.
51
+
52
+ % twurl /1/statuses/home_timeline.xml
53
+
54
+ Similar to curl, a GET request is performed by default.
55
+
56
+ You can implicitly perform a POST request by passing the -d option,
57
+ which specifies POST parameters.
58
+
59
+ % twurl -d 'status=Testing twurl' /1/statuses/update.xml
60
+
61
+ You can explicitly specify what request method to perform with
62
+ the -X (or --request-method) option.
63
+
64
+ % twurl -X DELETE /1/statuses/destroy/123456.xml
65
+
66
+ +------------------+
67
+ | Creating aliases |
68
+ +------------------+
69
+
70
+ % twurl alias h /1/statuses/home_timeline.xml
71
+
72
+ You can then use "h" in place of the full path.
73
+
74
+ % twurl h
75
+
76
+ Paths that require additional options such as request parameters for example can
77
+ be used with aliases the same as with full explicit paths, just as you might
78
+ expect.
79
+
80
+ % twurl alias tweet /1/statuses/update.xml
81
+ % twurl tweet -d "status=Aliases in twurl are convenient"
82
+
83
+ +-------------------------------+
84
+ | Changing your default profile |
85
+ +-------------------------------+
86
+
87
+ The first time you authorize a client application to make requests on behalf of your account, twurl stores your access token information in its .twurlrc file. Subsequent requests will use this profile as the default profile. You can use the 'accounts' subcommand to see what client applications have been authorized for what user names:
88
+
89
+ % twurl accounts
90
+ noradio
91
+ HQsAGcBm5MQT4n6j7qVJw
92
+ hhC7Koy2zRsTZvQh1hVlSA (default)
93
+ testiverse
94
+ guT9RsJbNQgVe6AwoY9BA
95
+
96
+ Notice that one of those consumer keys is marked as the default. To change the default use the 'set' subcommand, passing then either just the username, if it's unambiguous, or the username and consumer key pair if it isn't unambiguous:
97
+
98
+ % twurl set default testiverse
99
+ % twurl accounts
100
+ noradio
101
+ HQsAGcBm5MQT4n6j7qVJw
102
+ hhC7Koy2zRsTZvQh1hVlSA
103
+ testiverse
104
+ guT9RsJbNQgVe6AwoY9BA (default)
105
+
106
+ % twurl set default noradio HQsAGcBm5MQT4n6j7qVJw
107
+ % twurl accounts
108
+ noradio
109
+ HQsAGcBm5MQT4n6j7qVJw (default)
110
+ hhC7Koy2zRsTZvQh1hVlSA
111
+ testiverse
112
+ guT9RsJbNQgVe6AwoY9BA
113
+
114
+ +--------------+
115
+ | Contributors |
116
+ +--------------+
117
+
118
+ Marcel Molina <marcel@twitter.com> / @noradio
119
+ Raffi Krikorian <raffi@twitter.com> / @raffi
data/Rakefile ADDED
@@ -0,0 +1,72 @@
1
+ require 'rubygems'
2
+ require 'rake'
3
+ require 'rake/testtask'
4
+ require 'rake/gempackagetask'
5
+
6
+ require File.dirname(__FILE__) + '/lib/twurl'
7
+
8
+ library_root = File.dirname(__FILE__)
9
+
10
+ task :default => :test
11
+
12
+ Rake::TestTask.new do |test|
13
+ test.pattern = 'test/*_test.rb'
14
+ test.verbose = true
15
+ end
16
+
17
+ namespace :test do
18
+ desc "Analyze test coverage"
19
+ task :coverage do
20
+ system("rcov -x Library -x support --sort coverage #{File.join(library_root, 'test/*_test.rb')}")
21
+ system("open #{File.join(library_root, 'coverage/index.html')}") if PLATFORM['darwin']
22
+ end
23
+
24
+ namespace :coverage do
25
+ desc "Remove artifacts generated from coverage analysis"
26
+ task :clobber do
27
+ rm_r 'coverage' rescue nil
28
+ end
29
+ end
30
+ end
31
+
32
+ namespace :dist do
33
+ spec = Gem::Specification.new do |s|
34
+ s.name = 'twurl'
35
+ s.version = Gem::Version.new(Twurl::Version)
36
+ s.summary = "Curl for the Twitter API"
37
+ s.description = s.summary
38
+ s.email = ['marcel@twitter.com', 'raffi@twitter.com']
39
+ s.authors = ['Marcel Molina', 'Raffi Krikorian']
40
+ s.has_rdoc = true
41
+ s.extra_rdoc_files = %w(README COPYING INSTALL)
42
+ s.homepage = 'http://github.com/marcel/twurl'
43
+ s.rubyforge_project = 'twurl'
44
+ s.files = FileList['Rakefile', 'lib/**/*.rb', 'bin/*']
45
+ s.executables << 'twurl'
46
+ s.test_files = Dir['test/**/*']
47
+
48
+ s.add_dependency 'oauth'
49
+ s.rdoc_options = ['--title', "twurl -- OAuth-enabled curl for the Twitter API",
50
+ '--main', 'README',
51
+ '--line-numbers', '--inline-source']
52
+ end
53
+
54
+ Rake::GemPackageTask.new(spec) do |pkg|
55
+ pkg.need_tar_gz = true
56
+ pkg.package_files.include('{lib,bin,test}/**/*')
57
+ pkg.package_files.include('README')
58
+ pkg.package_files.include('COPYING')
59
+ pkg.package_files.include('INSTALL')
60
+ pkg.package_files.include('Rakefile')
61
+ end
62
+
63
+ task :spec do
64
+ puts spec.to_ruby
65
+ end
66
+
67
+ desc "Unpack current version of library into the twitter.com vendor directory"
68
+ task :unpack_to_vendor => :repackage do
69
+ cd 'pkg'
70
+ system("gem unpack '#{spec.name}-#{spec.version}.gem' --target=$TWITTER/vendor/gems")
71
+ end
72
+ end
data/bin/twurl ADDED
@@ -0,0 +1,4 @@
1
+ #!/usr/bin/env ruby
2
+ require File.dirname(__FILE__) + '/../lib/twurl'
3
+
4
+ Twurl::CLI.run(ARGV)
data/lib/twurl.rb ADDED
@@ -0,0 +1,21 @@
1
+ require 'rubygems'
2
+ require 'oauth'
3
+ require 'optparse'
4
+ require 'ostruct'
5
+ require 'stringio'
6
+ require 'yaml'
7
+
8
+ library_files = Dir[File.join(File.dirname(__FILE__), "/twurl/**/*.rb")]
9
+ library_files.each do |file|
10
+ require file
11
+ end
12
+
13
+ module Twurl
14
+ @options ||= Options.new
15
+ class << self
16
+ attr_accessor :options
17
+ end
18
+
19
+ class Exception < ::Exception
20
+ end
21
+ end
@@ -0,0 +1,16 @@
1
+ module Twurl
2
+ # Subclasses need to implement a `dispatch' instance method.
3
+ class AbstractCommandController
4
+ attr_reader :client, :options
5
+ class << self
6
+ def dispatch(*args, &block)
7
+ new(*args, &block).dispatch
8
+ end
9
+ end
10
+
11
+ def initialize(client, options)
12
+ @client = client
13
+ @options = options
14
+ end
15
+ end
16
+ end
@@ -0,0 +1,22 @@
1
+ module Twurl
2
+ class AccountInformationController < AbstractCommandController
3
+ NO_AUTHORIZED_ACCOUNTS_MESSAGE = "No authorized accounts"
4
+
5
+ def dispatch
6
+ rcfile = OAuthClient.rcfile
7
+ if rcfile.empty?
8
+ CLI.puts NO_AUTHORIZED_ACCOUNTS_MESSAGE
9
+ else
10
+ profiles = rcfile.profiles
11
+ profiles.keys.sort.each do |account_name|
12
+ CLI.puts account_name
13
+ profiles[account_name].each do |consumer_key, _|
14
+ account_summary = " #{consumer_key}"
15
+ account_summary << " (default)" if rcfile.default_profile == [account_name, consumer_key]
16
+ CLI.puts account_summary
17
+ end
18
+ end
19
+ end
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,25 @@
1
+ module Twurl
2
+ class AliasesController < AbstractCommandController
3
+ NO_ALIASES_MESSAGE = "No aliases exist. Set one this way: twurl alias h /1/statuses/home_timeline.xml"
4
+ NO_PATH_PROVIDED_MESSAGE = "No path was provided to alias. Paths must start with a forward slash (ex. /1/statuses/update.xml)."
5
+ def dispatch
6
+ case options.subcommands.size
7
+ when 0
8
+ aliases = OAuthClient.rcfile.aliases
9
+ if aliases && !aliases.empty?
10
+ aliases.keys.sort.each do |name|
11
+ CLI.puts "#{name}: #{aliases[name]}"
12
+ end
13
+ else
14
+ CLI.puts NO_ALIASES_MESSAGE
15
+ end
16
+ when 1
17
+ if options.path
18
+ OAuthClient.rcfile.alias(options.subcommands.first, options.path)
19
+ else
20
+ CLI.puts NO_PATH_PROVIDED_MESSAGE
21
+ end
22
+ end
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,13 @@
1
+ module Twurl
2
+ class AuthorizationController < AbstractCommandController
3
+ AUTHORIZATION_FAILED_MESSAGE = "Authorization failed. Check that your consumer key and secret are correct, as well as username and password."
4
+ AUTHORIZATION_SUCCEEDED_MESSAGE = "Authorization successful"
5
+ def dispatch
6
+ client.exchange_credentials_for_access_token
7
+ client.save
8
+ CLI.puts AUTHORIZATION_SUCCEEDED_MESSAGE
9
+ rescue OAuth::Unauthorized
10
+ raise Exception, AUTHORIZATION_FAILED_MESSAGE
11
+ end
12
+ end
13
+ end
data/lib/twurl/cli.rb ADDED
@@ -0,0 +1,258 @@
1
+ module Twurl
2
+ class CLI
3
+ SUPPORTED_COMMANDS = %w(authorize accounts alias set)
4
+ DEFAULT_COMMAND = 'request'
5
+ PATH_PATTERN = /^\/\w+/
6
+ README = File.dirname(__FILE__) + '/../../README'
7
+ @output ||= STDOUT
8
+
9
+ class << self
10
+ attr_accessor :output
11
+
12
+ def run(args)
13
+ options = parse_options(args)
14
+ dispatch(options)
15
+ end
16
+
17
+ def dispatch(options)
18
+ client = OAuthClient.load_from_options(options)
19
+ controller = case options.command
20
+ when 'authorize'
21
+ AuthorizationController
22
+ when 'accounts'
23
+ AccountInformationController
24
+ when 'alias'
25
+ AliasesController
26
+ when 'set'
27
+ ConfigurationController
28
+ when 'request'
29
+ RequestController
30
+ end
31
+ controller.dispatch(client, options)
32
+ rescue Twurl::Exception => exception
33
+ abort(exception.message)
34
+ end
35
+
36
+ def parse_options(args)
37
+ arguments = args.dup
38
+
39
+ Twurl.options = Options.new
40
+ Twurl.options.trace = false
41
+ Twurl.options.data = {}
42
+
43
+ option_parser = OptionParser.new do |o|
44
+ o.extend AvailableOptions
45
+
46
+ o.banner = "Usage: twurl authorize -u username -p password --consumer-key HQsAGcVm5MQT3n6j7qVJw --consumer-secret asdfasd223sd2\n" +
47
+ " twurl [options] /statuses/home_timeline.xml\n" +
48
+ "\n" +
49
+ "Supported Commands:\n#{SUPPORTED_COMMANDS.sort.join(', ')}"
50
+
51
+ o.section "Getting started:" do
52
+ tutorial
53
+ end
54
+
55
+ o.section "Authorization options:" do
56
+ username
57
+ password
58
+ consumer_key
59
+ consumer_secret
60
+ access_token
61
+ token_secret
62
+ end
63
+
64
+ o.section "Common options:" do
65
+ trace
66
+ data
67
+ host
68
+ quiet
69
+ disable_ssl
70
+ request_method
71
+ help
72
+ end
73
+ end
74
+
75
+ arguments = option_parser.parse!(args)
76
+ Twurl.options.command = extract_command!(arguments)
77
+ Twurl.options.path = extract_path!(arguments)
78
+ Twurl.options.subcommands = arguments
79
+ Twurl.options
80
+ end
81
+
82
+ def puts(*args, &block)
83
+ output.puts(*args, &block)
84
+ end
85
+
86
+ def prompt_for(label)
87
+ system "stty -echo"
88
+ print "#{label}: "
89
+ result = STDIN.gets.chomp
90
+ CLI.puts
91
+ result
92
+ rescue Interrupt
93
+ exit
94
+ ensure
95
+ system "stty echo"
96
+ end
97
+
98
+ private
99
+ def extract_command!(arguments)
100
+ if SUPPORTED_COMMANDS.include?(arguments.first)
101
+ arguments.shift
102
+ else
103
+ DEFAULT_COMMAND
104
+ end
105
+ end
106
+
107
+ def extract_path!(arguments)
108
+ path = nil
109
+ arguments.each_with_index do |argument, index|
110
+ if argument[PATH_PATTERN]
111
+ path = arguments.slice!(index)
112
+ break
113
+ end
114
+ end
115
+ path
116
+ end
117
+ end
118
+
119
+ module AvailableOptions
120
+ def options
121
+ Twurl.options
122
+ end
123
+
124
+ def section(heading, &block)
125
+ separator ""
126
+ separator heading
127
+
128
+ instance_eval(&block)
129
+ end
130
+
131
+ def tutorial
132
+ on('-T', '--tutorial', "Narrative overview of how to get started using Twurl") do
133
+ CLI.puts IO.read(README)
134
+ exit
135
+ end
136
+ end
137
+
138
+ def consumer_key
139
+ on('-c', '--consumer-key [key]', "Your consumer key (required)") do |key|
140
+ options.consumer_key = key ? key : CLI.prompt_for('Consumer key')
141
+ end
142
+ end
143
+
144
+ def consumer_secret
145
+ on('-s', '--consumer-secret [secret]', "Your consumer secret (required)") do |secret|
146
+ options.consumer_secret = secret ? secret : CLI.prompt_for('Consumer secret')
147
+ end
148
+ end
149
+
150
+ def access_token
151
+ on('-a', '--access-token [token]', 'Your access token') do |token|
152
+ options.access_token = token
153
+ end
154
+ end
155
+
156
+ def token_secret
157
+ on('-S', '--token-secret', "Your token secret") do |secret|
158
+ options.token_secret = secret
159
+ end
160
+ end
161
+
162
+ def username
163
+ on('-u', '--username [username]', 'Username of account to authorize (required)') do |username|
164
+ options.username = username
165
+ end
166
+ end
167
+
168
+ def password
169
+ on('-p', '--password [password]', 'Password of account to authorize (required)') do |password|
170
+ options.password = password ? password : CLI.prompt_for('Password')
171
+ end
172
+ end
173
+
174
+ def trace
175
+ on('-t', '--[no-]trace', 'Trace request/response traffic (default: --no-trace)') do |trace|
176
+ options.trace = trace
177
+ end
178
+ end
179
+
180
+ def data
181
+ on('-d', '--data [data]', 'Sends the specified data in a POST request to the HTTP server.') do |data|
182
+ data.split('&').each do |pair|
183
+ key, value = pair.split('=')
184
+ options.data[key] = value
185
+ end
186
+ end
187
+ end
188
+
189
+ def host
190
+ on('-H', '--host [host]', 'Specify host to make requests to (default: api.twitter.com)') do |host|
191
+ options.host = host
192
+ end
193
+ end
194
+
195
+ def quiet
196
+ on('-q', '--quiet', 'Suppress all output (default: output is printed to STDOUT)') do |quiet|
197
+ options.output = StringIO.new
198
+ end
199
+ end
200
+
201
+ def disable_ssl
202
+ on('-U', '--no-ssl', 'Disable SSL (default: SSL is enabled)') do |use_ssl|
203
+ options.protocol = 'http'
204
+ end
205
+ end
206
+
207
+ def request_method
208
+ on('-X', '--request-method [method]', 'Request method (default: GET)') do |request_method|
209
+ options.request_method = request_method.downcase
210
+ end
211
+ end
212
+
213
+ def help
214
+ on_tail("-h", "--help", "Show this message") do
215
+ CLI.puts self
216
+ exit
217
+ end
218
+ end
219
+ end
220
+ end
221
+
222
+ class Options < OpenStruct
223
+ DEFAULT_REQUEST_METHOD = 'get'
224
+ DEFAULT_HOST = 'api.twitter.com'
225
+ DEFAULT_PROTOCOL = 'https'
226
+
227
+ def oauth_client_options
228
+ OAuthClient::OAUTH_CLIENT_OPTIONS.inject({}) do |options, option|
229
+ options[option] = send(option)
230
+ options
231
+ end
232
+ end
233
+
234
+ def base_url
235
+ "#{protocol}://#{host}"
236
+ end
237
+
238
+ def ssl?
239
+ protocol == 'https'
240
+ end
241
+
242
+ def debug_output_io
243
+ super || STDERR
244
+ end
245
+
246
+ def request_method
247
+ super || (data.empty? ? DEFAULT_REQUEST_METHOD : 'post')
248
+ end
249
+
250
+ def protocol
251
+ super || DEFAULT_PROTOCOL
252
+ end
253
+
254
+ def host
255
+ super || DEFAULT_HOST
256
+ end
257
+ end
258
+ end