twurl 0.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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
|