mattmueller-twibot 0.1.7.1
Sign up to get free protection for your applications and to get access to all the features.
- data/History.txt +50 -0
- data/Rakefile +30 -0
- data/Readme.rdoc +269 -0
- data/lib/hash.rb +8 -0
- data/lib/twibot.rb +87 -0
- data/lib/twibot/bot.rb +422 -0
- data/lib/twibot/config.rb +140 -0
- data/lib/twibot/handlers.rb +122 -0
- data/lib/twibot/macros.rb +101 -0
- data/lib/twibot/tweets.rb +4 -0
- data/test/test_bot.rb +300 -0
- data/test/test_config.rb +89 -0
- data/test/test_handler.rb +191 -0
- data/test/test_hash.rb +34 -0
- data/test/test_helper.rb +44 -0
- data/test/test_twibot.rb +1 -0
- data/twibot.gemspec +38 -0
- metadata +96 -0
@@ -0,0 +1,140 @@
|
|
1
|
+
require 'optparse'
|
2
|
+
|
3
|
+
module Twibot
|
4
|
+
#
|
5
|
+
# Twibot configuration. Use either Twibot::CliConfig.new or
|
6
|
+
# TwibotFileConfig.new setup a new bot from either command line or file
|
7
|
+
# (respectively). Configurations can be chained so they override each other:
|
8
|
+
#
|
9
|
+
# config = Twibot::FileConfig.new
|
10
|
+
# config << Twibot::CliConfig.new
|
11
|
+
# config.to_hash
|
12
|
+
#
|
13
|
+
# The preceding example will create a configuration which is based on a
|
14
|
+
# configuration file but have certain values overridden from the command line.
|
15
|
+
# This can be used for instance to store everything but the Twitter account
|
16
|
+
# password in your configuration file. Then you can just provide the password
|
17
|
+
# when running the bot.
|
18
|
+
#
|
19
|
+
class Config
|
20
|
+
attr_reader :settings
|
21
|
+
|
22
|
+
DEFAULT = {
|
23
|
+
:host => "twitter.com",
|
24
|
+
:min_interval => 30,
|
25
|
+
:max_interval => 300,
|
26
|
+
:interval_step => 10,
|
27
|
+
:log_level => "info",
|
28
|
+
:log_file => nil,
|
29
|
+
:login => nil,
|
30
|
+
:password => nil,
|
31
|
+
:process => :new,
|
32
|
+
:prompt => false,
|
33
|
+
:daemonize => false,
|
34
|
+
:include_friends => false,
|
35
|
+
:timeline_for => :public
|
36
|
+
}
|
37
|
+
|
38
|
+
def initialize(settings = {})
|
39
|
+
@configs = []
|
40
|
+
@settings = settings
|
41
|
+
end
|
42
|
+
|
43
|
+
#
|
44
|
+
# Add a configuration object to override given settings
|
45
|
+
#
|
46
|
+
def add(config)
|
47
|
+
@configs << config
|
48
|
+
self
|
49
|
+
end
|
50
|
+
|
51
|
+
alias_method :<<, :add
|
52
|
+
|
53
|
+
#
|
54
|
+
# Makes it possible to access configuration settings as attributes
|
55
|
+
#
|
56
|
+
def method_missing(name, *args, &block)
|
57
|
+
regex = /=$/
|
58
|
+
attr_name = name.to_s.sub(regex, '').to_sym
|
59
|
+
return super if name == attr_name && !@settings.key?(attr_name)
|
60
|
+
|
61
|
+
if name != attr_name
|
62
|
+
@settings[attr_name] = args.first
|
63
|
+
end
|
64
|
+
|
65
|
+
@settings[attr_name]
|
66
|
+
end
|
67
|
+
|
68
|
+
#
|
69
|
+
# Merges configurations and returns a hash with all options
|
70
|
+
#
|
71
|
+
def to_hash
|
72
|
+
hash = {}.merge(@settings)
|
73
|
+
@configs.each { |conf| hash.merge!(conf.to_hash) }
|
74
|
+
hash
|
75
|
+
end
|
76
|
+
|
77
|
+
def self.default
|
78
|
+
Config.new({}.merge(DEFAULT))
|
79
|
+
end
|
80
|
+
end
|
81
|
+
|
82
|
+
#
|
83
|
+
# Configuration from command line
|
84
|
+
#
|
85
|
+
class CliConfig < Config
|
86
|
+
|
87
|
+
def initialize(args = $*)
|
88
|
+
super()
|
89
|
+
|
90
|
+
@parser = OptionParser.new do |opts|
|
91
|
+
opts.banner += "Usage: #{File.basename(Twibot.app_file)} [options]"
|
92
|
+
|
93
|
+
opts.on("-m", "--min-interval SECS", Integer, "Minimum poll interval in seconds") { |i| @settings[:min_interval] = i }
|
94
|
+
opts.on("-x", "--max-interval SECS", Integer, "Maximum poll interval in seconds") { |i| @settings[:max_interval] = i }
|
95
|
+
opts.on("-s", "--interval-step SECS", Integer, "Poll interval step in seconds") { |i| @settings[:interval_step] = i }
|
96
|
+
opts.on("-f", "--log-file FILE", "Log file") { |f| @settings[:log_file] = f }
|
97
|
+
opts.on("-l", "--log-level LEVEL", "Log level (err, warn, info, debug), default id info") { |l| @settings[:log_level] = l }
|
98
|
+
opts.on("-u", "--login LOGIN", "Twitter login") { |l| @settings[:login] = l }
|
99
|
+
opts.on("-p", "--password PASSWORD", "Twitter password") { |p| @settings[:password] = p }
|
100
|
+
opts.on("-h", "--help", "Show this message") { puts opts; exit }
|
101
|
+
|
102
|
+
begin
|
103
|
+
require 'daemons'
|
104
|
+
opts.on("-d", "--daemonize", "Run as background process (Not implemented)") { |t| @settings[:daemonize] = true }
|
105
|
+
rescue LoadError
|
106
|
+
end
|
107
|
+
|
108
|
+
end.parse!(args)
|
109
|
+
end
|
110
|
+
end
|
111
|
+
|
112
|
+
#
|
113
|
+
# Configuration from files
|
114
|
+
#
|
115
|
+
class FileConfig < Config
|
116
|
+
|
117
|
+
#
|
118
|
+
# Accepts a stream or a file to read configuration from
|
119
|
+
# Default is to read configuration from ./config/bot.yml
|
120
|
+
#
|
121
|
+
# If a stream is passed it is not closed from within the method
|
122
|
+
#
|
123
|
+
def initialize(fos = File.expand_path("config/bot.yml"))
|
124
|
+
stream = fos.is_a?(String) ? File.open(fos, "r") : fos
|
125
|
+
|
126
|
+
begin
|
127
|
+
config = YAML.load(stream.read)
|
128
|
+
config.symbolize_keys! if config
|
129
|
+
rescue Exception => err
|
130
|
+
puts err.message
|
131
|
+
puts "Unable to load configuration, aborting"
|
132
|
+
exit
|
133
|
+
ensure
|
134
|
+
stream.close if fos.is_a?(String)
|
135
|
+
end
|
136
|
+
|
137
|
+
super config.is_a?(Hash) ? config : {}
|
138
|
+
end
|
139
|
+
end
|
140
|
+
end
|
@@ -0,0 +1,122 @@
|
|
1
|
+
module Twibot
|
2
|
+
module Handlers
|
3
|
+
#
|
4
|
+
# Add a handler for this bot
|
5
|
+
#
|
6
|
+
def add_handler(type, handler)
|
7
|
+
handlers_for_type(type) << handler
|
8
|
+
handler
|
9
|
+
end
|
10
|
+
|
11
|
+
def handlers_for_type(type)
|
12
|
+
if type.is_a? Array
|
13
|
+
handlers[type.first][type.last] ||= []
|
14
|
+
else
|
15
|
+
handlers[type] || {}
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
def dispatch(type, message)
|
20
|
+
handlers_for_type(type).each { |handler| handler.dispatch(message) }
|
21
|
+
end
|
22
|
+
|
23
|
+
def handlers
|
24
|
+
@handlers ||= {
|
25
|
+
:message => [],
|
26
|
+
:reply => [],
|
27
|
+
:tweet => [],
|
28
|
+
:follower => [],
|
29
|
+
:search => {}
|
30
|
+
}
|
31
|
+
end
|
32
|
+
|
33
|
+
def handlers=(hash)
|
34
|
+
@handlers = hash
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
#
|
39
|
+
# A Handler object is an object which can handle a direct message, tweet or
|
40
|
+
# at reply.
|
41
|
+
#
|
42
|
+
class Handler
|
43
|
+
attr_reader :options
|
44
|
+
def initialize(pattern = nil, options = {}, &blk)
|
45
|
+
if pattern.is_a?(Hash)
|
46
|
+
options = pattern
|
47
|
+
pattern = nil
|
48
|
+
end
|
49
|
+
|
50
|
+
@options = options
|
51
|
+
@options[:from].collect! { |s| s.to_s } if @options[:from] && @options[:from].is_a?(Array)
|
52
|
+
@options[:from] = [@options[:from].to_s] if @options[:from] && @options[:from].is_a?(String)
|
53
|
+
@handler = nil
|
54
|
+
@handler = block_given? ? blk : nil
|
55
|
+
self.pattern = pattern
|
56
|
+
end
|
57
|
+
|
58
|
+
#
|
59
|
+
# Parse pattern string and set options
|
60
|
+
#
|
61
|
+
def pattern=(pattern)
|
62
|
+
return if pattern.nil? || pattern == ""
|
63
|
+
|
64
|
+
if pattern.is_a?(Regexp)
|
65
|
+
@options[:pattern] = pattern
|
66
|
+
return
|
67
|
+
end
|
68
|
+
|
69
|
+
words = pattern.split.collect { |s| s.strip } # Get all words in pattern
|
70
|
+
@options[:tokens] = words.inject([]) do |sum, token| # Find all tokens, ie :symbol :like :names
|
71
|
+
next sum unless token =~ /^:.*/ # Don't process regular words
|
72
|
+
sym = token.sub(":", "").to_sym # Turn token string into symbol, ie ":token" => :token
|
73
|
+
regex = @options[sym] || '[^\s]+' # Fetch regex if configured, else use any character but space matching
|
74
|
+
pattern.sub!(/(^|\s)#{token}(\s|$)/, '\1(' + regex.to_s + ')\2') # Make sure regex captures named switch
|
75
|
+
sum << sym
|
76
|
+
end
|
77
|
+
|
78
|
+
@options[:pattern] = /#{pattern}(\s.+)?/
|
79
|
+
end
|
80
|
+
|
81
|
+
#
|
82
|
+
# Determines if this handler is suited to handle an incoming message
|
83
|
+
#
|
84
|
+
def recognize?(message)
|
85
|
+
return false if @options[:pattern] && message.text !~ @options[:pattern] # Pattern check
|
86
|
+
|
87
|
+
users = @options[:from] ? @options[:from] : nil
|
88
|
+
sender = message.respond_to?(:sender) ? message.sender : message.user
|
89
|
+
return false if users && !users.include?(sender.screen_name.downcase) # Check allowed senders
|
90
|
+
true
|
91
|
+
end
|
92
|
+
|
93
|
+
#
|
94
|
+
# Process message to build params hash and pass message along with params of
|
95
|
+
# to +handle+
|
96
|
+
#
|
97
|
+
def dispatch(message)
|
98
|
+
return unless recognize?(message)
|
99
|
+
@params = {}
|
100
|
+
|
101
|
+
if @options[:pattern] && @options[:tokens]
|
102
|
+
matches = message.text.match(@options[:pattern])
|
103
|
+
@options[:tokens].each_with_index { |token, i| @params[token] = matches[i+1] }
|
104
|
+
@params[:text] = (matches[@options[:tokens].length+1] || "").strip
|
105
|
+
elsif @options[:pattern] && !@options[:tokens]
|
106
|
+
@params = message.text.match(@options[:pattern]).to_a[1..-1] || []
|
107
|
+
else
|
108
|
+
@params[:text] = message.text
|
109
|
+
end
|
110
|
+
|
111
|
+
handle(message, @params)
|
112
|
+
end
|
113
|
+
|
114
|
+
#
|
115
|
+
# Handle a message. Calls the internal Proc with the message and the params
|
116
|
+
# hash as parameters.
|
117
|
+
#
|
118
|
+
def handle(message, params)
|
119
|
+
@handler.call(message, params) if @handler
|
120
|
+
end
|
121
|
+
end
|
122
|
+
end
|
@@ -0,0 +1,101 @@
|
|
1
|
+
module Twibot
|
2
|
+
@@prompt = false
|
3
|
+
|
4
|
+
def self.prompt=(p)
|
5
|
+
@@prompt = f
|
6
|
+
end
|
7
|
+
|
8
|
+
module Macros
|
9
|
+
def self.included(mod)
|
10
|
+
@@bot = nil
|
11
|
+
end
|
12
|
+
|
13
|
+
def configure(&blk)
|
14
|
+
bot.configure(&blk)
|
15
|
+
end
|
16
|
+
|
17
|
+
def message(pattern = nil, options = {}, &blk)
|
18
|
+
add_handler(:message, pattern, options, &blk)
|
19
|
+
end
|
20
|
+
|
21
|
+
def reply(pattern = nil, options = {}, &blk)
|
22
|
+
add_handler(:reply, pattern, options, &blk)
|
23
|
+
end
|
24
|
+
|
25
|
+
def tweet(pattern = nil, options = {}, &blk)
|
26
|
+
add_handler(:tweet, pattern, options, &blk)
|
27
|
+
end
|
28
|
+
|
29
|
+
def follower(&blk)
|
30
|
+
add_handler(:follower, nil, {}, &blk)
|
31
|
+
end
|
32
|
+
|
33
|
+
def hashtag(tag_or_tags, pattern = nil, options = {}, &blk)
|
34
|
+
query = [tag_or_tags].flatten.map {|ht| ht.to_s[0] == ?# ? ht.to_s : "##{ht}"}.join(" OR ")
|
35
|
+
add_handler([:search, query], pattern, options, &blk)
|
36
|
+
end
|
37
|
+
alias_method :hashtags, :hashtag
|
38
|
+
|
39
|
+
def search(query, pattern = nil, options = {}, &blk)
|
40
|
+
add_handler([:search, query], pattern, options, &blk)
|
41
|
+
end
|
42
|
+
|
43
|
+
def after(event=:all, &blk)
|
44
|
+
add_hook :"after_#{event}", &blk
|
45
|
+
end
|
46
|
+
|
47
|
+
def before(event=:all, &blk)
|
48
|
+
add_hook :"before_#{event}", &blk
|
49
|
+
end
|
50
|
+
|
51
|
+
def twitter
|
52
|
+
bot.twitter
|
53
|
+
end
|
54
|
+
|
55
|
+
alias_method :client, :twitter
|
56
|
+
|
57
|
+
def post_tweet(msg)
|
58
|
+
message = msg.respond_to?(:text) ? msg.text : msg
|
59
|
+
puts message
|
60
|
+
client.status(:post, message)
|
61
|
+
end
|
62
|
+
|
63
|
+
def post_reply(status, msg)
|
64
|
+
text = msg.respond_to?(:text) ? msg.text : msg
|
65
|
+
reply_to_screen_name = status.user.screen_name
|
66
|
+
reply_to_status_id = status.id
|
67
|
+
message = "@#{reply_to_screen_name} #{text}"
|
68
|
+
puts message
|
69
|
+
client.status(:reply, message, reply_to_status_id)
|
70
|
+
end
|
71
|
+
|
72
|
+
def run?
|
73
|
+
!@@bot.nil?
|
74
|
+
end
|
75
|
+
|
76
|
+
private
|
77
|
+
def add_handler(type, pattern, options, &blk)
|
78
|
+
bot.add_handler(type, Twibot::Handler.new(pattern, options, &blk))
|
79
|
+
end
|
80
|
+
|
81
|
+
def add_hook(hook, &blk)
|
82
|
+
bot.add_hook(hook, &blk)
|
83
|
+
end
|
84
|
+
|
85
|
+
def bot
|
86
|
+
return @@bot unless @@bot.nil?
|
87
|
+
|
88
|
+
begin
|
89
|
+
@@bot = Twibot::Bot.new nil, true
|
90
|
+
rescue Exception
|
91
|
+
@@bot = Twibot::Bot.new(Twibot::Config.default << Twibot::CliConfig.new, true)
|
92
|
+
end
|
93
|
+
|
94
|
+
@@bot
|
95
|
+
end
|
96
|
+
|
97
|
+
def self.bot=(bot)
|
98
|
+
@@bot = bot
|
99
|
+
end
|
100
|
+
end
|
101
|
+
end
|
data/test/test_bot.rb
ADDED
@@ -0,0 +1,300 @@
|
|
1
|
+
require File.expand_path(File.join(File.dirname(__FILE__), 'test_helper')) unless defined?(Twibot)
|
2
|
+
require 'fileutils'
|
3
|
+
|
4
|
+
class TestBot < Test::Unit::TestCase
|
5
|
+
should "not raise errors when initialized" do
|
6
|
+
assert_nothing_raised do
|
7
|
+
Twibot::Bot.new Twibot::Config.new
|
8
|
+
end
|
9
|
+
end
|
10
|
+
|
11
|
+
should "raise errors when initialized without config file" do
|
12
|
+
assert_raise SystemExit do
|
13
|
+
Twibot::Bot.new
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
should "not raise error on initialize when config file exists" do
|
18
|
+
if File.exists?("config")
|
19
|
+
FileUtils.rm("config/bot.yml")
|
20
|
+
else
|
21
|
+
FileUtils.mkdir("config")
|
22
|
+
end
|
23
|
+
|
24
|
+
File.open("config/bot.yml", "w") { |f| f.puts "" }
|
25
|
+
|
26
|
+
assert_nothing_raised do
|
27
|
+
Twibot::Bot.new
|
28
|
+
end
|
29
|
+
|
30
|
+
FileUtils.rm_rf("config")
|
31
|
+
end
|
32
|
+
|
33
|
+
should "provide configuration settings as methods" do
|
34
|
+
bot = Twibot::Bot.new Twibot::Config.new(:max_interval => 3)
|
35
|
+
assert_equal 3, bot.max_interval
|
36
|
+
end
|
37
|
+
|
38
|
+
should "return logger instance" do
|
39
|
+
bot = Twibot::Bot.new(Twibot::Config.default << Twibot::Config.new)
|
40
|
+
assert bot.log.is_a?(Logger)
|
41
|
+
end
|
42
|
+
|
43
|
+
should "respect configured log level" do
|
44
|
+
bot = Twibot::Bot.new(Twibot::Config.new(:log_level => "info"))
|
45
|
+
assert_equal Logger::INFO, bot.log.level
|
46
|
+
|
47
|
+
bot = Twibot::Bot.new(Twibot::Config.new(:log_level => "warn"))
|
48
|
+
assert_equal Logger::WARN, bot.log.level
|
49
|
+
end
|
50
|
+
|
51
|
+
should "should return false from receive without handlers" do
|
52
|
+
bot = Twibot::Bot.new(Twibot::Config.new)
|
53
|
+
assert !bot.receive_messages
|
54
|
+
assert !bot.receive_replies
|
55
|
+
assert !bot.receive_tweets
|
56
|
+
end
|
57
|
+
|
58
|
+
context "with the process option specified" do
|
59
|
+
setup do
|
60
|
+
@bot = Twibot::Bot.new(@config = Twibot::Config.default)
|
61
|
+
@bot.stubs(:prompt?).returns(false)
|
62
|
+
@bot.stubs(:twitter).returns(stub)
|
63
|
+
@bot.stubs(:processed).returns(stub)
|
64
|
+
|
65
|
+
# stop Bot actually starting during tests
|
66
|
+
@bot.stubs(:poll)
|
67
|
+
end
|
68
|
+
|
69
|
+
should "not process tweets prior to bot launch if :process option is set to :new" do
|
70
|
+
@bot.stubs(:handlers).returns({:tweet => [stub], :reply => []})
|
71
|
+
|
72
|
+
# Should fetch the latest ID for both messages and tweets
|
73
|
+
@bot.twitter.expects(:messages).with(:received, { :count => 1 }).
|
74
|
+
returns([stub(:id => (message_id = stub))]).once
|
75
|
+
@bot.twitter.expects(:timeline_for).with(:public, { :count => 1 }).
|
76
|
+
returns([stub(:id => (tweet_id = stub))]).once
|
77
|
+
|
78
|
+
# And set them to the since_id value to be used for future polling
|
79
|
+
@bot.processed.expects(:[]=).with(:message, message_id)
|
80
|
+
@bot.processed.expects(:[]=).with(:tweet, tweet_id)
|
81
|
+
@bot.processed.expects(:[]=).with(:reply, tweet_id)
|
82
|
+
|
83
|
+
@bot.configure { |c| c.process = :new }
|
84
|
+
@bot.run!
|
85
|
+
end
|
86
|
+
|
87
|
+
[:all, nil].each do |value|
|
88
|
+
should "process all tweets if :process option is set to #{value.inspect}" do
|
89
|
+
@bot.twitter.expects(:messages).never
|
90
|
+
@bot.twitter.expects(:timeline_for).never
|
91
|
+
|
92
|
+
# Shout not set the any value for the since_id tweets
|
93
|
+
@bot.processed.expects(:[]=).never
|
94
|
+
|
95
|
+
@bot.configure { |c| c.process = value }
|
96
|
+
@bot.run!
|
97
|
+
end
|
98
|
+
end
|
99
|
+
|
100
|
+
should "process all tweets after the ID specified in the :process option" do
|
101
|
+
tweet_id = 12345
|
102
|
+
|
103
|
+
@bot.processed.expects(:[]=).with(anything, 12345).times(3)
|
104
|
+
|
105
|
+
@bot.configure { |c| c.process = tweet_id }
|
106
|
+
@bot.run!
|
107
|
+
end
|
108
|
+
|
109
|
+
should "raise exit when the :process option is not recognized" do
|
110
|
+
@bot.configure { |c| c.process = "something random" }
|
111
|
+
assert_raise(SystemExit) { @bot.run! }
|
112
|
+
end
|
113
|
+
|
114
|
+
end
|
115
|
+
|
116
|
+
should "receive message" do
|
117
|
+
bot = Twibot::Bot.new(Twibot::Config.new(:log_level => "error"))
|
118
|
+
bot.add_handler(:message, Twibot::Handler.new)
|
119
|
+
bot.twitter.expects(:messages).with(:received, {}).returns([twitter_message("cjno", "Hei der!")])
|
120
|
+
|
121
|
+
assert bot.receive_messages
|
122
|
+
end
|
123
|
+
|
124
|
+
should "remember last received message" do
|
125
|
+
bot = Twibot::Bot.new(Twibot::Config.new(:log_level => "error"))
|
126
|
+
bot.add_handler(:message, Twibot::Handler.new)
|
127
|
+
bot.twitter.expects(:messages).with(:received, {}).returns([twitter_message("cjno", "Hei der!")])
|
128
|
+
assert_equal 1, bot.receive_messages
|
129
|
+
|
130
|
+
bot.twitter.expects(:messages).with(:received, { :since_id => 1 }).returns([])
|
131
|
+
assert_equal 0, bot.receive_messages
|
132
|
+
end
|
133
|
+
|
134
|
+
should "receive tweet" do
|
135
|
+
bot = Twibot::Bot.new(Twibot::Config.new(:log_level => "error"))
|
136
|
+
bot.add_handler(:tweet, Twibot::Handler.new)
|
137
|
+
bot.twitter.expects(:timeline_for).with(:public, {}).returns([tweet("cjno", "Hei der!")])
|
138
|
+
|
139
|
+
assert_equal 1, bot.receive_tweets
|
140
|
+
end
|
141
|
+
|
142
|
+
should "receive friend tweets if configured" do
|
143
|
+
bot = Twibot::Bot.new(Twibot::Config.new({:log_level => "error", :timeline_for => :friends}))
|
144
|
+
bot.add_handler(:tweet, Twibot::Handler.new)
|
145
|
+
bot.twitter.expects(:timeline_for).with(:friends, {}).returns([tweet("cjno", "Hei der!")])
|
146
|
+
|
147
|
+
assert_equal 1, bot.receive_tweets
|
148
|
+
end
|
149
|
+
|
150
|
+
should "remember received tweets" do
|
151
|
+
bot = Twibot::Bot.new(Twibot::Config.new(:log_level => "error"))
|
152
|
+
bot.add_handler(:tweet, Twibot::Handler.new)
|
153
|
+
bot.twitter.expects(:timeline_for).with(:public, {}).returns([tweet("cjno", "Hei der!")])
|
154
|
+
assert_equal 1, bot.receive_tweets
|
155
|
+
|
156
|
+
bot.twitter.expects(:timeline_for).with(:public, { :since_id => 1 }).returns([])
|
157
|
+
assert_equal 0, bot.receive_tweets
|
158
|
+
end
|
159
|
+
|
160
|
+
should "receive reply when tweet starts with login" do
|
161
|
+
bot = Twibot::Bot.new(Twibot::Config.new(:log_level => "error", :login => "irbno"))
|
162
|
+
bot.add_handler(:reply, Twibot::Handler.new)
|
163
|
+
bot.twitter.expects(:status).with(:replies, {}).returns([tweet("cjno", "@irbno Hei der!")])
|
164
|
+
|
165
|
+
assert_equal 1, bot.receive_replies
|
166
|
+
end
|
167
|
+
|
168
|
+
should "remember received replies" do
|
169
|
+
bot = Twibot::Bot.new(Twibot::Config.new(:log_level => "error", :login => "irbno"))
|
170
|
+
bot.add_handler(:reply, Twibot::Handler.new)
|
171
|
+
bot.twitter.expects(:status).with(:replies, {}).returns([tweet("cjno", "@irbno Hei der!")])
|
172
|
+
assert_equal 1, bot.receive_replies
|
173
|
+
|
174
|
+
bot.twitter.expects(:status).with(:replies, { :since_id => 1 }).returns([])
|
175
|
+
assert_equal 0, bot.receive_replies
|
176
|
+
end
|
177
|
+
|
178
|
+
should "use public as default timeline method for tweet 'verb'" do
|
179
|
+
bot = Twibot::Bot.new(Twibot::Config.default)
|
180
|
+
assert_equal :public, bot.instance_eval { @config.to_hash[:timeline_for] }
|
181
|
+
end
|
182
|
+
|
183
|
+
context "sandboxed network errors" do
|
184
|
+
should "rescue certain errors" do
|
185
|
+
bot = Twibot::Bot.new(Twibot::Config.default)
|
186
|
+
|
187
|
+
assert_nothing_raised do
|
188
|
+
bot.send(:sandbox) { raise Twitter::RESTError.new }
|
189
|
+
bot.send(:sandbox) { raise Errno::ECONNRESET.new }
|
190
|
+
bot.send(:sandbox) { raise Timeout::Error.new }
|
191
|
+
bot.send(:sandbox) { raise EOFError.new }
|
192
|
+
bot.send(:sandbox) { raise Errno::ETIMEDOUT.new }
|
193
|
+
bot.send(:sandbox) { raise JSON::ParserError.new }
|
194
|
+
bot.send(:sandbox) { raise OpenSSL::SSL::SSLError.new }
|
195
|
+
bot.send(:sandbox) { raise SystemStackError.new }
|
196
|
+
end
|
197
|
+
end
|
198
|
+
|
199
|
+
should "return default value if error is rescued" do
|
200
|
+
bot = Twibot::Bot.new(Twibot::Config.default)
|
201
|
+
assert_equal(42, bot.send(:sandbox, 42) { raise Twitter::RESTError })
|
202
|
+
end
|
203
|
+
|
204
|
+
should "not return default value when no error was raised" do
|
205
|
+
bot = Twibot::Bot.new(Twibot::Config.default)
|
206
|
+
assert_equal(65, bot.send(:sandbox, 42) { 65 })
|
207
|
+
end
|
208
|
+
|
209
|
+
should "not swallow unknown errors" do
|
210
|
+
bot = Twibot::Bot.new(Twibot::Config.default)
|
211
|
+
|
212
|
+
assert_raise StandardError do
|
213
|
+
bot.send(:sandbox) { raise StandardError.new "Oops!" }
|
214
|
+
end
|
215
|
+
end
|
216
|
+
end
|
217
|
+
end
|
218
|
+
|
219
|
+
class TestBotMacros < Test::Unit::TestCase
|
220
|
+
should "provide configure macro" do
|
221
|
+
assert respond_to?(:configure)
|
222
|
+
end
|
223
|
+
|
224
|
+
should "yield configuration" do
|
225
|
+
Twibot::Macros.bot = Twibot::Bot.new Twibot::Config.default
|
226
|
+
bot.prompt = false
|
227
|
+
|
228
|
+
conf = nil
|
229
|
+
assert_nothing_raised { configure { |c| conf = c } }
|
230
|
+
assert conf.is_a?(Twibot::Config)
|
231
|
+
end
|
232
|
+
|
233
|
+
should "add handler" do
|
234
|
+
Twibot::Macros.bot = Twibot::Bot.new Twibot::Config.default
|
235
|
+
bot.prompt = false
|
236
|
+
|
237
|
+
handler = add_handler(:message, ":command", :from => :cjno)
|
238
|
+
assert handler.is_a?(Twibot::Handler), handler.class
|
239
|
+
end
|
240
|
+
|
241
|
+
should "provide twitter macro" do
|
242
|
+
assert respond_to?(:twitter)
|
243
|
+
assert respond_to?(:client)
|
244
|
+
end
|
245
|
+
|
246
|
+
context "posting replies" do
|
247
|
+
should "work with string messages" do
|
248
|
+
text = "Hey there"
|
249
|
+
status = Twitter::Status.new(:id => 123,
|
250
|
+
:text => "Some text",
|
251
|
+
:user => Twitter::User.new(:screen_name => "cjno"))
|
252
|
+
client.expects(:status).with(:reply, "@cjno #{text}", 123).returns(true)
|
253
|
+
|
254
|
+
assert post_reply(status, text)
|
255
|
+
end
|
256
|
+
|
257
|
+
should "work with status object messages" do
|
258
|
+
reply = Twitter::Status.new :text => "Hey there"
|
259
|
+
status = Twitter::Status.new(:id => 123,
|
260
|
+
:text => "Some text",
|
261
|
+
:user => Twitter::User.new(:screen_name => "cjno"))
|
262
|
+
client.expects(:status).with(:reply, "@cjno Hey there", 123).returns(true)
|
263
|
+
|
264
|
+
assert post_reply(status, reply)
|
265
|
+
end
|
266
|
+
end
|
267
|
+
end
|
268
|
+
|
269
|
+
class TestBotHandlers < Test::Unit::TestCase
|
270
|
+
|
271
|
+
should "include handlers" do
|
272
|
+
bot = Twibot::Bot.new(Twibot::Config.new)
|
273
|
+
|
274
|
+
assert_not_nil bot.handlers
|
275
|
+
assert_not_nil bot.handlers[:message]
|
276
|
+
assert_not_nil bot.handlers[:reply]
|
277
|
+
assert_not_nil bot.handlers[:tweet]
|
278
|
+
end
|
279
|
+
|
280
|
+
should "add handler" do
|
281
|
+
bot = Twibot::Bot.new(Twibot::Config.new)
|
282
|
+
bot.add_handler :message, Twibot::Handler.new
|
283
|
+
assert_equal 1, bot.handlers[:message].length
|
284
|
+
|
285
|
+
bot.add_handler :message, Twibot::Handler.new
|
286
|
+
assert_equal 2, bot.handlers[:message].length
|
287
|
+
|
288
|
+
bot.add_handler :reply, Twibot::Handler.new
|
289
|
+
assert_equal 1, bot.handlers[:reply].length
|
290
|
+
|
291
|
+
bot.add_handler :reply, Twibot::Handler.new
|
292
|
+
assert_equal 2, bot.handlers[:reply].length
|
293
|
+
|
294
|
+
bot.add_handler :tweet, Twibot::Handler.new
|
295
|
+
assert_equal 1, bot.handlers[:tweet].length
|
296
|
+
|
297
|
+
bot.add_handler :tweet, Twibot::Handler.new
|
298
|
+
assert_equal 2, bot.handlers[:tweet].length
|
299
|
+
end
|
300
|
+
end
|