mattmueller-twibot 0.1.7.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- data/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
|