tweetwine 0.2.12 → 0.3.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/CHANGELOG.rdoc +7 -0
- data/Gemfile +17 -0
- data/README.md +57 -47
- data/Rakefile +17 -26
- data/bin/tweetwine +11 -12
- data/contrib/tweetwine-completion.bash +2 -3
- data/example/application_behavior_example.rb +173 -0
- data/example/example_helper.rb +44 -28
- data/example/fixture/config.yaml +8 -0
- data/example/fixture/shorten_rubygems.html +5 -0
- data/example/fixture/shorten_rubylang.html +5 -0
- data/example/fixture/update_utf8.json +1 -0
- data/example/fixture/update_with_urls.json +1 -0
- data/example/fixture/{update.json → update_without_urls.json} +0 -0
- data/example/search_statuses_example.rb +49 -16
- data/example/show_followers_example.rb +7 -8
- data/example/show_friends_example.rb +7 -8
- data/example/show_home_example.rb +19 -16
- data/example/show_mentions_example.rb +8 -9
- data/example/show_user_example.rb +16 -13
- data/example/update_status_example.rb +143 -26
- data/example/use_http_proxy_example.rb +40 -20
- data/lib/tweetwine/basic_object.rb +19 -0
- data/lib/tweetwine/character_encoding.rb +59 -0
- data/lib/tweetwine/cli.rb +354 -230
- data/lib/tweetwine/config.rb +65 -0
- data/lib/tweetwine/http.rb +120 -0
- data/lib/tweetwine/oauth.rb +104 -0
- data/lib/tweetwine/obfuscate.rb +21 -0
- data/lib/tweetwine/option_parser.rb +31 -0
- data/lib/tweetwine/promise.rb +39 -0
- data/lib/tweetwine/twitter.rb +211 -0
- data/lib/tweetwine/{io.rb → ui.rb} +30 -21
- data/lib/tweetwine/url_shortener.rb +15 -9
- data/lib/tweetwine/util.rb +30 -15
- data/lib/tweetwine.rb +72 -12
- data/man/tweetwine.7 +43 -69
- data/man/tweetwine.7.ronn +57 -47
- data/test/character_encoding_test.rb +87 -0
- data/test/cli_test.rb +19 -6
- data/test/config_test.rb +244 -0
- data/test/fixture/oauth.rb +21 -0
- data/test/fixture/test_config.yaml +4 -4
- data/test/http_test.rb +199 -0
- data/test/oauth_test.rb +77 -0
- data/test/obfuscate_test.rb +16 -0
- data/test/option_parser_test.rb +60 -0
- data/test/promise_test.rb +56 -0
- data/test/test_helper.rb +76 -8
- data/test/twitter_test.rb +625 -0
- data/test/{io_test.rb → ui_test.rb} +92 -74
- data/test/url_shortener_test.rb +115 -135
- data/test/util_test.rb +136 -85
- data/tweetwine.gemspec +53 -0
- metadata +112 -56
- data/example/show_metadata_example.rb +0 -86
- data/lib/tweetwine/client.rb +0 -187
- data/lib/tweetwine/meta.rb +0 -5
- data/lib/tweetwine/options.rb +0 -24
- data/lib/tweetwine/retrying_http.rb +0 -99
- data/lib/tweetwine/startup_config.rb +0 -50
- data/man/tweetwine.1 +0 -109
- data/man/tweetwine.1.ronn +0 -69
- data/test/client_test.rb +0 -544
- data/test/options_test.rb +0 -45
- data/test/retrying_http_test.rb +0 -147
- data/test/startup_config_test.rb +0 -162
@@ -0,0 +1,65 @@
|
|
1
|
+
# coding: utf-8
|
2
|
+
|
3
|
+
require "set"
|
4
|
+
require "yaml"
|
5
|
+
|
6
|
+
module Tweetwine
|
7
|
+
class Config
|
8
|
+
def self.read(args = [], default_config = {}, &cmd_option_parser)
|
9
|
+
new parse_options(args, default_config, &cmd_option_parser)
|
10
|
+
end
|
11
|
+
|
12
|
+
def [](key)
|
13
|
+
@options[key]
|
14
|
+
end
|
15
|
+
|
16
|
+
def []=(key, value)
|
17
|
+
@options[key] = value
|
18
|
+
end
|
19
|
+
|
20
|
+
def keys
|
21
|
+
@options.keys
|
22
|
+
end
|
23
|
+
|
24
|
+
def save
|
25
|
+
raise "No config file specified" unless @file
|
26
|
+
to_file = @options.reject { |key, _| @excludes.include? key }
|
27
|
+
should_set_file_access_to_user_only = !File.exist?(@file)
|
28
|
+
File.open(@file, 'w') do |io|
|
29
|
+
io.chmod(0600) if should_set_file_access_to_user_only
|
30
|
+
YAML.dump(Util.stringify_hash_keys(to_file), io)
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
private
|
35
|
+
|
36
|
+
def initialize(options)
|
37
|
+
@options = options
|
38
|
+
@file = options[:config_file]
|
39
|
+
@excludes = Set.new(options[:excludes] || []).merge([:config_file, :env_lookouts, :excludes])
|
40
|
+
end
|
41
|
+
|
42
|
+
def self.parse_options(args, default_config, &cmd_option_parser)
|
43
|
+
env_lookouts = default_config[:env_lookouts]
|
44
|
+
cmd_options = if cmd_option_parser then cmd_option_parser.call(args) else {} end
|
45
|
+
env_options = if env_lookouts then parse_env_vars(env_lookouts) else {} end
|
46
|
+
launch_options = env_options.merge(cmd_options)
|
47
|
+
config_file = launch_options[:config_file] || default_config[:config_file]
|
48
|
+
file_options = if config_file && File.file?(config_file) then parse_config_file(config_file) else {} end
|
49
|
+
default_config.merge(file_options.merge(launch_options))
|
50
|
+
end
|
51
|
+
|
52
|
+
def self.parse_env_vars(env_lookouts)
|
53
|
+
env_lookouts.inject({}) do |result, env_var_name|
|
54
|
+
env_option = ENV[env_var_name.to_s]
|
55
|
+
result[env_var_name.to_sym] = env_option unless Util.blank?(env_option)
|
56
|
+
result
|
57
|
+
end
|
58
|
+
end
|
59
|
+
|
60
|
+
def self.parse_config_file(config_file)
|
61
|
+
options = File.open(config_file, 'r') { |io| YAML.load(io) }
|
62
|
+
Util.symbolize_hash_keys(options)
|
63
|
+
end
|
64
|
+
end
|
65
|
+
end
|
@@ -0,0 +1,120 @@
|
|
1
|
+
# coding: utf-8
|
2
|
+
|
3
|
+
require "net/http"
|
4
|
+
require "uri"
|
5
|
+
|
6
|
+
module Tweetwine
|
7
|
+
module Http
|
8
|
+
module Retrying
|
9
|
+
MAX_RETRIES = 3
|
10
|
+
RETRY_BASE_WAIT_TIMEOUT = 4
|
11
|
+
|
12
|
+
def retrying(max_retries = MAX_RETRIES, retry_base_wait_timeout = RETRY_BASE_WAIT_TIMEOUT)
|
13
|
+
retries = 0
|
14
|
+
begin
|
15
|
+
yield
|
16
|
+
rescue ConnectionError, TimeoutError
|
17
|
+
if retries < max_retries
|
18
|
+
retries += 1
|
19
|
+
timeout = retry_base_wait_timeout**retries
|
20
|
+
CLI.ui.warn("Could not connect -- retrying in #{timeout} seconds")
|
21
|
+
sleep timeout
|
22
|
+
retry
|
23
|
+
else
|
24
|
+
raise
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
class Client
|
31
|
+
include Retrying
|
32
|
+
|
33
|
+
def initialize(options = {})
|
34
|
+
@http = Net::HTTP::Proxy(*parse_proxy_url(options[:http_proxy]))
|
35
|
+
end
|
36
|
+
|
37
|
+
def get(url, headers = nil, &block)
|
38
|
+
retrying do
|
39
|
+
requesting(url) do |connection, uri|
|
40
|
+
req = Net::HTTP::Get.new(uri.request_uri, headers)
|
41
|
+
block.call(connection, req) if block
|
42
|
+
connection.request(req)
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
def post(url, payload = nil, headers = nil, &block)
|
48
|
+
retrying do
|
49
|
+
requesting(url) do |connection, uri|
|
50
|
+
req = Net::HTTP::Post.new(uri.request_uri, headers)
|
51
|
+
req.form_data = payload if payload
|
52
|
+
block.call(connection, req) if block
|
53
|
+
connection.request(req)
|
54
|
+
end
|
55
|
+
end
|
56
|
+
end
|
57
|
+
|
58
|
+
def as_resource(url)
|
59
|
+
Resource.new(self, url)
|
60
|
+
end
|
61
|
+
|
62
|
+
private
|
63
|
+
|
64
|
+
def parse_proxy_url(url)
|
65
|
+
return [nil, nil] unless url
|
66
|
+
url = url.sub(%r{\Ahttps?://}, '') # remove possible scheme
|
67
|
+
proxy_addr, proxy_port = url.split(':', 2)
|
68
|
+
begin
|
69
|
+
proxy_port = proxy_port ? Integer(proxy_port) : 8080
|
70
|
+
rescue ArgumentError
|
71
|
+
raise CommandLineError, "invalid proxy port: #{proxy_port}"
|
72
|
+
end
|
73
|
+
[proxy_addr, proxy_port]
|
74
|
+
end
|
75
|
+
|
76
|
+
def requesting(url)
|
77
|
+
uri = URI.parse(url)
|
78
|
+
connection = @http.new(uri.host, uri.port)
|
79
|
+
configure_for_ssl(connection) if https_scheme?(uri)
|
80
|
+
response = yield connection, uri
|
81
|
+
raise HttpError.new(response.code, response.message) unless response.is_a? Net::HTTPSuccess
|
82
|
+
response.body
|
83
|
+
rescue Errno::ECONNABORTED, Errno::ECONNRESET => e
|
84
|
+
raise ConnectionError, e
|
85
|
+
rescue Timeout::Error => e
|
86
|
+
raise TimeoutError, e
|
87
|
+
rescue Net::HTTPError => e
|
88
|
+
raise HttpError, e
|
89
|
+
end
|
90
|
+
|
91
|
+
def https_scheme?(uri)
|
92
|
+
uri.scheme == 'https'
|
93
|
+
end
|
94
|
+
|
95
|
+
def configure_for_ssl(connection)
|
96
|
+
connection.use_ssl = true
|
97
|
+
connection.verify_mode = OpenSSL::SSL::VERIFY_NONE
|
98
|
+
end
|
99
|
+
end
|
100
|
+
|
101
|
+
class Resource
|
102
|
+
def initialize(client, url)
|
103
|
+
@client = client
|
104
|
+
@url = url
|
105
|
+
end
|
106
|
+
|
107
|
+
def [](suburl)
|
108
|
+
self.class.new(@client, "#{@url}/#{suburl}")
|
109
|
+
end
|
110
|
+
|
111
|
+
def get(headers = nil, &block)
|
112
|
+
@client.get(@url, headers, &block)
|
113
|
+
end
|
114
|
+
|
115
|
+
def post(payload = nil, headers = nil, &block)
|
116
|
+
@client.post(@url, payload, headers, &block)
|
117
|
+
end
|
118
|
+
end
|
119
|
+
end
|
120
|
+
end
|
@@ -0,0 +1,104 @@
|
|
1
|
+
# coding: utf-8
|
2
|
+
|
3
|
+
require "cgi"
|
4
|
+
require "oauth"
|
5
|
+
|
6
|
+
module Tweetwine
|
7
|
+
class OAuth
|
8
|
+
SEP = ':'
|
9
|
+
CON = Obfuscate.read(<<-END).split(SEP)
|
10
|
+
enpGfklDSjc7K0s+cklwdipiRiY6cGk8J0U5diFfZHh0JzxnfD5lPzxJcXN6
|
11
|
+
PitkPXNhYGh7M194Qyc2PHgrRkdn
|
12
|
+
END
|
13
|
+
|
14
|
+
def initialize(access = nil)
|
15
|
+
@access_key, @access_secret = *if access
|
16
|
+
Obfuscate.read(access).split(SEP)
|
17
|
+
else
|
18
|
+
['', '']
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
def authorize
|
23
|
+
request_token = get_request_token
|
24
|
+
CLI.ui.info "Please authorize: #{request_token.authorize_url}"
|
25
|
+
pin = CLI.ui.prompt 'Enter PIN'
|
26
|
+
access_token = get_access_token(request_token, pin)
|
27
|
+
reset_access_token(access_token)
|
28
|
+
yield(obfuscate_access_token) if block_given?
|
29
|
+
end
|
30
|
+
|
31
|
+
def request_signer
|
32
|
+
@signer ||= lambda do |connection, request|
|
33
|
+
request.oauth! connection, consumer, access_token
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
private
|
38
|
+
|
39
|
+
def consumer
|
40
|
+
@consumer ||= ::OAuth::Consumer.new(CON[0], CON[1],
|
41
|
+
:site => 'https://api.twitter.com',
|
42
|
+
:scheme => :header,
|
43
|
+
:http_method => :post,
|
44
|
+
:request_token_path => '/oauth/request_token',
|
45
|
+
:authorize_path => '/oauth/authorize',
|
46
|
+
:access_token_path => '/oauth/access_token')
|
47
|
+
end
|
48
|
+
|
49
|
+
def access_token
|
50
|
+
@access_token ||= ::OAuth::AccessToken.from_hash(consumer,
|
51
|
+
:oauth_token => @access_key,
|
52
|
+
:oauth_token_secret => @access_secret)
|
53
|
+
end
|
54
|
+
|
55
|
+
def reset_access_token(access_token)
|
56
|
+
@access_token = access_token
|
57
|
+
@access_key = access_token.token
|
58
|
+
@access_secret = access_token.secret
|
59
|
+
end
|
60
|
+
|
61
|
+
def obfuscate_access_token
|
62
|
+
Obfuscate.write("#{@access_key}#{SEP}#{@access_secret}")
|
63
|
+
end
|
64
|
+
|
65
|
+
def get_request_token
|
66
|
+
response = http_request(consumer.request_token_url) do |connection, request|
|
67
|
+
request.oauth! connection, consumer, nil, :oauth_callback => 'oob'
|
68
|
+
end
|
69
|
+
::OAuth::RequestToken.from_hash(consumer, response)
|
70
|
+
end
|
71
|
+
|
72
|
+
def get_access_token(request_token, pin)
|
73
|
+
response = http_request(consumer.access_token_url) do |connection, request|
|
74
|
+
request.oauth! connection, consumer, request_token, :oauth_verifier => pin
|
75
|
+
end
|
76
|
+
::OAuth::AccessToken.from_hash(consumer, response)
|
77
|
+
end
|
78
|
+
|
79
|
+
def http_request(url, &block)
|
80
|
+
method = consumer.http_method
|
81
|
+
response = CLI.http.send(method, url) do |connection, request|
|
82
|
+
request['Content-Length'] = '0'
|
83
|
+
block.call(connection, request)
|
84
|
+
end
|
85
|
+
parse_url_encoding(response)
|
86
|
+
rescue HttpError => e
|
87
|
+
# Do not raise HttpError with 401 response since that is expected this
|
88
|
+
# module to deal with.
|
89
|
+
if (400...500).include? e.http_code
|
90
|
+
raise AuthorizationError, "Unauthorized to #{method.to_s.upcase} #{url} for OAuth: #{e}"
|
91
|
+
else
|
92
|
+
raise
|
93
|
+
end
|
94
|
+
end
|
95
|
+
|
96
|
+
def parse_url_encoding(response)
|
97
|
+
CGI.parse(response).inject({}) do |hash, (key, value)|
|
98
|
+
key = key.strip
|
99
|
+
hash[key] = hash[key.to_sym] = value.first
|
100
|
+
hash
|
101
|
+
end
|
102
|
+
end
|
103
|
+
end
|
104
|
+
end
|
@@ -0,0 +1,21 @@
|
|
1
|
+
# coding: utf-8
|
2
|
+
|
3
|
+
require "base64"
|
4
|
+
|
5
|
+
module Tweetwine
|
6
|
+
module Obfuscate
|
7
|
+
extend self
|
8
|
+
|
9
|
+
def obfuscate(str)
|
10
|
+
str.tr("\x21-\x7e", "\x50-\x7e\x21-\x4f")
|
11
|
+
end
|
12
|
+
|
13
|
+
def read(str)
|
14
|
+
obfuscate(Base64.decode64(str))
|
15
|
+
end
|
16
|
+
|
17
|
+
def write(str)
|
18
|
+
Base64.encode64(obfuscate(str))
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
@@ -0,0 +1,31 @@
|
|
1
|
+
# coding: utf-8
|
2
|
+
|
3
|
+
require "optparse"
|
4
|
+
|
5
|
+
module Tweetwine
|
6
|
+
# A wrapper for OptionParser in standard library that returns parsing
|
7
|
+
# results as a Hash.
|
8
|
+
#
|
9
|
+
# Not threadsafe.
|
10
|
+
class OptionParser
|
11
|
+
def initialize(&blk)
|
12
|
+
@options = {}
|
13
|
+
@parser = ::OptionParser.new do |parser|
|
14
|
+
blk.call(parser, @options)
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
def parse(args = ARGV)
|
19
|
+
@parser.order! args
|
20
|
+
result = @options.dup
|
21
|
+
@options.clear
|
22
|
+
result
|
23
|
+
rescue ::OptionParser::ParseError => e
|
24
|
+
raise CommandLineError, e.message
|
25
|
+
end
|
26
|
+
|
27
|
+
def help
|
28
|
+
@parser.summarize.join.chomp
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
@@ -0,0 +1,39 @@
|
|
1
|
+
# coding: utf-8
|
2
|
+
|
3
|
+
module Tweetwine
|
4
|
+
# Lazy evaluation via proxy object.
|
5
|
+
#
|
6
|
+
# A naive implementation, but it's enough.
|
7
|
+
#
|
8
|
+
# Adapted from the book Ruby Best Practices, by Gregory Brown.
|
9
|
+
class Promise < BasicObject
|
10
|
+
def initialize(&action)
|
11
|
+
@action = action
|
12
|
+
end
|
13
|
+
|
14
|
+
def __result__
|
15
|
+
if @action
|
16
|
+
@result = @action.call
|
17
|
+
@action = nil
|
18
|
+
end
|
19
|
+
@result
|
20
|
+
end
|
21
|
+
|
22
|
+
def inspect
|
23
|
+
if @action
|
24
|
+
"#<Tweetwine::Promise action=#{@action.inspect}>"
|
25
|
+
else
|
26
|
+
@result.inspect
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
def respond_to?(method)
|
31
|
+
method = method.to_sym
|
32
|
+
[:__result__, :inspect].include?(method) || __result__.respond_to?(method)
|
33
|
+
end
|
34
|
+
|
35
|
+
def method_missing(*args, &blk)
|
36
|
+
__result__.__send__(*args, &blk)
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
@@ -0,0 +1,211 @@
|
|
1
|
+
# coding: utf-8
|
2
|
+
|
3
|
+
require "json"
|
4
|
+
require "uri"
|
5
|
+
|
6
|
+
module Tweetwine
|
7
|
+
class Twitter
|
8
|
+
DEFAULT_NUM_STATUSES = 20
|
9
|
+
DEFAULT_PAGE_NUM = 1
|
10
|
+
MAX_STATUS_LENGTH = 140
|
11
|
+
|
12
|
+
attr_reader :num_statuses, :page, :username
|
13
|
+
|
14
|
+
def initialize(options = {})
|
15
|
+
@num_statuses = Util.parse_int_gt(options[:num_statuses], DEFAULT_NUM_STATUSES, 1, "number of statuses to show")
|
16
|
+
@page = Util.parse_int_gt(options[:page], DEFAULT_PAGE_NUM, 1, "page number")
|
17
|
+
@username = options[:username].to_s
|
18
|
+
end
|
19
|
+
|
20
|
+
def followers
|
21
|
+
response = get_from_rest_api "statuses/followers"
|
22
|
+
show_users_from_rest_api(*response)
|
23
|
+
end
|
24
|
+
|
25
|
+
def friends
|
26
|
+
response = get_from_rest_api "statuses/friends"
|
27
|
+
show_users_from_rest_api(*response)
|
28
|
+
end
|
29
|
+
|
30
|
+
def home
|
31
|
+
response = get_from_rest_api "statuses/home_timeline"
|
32
|
+
show_statuses_from_rest_api(*response)
|
33
|
+
end
|
34
|
+
|
35
|
+
def mentions
|
36
|
+
response = get_from_rest_api "statuses/mentions"
|
37
|
+
show_statuses_from_rest_api(*response)
|
38
|
+
end
|
39
|
+
|
40
|
+
def search(words = [], operator = nil)
|
41
|
+
raise ArgumentError, "No search words" if words.empty?
|
42
|
+
operator = :and unless operator
|
43
|
+
query = operator == :and ? words.join(' ') : words.join(' OR ')
|
44
|
+
response = get_from_search_api query
|
45
|
+
show_statuses_from_search_api(*response["results"])
|
46
|
+
end
|
47
|
+
|
48
|
+
def update(msg = nil)
|
49
|
+
new_status = create_status_update(msg)
|
50
|
+
completed = false
|
51
|
+
unless new_status.empty?
|
52
|
+
CLI.ui.show_status_preview(new_status)
|
53
|
+
status_in_utf8 = CharacterEncoding.to_utf8 new_status
|
54
|
+
if CLI.ui.confirm("Really send?")
|
55
|
+
response = post_to_rest_api("statuses/update", :status => status_in_utf8)
|
56
|
+
CLI.ui.info "Sent status update.\n\n"
|
57
|
+
show_statuses_from_rest_api response
|
58
|
+
completed = true
|
59
|
+
end
|
60
|
+
end
|
61
|
+
CLI.ui.info "Cancelled." unless completed
|
62
|
+
end
|
63
|
+
|
64
|
+
def user(who = username)
|
65
|
+
response = get_from_rest_api(
|
66
|
+
"statuses/user_timeline",
|
67
|
+
common_rest_api_query_params.merge!({ :screen_name => who })
|
68
|
+
)
|
69
|
+
show_statuses_from_rest_api(*response)
|
70
|
+
end
|
71
|
+
|
72
|
+
private
|
73
|
+
|
74
|
+
def common_rest_api_query_params
|
75
|
+
{
|
76
|
+
:count => @num_statuses,
|
77
|
+
:page => @page
|
78
|
+
}
|
79
|
+
end
|
80
|
+
|
81
|
+
def common_search_api_query_params
|
82
|
+
{
|
83
|
+
:rpp => @num_statuses,
|
84
|
+
:page => @page
|
85
|
+
}
|
86
|
+
end
|
87
|
+
|
88
|
+
def format_query_params(params)
|
89
|
+
params.each_pair.map { |k, v| "#{k}=#{v}" }.sort.join('&')
|
90
|
+
end
|
91
|
+
|
92
|
+
def rest_api
|
93
|
+
@rest_api ||= CLI.http.as_resource "https://api.twitter.com/1"
|
94
|
+
end
|
95
|
+
|
96
|
+
def search_api
|
97
|
+
@search_api ||= CLI.http.as_resource "http://search.twitter.com"
|
98
|
+
end
|
99
|
+
|
100
|
+
def get_from_rest_api(sub_url, params = common_rest_api_query_params)
|
101
|
+
query = format_query_params(params)
|
102
|
+
url_suffix = query.empty? ? "" : "?" << query
|
103
|
+
resource = rest_api["#{sub_url}.json#{url_suffix}"]
|
104
|
+
authorize_on_demand do
|
105
|
+
JSON.parse resource.get(&CLI.oauth.request_signer)
|
106
|
+
end
|
107
|
+
end
|
108
|
+
|
109
|
+
def post_to_rest_api(sub_url, payload)
|
110
|
+
resource = rest_api["#{sub_url}.json"]
|
111
|
+
authorize_on_demand do
|
112
|
+
JSON.parse resource.post(payload, &CLI.oauth.request_signer)
|
113
|
+
end
|
114
|
+
end
|
115
|
+
|
116
|
+
def get_from_search_api(query, params = common_search_api_query_params)
|
117
|
+
query = "q=#{Util.percent_encode(query)}&" << format_query_params(params)
|
118
|
+
JSON.parse search_api["search.json?#{query}"].get
|
119
|
+
end
|
120
|
+
|
121
|
+
def authorize_on_demand
|
122
|
+
yield
|
123
|
+
rescue HttpError => e
|
124
|
+
if e.http_code == 401
|
125
|
+
CLI.oauth.authorize { |access_token| save_config_with_access_token(access_token) }
|
126
|
+
retry
|
127
|
+
else
|
128
|
+
raise
|
129
|
+
end
|
130
|
+
end
|
131
|
+
|
132
|
+
def save_config_with_access_token(token)
|
133
|
+
CLI.config[:oauth_access] = token
|
134
|
+
CLI.config.save
|
135
|
+
end
|
136
|
+
|
137
|
+
def show_statuses_from_rest_api(*responses)
|
138
|
+
show_records(
|
139
|
+
responses,
|
140
|
+
{
|
141
|
+
:from_user => ["user", "screen_name"],
|
142
|
+
:to_user => "in_reply_to_screen_name",
|
143
|
+
:created_at => "created_at",
|
144
|
+
:status => "text"
|
145
|
+
}
|
146
|
+
)
|
147
|
+
end
|
148
|
+
|
149
|
+
def show_users_from_rest_api(*responses)
|
150
|
+
show_records(
|
151
|
+
responses,
|
152
|
+
{
|
153
|
+
:from_user => "screen_name",
|
154
|
+
:to_user => ["status", "in_reply_to_screen_name"],
|
155
|
+
:created_at => ["status", "created_at"],
|
156
|
+
:status => ["status", "text"]
|
157
|
+
}
|
158
|
+
)
|
159
|
+
end
|
160
|
+
|
161
|
+
def show_statuses_from_search_api(*responses)
|
162
|
+
show_records(
|
163
|
+
responses,
|
164
|
+
{
|
165
|
+
:from_user => "from_user",
|
166
|
+
:to_user => "to_user",
|
167
|
+
:created_at => "created_at",
|
168
|
+
:status => "text"
|
169
|
+
}
|
170
|
+
)
|
171
|
+
end
|
172
|
+
|
173
|
+
def show_records(twitter_records, paths)
|
174
|
+
twitter_records.each do |twitter_record|
|
175
|
+
internal_record = [ :from_user, :to_user, :created_at, :status ].inject({}) do |result, key|
|
176
|
+
result[key] = Util.find_hash_path(twitter_record, paths[key])
|
177
|
+
result
|
178
|
+
end
|
179
|
+
CLI.ui.show_record(internal_record)
|
180
|
+
end
|
181
|
+
end
|
182
|
+
|
183
|
+
def create_status_update(status)
|
184
|
+
status = if Util.blank? status
|
185
|
+
CLI.ui.prompt("Status update")
|
186
|
+
else
|
187
|
+
status.dup
|
188
|
+
end
|
189
|
+
status.strip!
|
190
|
+
shorten_urls_in(status) if CLI.config[:shorten_urls] && !CLI.config[:shorten_urls][:disable]
|
191
|
+
truncate_status(status) if status.length > MAX_STATUS_LENGTH
|
192
|
+
status
|
193
|
+
end
|
194
|
+
|
195
|
+
def shorten_urls_in(status)
|
196
|
+
url_pairs = URI.
|
197
|
+
extract(status, %w{http http}).
|
198
|
+
uniq.
|
199
|
+
map { |full_url| [full_url, CLI.url_shortener.shorten(full_url)] }.
|
200
|
+
reject { |(full_url, short_url)| Util.blank? short_url }
|
201
|
+
url_pairs.each { |(full_url, short_url)| status.gsub!(full_url, short_url) }
|
202
|
+
rescue HttpError, LoadError => e
|
203
|
+
CLI.ui.warn "#{e}\nSkipping URL shortening..."
|
204
|
+
end
|
205
|
+
|
206
|
+
def truncate_status(status)
|
207
|
+
status.replace status[0...MAX_STATUS_LENGTH]
|
208
|
+
CLI.ui.warn("Status will be truncated.")
|
209
|
+
end
|
210
|
+
end
|
211
|
+
end
|
@@ -1,47 +1,56 @@
|
|
1
1
|
# coding: utf-8
|
2
2
|
|
3
|
-
require "strscan"
|
4
3
|
require "uri"
|
5
4
|
|
6
5
|
module Tweetwine
|
7
|
-
class
|
6
|
+
class UI
|
8
7
|
COLOR_CODES = {
|
9
8
|
:cyan => 36,
|
10
9
|
:green => 32,
|
11
10
|
:magenta => 35,
|
12
11
|
:yellow => 33
|
13
|
-
}
|
14
|
-
|
12
|
+
}.freeze
|
15
13
|
HASHTAG_REGEX = /#[\w-]+/
|
16
14
|
USERNAME_REGEX = /^(@\w+)|\s+(@\w+)/
|
17
15
|
|
18
|
-
def initialize(options)
|
19
|
-
@
|
20
|
-
@
|
21
|
-
@
|
16
|
+
def initialize(options = {})
|
17
|
+
@in = options[:in] || $stdin
|
18
|
+
@out = options[:out] || $stdout
|
19
|
+
@err = options[:err] || $stderr
|
20
|
+
@colors = options[:colors] || false
|
22
21
|
end
|
23
22
|
|
24
|
-
def
|
25
|
-
|
26
|
-
|
23
|
+
def info(start_msg = "\n", end_msg = " done.")
|
24
|
+
if block_given?
|
25
|
+
@out.print start_msg
|
26
|
+
yield
|
27
|
+
@out.puts end_msg
|
28
|
+
else
|
29
|
+
@out.puts start_msg
|
30
|
+
end
|
27
31
|
end
|
28
32
|
|
29
|
-
def
|
30
|
-
@
|
33
|
+
def error(msg)
|
34
|
+
@err.puts "ERROR: #{msg}"
|
31
35
|
end
|
32
36
|
|
33
37
|
def warn(msg)
|
34
|
-
@
|
38
|
+
@out.puts "Warning: #{msg}"
|
39
|
+
end
|
40
|
+
|
41
|
+
def prompt(prompt)
|
42
|
+
@out.print "#{prompt}: "
|
43
|
+
@in.gets.strip!
|
35
44
|
end
|
36
45
|
|
37
46
|
def confirm(msg)
|
38
|
-
@
|
39
|
-
confirmation = @
|
47
|
+
@out.print "#{msg} [yN] "
|
48
|
+
confirmation = @in.gets.strip
|
40
49
|
confirmation.downcase[0, 1] == "y"
|
41
50
|
end
|
42
51
|
|
43
52
|
def show_status_preview(status)
|
44
|
-
@
|
53
|
+
@out.puts <<-END
|
45
54
|
|
46
55
|
#{format_status(status)}
|
47
56
|
|
@@ -73,14 +82,14 @@ module Tweetwine
|
|
73
82
|
end
|
74
83
|
|
75
84
|
def show_record_as_user(record)
|
76
|
-
@
|
85
|
+
@out.puts <<-END
|
77
86
|
#{format_user(record[:from_user])}
|
78
87
|
|
79
88
|
END
|
80
89
|
end
|
81
90
|
|
82
91
|
def show_record_as_user_with_status(record)
|
83
|
-
@
|
92
|
+
@out.puts <<-END
|
84
93
|
#{format_record_header(record[:from_user], record[:to_user], record[:created_at])}
|
85
94
|
#{format_status(record[:status])}
|
86
95
|
|
@@ -97,7 +106,7 @@ module Tweetwine
|
|
97
106
|
if @colors
|
98
107
|
status = colorize_matching(:yellow, status, USERNAME_REGEX)
|
99
108
|
status = colorize_matching(:magenta, status, HASHTAG_REGEX)
|
100
|
-
status = colorize_matching(:cyan, status, URI.extract(status,
|
109
|
+
status = colorize_matching(:cyan, status, URI.extract(status, %w{http https}).uniq)
|
101
110
|
end
|
102
111
|
status
|
103
112
|
end
|
@@ -122,7 +131,7 @@ module Tweetwine
|
|
122
131
|
when Regexp
|
123
132
|
pattern
|
124
133
|
else
|
125
|
-
raise "
|
134
|
+
raise "unknown kind of pattern"
|
126
135
|
end
|
127
136
|
Util.str_gsub_by_group(str, regexp) { |s| colorize(color, s) }
|
128
137
|
end
|