tweetwine 0.2.4
Sign up to get free protection for your applications and to get access to all the features.
- 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
|