tweetwine 0.2.5 → 0.2.7
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 +30 -17
- data/README.rdoc +16 -17
- data/Rakefile +2 -0
- data/bin/tweetwine +3 -68
- data/example/example_helper.rb +31 -41
- data/example/fixtures/{statuses.json → home.json} +0 -0
- data/example/fixtures/mentions.json +1 -0
- data/example/fixtures/search.json +1 -0
- data/example/fixtures/update.json +1 -0
- data/example/fixtures/user.json +1 -0
- data/example/fixtures/users.json +1 -0
- data/example/search_statuses_example.rb +36 -0
- data/example/show_followers_example.rb +23 -0
- data/example/show_friends_example.rb +23 -0
- data/example/show_home_example.rb +51 -0
- data/example/show_mentions_example.rb +23 -0
- data/example/show_metadata_example.rb +54 -7
- data/example/show_user_example.rb +37 -0
- data/example/update_status_example.rb +65 -0
- data/lib/tweetwine/cli.rb +241 -0
- data/lib/tweetwine/client.rb +94 -57
- data/lib/tweetwine/io.rb +39 -28
- data/lib/tweetwine/meta.rb +1 -1
- data/lib/tweetwine/retrying_http.rb +93 -0
- data/lib/tweetwine/startup_config.rb +14 -15
- data/lib/tweetwine/url_shortener.rb +13 -8
- data/lib/tweetwine/util.rb +14 -0
- data/lib/tweetwine.rb +2 -1
- data/test/cli_test.rb +16 -0
- data/test/client_test.rb +275 -205
- data/test/fixtures/test_config.yaml +2 -1
- data/test/io_test.rb +89 -62
- data/test/retrying_http_test.rb +127 -0
- data/test/startup_config_test.rb +52 -27
- data/test/test_helper.rb +32 -17
- data/test/url_shortener_test.rb +18 -18
- data/test/util_test.rb +145 -47
- metadata +20 -7
- data/example/show_latest_statuses_example.rb +0 -45
- data/lib/tweetwine/rest_client_wrapper.rb +0 -37
- data/test/rest_client_wrapper_test.rb +0 -68
data/lib/tweetwine/io.rb
CHANGED
@@ -15,7 +15,7 @@ module Tweetwine
|
|
15
15
|
def initialize(options)
|
16
16
|
@input = options[:input] || $stdin
|
17
17
|
@output = options[:output] || $stdout
|
18
|
-
@
|
18
|
+
@colors = options[:colors] || false
|
19
19
|
end
|
20
20
|
|
21
21
|
def prompt(prompt)
|
@@ -34,7 +34,7 @@ module Tweetwine
|
|
34
34
|
def confirm(msg)
|
35
35
|
@output.print "#{msg} [yN] "
|
36
36
|
confirmation = @input.gets.strip
|
37
|
-
confirmation.downcase[0,1] == "y"
|
37
|
+
confirmation.downcase[0, 1] == "y"
|
38
38
|
end
|
39
39
|
|
40
40
|
def show_status_preview(status)
|
@@ -46,6 +46,7 @@ module Tweetwine
|
|
46
46
|
end
|
47
47
|
|
48
48
|
def show_record(record)
|
49
|
+
clean_record!(record)
|
49
50
|
if record[:status]
|
50
51
|
show_record_as_user_with_status(record)
|
51
52
|
else
|
@@ -55,63 +56,73 @@ module Tweetwine
|
|
55
56
|
|
56
57
|
private
|
57
58
|
|
59
|
+
def clean_record!(record)
|
60
|
+
record.each_pair do |key, value|
|
61
|
+
if value.is_a? Hash
|
62
|
+
clean_record!(value)
|
63
|
+
else
|
64
|
+
unless value.nil?
|
65
|
+
value = value.to_s
|
66
|
+
record[key] = value.empty? ? nil : value
|
67
|
+
end
|
68
|
+
end
|
69
|
+
end
|
70
|
+
end
|
71
|
+
|
58
72
|
def show_record_as_user(record)
|
59
73
|
@output.puts <<-END
|
60
|
-
#{format_user(record[:
|
74
|
+
#{format_user(record[:from_user])}
|
61
75
|
|
62
76
|
END
|
63
77
|
end
|
64
78
|
|
65
79
|
def show_record_as_user_with_status(record)
|
66
80
|
@output.puts <<-END
|
67
|
-
#{format_record_header(record)}
|
68
|
-
#{format_status(record[:status]
|
81
|
+
#{format_record_header(record[:from_user], record[:to_user], record[:created_at])}
|
82
|
+
#{format_status(record[:status])}
|
69
83
|
|
70
84
|
END
|
71
85
|
end
|
72
86
|
|
73
87
|
def format_user(user)
|
74
|
-
user = user
|
75
|
-
colorize!(:green, user) if @colorize
|
88
|
+
user = colorize(:green, user) if @colors
|
76
89
|
user
|
77
90
|
end
|
78
91
|
|
79
92
|
def format_status(status)
|
80
|
-
|
81
|
-
|
82
|
-
colorize_all_by_group
|
83
|
-
colorize_all_by_group!(:magenta, status, HASHTAG_REGEX)
|
93
|
+
if @colors
|
94
|
+
status = colorize_all_by_group(:yellow, status, USERNAME_REGEX)
|
95
|
+
status = colorize_all_by_group(:magenta, status, HASHTAG_REGEX)
|
84
96
|
URI.extract(status, ["http", "https"]).uniq.each do |url|
|
85
|
-
colorize_all
|
97
|
+
status = colorize_all(:cyan, status, url)
|
86
98
|
end
|
87
99
|
end
|
88
100
|
status
|
89
101
|
end
|
90
102
|
|
91
|
-
def format_record_header(
|
92
|
-
time_diff_value, time_diff_unit = Util.humanize_time_diff(
|
93
|
-
|
94
|
-
|
95
|
-
|
96
|
-
|
97
|
-
|
98
|
-
"in reply to #{
|
103
|
+
def format_record_header(from_user, to_user, created_at)
|
104
|
+
time_diff_value, time_diff_unit = Util.humanize_time_diff(created_at, Time.now)
|
105
|
+
if @colors
|
106
|
+
from_user = colorize(:green, from_user)
|
107
|
+
to_user = colorize(:green, to_user) if to_user
|
108
|
+
end
|
109
|
+
if to_user
|
110
|
+
"#{from_user}, in reply to #{to_user}, #{time_diff_value} #{time_diff_unit} ago:"
|
99
111
|
else
|
100
|
-
""
|
112
|
+
"#{from_user}, #{time_diff_value} #{time_diff_unit} ago:"
|
101
113
|
end
|
102
|
-
"#{from_user}, #{in_reply_to}#{time_diff_value} #{time_diff_unit} ago:"
|
103
114
|
end
|
104
115
|
|
105
|
-
def colorize_all
|
106
|
-
str.gsub
|
116
|
+
def colorize_all(color, str, pattern)
|
117
|
+
str.gsub(pattern) { |s| colorize_str(COLOR_CODES[color.to_sym], s) }
|
107
118
|
end
|
108
119
|
|
109
|
-
def colorize_all_by_group
|
110
|
-
|
120
|
+
def colorize_all_by_group(color, str, pattern)
|
121
|
+
Util.str_gsub_by_group(str, pattern) { |s| colorize_str(COLOR_CODES[color.to_sym], s) }
|
111
122
|
end
|
112
123
|
|
113
|
-
def colorize
|
114
|
-
|
124
|
+
def colorize(color, str)
|
125
|
+
colorize_str(COLOR_CODES[color.to_sym], str)
|
115
126
|
end
|
116
127
|
|
117
128
|
def colorize_str(color_code, str)
|
data/lib/tweetwine/meta.rb
CHANGED
@@ -0,0 +1,93 @@
|
|
1
|
+
require "rest_client"
|
2
|
+
|
3
|
+
module Tweetwine
|
4
|
+
class HttpError < RuntimeError; end
|
5
|
+
|
6
|
+
module RetryingHttp
|
7
|
+
class Base
|
8
|
+
MAX_RETRIES = 3
|
9
|
+
RETRY_BASE_WAIT_TIMEOUT = 4
|
10
|
+
|
11
|
+
def self.use_retries_with(*methods)
|
12
|
+
methods.each do |method_name|
|
13
|
+
module_eval do
|
14
|
+
non_retrying_method_name = "original_#{method_name}".to_sym
|
15
|
+
alias_method non_retrying_method_name, method_name
|
16
|
+
define_method(method_name) do |*args|
|
17
|
+
do_with_retries { send(non_retrying_method_name, *args) }
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
private
|
24
|
+
|
25
|
+
def do_with_retries
|
26
|
+
tries = 0
|
27
|
+
begin
|
28
|
+
tries += 1
|
29
|
+
yield
|
30
|
+
rescue Errno::ECONNRESET => e
|
31
|
+
if tries < MAX_RETRIES
|
32
|
+
timeout = RETRY_BASE_WAIT_TIMEOUT**tries
|
33
|
+
@io.warn("Could not connect -- retrying in #{timeout} seconds") if @io
|
34
|
+
sleep timeout
|
35
|
+
retry
|
36
|
+
else
|
37
|
+
raise HttpError, e
|
38
|
+
end
|
39
|
+
rescue RestClient::Exception, SocketError, SystemCallError => e
|
40
|
+
raise HttpError, e
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
class Client < Base
|
46
|
+
attr_accessor :io
|
47
|
+
|
48
|
+
def initialize(io)
|
49
|
+
@io = io
|
50
|
+
end
|
51
|
+
|
52
|
+
def get(*args)
|
53
|
+
RestClient.get(*args)
|
54
|
+
end
|
55
|
+
|
56
|
+
def post(*args)
|
57
|
+
RestClient.post(*args)
|
58
|
+
end
|
59
|
+
|
60
|
+
def as_resource(url, options = {})
|
61
|
+
resource = Resource.new(RestClient::Resource.new(url, options))
|
62
|
+
resource.io = @io
|
63
|
+
resource
|
64
|
+
end
|
65
|
+
|
66
|
+
use_retries_with :get, :post
|
67
|
+
end
|
68
|
+
|
69
|
+
class Resource < Base
|
70
|
+
attr_accessor :io
|
71
|
+
|
72
|
+
def initialize(wrapped_resource)
|
73
|
+
@wrapped = wrapped_resource
|
74
|
+
end
|
75
|
+
|
76
|
+
def [](suburl)
|
77
|
+
instance = self.class.new(@wrapped[suburl])
|
78
|
+
instance.io = @io
|
79
|
+
instance
|
80
|
+
end
|
81
|
+
|
82
|
+
def get(*args)
|
83
|
+
@wrapped.get(*args)
|
84
|
+
end
|
85
|
+
|
86
|
+
def post(*args)
|
87
|
+
@wrapped.post(*args)
|
88
|
+
end
|
89
|
+
|
90
|
+
use_retries_with :get, :post
|
91
|
+
end
|
92
|
+
end
|
93
|
+
end
|
@@ -2,34 +2,33 @@ require "yaml"
|
|
2
2
|
|
3
3
|
module Tweetwine
|
4
4
|
class StartupConfig
|
5
|
-
attr_reader :options, :command
|
5
|
+
attr_reader :options, :command
|
6
6
|
|
7
|
-
def initialize(supported_commands)
|
8
|
-
|
9
|
-
raise ArgumentError, "
|
10
|
-
@
|
11
|
-
@command =
|
12
|
-
@args = []
|
7
|
+
def initialize(supported_commands, default_command, default_opts = {})
|
8
|
+
raise ArgumentError, "Must give at least one supported command" if supported_commands.empty?
|
9
|
+
raise ArgumentError, "Default command is not a supported command" unless supported_commands.include? default_command
|
10
|
+
@supported_commands, @default_command = supported_commands, default_command
|
11
|
+
@options, @command = default_opts, nil
|
13
12
|
end
|
14
13
|
|
15
|
-
def parse(args = [], config_file = nil, &
|
16
|
-
options = parse_options(args, config_file, &
|
17
|
-
command = if args.empty? then @
|
14
|
+
def parse(args = [], config_file = nil, &cmd_option_parser)
|
15
|
+
options = @options.merge(parse_options(args, config_file, &cmd_option_parser))
|
16
|
+
command = if args.empty? then @default_command else args.shift.to_sym end
|
18
17
|
raise ArgumentError, "Unknown command" unless @supported_commands.include? command
|
19
|
-
@options, @command
|
18
|
+
@options, @command = options, command
|
20
19
|
self
|
21
20
|
end
|
22
21
|
|
23
22
|
private
|
24
23
|
|
25
|
-
def parse_options(args, config_file, &
|
26
|
-
cmd_options = if
|
24
|
+
def parse_options(args, config_file, &cmd_option_parser)
|
25
|
+
cmd_options = if cmd_option_parser then parse_cmdline_args(args, &cmd_option_parser) else {} end
|
27
26
|
config_options = if config_file && File.exists?(config_file) then parse_config_file(config_file) else {} end
|
28
27
|
config_options.merge(cmd_options)
|
29
28
|
end
|
30
29
|
|
31
|
-
def parse_cmdline_args(args, &
|
32
|
-
|
30
|
+
def parse_cmdline_args(args, &cmd_option_parser)
|
31
|
+
cmd_option_parser.call(args)
|
33
32
|
end
|
34
33
|
|
35
34
|
def parse_config_file(config_file)
|
@@ -1,7 +1,7 @@
|
|
1
1
|
module Tweetwine
|
2
2
|
class UrlShortener
|
3
|
-
def initialize(
|
4
|
-
@
|
3
|
+
def initialize(http_client, options)
|
4
|
+
@http_client = http_client
|
5
5
|
options = Options.new(options, "URL shortening")
|
6
6
|
@method = (options[:method] || :get).to_sym
|
7
7
|
@service_url = options.require :service_url
|
@@ -17,22 +17,27 @@ module Tweetwine
|
|
17
17
|
|
18
18
|
def shorten(url)
|
19
19
|
require "nokogiri"
|
20
|
-
|
20
|
+
response = @http_client.send(@method, *get_service_url_and_params(url))
|
21
|
+
doc = Nokogiri::HTML(response)
|
22
|
+
doc.xpath(@xpath_selector).first.to_s
|
23
|
+
end
|
24
|
+
|
25
|
+
private
|
26
|
+
|
27
|
+
def get_service_url_and_params(url_to_shorten)
|
28
|
+
case @method
|
21
29
|
when :get
|
22
30
|
tmp = @extra_params.dup
|
23
|
-
tmp << "#{@url_param_name}=#{
|
31
|
+
tmp << "#{@url_param_name}=#{url_to_shorten}"
|
24
32
|
service_url = "#{@service_url}?#{tmp.join('&')}"
|
25
33
|
[service_url]
|
26
34
|
when :post
|
27
35
|
service_url = @service_url
|
28
|
-
params = @extra_params.merge({ @url_param_name.to_sym =>
|
36
|
+
params = @extra_params.merge({ @url_param_name.to_sym => url_to_shorten })
|
29
37
|
[service_url, params]
|
30
38
|
else
|
31
39
|
raise "Unrecognized HTTP request method"
|
32
40
|
end
|
33
|
-
response = @rest_client.send(@method, *rest)
|
34
|
-
doc = Nokogiri::HTML(response)
|
35
|
-
doc.xpath(@xpath_selector).first.to_s
|
36
41
|
end
|
37
42
|
end
|
38
43
|
end
|
data/lib/tweetwine/util.rb
CHANGED
@@ -1,4 +1,5 @@
|
|
1
1
|
require "time"
|
2
|
+
require "uri"
|
2
3
|
|
3
4
|
module Tweetwine
|
4
5
|
module Util
|
@@ -59,6 +60,19 @@ module Tweetwine
|
|
59
60
|
dup_str
|
60
61
|
end
|
61
62
|
|
63
|
+
def self.percent_encode(str)
|
64
|
+
URI.escape(str.to_s, /[^#{URI::PATTERN::UNRESERVED}]/)
|
65
|
+
end
|
66
|
+
|
67
|
+
def self.find_hash_path(hash, path)
|
68
|
+
return nil if hash.nil?
|
69
|
+
path = [path] if !path.is_a? Array
|
70
|
+
path.inject(hash) do |result, key|
|
71
|
+
return hash.default if key.nil? || result.nil?
|
72
|
+
result[key]
|
73
|
+
end
|
74
|
+
end
|
75
|
+
|
62
76
|
private
|
63
77
|
|
64
78
|
def self.pluralize_unit(value, unit)
|
data/lib/tweetwine.rb
CHANGED
data/test/cli_test.rb
ADDED
@@ -0,0 +1,16 @@
|
|
1
|
+
require "test_helper"
|
2
|
+
|
3
|
+
module Tweetwine
|
4
|
+
|
5
|
+
class CLITest < Test::Unit::TestCase
|
6
|
+
context "A CLI, upon initialization" do
|
7
|
+
should "disallow using #new to create a new instance" do
|
8
|
+
assert_raise(NoMethodError) { CLI.new("-v", "test", "") {} }
|
9
|
+
end
|
10
|
+
end
|
11
|
+
|
12
|
+
# Other unit tests are meaningless. See /example directory for tests
|
13
|
+
# about the functionality of the application.
|
14
|
+
end
|
15
|
+
|
16
|
+
end
|