tweetwine 0.2.4
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 +84 -0
- data/MIT-LICENSE.txt +19 -0
- data/README.rdoc +87 -0
- data/Rakefile +86 -0
- data/bin/tweetwine +79 -0
- data/lib/tweetwine/client.rb +174 -0
- data/lib/tweetwine/io.rb +117 -0
- data/lib/tweetwine/meta.rb +3 -0
- data/lib/tweetwine/options.rb +22 -0
- data/lib/tweetwine/rest_client_wrapper.rb +37 -0
- data/lib/tweetwine/startup_config.rb +40 -0
- data/lib/tweetwine/url_shortener.rb +38 -0
- data/lib/tweetwine/util.rb +52 -0
- data/lib/tweetwine.rb +12 -0
- data/test/client_test.rb +475 -0
- data/test/io_test.rb +265 -0
- data/test/options_test.rb +43 -0
- data/test/rest_client_wrapper_test.rb +68 -0
- data/test/startup_config_test.rb +87 -0
- data/test/test_config.yaml +3 -0
- data/test/test_helper.rb +57 -0
- data/test/url_shortener_test.rb +161 -0
- data/test/util_test.rb +55 -0
- metadata +93 -0
data/CHANGELOG.rdoc
ADDED
@@ -0,0 +1,84 @@
|
|
1
|
+
=== 0.2.4 released 2009-09-16
|
2
|
+
|
3
|
+
* Retry connection upon connection reset, trying maximum of three times.
|
4
|
+
* Display proper version info on Ruby 1.8 when using option "-v".
|
5
|
+
* Minor code cleanups.
|
6
|
+
* Release 0.2.3 is skipped due to my error in tagging the wrong commit.
|
7
|
+
|
8
|
+
=== 0.2.2 released 2009-09-03
|
9
|
+
|
10
|
+
* Highlight hashtags in statuses.
|
11
|
+
* URL shortening step shortens only unique URLs.
|
12
|
+
* URL shortening step is skipped if
|
13
|
+
- there is a connection error to the URL shortening service, or
|
14
|
+
- required libraries are not installed (nokogiri).
|
15
|
+
* Fixed a colorization bug for duplicate URLs.
|
16
|
+
* Removed dependencies to Rubygems.
|
17
|
+
|
18
|
+
=== 0.2.1 released 2009-08-17
|
19
|
+
|
20
|
+
* Command line option "-v" show version information.
|
21
|
+
* Slight implementation code cleaning.
|
22
|
+
|
23
|
+
=== 0.2.0 released 2009-08-17
|
24
|
+
|
25
|
+
* URL shortening by using an external web service.
|
26
|
+
* Show a preview before sending status update.
|
27
|
+
* Avoid stack trace upon erroneous connection.
|
28
|
+
|
29
|
+
=== 0.1.11 released 2009-08-10
|
30
|
+
|
31
|
+
* Fixed highlighting multiple nicks in statuses.
|
32
|
+
|
33
|
+
=== 0.1.10 released 2009-08-09
|
34
|
+
|
35
|
+
* Improved URL highlight support.
|
36
|
+
|
37
|
+
=== 0.1.9 released 2009-07-15
|
38
|
+
|
39
|
+
* Added commands "friends" and "followers"
|
40
|
+
* Removed dependency to the "json" gem. It is included in Ruby 1.9.
|
41
|
+
|
42
|
+
=== 0.1.8 released 2009-07-01
|
43
|
+
|
44
|
+
* SIGINT (Ctrl+c) is trapper earlier, resulting in clean abort while Ruby
|
45
|
+
loads the program's required libraries.
|
46
|
+
|
47
|
+
=== 0.1.7 released 2009-06-07
|
48
|
+
|
49
|
+
* Small compatibility fix with Ruby 1.9.
|
50
|
+
|
51
|
+
=== 0.1.6 released 2009-06-07
|
52
|
+
|
53
|
+
* Improved URL highlighting.
|
54
|
+
|
55
|
+
=== 0.1.5 released 2009-06-06
|
56
|
+
|
57
|
+
* URLs of http(s) scheme are highlighted. Changed colors.
|
58
|
+
* Friendly abort message when interrupting the program with Ctrl+C.
|
59
|
+
|
60
|
+
=== 0.1.4 released 2009-05-12
|
61
|
+
|
62
|
+
* Command line option "--page N" fetches a specific status page
|
63
|
+
|
64
|
+
=== 0.1.3 released 2009-05-05
|
65
|
+
|
66
|
+
* Empty status update indicates cancellation of the command
|
67
|
+
* Other minor improvements
|
68
|
+
|
69
|
+
=== 0.1.2 released 2009-05-04
|
70
|
+
|
71
|
+
* Renamed command "friends" to "home"
|
72
|
+
* Added command "mentions"
|
73
|
+
* When showing a status, indicate if it is a reply
|
74
|
+
* Improved command line argument and configuration file parsing
|
75
|
+
|
76
|
+
=== 0.1.1 released 2009-04-23
|
77
|
+
|
78
|
+
* Renamed command "msg" to "update"
|
79
|
+
* If status update if longer than 140 characters, warn about it
|
80
|
+
|
81
|
+
=== 0.1.0 released 2009-04-22
|
82
|
+
|
83
|
+
* Initial release with minimal functionality
|
84
|
+
* Usable for quickly checking friends' statuses and sending status updates
|
data/MIT-LICENSE.txt
ADDED
@@ -0,0 +1,19 @@
|
|
1
|
+
Copyright (c) 2009 Tuomas Kareinen.
|
2
|
+
|
3
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
4
|
+
of this software and associated documentation files (the "Software"), to deal
|
5
|
+
in the Software without restriction, including without limitation the rights
|
6
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
7
|
+
copies of the Software, and to permit persons to whom the Software is
|
8
|
+
furnished to do so, subject to the following conditions:
|
9
|
+
|
10
|
+
The above copyright notice and this permission notice shall be included in
|
11
|
+
all copies or substantial portions of the Software.
|
12
|
+
|
13
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
14
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
15
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
16
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
17
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
18
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
19
|
+
THE SOFTWARE.
|
data/README.rdoc
ADDED
@@ -0,0 +1,87 @@
|
|
1
|
+
= Tweetwine
|
2
|
+
|
3
|
+
A simple but tasty Twitter agent for command line use, made for fun.
|
4
|
+
|
5
|
+
== Installation
|
6
|
+
|
7
|
+
Install Tweetwine as a RubyGem from GitHub:
|
8
|
+
|
9
|
+
$ sudo gem install tuomas-tweetwine --source http://gems.github.com
|
10
|
+
|
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.
|
13
|
+
|
14
|
+
== Usage
|
15
|
+
|
16
|
+
In the command line, run the program by entering
|
17
|
+
|
18
|
+
$ tweetwine [options...] [command]
|
19
|
+
|
20
|
+
The program needs the user's username and password for authentication. This
|
21
|
+
information can be supplied either via a configuration file or as an option
|
22
|
+
(<tt>-a USERNAME:PASSWORD</tt>) to the program. It is recommended to use the
|
23
|
+
former method over the latter.
|
24
|
+
|
25
|
+
The configuration file, in <tt>~/.tweetwine</tt>, is in YAML syntax. The
|
26
|
+
program recognizes the following basic settings:
|
27
|
+
|
28
|
+
username: <your_username>
|
29
|
+
password: <your_password>
|
30
|
+
colorize: true|false
|
31
|
+
|
32
|
+
When invoking the program, the supported commands are
|
33
|
+
|
34
|
+
<tt>home</tt>:: Fetch friends' latest statuses (the contents of the authenticated user's home).
|
35
|
+
<tt>mentions</tt>:: Fetch latest mentions for the authenticated user.
|
36
|
+
<tt>user [name]</tt>:: Fetch a specific user's latest statuses, identified by the argument; if given no argument, fetch the statuses of the authenticated user.
|
37
|
+
<tt>update [status]</tt>:: Send a status update, but confirm the action first before actually sending. The status update can either be given as an argument or via STDIN if no argument is given.
|
38
|
+
<tt>friends</tt>:: Fetch friends and their latest statuses.
|
39
|
+
<tt>followers</tt>:: Fetch followers and their latest statuses.
|
40
|
+
|
41
|
+
If no <tt>[command]</tt> is given, the default action is <tt>home</tt>.
|
42
|
+
|
43
|
+
For all the options, see:
|
44
|
+
|
45
|
+
$ tweetwine -h
|
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
|
+
|
80
|
+
== Contacting
|
81
|
+
|
82
|
+
Please send feedback by email to Tuomas Kareinen < tkareine (at) gmail (dot)
|
83
|
+
com >.
|
84
|
+
|
85
|
+
== Legal notes
|
86
|
+
|
87
|
+
Copyright (c) 2009 Tuomas Kareinen. See MIT-LICENSE.txt in this directory.
|
data/Rakefile
ADDED
@@ -0,0 +1,86 @@
|
|
1
|
+
$LOAD_PATH.unshift(File.join(File.dirname(__FILE__), "lib"))
|
2
|
+
|
3
|
+
require "rubygems"
|
4
|
+
|
5
|
+
name = "tweetwine"
|
6
|
+
require "#{name}"
|
7
|
+
version = Tweetwine::VERSION
|
8
|
+
|
9
|
+
require "rake/clean"
|
10
|
+
|
11
|
+
require "rake/gempackagetask"
|
12
|
+
spec = Gem::Specification.new do |s|
|
13
|
+
s.name = name
|
14
|
+
s.version = version
|
15
|
+
s.homepage = "http://github.com/tuomas/tweetwine"
|
16
|
+
s.summary = "A simple Twitter agent for command line use"
|
17
|
+
s.description = "A simple but tasty Twitter agent for command line use, made for fun."
|
18
|
+
|
19
|
+
s.author = "Tuomas Kareinen"
|
20
|
+
s.email = "tkareine@gmail.com"
|
21
|
+
|
22
|
+
s.platform = Gem::Platform::RUBY
|
23
|
+
s.files = FileList["Rakefile", "MIT-LICENSE.txt", "*.rdoc", "bin/**/*", "lib/**/*", "test/**/*"].to_a
|
24
|
+
s.executables = ["tweetwine"]
|
25
|
+
|
26
|
+
s.add_dependency("rest-client", ">= 1.0.0")
|
27
|
+
|
28
|
+
s.has_rdoc = true
|
29
|
+
s.extra_rdoc_files = FileList["MIT-LICENSE.txt", "*.rdoc"].to_a
|
30
|
+
s.rdoc_options << "--title" << "#{name} #{version}" \
|
31
|
+
<< "--main" << "README.rdoc" \
|
32
|
+
<< "--exclude" << "test" \
|
33
|
+
<< "--line-numbers"
|
34
|
+
end
|
35
|
+
|
36
|
+
Rake::GemPackageTask.new(spec) do |pkg|
|
37
|
+
pkg.need_zip = false
|
38
|
+
pkg.need_tar = true
|
39
|
+
end
|
40
|
+
|
41
|
+
desc "Generate a gemspec file"
|
42
|
+
task :gemspec do
|
43
|
+
File.open("#{spec.name}.gemspec", "w") do |f|
|
44
|
+
f.write spec.to_ruby
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
task :install => [:package] do
|
49
|
+
sh %{sudo gem install pkg/#{name}-#{version}.gem}
|
50
|
+
end
|
51
|
+
|
52
|
+
task :uninstall => [:clean] do
|
53
|
+
sh %{sudo gem uninstall #{name}}
|
54
|
+
end
|
55
|
+
|
56
|
+
require "rake/rdoctask"
|
57
|
+
desc "Create documentation"
|
58
|
+
Rake::RDocTask.new(:rdoc) do |rd|
|
59
|
+
rd.rdoc_dir = "rdoc"
|
60
|
+
rd.title = "#{name} #{version}"
|
61
|
+
rd.main = "README.rdoc"
|
62
|
+
rd.rdoc_files.include("MIT-LICENSE.txt", "*.rdoc", "lib/**/*.rb")
|
63
|
+
rd.options << "--line-numbers"
|
64
|
+
end
|
65
|
+
|
66
|
+
require "rake/testtask"
|
67
|
+
desc "Run tests"
|
68
|
+
Rake::TestTask.new(:test) do |t|
|
69
|
+
t.test_files = FileList["test/**/*_test.rb"]
|
70
|
+
t.verbose = true
|
71
|
+
t.warning = true
|
72
|
+
t.ruby_opts << "-rrubygems"
|
73
|
+
t.libs << "test"
|
74
|
+
end
|
75
|
+
|
76
|
+
desc "Find code smells"
|
77
|
+
task :roodi do
|
78
|
+
sh %{roodi "**/*.rb"}
|
79
|
+
end
|
80
|
+
|
81
|
+
desc "Search unfinished parts of source code"
|
82
|
+
task :todo do
|
83
|
+
FileList["**/*.rb", "**/*.rdoc", "**/*.txt"].egrep /(TODO|FIXME)/
|
84
|
+
end
|
85
|
+
|
86
|
+
task :default => :test
|
data/bin/tweetwine
ADDED
@@ -0,0 +1,79 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
EXIT_HELP = 1
|
4
|
+
EXIT_VERSION = 2
|
5
|
+
EXIT_SIGINT = 6
|
6
|
+
EXIT_ERROR = 64
|
7
|
+
|
8
|
+
trap("INT") do
|
9
|
+
puts "\nAbort"
|
10
|
+
exit(EXIT_SIGINT)
|
11
|
+
end
|
12
|
+
|
13
|
+
require "optparse"
|
14
|
+
require "tweetwine"
|
15
|
+
|
16
|
+
include Tweetwine
|
17
|
+
|
18
|
+
cmd_parser = lambda do |args|
|
19
|
+
options = {}
|
20
|
+
begin
|
21
|
+
OptionParser.new do |opt|
|
22
|
+
opt.banner =<<-END
|
23
|
+
Usage: tweetwine [options...] [command]
|
24
|
+
|
25
|
+
Commands: #{Client::COMMANDS.join(", ")}
|
26
|
+
|
27
|
+
Options:
|
28
|
+
|
29
|
+
END
|
30
|
+
opt.on("-a", "--auth USERNAME:PASSWORD", "Authentication") do |arg|
|
31
|
+
options[:username], options[:password] = arg.split(":", 2)
|
32
|
+
end
|
33
|
+
opt.on("-c", "--colorize", "Colorize output with ANSI escape codes") do
|
34
|
+
options[:colorize] = true
|
35
|
+
end
|
36
|
+
opt.on("-n", "--num N", Integer, "The number of statuses to fetch, defaults to #{Client::DEFAULT_NUM_STATUSES}") do |arg|
|
37
|
+
options[:num_statuses] = arg
|
38
|
+
end
|
39
|
+
opt.on("--no-url-shorten", "Do not shorten URLs for status update") do
|
40
|
+
options[:shorten_urls] = { :enable => false }
|
41
|
+
end
|
42
|
+
opt.on("-p", "--page N", Integer, "The page number of the statuses to fetch, defaults to #{Client::DEFAULT_PAGE_NUM}") do |arg|
|
43
|
+
options[:page_num] = arg
|
44
|
+
end
|
45
|
+
opt.on("-v", "--version", "Show version information and exit") do
|
46
|
+
puts "#{File.basename($0)} #{Tweetwine::VERSION}"
|
47
|
+
exit(EXIT_VERSION)
|
48
|
+
end
|
49
|
+
opt.on_tail("-h", "--help", "Show this help message and exit") do
|
50
|
+
puts opt
|
51
|
+
exit(EXIT_HELP)
|
52
|
+
end
|
53
|
+
end.parse!(args)
|
54
|
+
rescue OptionParser::ParseError => e
|
55
|
+
raise ArgumentError, e.message
|
56
|
+
end
|
57
|
+
options
|
58
|
+
end
|
59
|
+
|
60
|
+
def create_dependencies(options)
|
61
|
+
io = Tweetwine::IO.new(options)
|
62
|
+
rest_client = RestClientWrapper.new(io)
|
63
|
+
url_shortener = lambda { |opts| UrlShortener.new(rest_client, opts) }
|
64
|
+
{
|
65
|
+
:io => io,
|
66
|
+
:rest_client => rest_client,
|
67
|
+
:url_shortener => url_shortener
|
68
|
+
}
|
69
|
+
end
|
70
|
+
|
71
|
+
begin
|
72
|
+
config = StartupConfig.new(Client::COMMANDS)
|
73
|
+
config.parse(ARGV, ENV["HOME"] + "/.tweetwine", &cmd_parser)
|
74
|
+
client = Client.new(create_dependencies(config.options), config.options)
|
75
|
+
client.send(config.command, *config.args)
|
76
|
+
rescue ArgumentError, ClientError => e
|
77
|
+
puts "Error: #{e.message}"
|
78
|
+
exit(EXIT_ERROR)
|
79
|
+
end
|
@@ -0,0 +1,174 @@
|
|
1
|
+
require "json"
|
2
|
+
require "uri"
|
3
|
+
|
4
|
+
module Tweetwine
|
5
|
+
class Client
|
6
|
+
attr_reader :num_statuses, :page_num
|
7
|
+
|
8
|
+
COMMANDS = [:home, :mentions, :user, :update, :friends, :followers]
|
9
|
+
|
10
|
+
DEFAULT_NUM_STATUSES = 20
|
11
|
+
DEFAULT_PAGE_NUM = 1
|
12
|
+
MAX_STATUS_LENGTH = 140
|
13
|
+
|
14
|
+
def initialize(dependencies, options)
|
15
|
+
@io = dependencies[:io]
|
16
|
+
@rest_client = dependencies[:rest_client]
|
17
|
+
@username = options[:username].to_s
|
18
|
+
raise ArgumentError, "No authentication data given" if @username.empty?
|
19
|
+
@base_url = "https://#{@username}:#{options[:password]}@twitter.com/"
|
20
|
+
@num_statuses = Util.parse_int_gt(options[:num_statuses], DEFAULT_NUM_STATUSES, 1, "number of statuses_to_show")
|
21
|
+
@page_num = Util.parse_int_gt(options[:page_num], DEFAULT_PAGE_NUM, 1, "page number")
|
22
|
+
@url_shortener = if options[:shorten_urls] && options[:shorten_urls][:enable]
|
23
|
+
dependencies[:url_shortener].call(options[:shorten_urls])
|
24
|
+
else
|
25
|
+
nil
|
26
|
+
end
|
27
|
+
@status_update_factory = StatusUpdateFactory.new(@io, @url_shortener)
|
28
|
+
end
|
29
|
+
|
30
|
+
def home
|
31
|
+
show_statuses(get_response_as_json("statuses/friends_timeline", :num_statuses, :page))
|
32
|
+
end
|
33
|
+
|
34
|
+
def mentions
|
35
|
+
show_statuses(get_response_as_json("statuses/mentions", :num_statuses, :page))
|
36
|
+
end
|
37
|
+
|
38
|
+
def user(user = @username)
|
39
|
+
show_statuses(get_response_as_json("statuses/user_timeline/#{user}", :num_statuses, :page))
|
40
|
+
end
|
41
|
+
|
42
|
+
def update(new_status = nil)
|
43
|
+
new_status = @status_update_factory.prepare(new_status)
|
44
|
+
completed = false
|
45
|
+
unless new_status.empty?
|
46
|
+
@io.show_status_preview(new_status)
|
47
|
+
if @io.confirm("Really send?")
|
48
|
+
status = JSON.parse(post("statuses/update.json", {:status => new_status.to_s}))
|
49
|
+
@io.info "Sent status update.\n\n"
|
50
|
+
show_statuses([status])
|
51
|
+
completed = true
|
52
|
+
end
|
53
|
+
end
|
54
|
+
@io.info "Cancelled." unless completed
|
55
|
+
end
|
56
|
+
|
57
|
+
def friends
|
58
|
+
show_users(get_response_as_json("statuses/friends/#{@username}", :page))
|
59
|
+
end
|
60
|
+
|
61
|
+
def followers
|
62
|
+
show_users(get_response_as_json("statuses/followers/#{@username}", :page))
|
63
|
+
end
|
64
|
+
|
65
|
+
private
|
66
|
+
|
67
|
+
def get_response_as_json(url_body, *query_opts)
|
68
|
+
url = url_body + ".json?#{parse_query_options(query_opts)}"
|
69
|
+
JSON.parse(get(url))
|
70
|
+
end
|
71
|
+
|
72
|
+
def parse_query_options(query_opts)
|
73
|
+
str = []
|
74
|
+
query_opts.each do |opt|
|
75
|
+
case opt
|
76
|
+
when :page
|
77
|
+
str << "page=#{@page_num}"
|
78
|
+
when :num_statuses
|
79
|
+
str << "count=#{@num_statuses}"
|
80
|
+
# do nothing on else
|
81
|
+
end
|
82
|
+
end
|
83
|
+
str.join("&")
|
84
|
+
end
|
85
|
+
|
86
|
+
def show_statuses(data)
|
87
|
+
show_responses(data) { |entry| [entry["user"], entry] }
|
88
|
+
end
|
89
|
+
|
90
|
+
def show_users(data)
|
91
|
+
show_responses(data) { |entry| [entry, entry["status"]] }
|
92
|
+
end
|
93
|
+
|
94
|
+
def show_responses(data)
|
95
|
+
data.each do |entry|
|
96
|
+
user_data, status_data = yield entry
|
97
|
+
@io.show_record(parse_response(user_data, status_data))
|
98
|
+
end
|
99
|
+
end
|
100
|
+
|
101
|
+
def parse_response(user_data, status_data)
|
102
|
+
record = { :user => user_data["screen_name"] }
|
103
|
+
if status_data
|
104
|
+
record[:status] = {
|
105
|
+
:created_at => status_data["created_at"],
|
106
|
+
:in_reply_to => status_data["in_reply_to_screen_name"],
|
107
|
+
:text => status_data["text"]
|
108
|
+
}
|
109
|
+
end
|
110
|
+
record
|
111
|
+
end
|
112
|
+
|
113
|
+
def get(body_url)
|
114
|
+
@rest_client.get @base_url + body_url
|
115
|
+
end
|
116
|
+
|
117
|
+
def post(body_url, body)
|
118
|
+
@rest_client.post @base_url + body_url, body
|
119
|
+
end
|
120
|
+
|
121
|
+
class StatusUpdateFactory
|
122
|
+
def initialize(io, url_shortener)
|
123
|
+
@io = io
|
124
|
+
@url_shortener = url_shortener
|
125
|
+
end
|
126
|
+
|
127
|
+
def prepare(status)
|
128
|
+
StatusUpdate.new(status, @io, @url_shortener).to_s
|
129
|
+
end
|
130
|
+
end
|
131
|
+
|
132
|
+
class StatusUpdate
|
133
|
+
def initialize(status, io, url_shortener)
|
134
|
+
@io = io
|
135
|
+
@url_shortener = url_shortener
|
136
|
+
@text = prepare(status)
|
137
|
+
end
|
138
|
+
|
139
|
+
def to_s
|
140
|
+
@text.to_s
|
141
|
+
end
|
142
|
+
|
143
|
+
private
|
144
|
+
|
145
|
+
def prepare(status)
|
146
|
+
status = unless status
|
147
|
+
@io.prompt("Status update")
|
148
|
+
else
|
149
|
+
status.dup
|
150
|
+
end
|
151
|
+
status.strip!
|
152
|
+
shorten_urls!(status) if @url_shortener
|
153
|
+
truncate!(status) if status.length > MAX_STATUS_LENGTH
|
154
|
+
status
|
155
|
+
end
|
156
|
+
|
157
|
+
def truncate!(status)
|
158
|
+
status.replace status[0...MAX_STATUS_LENGTH]
|
159
|
+
@io.warn("Status will be truncated.")
|
160
|
+
end
|
161
|
+
|
162
|
+
def shorten_urls!(status)
|
163
|
+
url_pairs = URI.extract(status, ["http", "https"]).uniq.map do |url_to_be_shortened|
|
164
|
+
[url_to_be_shortened, @url_shortener.shorten(url_to_be_shortened)]
|
165
|
+
end
|
166
|
+
url_pairs.reject { |pair| pair.last.nil? || pair.last.empty? }.each do |url_pair|
|
167
|
+
status.gsub!(url_pair.first, url_pair.last)
|
168
|
+
end
|
169
|
+
rescue ClientError, LoadError => e
|
170
|
+
@io.warn "#{e}. Skipping URL shortening..."
|
171
|
+
end
|
172
|
+
end
|
173
|
+
end
|
174
|
+
end
|
data/lib/tweetwine/io.rb
ADDED
@@ -0,0 +1,117 @@
|
|
1
|
+
require "uri"
|
2
|
+
|
3
|
+
module Tweetwine
|
4
|
+
class IO
|
5
|
+
COLOR_CODES = {
|
6
|
+
:cyan => 36,
|
7
|
+
:green => 32,
|
8
|
+
:magenta => 35,
|
9
|
+
:yellow => 33
|
10
|
+
}
|
11
|
+
|
12
|
+
HASHTAG_REGEX = /#[\w-]+/
|
13
|
+
USERNAME_REGEX = /@\w+/
|
14
|
+
|
15
|
+
def initialize(options)
|
16
|
+
@input = options[:input] || $stdin
|
17
|
+
@output = options[:output] || $stdout
|
18
|
+
@colorize = options[:colorize] || false
|
19
|
+
end
|
20
|
+
|
21
|
+
def prompt(prompt)
|
22
|
+
@output.print "#{prompt}: "
|
23
|
+
@input.gets.strip!
|
24
|
+
end
|
25
|
+
|
26
|
+
def info(msg)
|
27
|
+
@output.puts(msg)
|
28
|
+
end
|
29
|
+
|
30
|
+
def warn(msg)
|
31
|
+
@output.puts "Warning: #{msg}"
|
32
|
+
end
|
33
|
+
|
34
|
+
def confirm(msg)
|
35
|
+
@output.print "#{msg} [yN] "
|
36
|
+
confirmation = @input.gets.strip
|
37
|
+
confirmation.downcase[0,1] == "y"
|
38
|
+
end
|
39
|
+
|
40
|
+
def show_status_preview(status)
|
41
|
+
@output.puts <<-END
|
42
|
+
|
43
|
+
#{format_status(status)}
|
44
|
+
|
45
|
+
END
|
46
|
+
end
|
47
|
+
|
48
|
+
def show_record(record)
|
49
|
+
if record[:status]
|
50
|
+
show_record_as_user_with_status(record)
|
51
|
+
else
|
52
|
+
show_record_as_user(record)
|
53
|
+
end
|
54
|
+
end
|
55
|
+
|
56
|
+
private
|
57
|
+
|
58
|
+
def show_record_as_user(record)
|
59
|
+
@output.puts <<-END
|
60
|
+
#{format_user(record[:user])}
|
61
|
+
|
62
|
+
END
|
63
|
+
end
|
64
|
+
|
65
|
+
def show_record_as_user_with_status(record)
|
66
|
+
@output.puts <<-END
|
67
|
+
#{format_record_header(record)}
|
68
|
+
#{format_status(record[:status][:text])}
|
69
|
+
|
70
|
+
END
|
71
|
+
end
|
72
|
+
|
73
|
+
def format_user(user)
|
74
|
+
user = user.dup
|
75
|
+
colorize!(:green, user) if @colorize
|
76
|
+
user
|
77
|
+
end
|
78
|
+
|
79
|
+
def format_status(status)
|
80
|
+
status = status.dup
|
81
|
+
if @colorize
|
82
|
+
colorize_all!(:yellow, status, USERNAME_REGEX)
|
83
|
+
colorize_all!(:magenta, status, HASHTAG_REGEX)
|
84
|
+
URI.extract(status, ["http", "https"]).uniq.each do |url|
|
85
|
+
colorize_all!(:cyan, status, url)
|
86
|
+
end
|
87
|
+
end
|
88
|
+
status
|
89
|
+
end
|
90
|
+
|
91
|
+
def format_record_header(record)
|
92
|
+
time_diff_value, time_diff_unit = Util.humanize_time_diff(record[:status][:created_at], Time.now)
|
93
|
+
from_user = record[:user].dup
|
94
|
+
colorize!(:green, from_user) if @colorize
|
95
|
+
in_reply_to = record[:status][:in_reply_to]
|
96
|
+
in_reply_to = if in_reply_to && !in_reply_to.empty?
|
97
|
+
in_reply_to = colorize!(:green, in_reply_to.dup) if @colorize
|
98
|
+
"in reply to #{in_reply_to}, "
|
99
|
+
else
|
100
|
+
""
|
101
|
+
end
|
102
|
+
"#{from_user}, #{in_reply_to}#{time_diff_value} #{time_diff_unit} ago:"
|
103
|
+
end
|
104
|
+
|
105
|
+
def colorize_all!(color, str, pattern)
|
106
|
+
str.gsub!(pattern) { |s| colorize_str(COLOR_CODES[color.to_sym], s) }
|
107
|
+
end
|
108
|
+
|
109
|
+
def colorize!(color, str)
|
110
|
+
str.replace colorize_str(COLOR_CODES[color.to_sym], str)
|
111
|
+
end
|
112
|
+
|
113
|
+
def colorize_str(color_code, str)
|
114
|
+
"\033[#{color_code}m#{str}\033[0m"
|
115
|
+
end
|
116
|
+
end
|
117
|
+
end
|
@@ -0,0 +1,22 @@
|
|
1
|
+
module Tweetwine
|
2
|
+
class Options
|
3
|
+
def initialize(options, source = nil)
|
4
|
+
@hash = options.to_hash
|
5
|
+
@source = source
|
6
|
+
end
|
7
|
+
|
8
|
+
def [](key)
|
9
|
+
@hash[key]
|
10
|
+
end
|
11
|
+
|
12
|
+
def require(key)
|
13
|
+
value = @hash[key]
|
14
|
+
if value.nil?
|
15
|
+
msg = "Option #{key} is required"
|
16
|
+
msg << " for #{@source}" if @source
|
17
|
+
raise ArgumentError, msg
|
18
|
+
end
|
19
|
+
value
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|