tweetwine 0.2.12 → 0.3.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (67) hide show
  1. data/CHANGELOG.rdoc +7 -0
  2. data/Gemfile +17 -0
  3. data/README.md +57 -47
  4. data/Rakefile +17 -26
  5. data/bin/tweetwine +11 -12
  6. data/contrib/tweetwine-completion.bash +2 -3
  7. data/example/application_behavior_example.rb +173 -0
  8. data/example/example_helper.rb +44 -28
  9. data/example/fixture/config.yaml +8 -0
  10. data/example/fixture/shorten_rubygems.html +5 -0
  11. data/example/fixture/shorten_rubylang.html +5 -0
  12. data/example/fixture/update_utf8.json +1 -0
  13. data/example/fixture/update_with_urls.json +1 -0
  14. data/example/fixture/{update.json → update_without_urls.json} +0 -0
  15. data/example/search_statuses_example.rb +49 -16
  16. data/example/show_followers_example.rb +7 -8
  17. data/example/show_friends_example.rb +7 -8
  18. data/example/show_home_example.rb +19 -16
  19. data/example/show_mentions_example.rb +8 -9
  20. data/example/show_user_example.rb +16 -13
  21. data/example/update_status_example.rb +143 -26
  22. data/example/use_http_proxy_example.rb +40 -20
  23. data/lib/tweetwine/basic_object.rb +19 -0
  24. data/lib/tweetwine/character_encoding.rb +59 -0
  25. data/lib/tweetwine/cli.rb +354 -230
  26. data/lib/tweetwine/config.rb +65 -0
  27. data/lib/tweetwine/http.rb +120 -0
  28. data/lib/tweetwine/oauth.rb +104 -0
  29. data/lib/tweetwine/obfuscate.rb +21 -0
  30. data/lib/tweetwine/option_parser.rb +31 -0
  31. data/lib/tweetwine/promise.rb +39 -0
  32. data/lib/tweetwine/twitter.rb +211 -0
  33. data/lib/tweetwine/{io.rb → ui.rb} +30 -21
  34. data/lib/tweetwine/url_shortener.rb +15 -9
  35. data/lib/tweetwine/util.rb +30 -15
  36. data/lib/tweetwine.rb +72 -12
  37. data/man/tweetwine.7 +43 -69
  38. data/man/tweetwine.7.ronn +57 -47
  39. data/test/character_encoding_test.rb +87 -0
  40. data/test/cli_test.rb +19 -6
  41. data/test/config_test.rb +244 -0
  42. data/test/fixture/oauth.rb +21 -0
  43. data/test/fixture/test_config.yaml +4 -4
  44. data/test/http_test.rb +199 -0
  45. data/test/oauth_test.rb +77 -0
  46. data/test/obfuscate_test.rb +16 -0
  47. data/test/option_parser_test.rb +60 -0
  48. data/test/promise_test.rb +56 -0
  49. data/test/test_helper.rb +76 -8
  50. data/test/twitter_test.rb +625 -0
  51. data/test/{io_test.rb → ui_test.rb} +92 -74
  52. data/test/url_shortener_test.rb +115 -135
  53. data/test/util_test.rb +136 -85
  54. data/tweetwine.gemspec +53 -0
  55. metadata +112 -56
  56. data/example/show_metadata_example.rb +0 -86
  57. data/lib/tweetwine/client.rb +0 -187
  58. data/lib/tweetwine/meta.rb +0 -5
  59. data/lib/tweetwine/options.rb +0 -24
  60. data/lib/tweetwine/retrying_http.rb +0 -99
  61. data/lib/tweetwine/startup_config.rb +0 -50
  62. data/man/tweetwine.1 +0 -109
  63. data/man/tweetwine.1.ronn +0 -69
  64. data/test/client_test.rb +0 -544
  65. data/test/options_test.rb +0 -45
  66. data/test/retrying_http_test.rb +0 -147
  67. 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 IO
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
- @input = options[:input] || $stdin
20
- @output = options[:output] || $stdout
21
- @colors = options[:colors] || false
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 prompt(prompt)
25
- @output.print "#{prompt}: "
26
- @input.gets.strip!
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 info(msg)
30
- @output.puts(msg)
33
+ def error(msg)
34
+ @err.puts "ERROR: #{msg}"
31
35
  end
32
36
 
33
37
  def warn(msg)
34
- @output.puts "Warning: #{msg}"
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
- @output.print "#{msg} [yN] "
39
- confirmation = @input.gets.strip
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
- @output.puts <<-END
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
- @output.puts <<-END
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
- @output.puts <<-END
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, ["http", "https"]).uniq)
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 "Unknown kind of pattern"
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