tweetwine 0.2.12 → 0.3.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
|