tuomas-tweetwine 0.1.1 → 0.1.2
Sign up to get free protection for your applications and to get access to all the features.
- data/CHANGELOG.rdoc +6 -0
- data/README.rdoc +15 -14
- data/Rakefile +3 -3
- data/bin/tweetwine +31 -85
- data/lib/tweetwine.rb +1 -1
- data/lib/tweetwine/client.rb +22 -6
- data/lib/tweetwine/io.rb +10 -4
- data/lib/tweetwine/startup_config.rb +41 -0
- data/test/client_test.rb +159 -0
- data/test/io_test.rb +139 -0
- data/test/startup_config_test.rb +67 -0
- data/test/test_config.yaml +3 -0
- data/test/test_helper.rb +7 -0
- data/test/util_test.rb +28 -0
- metadata +11 -5
- data/lib/tweetwine/config.rb +0 -25
data/CHANGELOG.rdoc
CHANGED
data/README.rdoc
CHANGED
@@ -14,28 +14,29 @@ The program is compatible with Ruby 1.9.1.
|
|
14
14
|
|
15
15
|
In the command line, run the program by entering
|
16
16
|
|
17
|
-
$ tweetwine [
|
17
|
+
$ tweetwine [options...] [command]
|
18
18
|
|
19
19
|
The program needs the user's username and password for authentication. This
|
20
|
-
information can be supplied either
|
21
|
-
|
22
|
-
|
20
|
+
information can be supplied either via a configuration file or as an option
|
21
|
+
(<tt>-a USERNAME:PASSWORD</tt>) to the program. It is recommended to use the
|
22
|
+
former method over the latter.
|
23
23
|
|
24
|
-
The
|
25
|
-
|
26
|
-
<tt>friends</tt>:: Fetch friends' latest statuses (the contents of the user's home).
|
27
|
-
<tt>user [name]</tt>:: Fetch a specific user's latest statuses, identified by the argument; if given no argument, fetch your own statuses.
|
28
|
-
<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.
|
29
|
-
|
30
|
-
If <tt>[command]</tt> is not given, it defaults to <tt>friends</tt>.
|
31
|
-
|
32
|
-
The configuration file, in <tt>~/.tweetwine</tt>, is in YAML syntax. It can
|
33
|
-
contain the following settings:
|
24
|
+
The configuration file, in <tt>~/.tweetwine</tt>, is in YAML syntax. The
|
25
|
+
program recognizes the following settings:
|
34
26
|
|
35
27
|
username: <your_username>
|
36
28
|
password: <your_password>
|
37
29
|
colorize: true|false
|
38
30
|
|
31
|
+
When invoking the program, the supported commands are
|
32
|
+
|
33
|
+
<tt>home</tt>:: Fetch friends' latest statuses (the contents of the authenticated user's home).
|
34
|
+
<tt>mentions</tt>:: Fetch latest mentions for the authenticated user.
|
35
|
+
<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.
|
36
|
+
<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.
|
37
|
+
|
38
|
+
If no <tt>[command]</tt> is given, the default action is <tt>home</tt>.
|
39
|
+
|
39
40
|
For all the options, see:
|
40
41
|
|
41
42
|
$ tweetwine -h
|
data/Rakefile
CHANGED
@@ -2,7 +2,7 @@ require "rubygems"
|
|
2
2
|
|
3
3
|
full_name = "Tweetwine"
|
4
4
|
package_name = "tweetwine"
|
5
|
-
version = "0.1.
|
5
|
+
version = "0.1.2"
|
6
6
|
|
7
7
|
require "lib/#{package_name}"
|
8
8
|
|
@@ -20,7 +20,7 @@ spec = Gem::Specification.new do |s|
|
|
20
20
|
s.email = "tkareine@gmail.com"
|
21
21
|
|
22
22
|
s.platform = Gem::Platform::RUBY
|
23
|
-
s.files = FileList["Rakefile", "*.rdoc", "bin/**/*", "lib/**/*", "
|
23
|
+
s.files = FileList["Rakefile", "*.rdoc", "bin/**/*", "lib/**/*", "test/**/*"].to_a
|
24
24
|
s.executables = ["tweetwine"]
|
25
25
|
|
26
26
|
s.add_dependency("json", ">= 1.1.4")
|
@@ -30,7 +30,7 @@ spec = Gem::Specification.new do |s|
|
|
30
30
|
s.extra_rdoc_files = FileList["*.rdoc"].to_a
|
31
31
|
s.rdoc_options << "--title" << "#{full_name} #{version}" \
|
32
32
|
<< "--main" << "README.rdoc" \
|
33
|
-
<< "--exclude" << "
|
33
|
+
<< "--exclude" << "test" \
|
34
34
|
<< "--line-numbers"
|
35
35
|
end
|
36
36
|
|
data/bin/tweetwine
CHANGED
@@ -1,102 +1,48 @@
|
|
1
1
|
#!/usr/bin/env ruby
|
2
2
|
|
3
3
|
require "rubygems"
|
4
|
-
require "
|
4
|
+
require "optparse"
|
5
|
+
|
5
6
|
require File.dirname(__FILE__) << "/../lib/tweetwine"
|
6
7
|
|
7
8
|
include Tweetwine
|
8
9
|
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
def exit_with_error(why = nil)
|
18
|
-
puts "Error: #{why}" if why
|
19
|
-
exit(2)
|
20
|
-
end
|
21
|
-
|
22
|
-
def exit_with_usage(why = nil)
|
23
|
-
puts "#{why}\n\n" if why
|
24
|
-
puts <<-END
|
10
|
+
begin
|
11
|
+
config = StartupConfig.new(Client::COMMANDS)
|
12
|
+
config.parse(ARGV, ENV["HOME"] + "/.tweetwine") do |args|
|
13
|
+
options = {}
|
14
|
+
begin
|
15
|
+
OptionParser.new do |opt|
|
16
|
+
opt.banner =<<-END
|
25
17
|
Usage: tweetwine [options...] [command]
|
26
18
|
|
27
|
-
|
28
|
-
This information can be given either as an option to the program or via a
|
29
|
-
configuration file. Without option [-a], the program attempts to read the
|
30
|
-
configuration file in YAML format at "~/.tweetwine" for authentication.
|
31
|
-
|
32
|
-
Argument [command] can be one of #{Client::COMMANDS.map {|cmd| "\"#{cmd}\"" }.join(", ")}.
|
33
|
-
If [command] is not given, it defaults to "#{Client::COMMANDS[0]}".
|
19
|
+
Commands: #{Client::COMMANDS.join(", ")}
|
34
20
|
|
35
21
|
Options:
|
36
22
|
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
end
|
46
|
-
|
47
|
-
def parse_args
|
48
|
-
options = DEFAULT_OPTIONS.dup
|
49
|
-
|
50
|
-
opts = GetoptLong.new(
|
51
|
-
[ "--auth", "-a", GetoptLong::REQUIRED_ARGUMENT ],
|
52
|
-
[ "--color", "-c", GetoptLong::NO_ARGUMENT ],
|
53
|
-
[ "--help", "-h", GetoptLong::NO_ARGUMENT ],
|
54
|
-
[ "--num", "-n", GetoptLong::REQUIRED_ARGUMENT ]
|
55
|
-
)
|
56
|
-
|
57
|
-
begin
|
58
|
-
opts.each do |opt, arg|
|
59
|
-
case opt
|
60
|
-
when "--auth" then options[:username], options[:password] = arg.split(":", 2)
|
61
|
-
when "--color" then options[:colorize] = true
|
62
|
-
when "--help" then raise HelpNeeded
|
63
|
-
when "--num"
|
64
|
-
arg = arg.to_i
|
65
|
-
if (1..Client::MAX_NUM_STATUSES).include? arg
|
23
|
+
END
|
24
|
+
opt.on("-a", "--auth USERNAME:PASSWORD", "Authentication") do |arg|
|
25
|
+
options[:username], options[:password] = arg.split(":", 2)
|
26
|
+
end
|
27
|
+
opt.on("-c", "--colorize", "Colorize output with ANSI escape codes") do
|
28
|
+
options[:colorize] = true
|
29
|
+
end
|
30
|
+
opt.on("-n", "--num N", Integer, "The number of statuses to fetch, defaults to #{Client::DEFAULT_NUM_STATUSES}") do |arg|
|
66
31
|
options[:num_statuses] = arg
|
67
|
-
else
|
68
|
-
raise ArgumentError, "Invalid number of statuses to show -- must be between 1..#{Client::MAX_NUM_STATUSES}"
|
69
32
|
end
|
70
|
-
|
71
|
-
|
72
|
-
|
73
|
-
|
74
|
-
|
75
|
-
|
76
|
-
|
77
|
-
begin
|
78
|
-
config = Tweetwine::Config.load(ENV["HOME"] + "/.tweetwine")
|
79
|
-
options[:username], options[:password] = [config.username, config.password]
|
80
|
-
options[:colorize] = config.colorize if config.colorize?
|
81
|
-
rescue Exception => e
|
82
|
-
raise ArgumentError, "No auth info given as argument and no configuration file (~/.tweetwine) found."
|
33
|
+
opt.on_tail("-h", "--help", "Show this help message") do
|
34
|
+
puts opt
|
35
|
+
exit(1)
|
36
|
+
end
|
37
|
+
end.parse!(args)
|
38
|
+
rescue OptionParser::ParseError => e
|
39
|
+
raise ArgumentError.new(e)
|
83
40
|
end
|
41
|
+
options
|
84
42
|
end
|
85
|
-
|
86
|
-
command
|
87
|
-
|
88
|
-
|
89
|
-
|
90
|
-
end
|
91
|
-
|
92
|
-
begin
|
93
|
-
options, command, args = parse_args
|
94
|
-
client = Client.new(options)
|
95
|
-
client.send(command.to_sym, *args)
|
96
|
-
rescue HelpNeeded, AlreadyReportedArgumentError
|
97
|
-
exit_with_usage
|
98
|
-
rescue ArgumentError => e
|
99
|
-
exit_with_usage e.message
|
100
|
-
rescue ClientError => e
|
101
|
-
exit_with_error e.message
|
43
|
+
client = Client.new(config.options)
|
44
|
+
client.send(config.command, *config.args)
|
45
|
+
rescue ArgumentError, ClientError => e
|
46
|
+
puts "Error: #{e.message}"
|
47
|
+
exit(2)
|
102
48
|
end
|
data/lib/tweetwine.rb
CHANGED
data/lib/tweetwine/client.rb
CHANGED
@@ -5,23 +5,39 @@ module Tweetwine
|
|
5
5
|
class ClientError < RuntimeError; end
|
6
6
|
|
7
7
|
class Client
|
8
|
-
|
8
|
+
attr_reader :num_statuses
|
9
9
|
|
10
|
-
|
10
|
+
COMMANDS = [:home, :mentions, :user, :update]
|
11
|
+
|
12
|
+
DEFAULT_NUM_STATUSES = 20
|
13
|
+
MAX_NUM_STATUSES = 200
|
11
14
|
MAX_STATUS_LENGTH = 140
|
12
15
|
|
13
16
|
def initialize(options)
|
14
|
-
@username
|
15
|
-
|
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/"
|
16
20
|
@colorize = options[:colorize] || false
|
17
|
-
@num_statuses = options[:num_statuses]
|
21
|
+
@num_statuses = if options[:num_statuses]
|
22
|
+
if (1..MAX_NUM_STATUSES).include? options[:num_statuses]
|
23
|
+
options[:num_statuses]
|
24
|
+
else
|
25
|
+
raise ArgumentError, "Invalid number of statuses to show -- must be between 1..#{Client::MAX_NUM_STATUSES}"
|
26
|
+
end
|
27
|
+
else
|
28
|
+
DEFAULT_NUM_STATUSES
|
29
|
+
end
|
18
30
|
@io = IO.new(options)
|
19
31
|
end
|
20
32
|
|
21
|
-
def
|
33
|
+
def home
|
22
34
|
@io.show_statuses JSON.parse(get("statuses/friends_timeline.json?count=#{@num_statuses}"))
|
23
35
|
end
|
24
36
|
|
37
|
+
def mentions
|
38
|
+
@io.show_statuses JSON.parse(get("statuses/mentions.json?count=#{@num_statuses}"))
|
39
|
+
end
|
40
|
+
|
25
41
|
def user(user = @username)
|
26
42
|
@io.show_statuses JSON.parse(get("statuses/user_timeline/#{user}.json?count=#{@num_statuses}"))
|
27
43
|
end
|
data/lib/tweetwine/io.rb
CHANGED
@@ -30,10 +30,17 @@ module Tweetwine
|
|
30
30
|
time_diff_value, time_diff_unit = Util.humanize_time_diff(Time.now, status["created_at"])
|
31
31
|
from_user = status["user"]["screen_name"]
|
32
32
|
from_user = colorize(:green, from_user) if @colorize
|
33
|
+
in_reply_to = status["in_reply_to_screen_name"]
|
34
|
+
in_reply_to = if in_reply_to && !in_reply_to.empty?
|
35
|
+
in_reply_to = colorize(:green, in_reply_to) if @colorize
|
36
|
+
"in reply to #{in_reply_to}, "
|
37
|
+
else
|
38
|
+
""
|
39
|
+
end
|
33
40
|
text = status["text"]
|
34
41
|
text = colorize(:red, text, /@\w+/) if @colorize
|
35
42
|
@output.puts <<-END
|
36
|
-
#{from_user}, #{time_diff_value} #{time_diff_unit} ago:
|
43
|
+
#{from_user}, #{in_reply_to}#{time_diff_value} #{time_diff_unit} ago:
|
37
44
|
#{text}
|
38
45
|
|
39
46
|
END
|
@@ -44,8 +51,7 @@ module Tweetwine
|
|
44
51
|
|
45
52
|
COLOR_CODES = {
|
46
53
|
:green => "\033[32m",
|
47
|
-
:red => "\033[31m"
|
48
|
-
:neutral => "\033[0m"
|
54
|
+
:red => "\033[31m"
|
49
55
|
}
|
50
56
|
|
51
57
|
def colorize(color, str, matcher = nil)
|
@@ -59,7 +65,7 @@ module Tweetwine
|
|
59
65
|
end
|
60
66
|
|
61
67
|
def colorize_str(color_code, str)
|
62
|
-
"#{color_code}#{str}
|
68
|
+
"#{color_code}#{str}\033[0m"
|
63
69
|
end
|
64
70
|
end
|
65
71
|
end
|
@@ -0,0 +1,41 @@
|
|
1
|
+
module Tweetwine
|
2
|
+
class StartupConfig
|
3
|
+
attr_reader :options, :command, :args, :supported_commands
|
4
|
+
|
5
|
+
def initialize(supported_commands)
|
6
|
+
@supported_commands = supported_commands.to_a
|
7
|
+
raise ArgumentError, "Must give at least one supported command" if @supported_commands.empty?
|
8
|
+
@options = {}
|
9
|
+
@command = @supported_commands.first
|
10
|
+
@args = []
|
11
|
+
end
|
12
|
+
|
13
|
+
def parse(args = [], config_file = nil, &cmd_parser)
|
14
|
+
options = parse_options(args, config_file, &cmd_parser)
|
15
|
+
command = if args.empty? then @supported_commands.first else args.shift.to_sym end
|
16
|
+
raise ArgumentError, "Unknown command." unless @supported_commands.include? command
|
17
|
+
@options, @command, @args = options, command, args
|
18
|
+
self
|
19
|
+
end
|
20
|
+
|
21
|
+
private
|
22
|
+
|
23
|
+
def parse_options(args, config_file, &cmd_parser)
|
24
|
+
cmd_options = if cmd_parser then parse_cmdline_args(args, &cmd_parser) else {} end
|
25
|
+
config_options = if config_file && File.exists?(config_file) then parse_config_file(config_file) else {} end
|
26
|
+
config_options.merge(cmd_options)
|
27
|
+
end
|
28
|
+
|
29
|
+
def parse_cmdline_args(args, &cmd_parser)
|
30
|
+
cmd_parser.call(args)
|
31
|
+
end
|
32
|
+
|
33
|
+
def parse_config_file(config_file)
|
34
|
+
options = YAML.load(File.read(config_file))
|
35
|
+
options.inject({}) do |result, pair|
|
36
|
+
result[pair.first.to_sym] = pair.last
|
37
|
+
result
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
data/test/client_test.rb
ADDED
@@ -0,0 +1,159 @@
|
|
1
|
+
require File.dirname(__FILE__) << "/test_helper"
|
2
|
+
require "json"
|
3
|
+
|
4
|
+
module Tweetwine
|
5
|
+
|
6
|
+
class ClientTest < Test::Unit::TestCase
|
7
|
+
context "Upon initializing a client" do
|
8
|
+
should "raise exception when no authentication data is given" do
|
9
|
+
assert_raises(ArgumentError) { Client.new({}) }
|
10
|
+
assert_raises(ArgumentError) { Client.new({ :password => "bar" }) }
|
11
|
+
assert_raises(ArgumentError) { Client.new({ :username => "", :password => "bar" }) }
|
12
|
+
end
|
13
|
+
|
14
|
+
should "use default num of statuses if not configured otherwise" do
|
15
|
+
@client = Client.new({ :username => "foo", :password => "bar" })
|
16
|
+
assert_equal Client::DEFAULT_NUM_STATUSES, @client.num_statuses
|
17
|
+
end
|
18
|
+
|
19
|
+
should "use configured num of statuses if in allowed range" do
|
20
|
+
@client = Client.new({ :username => "foo", :password => "bar", :num_statuses => 12 })
|
21
|
+
assert_equal 12, @client.num_statuses
|
22
|
+
end
|
23
|
+
|
24
|
+
should "raise an exception for configured num of statuses if not in allowed range" do
|
25
|
+
assert_raises(ArgumentError) { Client.new({ :username => "foo", :password => "bar", :num_statuses => Client::MAX_NUM_STATUSES + 1 }) }
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
context "A client" do
|
30
|
+
setup do
|
31
|
+
@client = Client.new({ :username => "foo", :password => "bar" })
|
32
|
+
@io = mock()
|
33
|
+
@client.instance_variable_set(:@io, @io)
|
34
|
+
end
|
35
|
+
|
36
|
+
should "raise ClientError for invalid request" do
|
37
|
+
RestClient.expects(:get) \
|
38
|
+
.with("https://foo:bar@twitter.com/statuses/friends_timeline.json?count=20") \
|
39
|
+
.raises(RestClient::Unauthorized)
|
40
|
+
assert_raises(ClientError) { @client.home }
|
41
|
+
end
|
42
|
+
|
43
|
+
should "fetch friends' statuses (home view)" do
|
44
|
+
statuses = [
|
45
|
+
{
|
46
|
+
"created_at" => Time.at(1).to_s,
|
47
|
+
"user" => { "username" => "zanzibar" },
|
48
|
+
"text" => "wassup?"
|
49
|
+
},
|
50
|
+
{
|
51
|
+
"created_at" => Time.at(2).to_s,
|
52
|
+
"user" => { "username" => "lulzwoo" },
|
53
|
+
"text" => "nuttin"
|
54
|
+
}
|
55
|
+
]
|
56
|
+
RestClient.expects(:get) \
|
57
|
+
.with("https://foo:bar@twitter.com/statuses/friends_timeline.json?count=20") \
|
58
|
+
.returns(statuses.to_json)
|
59
|
+
@io.expects(:show_statuses).with(statuses)
|
60
|
+
@client.home
|
61
|
+
end
|
62
|
+
|
63
|
+
should "fetch mentions" do
|
64
|
+
statuses = [
|
65
|
+
{
|
66
|
+
"created_at" => Time.at(1).to_s,
|
67
|
+
"in_reply_to_screen_name" => "foo",
|
68
|
+
"user" => { "username" => "zanzibar" },
|
69
|
+
"text" => "wassup, @foo?"
|
70
|
+
},
|
71
|
+
{
|
72
|
+
"created_at" => Time.at(2).to_s,
|
73
|
+
"in_reply_to_screen_name" => "foo",
|
74
|
+
"user" => { "username" => "lulzwoo" },
|
75
|
+
"text" => "@foo, doing nuttin"
|
76
|
+
}
|
77
|
+
]
|
78
|
+
RestClient.expects(:get) \
|
79
|
+
.with("https://foo:bar@twitter.com/statuses/mentions.json?count=20") \
|
80
|
+
.returns(statuses.to_json)
|
81
|
+
@io.expects(:show_statuses).with(statuses)
|
82
|
+
@client.mentions
|
83
|
+
end
|
84
|
+
|
85
|
+
should "fetch a specific user's statuses, with the user identified by given argument" do
|
86
|
+
statuses = [
|
87
|
+
{
|
88
|
+
"created_at" => Time.at(1).to_s,
|
89
|
+
"user" => { "username" => "zanzibar" },
|
90
|
+
"text" => "wassup?"
|
91
|
+
}
|
92
|
+
]
|
93
|
+
RestClient.expects(:get) \
|
94
|
+
.with("https://foo:bar@twitter.com/statuses/user_timeline/zanzibar.json?count=20") \
|
95
|
+
.returns(statuses.to_json)
|
96
|
+
@io.expects(:show_statuses).with(statuses)
|
97
|
+
@client.user("zanzibar")
|
98
|
+
end
|
99
|
+
|
100
|
+
should "fetch a specific user's statuses, with the user being the authenticated user itself when given no argument" do
|
101
|
+
statuses = [
|
102
|
+
{
|
103
|
+
"created_at" => Time.at(1).to_s,
|
104
|
+
"user" => { "username" => "foo" },
|
105
|
+
"text" => "wassup?"
|
106
|
+
}
|
107
|
+
]
|
108
|
+
RestClient.expects(:get) \
|
109
|
+
.with("https://foo:bar@twitter.com/statuses/user_timeline/foo.json?count=20") \
|
110
|
+
.returns(statuses.to_json)
|
111
|
+
@io.expects(:show_statuses).with(statuses)
|
112
|
+
@client.user
|
113
|
+
end
|
114
|
+
|
115
|
+
should "post a status update, when positive confirmation" do
|
116
|
+
status = {
|
117
|
+
"created_at" => Time.at(1).to_s,
|
118
|
+
"user" => { "username" => "foo" },
|
119
|
+
"text" => "wondering about"
|
120
|
+
}
|
121
|
+
RestClient.expects(:post) \
|
122
|
+
.with("https://foo:bar@twitter.com/statuses/update.json", {:status => "wondering about"}) \
|
123
|
+
.returns(status.to_json)
|
124
|
+
@io.expects(:confirm).with("Really send?").returns(true)
|
125
|
+
@io.expects(:info).with("Sent status update.\n\n")
|
126
|
+
@io.expects(:show_statuses).with([status])
|
127
|
+
@client.update("wondering about")
|
128
|
+
end
|
129
|
+
|
130
|
+
should "cancel a status update, when negative confirmation" do
|
131
|
+
RestClient.expects(:post).never
|
132
|
+
@io.expects(:confirm).with("Really send?").returns(false)
|
133
|
+
@io.expects(:info).with("Cancelled.")
|
134
|
+
@io.expects(:show_statuses).never
|
135
|
+
@client.update("wondering about")
|
136
|
+
end
|
137
|
+
|
138
|
+
should "truncate a status update too long and warn the user" do
|
139
|
+
long_status_update = "x aaa bbb ccc ddd eee fff ggg hhh iii jjj kkk lll mmm nnn ooo ppp qqq rrr sss ttt uuu vvv www xxx yyy zzz 111 222 333 444 555 666 777 888 999 000"
|
140
|
+
truncated_status_update = "x aaa bbb ccc ddd eee fff ggg hhh iii jjj kkk lll mmm nnn ooo ppp qqq rrr sss ttt uuu vvv www xxx yyy zzz 111 222 333 444 555 666 777 888 99"
|
141
|
+
status = {
|
142
|
+
"created_at" => Time.at(1).to_s,
|
143
|
+
"user" => { "username" => "foo" },
|
144
|
+
"text" => truncated_status_update
|
145
|
+
}
|
146
|
+
|
147
|
+
RestClient.expects(:post) \
|
148
|
+
.with("https://foo:bar@twitter.com/statuses/update.json", {:status => truncated_status_update}) \
|
149
|
+
.returns(status.to_json)
|
150
|
+
@io.expects(:warn).with("Update will be truncated: #{truncated_status_update}")
|
151
|
+
@io.expects(:confirm).with("Really send?").returns(true)
|
152
|
+
@io.expects(:info).with("Sent status update.\n\n")
|
153
|
+
@io.expects(:show_statuses).with([status])
|
154
|
+
@client.update(long_status_update)
|
155
|
+
end
|
156
|
+
end
|
157
|
+
end
|
158
|
+
|
159
|
+
end
|
data/test/io_test.rb
ADDED
@@ -0,0 +1,139 @@
|
|
1
|
+
require File.dirname(__FILE__) << "/test_helper"
|
2
|
+
|
3
|
+
module Tweetwine
|
4
|
+
|
5
|
+
class IOTest < Test::Unit::TestCase
|
6
|
+
context "An IO" do
|
7
|
+
setup do
|
8
|
+
@input = mock()
|
9
|
+
@output = mock()
|
10
|
+
@io = IO.new({ :input => @input, :output => @output })
|
11
|
+
end
|
12
|
+
|
13
|
+
should "output prompt and return input as trimmed" do
|
14
|
+
@output.expects(:print).with("The answer: ")
|
15
|
+
@input.expects(:gets).returns(" 42 ")
|
16
|
+
assert_equal "42", @io.prompt("The answer")
|
17
|
+
end
|
18
|
+
|
19
|
+
should "output info message" do
|
20
|
+
@output.expects(:puts).with("foo")
|
21
|
+
@io.info("foo")
|
22
|
+
end
|
23
|
+
|
24
|
+
should "output warning message" do
|
25
|
+
@output.expects(:puts).with("Warning: monkey patching ahead")
|
26
|
+
@io.warn("monkey patching ahead")
|
27
|
+
end
|
28
|
+
|
29
|
+
should "confirm action, with positive answer" do
|
30
|
+
@output.expects(:print).with("Fire nukes? [yN] ")
|
31
|
+
@input.expects(:gets).returns("y")
|
32
|
+
assert_equal true, @io.confirm("Fire nukes?")
|
33
|
+
end
|
34
|
+
|
35
|
+
should "confirm action, with negative answer" do
|
36
|
+
@output.expects(:print).with("Fire nukes? [yN] ")
|
37
|
+
@input.expects(:gets).returns("n")
|
38
|
+
assert_equal false, @io.confirm("Fire nukes?")
|
39
|
+
end
|
40
|
+
|
41
|
+
should "confirm action, with default answer" do
|
42
|
+
@output.expects(:print).with("Fire nukes? [yN] ")
|
43
|
+
@input.expects(:gets).returns("")
|
44
|
+
assert_equal false, @io.confirm("Fire nukes?")
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
context "An IO, with colorization disabled" do
|
49
|
+
setup do
|
50
|
+
@input = mock()
|
51
|
+
@output = mock()
|
52
|
+
@io = IO.new({ :input => @input, :output => @output, :colorize => false })
|
53
|
+
end
|
54
|
+
|
55
|
+
should "print statuses without in-reply info" do
|
56
|
+
statuses = [
|
57
|
+
{
|
58
|
+
"created_at" => Time.at(1),
|
59
|
+
"user" => { "screen_name" => "fooman" },
|
60
|
+
"text" => "Hi, @barman! Lulz woo!"
|
61
|
+
}
|
62
|
+
]
|
63
|
+
Util.expects(:humanize_time_diff).returns([2, "secs"])
|
64
|
+
@output.expects(:puts).with(<<-END
|
65
|
+
fooman, 2 secs ago:
|
66
|
+
Hi, @barman! Lulz woo!
|
67
|
+
|
68
|
+
END
|
69
|
+
)
|
70
|
+
@io.show_statuses(statuses)
|
71
|
+
end
|
72
|
+
|
73
|
+
should "print statuses with in-reply info" do
|
74
|
+
statuses = [
|
75
|
+
{
|
76
|
+
"created_at" => Time.at(1),
|
77
|
+
"in_reply_to_screen_name" => "barman",
|
78
|
+
"user" => { "screen_name" => "fooman" },
|
79
|
+
"text" => "Hi, @barman! Lulz woo!"
|
80
|
+
}
|
81
|
+
]
|
82
|
+
Util.expects(:humanize_time_diff).returns([2, "secs"])
|
83
|
+
@output.expects(:puts).with(<<-END
|
84
|
+
fooman, in reply to barman, 2 secs ago:
|
85
|
+
Hi, @barman! Lulz woo!
|
86
|
+
|
87
|
+
END
|
88
|
+
)
|
89
|
+
@io.show_statuses(statuses)
|
90
|
+
end
|
91
|
+
end
|
92
|
+
|
93
|
+
context "An IO, with colorization enabled" do
|
94
|
+
setup do
|
95
|
+
@input = mock()
|
96
|
+
@output = mock()
|
97
|
+
@io = IO.new({ :input => @input, :output => @output, :colorize => true })
|
98
|
+
end
|
99
|
+
|
100
|
+
should "print statuses without in-reply info" do
|
101
|
+
statuses = [
|
102
|
+
{
|
103
|
+
"created_at" => Time.at(1),
|
104
|
+
"user" => { "screen_name" => "fooman" },
|
105
|
+
"text" => "Hi, @barman! Lulz woo!"
|
106
|
+
}
|
107
|
+
]
|
108
|
+
Util.expects(:humanize_time_diff).returns([2, "secs"])
|
109
|
+
@output.expects(:puts).with(<<-END
|
110
|
+
\033[32mfooman\033[0m, 2 secs ago:
|
111
|
+
Hi, \033[31m@barman\033[0m! Lulz woo!
|
112
|
+
|
113
|
+
END
|
114
|
+
)
|
115
|
+
@io.show_statuses(statuses)
|
116
|
+
end
|
117
|
+
|
118
|
+
should "print statuses with in-reply info" do
|
119
|
+
statuses = [
|
120
|
+
{
|
121
|
+
"created_at" => Time.at(1),
|
122
|
+
"in_reply_to_screen_name" => "barman",
|
123
|
+
"user" => { "screen_name" => "fooman" },
|
124
|
+
"text" => "Hi, @barman! Lulz woo!"
|
125
|
+
}
|
126
|
+
]
|
127
|
+
Util.expects(:humanize_time_diff).returns([2, "secs"])
|
128
|
+
@output.expects(:puts).with(<<-END
|
129
|
+
\033[32mfooman\033[0m, in reply to \033[32mbarman\033[0m, 2 secs ago:
|
130
|
+
Hi, \033[31m@barman\033[0m! Lulz woo!
|
131
|
+
|
132
|
+
END
|
133
|
+
)
|
134
|
+
@io.show_statuses(statuses)
|
135
|
+
end
|
136
|
+
end
|
137
|
+
end
|
138
|
+
|
139
|
+
end
|
@@ -0,0 +1,67 @@
|
|
1
|
+
require File.dirname(__FILE__) << "/test_helper"
|
2
|
+
|
3
|
+
module Tweetwine
|
4
|
+
|
5
|
+
class StartupConfigTest < Test::Unit::TestCase
|
6
|
+
TEST_CONFIG_FILE = File.dirname(__FILE__) << "/test_config.yaml"
|
7
|
+
|
8
|
+
context "To initialize a StartupConfig" do
|
9
|
+
should "require at least one supported command" do
|
10
|
+
assert_raise(ArgumentError) { StartupConfig.new([]) }
|
11
|
+
assert_nothing_raised { StartupConfig.new([:default_action]) }
|
12
|
+
end
|
13
|
+
end
|
14
|
+
|
15
|
+
context "An initialized StartupConfig" do
|
16
|
+
setup do
|
17
|
+
@config = StartupConfig.new([:default_action, :another_action])
|
18
|
+
end
|
19
|
+
|
20
|
+
should "use the first supported command as a default command when given no command as a cmdline argument" do
|
21
|
+
@config.parse
|
22
|
+
assert_equal :default_action, @config.command
|
23
|
+
end
|
24
|
+
|
25
|
+
context "when given no cmdline args and a config file" do
|
26
|
+
setup do
|
27
|
+
@config.parse([], TEST_CONFIG_FILE)
|
28
|
+
end
|
29
|
+
|
30
|
+
should "have the parsed option defined" do
|
31
|
+
assert_equal false, @config.options[:colorize]
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
context "when given cmdline args and no config file" do
|
36
|
+
setup do
|
37
|
+
@config.parse(%w{--opt foo}) do |args|
|
38
|
+
args.clear
|
39
|
+
{:opt => "foo"}
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
should "have the parsed option defined" do
|
44
|
+
assert_equal "foo", @config.options[:opt]
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
context "when given an option both as a cmdline option and in a config file" do
|
49
|
+
setup do
|
50
|
+
@config.parse(%w{--colorize}, TEST_CONFIG_FILE) do |args|
|
51
|
+
args.clear
|
52
|
+
{:colorize => true}
|
53
|
+
end
|
54
|
+
end
|
55
|
+
|
56
|
+
should "the command line option should override the config file option" do
|
57
|
+
assert_equal true, @config.options[:colorize]
|
58
|
+
end
|
59
|
+
|
60
|
+
should "have nil for an undefined option" do
|
61
|
+
assert_nil @config.options[:num_statuses]
|
62
|
+
end
|
63
|
+
end
|
64
|
+
end
|
65
|
+
end
|
66
|
+
|
67
|
+
end
|
data/test/test_helper.rb
ADDED
data/test/util_test.rb
ADDED
@@ -0,0 +1,28 @@
|
|
1
|
+
require File.dirname(__FILE__) << "/test_helper"
|
2
|
+
require "time"
|
3
|
+
|
4
|
+
module Tweetwine
|
5
|
+
|
6
|
+
class UtilTest < Test::Unit::TestCase
|
7
|
+
context "The module" do
|
8
|
+
should "humanize time difference" do
|
9
|
+
assert_equal [1, "sec"], Util.humanize_time_diff(Time.parse("2009-01-01 00:00:59").to_s, Time.parse("2009-01-01 00:01:00"))
|
10
|
+
assert_equal [0, "sec"], Util.humanize_time_diff(Time.parse("2009-01-01 01:00:00").to_s, Time.parse("2009-01-01 01:00:00"))
|
11
|
+
assert_equal [1, "sec"], Util.humanize_time_diff(Time.parse("2009-01-01 01:00:00").to_s, Time.parse("2009-01-01 01:00:01"))
|
12
|
+
assert_equal [59, "sec"], Util.humanize_time_diff(Time.parse("2009-01-01 01:00:00").to_s, Time.parse("2009-01-01 01:00:59"))
|
13
|
+
assert_equal [59, "min"], Util.humanize_time_diff(Time.parse("2009-01-01 01:00").to_s, Time.parse("2009-01-01 01:59"))
|
14
|
+
assert_equal [59, "min"], Util.humanize_time_diff(Time.parse("2009-01-01 01:00:30").to_s, Time.parse("2009-01-01 01:59:00"))
|
15
|
+
assert_equal [57, "min"], Util.humanize_time_diff(Time.parse("2009-01-01 01:01:00").to_s, Time.parse("2009-01-01 01:58:00"))
|
16
|
+
assert_equal [56, "min"], Util.humanize_time_diff(Time.parse("2009-01-01 01:01:31").to_s, Time.parse("2009-01-01 01:58:00"))
|
17
|
+
assert_equal [57, "min"], Util.humanize_time_diff(Time.parse("2009-01-01 01:01:00").to_s, Time.parse("2009-01-01 01:58:29"))
|
18
|
+
assert_equal [58, "min"], Util.humanize_time_diff(Time.parse("2009-01-01 01:01:00").to_s, Time.parse("2009-01-01 01:58:30"))
|
19
|
+
assert_equal [1, "hour"], Util.humanize_time_diff(Time.parse("2009-01-01 01:00").to_s, Time.parse("2009-01-01 02:00"))
|
20
|
+
assert_equal [1, "hour"], Util.humanize_time_diff(Time.parse("2009-01-01 02:00").to_s, Time.parse("2009-01-01 01:00"))
|
21
|
+
assert_equal [2, "hours"], Util.humanize_time_diff(Time.parse("2009-01-01 01:00").to_s, Time.parse("2009-01-01 03:00"))
|
22
|
+
assert_equal [1, "day"], Util.humanize_time_diff(Time.parse("2009-01-01 01:00").to_s, Time.parse("2009-01-02 03:00"))
|
23
|
+
assert_equal [2, "days"], Util.humanize_time_diff(Time.parse("2009-01-01 01:00").to_s, Time.parse("2009-01-03 03:00"))
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
end
|
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: tuomas-tweetwine
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.1.
|
4
|
+
version: 0.1.2
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Tuomas Kareinen
|
@@ -9,7 +9,7 @@ autorequire:
|
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
11
|
|
12
|
-
date: 2009-04
|
12
|
+
date: 2009-05-04 00:00:00 -07:00
|
13
13
|
default_executable: tweetwine
|
14
14
|
dependencies:
|
15
15
|
- !ruby/object:Gem::Dependency
|
@@ -48,20 +48,26 @@ files:
|
|
48
48
|
- bin/tweetwine
|
49
49
|
- lib/tweetwine
|
50
50
|
- lib/tweetwine/client.rb
|
51
|
-
- lib/tweetwine/config.rb
|
52
51
|
- lib/tweetwine/io.rb
|
52
|
+
- lib/tweetwine/startup_config.rb
|
53
53
|
- lib/tweetwine/util.rb
|
54
54
|
- lib/tweetwine.rb
|
55
|
+
- test/client_test.rb
|
56
|
+
- test/io_test.rb
|
57
|
+
- test/startup_config_test.rb
|
58
|
+
- test/test_config.yaml
|
59
|
+
- test/test_helper.rb
|
60
|
+
- test/util_test.rb
|
55
61
|
has_rdoc: true
|
56
62
|
homepage: http://github.com/tuomas/tweetwine
|
57
63
|
post_install_message:
|
58
64
|
rdoc_options:
|
59
65
|
- --title
|
60
|
-
- Tweetwine 0.1.
|
66
|
+
- Tweetwine 0.1.2
|
61
67
|
- --main
|
62
68
|
- README.rdoc
|
63
69
|
- --exclude
|
64
|
-
-
|
70
|
+
- test
|
65
71
|
- --line-numbers
|
66
72
|
require_paths:
|
67
73
|
- lib
|
data/lib/tweetwine/config.rb
DELETED
@@ -1,25 +0,0 @@
|
|
1
|
-
require "yaml"
|
2
|
-
|
3
|
-
module Tweetwine
|
4
|
-
class Config
|
5
|
-
def self.load(file)
|
6
|
-
new(file)
|
7
|
-
end
|
8
|
-
|
9
|
-
private_class_method :new
|
10
|
-
|
11
|
-
def initialize(file)
|
12
|
-
@config = YAML.load(File.read(file))
|
13
|
-
end
|
14
|
-
|
15
|
-
def method_missing(sym)
|
16
|
-
key = sym.to_s
|
17
|
-
if key[-1,1] == "?"
|
18
|
-
result = @config.has_key? key[0...-1]
|
19
|
-
else
|
20
|
-
result = @config[key]
|
21
|
-
end
|
22
|
-
result
|
23
|
-
end
|
24
|
-
end
|
25
|
-
end
|