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.
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