twurl 0.6.0
Sign up to get free protection for your applications and to get access to all the features.
- data/COPYING +18 -0
- data/INSTALL +18 -0
- data/README +119 -0
- data/Rakefile +72 -0
- data/bin/twurl +4 -0
- data/lib/twurl.rb +21 -0
- data/lib/twurl/abstract_command_controller.rb +16 -0
- data/lib/twurl/account_information_controller.rb +22 -0
- data/lib/twurl/aliases_controller.rb +25 -0
- data/lib/twurl/authorization_controller.rb +13 -0
- data/lib/twurl/cli.rb +258 -0
- data/lib/twurl/configuration_controller.rb +22 -0
- data/lib/twurl/oauth_client.rb +157 -0
- data/lib/twurl/rcfile.rb +93 -0
- data/lib/twurl/request_controller.rb +19 -0
- data/lib/twurl/version.rb +10 -0
- data/test/account_information_controller_test.rb +61 -0
- data/test/alias_controller_test.rb +53 -0
- data/test/authorization_controller_test.rb +30 -0
- data/test/cli_options_test.rb +23 -0
- data/test/cli_test.rb +129 -0
- data/test/configuration_controller_test.rb +44 -0
- data/test/oauth_client_test.rb +162 -0
- data/test/rcfile_test.rb +141 -0
- data/test/request_controller_test.rb +58 -0
- data/test/test_helper.rb +40 -0
- metadata +107 -0
@@ -0,0 +1,22 @@
|
|
1
|
+
module Twurl
|
2
|
+
class ConfigurationController < AbstractCommandController
|
3
|
+
UNRECOGNIZED_SETTING_MESSAGE = "Unknown configuration setting: '%s'"
|
4
|
+
def dispatch
|
5
|
+
case options.subcommands.first
|
6
|
+
when 'default'
|
7
|
+
if profile = case options.subcommands.size
|
8
|
+
when 2
|
9
|
+
OAuthClient.load_client_for_username(options.subcommands.last)
|
10
|
+
when 3
|
11
|
+
OAuthClient.load_client_for_username_and_consumer_key(*options.subcommands[-2, 2])
|
12
|
+
end
|
13
|
+
|
14
|
+
OAuthClient.rcfile.default_profile = profile
|
15
|
+
OAuthClient.rcfile.save
|
16
|
+
end
|
17
|
+
else
|
18
|
+
CLI.puts(UNRECOGNIZED_SETTING_MESSAGE % options.subcommands.first)
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
@@ -0,0 +1,157 @@
|
|
1
|
+
module Twurl
|
2
|
+
class OAuthClient
|
3
|
+
class << self
|
4
|
+
def rcfile(reload = false)
|
5
|
+
if reload || @rcfile.nil?
|
6
|
+
@rcfile = RCFile.new
|
7
|
+
end
|
8
|
+
@rcfile
|
9
|
+
end
|
10
|
+
|
11
|
+
def load_from_options(options)
|
12
|
+
if rcfile.has_oauth_profile_for_username_with_consumer_key?(options.username, options.consumer_key)
|
13
|
+
load_client_for_username_and_consumer_key(options.username, options.consumer_key)
|
14
|
+
else
|
15
|
+
options.username ? load_new_client_from_options(options) : load_default_client
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
def load_client_for_username_and_consumer_key(username, consumer_key)
|
20
|
+
user_profiles = rcfile[username]
|
21
|
+
if user_profiles && attributes = user_profiles[consumer_key]
|
22
|
+
new(attributes)
|
23
|
+
else
|
24
|
+
raise Exception, "No profile for #{username}"
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
def load_client_for_username(username)
|
29
|
+
if user_profiles = rcfile[username]
|
30
|
+
if user_profiles.values.size == 1
|
31
|
+
new(user_profiles.values.first)
|
32
|
+
else
|
33
|
+
raise Exception, "There is more than one consumer key associated with #{username}. Please specify which consumer key you want as well."
|
34
|
+
end
|
35
|
+
else
|
36
|
+
raise Exception, "No profile for #{username}"
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
def load_new_client_from_options(options)
|
41
|
+
new(options.oauth_client_options.merge('password' => options.password))
|
42
|
+
end
|
43
|
+
|
44
|
+
def load_default_client
|
45
|
+
raise Exception, "You must authorize first" unless rcfile.default_profile
|
46
|
+
load_client_for_username_and_consumer_key(*rcfile.default_profile)
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
OAUTH_CLIENT_OPTIONS = %w[username consumer_key consumer_secret token secret]
|
51
|
+
attr_reader *OAUTH_CLIENT_OPTIONS
|
52
|
+
attr_reader :password
|
53
|
+
def initialize(options = {})
|
54
|
+
@username = options['username']
|
55
|
+
@password = options['password']
|
56
|
+
@consumer_key = options['consumer_key']
|
57
|
+
@consumer_secret = options['consumer_secret']
|
58
|
+
@token = options['token']
|
59
|
+
@secret = options['secret']
|
60
|
+
configure_http!
|
61
|
+
end
|
62
|
+
|
63
|
+
[:get, :post, :put, :delete, :options, :head, :copy].each do |request_method|
|
64
|
+
class_eval(<<-EVAL, __FILE__, __LINE__)
|
65
|
+
def #{request_method}(url, options = {})
|
66
|
+
# configure_http!
|
67
|
+
access_token.#{request_method}(url, options)
|
68
|
+
end
|
69
|
+
EVAL
|
70
|
+
end
|
71
|
+
|
72
|
+
def perform_request_from_options(options)
|
73
|
+
send(options.request_method, options.path, options.data)
|
74
|
+
end
|
75
|
+
|
76
|
+
def exchange_credentials_for_access_token
|
77
|
+
response = begin
|
78
|
+
consumer.token_request(:post, consumer.access_token_path, nil, {}, client_auth_parameters)
|
79
|
+
rescue OAuth::Unauthorized
|
80
|
+
perform_pin_authorize_workflow
|
81
|
+
end
|
82
|
+
@token = response[:oauth_token]
|
83
|
+
@secret = response[:oauth_token_secret]
|
84
|
+
end
|
85
|
+
|
86
|
+
def client_auth_parameters
|
87
|
+
{:x_auth_username => username, :x_auth_password => password, :x_auth_mode => 'client_auth'}
|
88
|
+
end
|
89
|
+
|
90
|
+
def perform_pin_authorize_workflow
|
91
|
+
@request_token = consumer.get_request_token
|
92
|
+
CLI.puts("Go to #{generate_authorize_url} and paste in the supplied PIN")
|
93
|
+
pin = gets
|
94
|
+
access_token = @request_token.get_access_token(:oauth_verifier => pin.chomp)
|
95
|
+
{:oauth_token => access_token.token, :oauth_token_secret => access_token.secret}
|
96
|
+
end
|
97
|
+
|
98
|
+
def generate_authorize_url
|
99
|
+
request = consumer.create_signed_request(:get, consumer.authorize_path, @request_token, pin_auth_parameters)
|
100
|
+
params = request['Authorization'].sub(/^OAuth\s+/, '').split(/,\s+/).map { |p|
|
101
|
+
k, v = p.split('=')
|
102
|
+
v =~ /"(.*?)"/
|
103
|
+
"#{k}=#{CGI::escape($1)}"
|
104
|
+
}.join('&')
|
105
|
+
"#{Twurl.options.base_url}#{request.path}?#{params}"
|
106
|
+
end
|
107
|
+
|
108
|
+
def pin_auth_parameters
|
109
|
+
{:oauth_callback => 'oob'}
|
110
|
+
end
|
111
|
+
|
112
|
+
def authorized?
|
113
|
+
oauth_response = access_token.get('/1/account/verify_credentials.json')
|
114
|
+
oauth_response.class == Net::HTTPOK
|
115
|
+
end
|
116
|
+
|
117
|
+
def needs_to_authorize?
|
118
|
+
token.nil? || secret.nil?
|
119
|
+
end
|
120
|
+
|
121
|
+
def save
|
122
|
+
self.class.rcfile << self
|
123
|
+
end
|
124
|
+
|
125
|
+
def to_hash
|
126
|
+
OAUTH_CLIENT_OPTIONS.inject({}) do |hash, attribute|
|
127
|
+
if value = send(attribute)
|
128
|
+
hash[attribute] = value
|
129
|
+
end
|
130
|
+
hash
|
131
|
+
end
|
132
|
+
end
|
133
|
+
|
134
|
+
def configure_http!
|
135
|
+
consumer.http.set_debug_output(Twurl.options.debug_output_io) if Twurl.options.trace
|
136
|
+
if Twurl.options.ssl?
|
137
|
+
consumer.http.use_ssl = true
|
138
|
+
consumer.http.verify_mode = OpenSSL::SSL::VERIFY_NONE
|
139
|
+
end
|
140
|
+
end
|
141
|
+
|
142
|
+
def consumer
|
143
|
+
@consumer ||=
|
144
|
+
begin
|
145
|
+
OAuth::Consumer.new(
|
146
|
+
consumer_key,
|
147
|
+
consumer_secret,
|
148
|
+
:site => Twurl.options.base_url
|
149
|
+
)
|
150
|
+
end
|
151
|
+
end
|
152
|
+
|
153
|
+
def access_token
|
154
|
+
@access_token ||= OAuth::AccessToken.new(consumer, token, secret)
|
155
|
+
end
|
156
|
+
end
|
157
|
+
end
|
data/lib/twurl/rcfile.rb
ADDED
@@ -0,0 +1,93 @@
|
|
1
|
+
module Twurl
|
2
|
+
class RCFile
|
3
|
+
FILE = '.twurlrc'
|
4
|
+
@directory ||= ENV['HOME']
|
5
|
+
class << self
|
6
|
+
attr_accessor :directory
|
7
|
+
|
8
|
+
def file_path
|
9
|
+
File.join(directory, FILE)
|
10
|
+
end
|
11
|
+
|
12
|
+
def load
|
13
|
+
YAML.load_file(file_path)
|
14
|
+
rescue Errno::ENOENT
|
15
|
+
default_rcfile_structure
|
16
|
+
end
|
17
|
+
|
18
|
+
def default_rcfile_structure
|
19
|
+
{'profiles' => {}, 'configuration' => {}}
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
attr_reader :data
|
24
|
+
def initialize
|
25
|
+
@data = self.class.load
|
26
|
+
end
|
27
|
+
|
28
|
+
def empty?
|
29
|
+
data == self.class.default_rcfile_structure
|
30
|
+
end
|
31
|
+
|
32
|
+
def save
|
33
|
+
File.open(self.class.file_path, 'w') do |rcfile|
|
34
|
+
rcfile.write data.to_yaml
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
def [](username)
|
39
|
+
profiles[username]
|
40
|
+
end
|
41
|
+
|
42
|
+
def profiles
|
43
|
+
data['profiles']
|
44
|
+
end
|
45
|
+
|
46
|
+
def default_profile
|
47
|
+
configuration['default_profile']
|
48
|
+
end
|
49
|
+
|
50
|
+
def default_profile=(profile)
|
51
|
+
configuration['default_profile'] = [profile.username, profile.consumer_key]
|
52
|
+
end
|
53
|
+
|
54
|
+
def configuration
|
55
|
+
data['configuration']
|
56
|
+
end
|
57
|
+
|
58
|
+
def alias(name, path)
|
59
|
+
data['aliases'] ||= {}
|
60
|
+
data['aliases'][name] = path
|
61
|
+
save
|
62
|
+
end
|
63
|
+
|
64
|
+
def aliases
|
65
|
+
data['aliases']
|
66
|
+
end
|
67
|
+
|
68
|
+
def alias_from_options(options)
|
69
|
+
options.subcommands.each do |potential_alias|
|
70
|
+
if path = alias_from_name(potential_alias)
|
71
|
+
break path
|
72
|
+
end
|
73
|
+
end
|
74
|
+
end
|
75
|
+
|
76
|
+
def alias_from_name(name)
|
77
|
+
aliases[name]
|
78
|
+
end
|
79
|
+
|
80
|
+
def has_oauth_profile_for_username_with_consumer_key?(username, consumer_key)
|
81
|
+
user_profiles = self[username]
|
82
|
+
!user_profiles.nil? && !user_profiles[consumer_key].nil?
|
83
|
+
end
|
84
|
+
|
85
|
+
def <<(oauth_client)
|
86
|
+
client_from_file = self[oauth_client.username] || {}
|
87
|
+
client_from_file[oauth_client.consumer_key] = oauth_client.to_hash
|
88
|
+
(profiles[oauth_client.username] ||= {}).update(client_from_file)
|
89
|
+
self.default_profile = oauth_client unless default_profile
|
90
|
+
save
|
91
|
+
end
|
92
|
+
end
|
93
|
+
end
|
@@ -0,0 +1,19 @@
|
|
1
|
+
module Twurl
|
2
|
+
class RequestController < AbstractCommandController
|
3
|
+
NO_URI_MESSAGE = "No URI specified"
|
4
|
+
def dispatch
|
5
|
+
if client.needs_to_authorize?
|
6
|
+
raise Exception, "You need to authorize first."
|
7
|
+
end
|
8
|
+
options.path ||= OAuthClient.rcfile.alias_from_options(options)
|
9
|
+
perform_request
|
10
|
+
end
|
11
|
+
|
12
|
+
def perform_request
|
13
|
+
response = client.perform_request_from_options(options)
|
14
|
+
CLI.puts response.body
|
15
|
+
rescue URI::InvalidURIError
|
16
|
+
CLI.puts NO_URI_MESSAGE
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
@@ -0,0 +1,61 @@
|
|
1
|
+
require File.dirname(__FILE__) + '/test_helper'
|
2
|
+
|
3
|
+
class Twurl::AccountInformationController::DispatchWithNoAuthorizedAccountsTest < Test::Unit::TestCase
|
4
|
+
attr_reader :options, :client, :controller
|
5
|
+
def setup
|
6
|
+
@options = Twurl::Options.new
|
7
|
+
@client = Twurl::OAuthClient.load_new_client_from_options(options)
|
8
|
+
@controller = Twurl::AccountInformationController.new(client, options)
|
9
|
+
mock(Twurl::OAuthClient.rcfile).empty? { true }
|
10
|
+
end
|
11
|
+
|
12
|
+
def test_message_indicates_when_no_accounts_are_authorized
|
13
|
+
mock(Twurl::CLI).puts(Twurl::AccountInformationController::NO_AUTHORIZED_ACCOUNTS_MESSAGE).times(1)
|
14
|
+
|
15
|
+
controller.dispatch
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
class Twurl::AccountInformationController::DispatchWithOneAuthorizedAccountTest < Test::Unit::TestCase
|
20
|
+
attr_reader :options, :client, :controller
|
21
|
+
def setup
|
22
|
+
@options = Twurl::Options.test_exemplar
|
23
|
+
@client = Twurl::OAuthClient.load_new_client_from_options(options)
|
24
|
+
mock(Twurl::OAuthClient.rcfile).save.times(1)
|
25
|
+
Twurl::OAuthClient.rcfile << client
|
26
|
+
@controller = Twurl::AccountInformationController.new(client, options)
|
27
|
+
end
|
28
|
+
|
29
|
+
def test_authorized_account_is_displayed_and_marked_as_the_default
|
30
|
+
mock(Twurl::CLI).puts(client.username).times(1).ordered
|
31
|
+
mock(Twurl::CLI).puts(" #{client.consumer_key} (default)").times(1).ordered
|
32
|
+
|
33
|
+
controller.dispatch
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
class Twurl::AccountInformationController::DispatchWithOneUsernameThatHasAuthorizedMultipleAccountsTest < Test::Unit::TestCase
|
38
|
+
attr_reader :default_client_options, :default_client, :other_client_options, :other_client, :controller
|
39
|
+
def setup
|
40
|
+
@default_client_options = Twurl::Options.test_exemplar
|
41
|
+
@default_client = Twurl::OAuthClient.load_new_client_from_options(default_client_options)
|
42
|
+
|
43
|
+
@other_client_options = Twurl::Options.test_exemplar
|
44
|
+
other_client_options.consumer_key = default_client_options.consumer_key.reverse
|
45
|
+
@other_client = Twurl::OAuthClient.load_new_client_from_options(other_client_options)
|
46
|
+
mock(Twurl::OAuthClient.rcfile).save.times(2)
|
47
|
+
|
48
|
+
Twurl::OAuthClient.rcfile << default_client
|
49
|
+
Twurl::OAuthClient.rcfile << other_client
|
50
|
+
|
51
|
+
@controller = Twurl::AccountInformationController.new(other_client, other_client_options)
|
52
|
+
end
|
53
|
+
|
54
|
+
def test_authorized_account_is_displayed_and_marked_as_the_default
|
55
|
+
mock(Twurl::CLI).puts(default_client.username).times(1)
|
56
|
+
mock(Twurl::CLI).puts(" #{default_client.consumer_key} (default)").times(1)
|
57
|
+
mock(Twurl::CLI).puts(" #{other_client.consumer_key}").times(1)
|
58
|
+
|
59
|
+
controller.dispatch
|
60
|
+
end
|
61
|
+
end
|
@@ -0,0 +1,53 @@
|
|
1
|
+
require File.dirname(__FILE__) + '/test_helper'
|
2
|
+
|
3
|
+
class Twurl::AliasesController::DispatchTest < Test::Unit::TestCase
|
4
|
+
attr_reader :options, :client
|
5
|
+
def setup
|
6
|
+
@options = Twurl::Options.test_exemplar
|
7
|
+
@client = Twurl::OAuthClient.test_exemplar
|
8
|
+
|
9
|
+
# Clean slate
|
10
|
+
if Twurl::OAuthClient.rcfile.aliases
|
11
|
+
Twurl::OAuthClient.rcfile.aliases.clear
|
12
|
+
end
|
13
|
+
|
14
|
+
stub(Twurl::OAuthClient.rcfile).save
|
15
|
+
end
|
16
|
+
|
17
|
+
def test_when_no_subcommands_are_provided_and_no_aliases_exist_nothing_is_displayed
|
18
|
+
assert options.subcommands.empty?
|
19
|
+
mock(Twurl::CLI).puts(Twurl::AliasesController::NO_ALIASES_MESSAGE).times(1)
|
20
|
+
|
21
|
+
controller = Twurl::AliasesController.new(client, options)
|
22
|
+
controller.dispatch
|
23
|
+
end
|
24
|
+
|
25
|
+
def test_when_no_subcommands_are_provided_and_aliases_exist_they_are_displayed
|
26
|
+
assert options.subcommands.empty?
|
27
|
+
|
28
|
+
Twurl::OAuthClient.rcfile.alias('h', '/1/statuses/home_timeline.xml')
|
29
|
+
mock(Twurl::CLI).puts("h: /1/statuses/home_timeline.xml").times(1)
|
30
|
+
|
31
|
+
controller = Twurl::AliasesController.new(client, options)
|
32
|
+
controller.dispatch
|
33
|
+
end
|
34
|
+
|
35
|
+
def test_when_alias_and_value_are_provided_they_are_added
|
36
|
+
options.subcommands = ['h']
|
37
|
+
options.path = '/1/statuses/home_timeline.xml'
|
38
|
+
mock(Twurl::OAuthClient.rcfile).alias('h', '/1/statuses/home_timeline.xml').times(1)
|
39
|
+
|
40
|
+
controller = Twurl::AliasesController.new(client, options)
|
41
|
+
controller.dispatch
|
42
|
+
end
|
43
|
+
|
44
|
+
def test_when_no_path_is_provided_nothing_happens
|
45
|
+
options.subcommands = ['a']
|
46
|
+
assert_nil options.path
|
47
|
+
|
48
|
+
mock(Twurl::CLI).puts(Twurl::AliasesController::NO_PATH_PROVIDED_MESSAGE).times(1)
|
49
|
+
|
50
|
+
controller = Twurl::AliasesController.new(client, options)
|
51
|
+
controller.dispatch
|
52
|
+
end
|
53
|
+
end
|
@@ -0,0 +1,30 @@
|
|
1
|
+
require File.dirname(__FILE__) + '/test_helper'
|
2
|
+
|
3
|
+
class Twurl::AuthorizationController::DispatchTest < Test::Unit::TestCase
|
4
|
+
attr_reader :options, :client, :controller
|
5
|
+
def setup
|
6
|
+
@options = Twurl::Options.new
|
7
|
+
@client = Twurl::OAuthClient.load_new_client_from_options(options)
|
8
|
+
@controller = Twurl::AuthorizationController.new(client, options)
|
9
|
+
end
|
10
|
+
|
11
|
+
def test_successful_authentication_saves_retrieved_access_token
|
12
|
+
mock(client).exchange_credentials_for_access_token.times(1)
|
13
|
+
mock(client).save.times(1)
|
14
|
+
mock(controller).raise(Twurl::Exception, Twurl::AuthorizationController::AUTHORIZATION_FAILED_MESSAGE).never
|
15
|
+
mock(Twurl::CLI).puts(Twurl::AuthorizationController::AUTHORIZATION_SUCCEEDED_MESSAGE).times(1)
|
16
|
+
|
17
|
+
controller.dispatch
|
18
|
+
end
|
19
|
+
|
20
|
+
module ErrorCases
|
21
|
+
def test_failed_authorization_does_not_save_client
|
22
|
+
mock(client).exchange_credentials_for_access_token { raise OAuth::Unauthorized }
|
23
|
+
mock(client).save.never
|
24
|
+
mock(controller).raise(Twurl::Exception, Twurl::AuthorizationController::AUTHORIZATION_FAILED_MESSAGE).times(1)
|
25
|
+
|
26
|
+
controller.dispatch
|
27
|
+
end
|
28
|
+
end
|
29
|
+
include ErrorCases
|
30
|
+
end
|