tuomas-tweetwine 0.1.11 → 0.2.1

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 CHANGED
@@ -1,3 +1,14 @@
1
+ === 0.2.1 released 2009-08-17
2
+
3
+ * Command line option "-v" show version information.
4
+ * Slight implementation code cleaning.
5
+
6
+ === 0.2.0 released 2009-08-17
7
+
8
+ * URL shortening by using an external web service.
9
+ * Show a preview before sending status update.
10
+ * Avoid stack trace upon erroneous connection.
11
+
1
12
  === 0.1.11 released 2009-08-10
2
13
 
3
14
  * Fixed highlighting multiple nicks in statuses.
data/README.rdoc CHANGED
@@ -8,7 +8,8 @@ Install Tweetwine as a RubyGem from GitHub:
8
8
 
9
9
  $ sudo gem install tuomas-tweetwine --source http://gems.github.com
10
10
 
11
- The program is compatible with Ruby 1.9.1.
11
+ The program is compatible with Ruby 1.9.1, and it requires <b>rest_client</b>
12
+ gem to be installed. In addition, <b>json</b> gem is needed for Ruby 1.8.6.
12
13
 
13
14
  == Usage
14
15
 
@@ -22,7 +23,7 @@ information can be supplied either via a configuration file or as an option
22
23
  former method over the latter.
23
24
 
24
25
  The configuration file, in <tt>~/.tweetwine</tt>, is in YAML syntax. The
25
- program recognizes the following settings:
26
+ program recognizes the following basic settings:
26
27
 
27
28
  username: <your_username>
28
29
  password: <your_password>
@@ -43,6 +44,39 @@ For all the options, see:
43
44
 
44
45
  $ tweetwine -h
45
46
 
47
+ === URL shortening for status update
48
+
49
+ Before actually sending a status update, it is possible for the software to
50
+ shorten the URLs in the update by using an external web service. This can be
51
+ enabled via the <tt>shorten_urls</tt> key in configuration file; for
52
+ example:
53
+
54
+ username: spoonman
55
+ password: withyourhands
56
+ colorize: true
57
+ shorten_urls:
58
+ enable: true
59
+ service_url: http://is.gd/create.php
60
+ method: post
61
+ url_param_name: URL
62
+ xpath_selector: //input[@id='short_url']/@value
63
+
64
+ The supported methods are <tt>get</tt> and <tt>post</tt>. The method chosen
65
+ affects whether parameters are passed as URL query parameters or as payload
66
+ in the HTTP request, respectively. Extra parameters can be given via
67
+ <tt>extra_params</tt> key, as a hash.
68
+
69
+ The <tt>xpath_selector</tt> is needed to extract the shortened URL from the
70
+ result.
71
+
72
+ The feature can be disabled by
73
+
74
+ * not defining <tt>shorten_urls</tt> key in the configuration file,
75
+ * setting key <tt>enable</tt> to <tt>false</tt>, or
76
+ * using the command line option <tt>--no-url-shorten</tt>.
77
+
78
+ The use of the feature requires <b>nokogiri</b> gem to be installed.
79
+
46
80
  == Contacting
47
81
 
48
82
  Please send feedback by email to Tuomas Kareinen < tkareine (at) gmail (dot)
data/Rakefile CHANGED
@@ -1,10 +1,9 @@
1
1
  require "rubygems"
2
2
 
3
- full_name = "Tweetwine"
4
3
  package_name = "tweetwine"
5
- version = "0.1.11"
6
-
7
4
  require "lib/#{package_name}"
5
+ full_name = Tweetwine::Meta::NAME
6
+ version = Tweetwine::Meta::VERSION
8
7
 
9
8
  require "rake/clean"
10
9
 
data/bin/tweetwine CHANGED
@@ -1,11 +1,12 @@
1
1
  #!/usr/bin/env ruby
2
2
 
3
3
  EXIT_HELP = 1
4
- EXIT_SIGINT = 2
4
+ EXIT_VERSION = 2
5
+ EXIT_SIGINT = 6
5
6
  EXIT_ERROR = 64
6
7
 
7
8
  trap("INT") do
8
- puts "\nAbort."
9
+ puts "\nAbort"
9
10
  exit(EXIT_SIGINT)
10
11
  end
11
12
 
@@ -39,10 +40,17 @@ Usage: tweetwine [options...] [command]
39
40
  opt.on("-n", "--num N", Integer, "The number of statuses to fetch, defaults to #{Client::DEFAULT_NUM_STATUSES}") do |arg|
40
41
  options[:num_statuses] = arg
41
42
  end
43
+ opt.on("--no-url-shorten", "Do not shorten URLs for status update") do
44
+ options[:shorten_urls] = { :enable => false }
45
+ end
42
46
  opt.on("-p", "--page N", Integer, "The page number of the statuses to fetch, defaults to #{Client::DEFAULT_PAGE_NUM}") do |arg|
43
47
  options[:page_num] = arg
44
48
  end
45
- opt.on_tail("-h", "--help", "Show this help message") do
49
+ opt.on("-v", "--version", "Show version information and exit") do
50
+ puts Meta
51
+ exit(EXIT_VERSION)
52
+ end
53
+ opt.on_tail("-h", "--help", "Show this help message and exit") do
46
54
  puts opt
47
55
  exit(EXIT_HELP)
48
56
  end
@@ -52,7 +60,8 @@ Usage: tweetwine [options...] [command]
52
60
  end
53
61
  options
54
62
  end
55
- client = Client.new(config.options)
63
+ io = Tweetwine::IO.new(config.options)
64
+ client = Client.new(io, config.options)
56
65
  client.send(config.command, *config.args)
57
66
  rescue ArgumentError, ClientError => e
58
67
  puts "Error: #{e.message}"
@@ -1,9 +1,7 @@
1
1
  require "json"
2
- require "rest_client"
2
+ require "uri"
3
3
 
4
4
  module Tweetwine
5
- class ClientError < RuntimeError; end
6
-
7
5
  class Client
8
6
  attr_reader :num_statuses, :page_num
9
7
 
@@ -13,14 +11,19 @@ module Tweetwine
13
11
  DEFAULT_PAGE_NUM = 1
14
12
  MAX_STATUS_LENGTH = 140
15
13
 
16
- def initialize(options)
14
+ def initialize(io, options)
15
+ @io = io
17
16
  @username = options[:username].to_s
18
17
  raise ArgumentError, "No authentication data given" if @username.empty?
19
18
  @base_url = "https://#{@username}:#{options[:password]}@twitter.com/"
20
- @colorize = options[:colorize] || false
21
- @num_statuses = parse_int_gt_option(options[:num_statuses], DEFAULT_NUM_STATUSES, 1, "number of statuses_to_show")
22
- @page_num = parse_int_gt_option(options[:page_num], DEFAULT_PAGE_NUM, 1, "page number")
23
- @io = IO.new(options)
19
+ @num_statuses = Util.parse_int_gt(options[:num_statuses], DEFAULT_NUM_STATUSES, 1, "number of statuses_to_show")
20
+ @page_num = Util.parse_int_gt(options[:page_num], DEFAULT_PAGE_NUM, 1, "page number")
21
+ @url_shortener = if options[:shorten_urls] && options[:shorten_urls][:enable]
22
+ UrlShortener.new(options[:shorten_urls])
23
+ else
24
+ nil
25
+ end
26
+ @status_update_factory = StatusUpdateFactory.new(@io, @url_shortener)
24
27
  end
25
28
 
26
29
  def home
@@ -36,18 +39,18 @@ module Tweetwine
36
39
  end
37
40
 
38
41
  def update(new_status = nil)
39
- new_status = @io.prompt("Status update") unless new_status
40
- if new_status.length > MAX_STATUS_LENGTH
41
- new_status = new_status[0...MAX_STATUS_LENGTH]
42
- @io.warn("Status will be truncated: #{new_status}")
43
- end
44
- if !new_status.empty? && @io.confirm("Really send?")
45
- status = JSON.parse(post("statuses/update.json", {:status => new_status}))
46
- @io.info "Sent status update.\n\n"
47
- show_statuses([status])
48
- else
49
- @io.info "Cancelled."
42
+ new_status = @status_update_factory.prepare(new_status)
43
+ completed = false
44
+ unless new_status.empty?
45
+ @io.show_status_preview(new_status)
46
+ if @io.confirm("Really send?")
47
+ status = JSON.parse(post("statuses/update.json", {:status => new_status.to_s}))
48
+ @io.info "Sent status update.\n\n"
49
+ show_statuses([status])
50
+ completed = true
51
+ end
50
52
  end
53
+ @io.info "Cancelled." unless completed
51
54
  end
52
55
 
53
56
  def friends
@@ -60,19 +63,6 @@ module Tweetwine
60
63
 
61
64
  private
62
65
 
63
- def parse_int_gt_option(value, default, min, name_for_error)
64
- if value
65
- value = value.to_i
66
- if value >= min
67
- value
68
- else
69
- raise ArgumentError, "Invalid #{name_for_error} -- must be greater than or equal to #{min}"
70
- end
71
- else
72
- default
73
- end
74
- end
75
-
76
66
  def get_response_as_json(url_body, *query_opts)
77
67
  url = url_body + ".json?#{parse_query_options(query_opts)}"
78
68
  JSON.parse(get(url))
@@ -103,7 +93,7 @@ module Tweetwine
103
93
  def show_responses(data)
104
94
  data.each do |entry|
105
95
  user_data, status_data = yield entry
106
- @io.show(parse_response(user_data, status_data))
96
+ @io.show_record(parse_response(user_data, status_data))
107
97
  end
108
98
  end
109
99
 
@@ -120,17 +110,62 @@ module Tweetwine
120
110
  end
121
111
 
122
112
  def get(body_url)
123
- rest_client_action(:get, @base_url + body_url)
113
+ RestClientWrapper.get @base_url + body_url
124
114
  end
125
115
 
126
116
  def post(body_url, body)
127
- rest_client_action(:post, @base_url + body_url, body)
117
+ RestClientWrapper.post @base_url + body_url, body
128
118
  end
129
119
 
130
- def rest_client_action(action, *args)
131
- RestClient.send(action, *args)
132
- rescue RestClient::Exception => e
133
- raise ClientError, e.message
120
+ class StatusUpdateFactory
121
+ def initialize(io, url_shortener)
122
+ @io = io
123
+ @url_shortener = url_shortener
124
+ end
125
+
126
+ def prepare(status)
127
+ StatusUpdate.new(status, @io, @url_shortener).to_s
128
+ end
129
+ end
130
+
131
+ class StatusUpdate
132
+ def initialize(status, io, url_shortener)
133
+ @io = io
134
+ @url_shortener = url_shortener
135
+ @text = prepare(status)
136
+ end
137
+
138
+ def to_s
139
+ @text.to_s
140
+ end
141
+
142
+ private
143
+
144
+ def prepare(status)
145
+ status = unless status
146
+ @io.prompt("Status update")
147
+ else
148
+ status.dup
149
+ end
150
+ status.strip!
151
+ shorten_urls!(status) if @url_shortener
152
+ truncate!(status) if status.length > MAX_STATUS_LENGTH
153
+ status
154
+ end
155
+
156
+ def truncate!(status)
157
+ status.replace status[0...MAX_STATUS_LENGTH]
158
+ @io.warn("Status will be truncated.")
159
+ end
160
+
161
+ def shorten_urls!(status)
162
+ url_pairs = URI.extract(status, ["http", "https"]).map do |url_to_be_shortened|
163
+ [url_to_be_shortened, @url_shortener.shorten(url_to_be_shortened)]
164
+ end
165
+ url_pairs.reject { |pair| pair.last.nil? || pair.last.empty? }.each do |url_pair|
166
+ status.sub!(url_pair.first, url_pair.last)
167
+ end
168
+ end
134
169
  end
135
170
  end
136
171
  end
data/lib/tweetwine/io.rb CHANGED
@@ -36,49 +36,69 @@ module Tweetwine
36
36
  confirmation.downcase[0,1] == "y"
37
37
  end
38
38
 
39
- def show(record)
39
+ def show_status_preview(status)
40
+ @output.puts <<-END
41
+
42
+ #{format_status(status)}
43
+
44
+ END
45
+ end
46
+
47
+ def show_record(record)
40
48
  if record[:status]
41
- show_as_status(record)
49
+ show_record_as_user_with_status(record)
42
50
  else
43
- show_as_user(record)
51
+ show_record_as_user(record)
44
52
  end
45
53
  end
46
54
 
47
- def show_as_status(record)
48
- time_diff_value, time_diff_unit = Util.humanize_time_diff(record[:status][:created_at], Time.now)
49
- from_user = record[:user]
50
- colorize!(:green, from_user) if @colorize
51
- in_reply_to = record[:status][:in_reply_to]
52
- in_reply_to = if in_reply_to && !in_reply_to.empty?
53
- colorize!(:green, in_reply_to) if @colorize
54
- "in reply to #{in_reply_to}, "
55
- else
56
- ""
57
- end
58
- status = record[:status][:text]
59
- if @colorize
60
- colorize_all!(:yellow, status, NICK_REGEX)
61
- URI.extract(status, ["http", "https"]).each do |url|
62
- colorize_first!(:cyan, status, url)
63
- end
64
- end
55
+ private
56
+
57
+ def show_record_as_user(record)
65
58
  @output.puts <<-END
66
- #{from_user}, #{in_reply_to}#{time_diff_value} #{time_diff_unit} ago:
67
- #{status}
59
+ #{format_user(record[:user])}
68
60
 
69
61
  END
70
62
  end
71
63
 
72
- def show_as_user(record)
73
- user = record[:user]
74
- colorize!(:green, user) if @colorize
64
+ def show_record_as_user_with_status(record)
75
65
  @output.puts <<-END
76
- #{user}
66
+ #{format_record_header(record)}
67
+ #{format_status(record[:status][:text])}
77
68
 
78
69
  END
79
70
  end
80
71
 
81
- private
72
+ def format_user(user)
73
+ user = user.dup
74
+ colorize!(:green, user) if @colorize
75
+ user
76
+ end
77
+
78
+ def format_status(status)
79
+ status = status.dup
80
+ if @colorize
81
+ colorize_all!(:yellow, status, NICK_REGEX)
82
+ URI.extract(status, ["http", "https"]).each do |url|
83
+ colorize_first!(:cyan, status, url)
84
+ end
85
+ end
86
+ status
87
+ end
88
+
89
+ def format_record_header(record)
90
+ time_diff_value, time_diff_unit = Util.humanize_time_diff(record[:status][:created_at], Time.now)
91
+ from_user = record[:user].dup
92
+ colorize!(:green, from_user) if @colorize
93
+ in_reply_to = record[:status][:in_reply_to]
94
+ in_reply_to = if in_reply_to && !in_reply_to.empty?
95
+ in_reply_to = colorize!(:green, in_reply_to.dup) if @colorize
96
+ "in reply to #{in_reply_to}, "
97
+ else
98
+ ""
99
+ end
100
+ "#{from_user}, #{in_reply_to}#{time_diff_value} #{time_diff_unit} ago:"
101
+ end
82
102
 
83
103
  def colorize_all!(color, str, pattern)
84
104
  str.gsub!(pattern) { |s| colorize_str(COLOR_CODES[color.to_sym], s) }
@@ -0,0 +1,10 @@
1
+ module Tweetwine
2
+ module Meta #:nodoc:
3
+ NAME = "Tweetwine"
4
+ VERSION = "0.2.1"
5
+
6
+ def self.to_s
7
+ "#{NAME} #{VERSION}"
8
+ end
9
+ end
10
+ end
@@ -0,0 +1,17 @@
1
+ module Tweetwine
2
+ class Options
3
+ def initialize(options)
4
+ @hash = options.to_hash
5
+ end
6
+
7
+ def [](key)
8
+ @hash[key]
9
+ end
10
+
11
+ def require(key)
12
+ value = @hash[key]
13
+ raise RuntimeError, "Option #{key} is required" if value.nil?
14
+ value
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,17 @@
1
+ require "rest_client"
2
+
3
+ module Tweetwine
4
+ class ClientError < RuntimeError; end
5
+
6
+ class RestClientWrapper
7
+ instance_methods.each { |m| undef_method m unless m =~ /(^__|^send$|^object_id$)/ }
8
+
9
+ protected
10
+
11
+ def self.method_missing(name, *args, &block)
12
+ RestClient.send(name, *args, &block)
13
+ rescue RestClient::Exception, SystemCallError => e
14
+ raise ClientError, e.message
15
+ end
16
+ end
17
+ end
@@ -34,10 +34,7 @@ module Tweetwine
34
34
 
35
35
  def parse_config_file(config_file)
36
36
  options = YAML.load(File.read(config_file))
37
- options.inject({}) do |result, pair|
38
- result[pair.first.to_sym] = pair.last
39
- result
40
- end
37
+ Util.symbolize_hash_keys(options)
41
38
  end
42
39
  end
43
40
  end
@@ -0,0 +1,37 @@
1
+ module Tweetwine
2
+ class UrlShortener
3
+ def initialize(options)
4
+ require "nokogiri"
5
+ options = Options.new(options)
6
+ @method = options[:method].to_sym || :get
7
+ @service_url = options.require :service_url
8
+ @url_param_name = options.require :url_param_name
9
+ @extra_params = options[:extra_params] || {}
10
+ if @method == :get
11
+ tmp = []
12
+ @extra_params.each_pair { |k, v| tmp << "#{k}=#{v}" }
13
+ @extra_params = tmp
14
+ end
15
+ @xpath_selector = options.require :xpath_selector
16
+ end
17
+
18
+ def shorten(url)
19
+ rest = case @method
20
+ when :get
21
+ tmp = @extra_params.dup
22
+ tmp << "#{@url_param_name}=#{url}"
23
+ service_url = "#{@service_url}?#{tmp.join('&')}"
24
+ [service_url]
25
+ when :post
26
+ service_url = @service_url
27
+ params = @extra_params.merge({ @url_param_name.to_sym => url })
28
+ [service_url, params]
29
+ else
30
+ raise "Unrecognized HTTP request method"
31
+ end
32
+ response = RestClientWrapper.send(@method, *rest)
33
+ doc = Nokogiri::HTML(response)
34
+ doc.xpath(@xpath_selector).first.to_s
35
+ end
36
+ end
37
+ end
@@ -18,6 +18,28 @@ module Tweetwine
18
18
  [value, pluralize_unit(value, unit)]
19
19
  end
20
20
 
21
+ def self.symbolize_hash_keys(hash)
22
+ hash.inject({}) do |result, pair|
23
+ value = pair.last
24
+ value = symbolize_hash_keys(value) if value.is_a? Hash
25
+ result[pair.first.to_sym] = value
26
+ result
27
+ end
28
+ end
29
+
30
+ def self.parse_int_gt(value, default, min, name_for_error)
31
+ if value
32
+ value = value.to_i
33
+ if value >= min
34
+ value
35
+ else
36
+ raise ArgumentError, "Invalid #{name_for_error} -- must be greater than or equal to #{min}"
37
+ end
38
+ else
39
+ default
40
+ end
41
+ end
42
+
21
43
  private
22
44
 
23
45
  def self.pluralize_unit(value, unit)
data/lib/tweetwine.rb CHANGED
@@ -1,3 +1,12 @@
1
- %w{util startup_config io client}.each do |f|
1
+ %w{
2
+ meta
3
+ util
4
+ options
5
+ startup_config
6
+ io
7
+ rest_client_wrapper
8
+ url_shortener
9
+ client
10
+ }.each do |f|
2
11
  require File.dirname(__FILE__) << "/tweetwine/#{f}"
3
12
  end