chewbranca-twibot 0.1.7.2

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.
@@ -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
@@ -0,0 +1,4 @@
1
+ module Twibot
2
+ module Tweets
3
+ end
4
+ end
data/lib/twibot.rb ADDED
@@ -0,0 +1,87 @@
1
+ require 'time'
2
+ require 'twitter'
3
+ require 'twitter/client'
4
+ require 'yaml'
5
+ require File.join(File.dirname(__FILE__), 'hash')
6
+
7
+ module Twibot
8
+
9
+ # :stopdoc:
10
+ VERSION = '0.1.7'
11
+ LIBPATH = ::File.expand_path(::File.dirname(__FILE__)) + ::File::SEPARATOR
12
+ PATH = ::File.dirname(LIBPATH) + ::File::SEPARATOR
13
+ # :startdoc:
14
+
15
+ # Returns the version string for the library.
16
+ #
17
+ def self.version
18
+ VERSION
19
+ end
20
+
21
+ # Returns the library path for the module. If any arguments are given,
22
+ # they will be joined to the end of the libray path using
23
+ # <tt>File.join</tt>.
24
+ #
25
+ def self.libpath( *args )
26
+ args.empty? ? LIBPATH : ::File.join(LIBPATH, args.flatten)
27
+ end
28
+
29
+ # Returns the lpath for the module. If any arguments are given,
30
+ # they will be joined to the end of the path using
31
+ # <tt>File.join</tt>.
32
+ #
33
+ def self.path( *args )
34
+ args.empty? ? PATH : ::File.join(PATH, args.flatten)
35
+ end
36
+
37
+ # Utility method used to require all files ending in .rb that lie in the
38
+ # directory below this file that has the same name as the filename passed
39
+ # in. Optionally, a specific _directory_ name can be passed in such that
40
+ # the _filename_ does not have to be equivalent to the directory.
41
+ #
42
+ def self.require_all_libs_relative_to( fname, dir = nil )
43
+ dir ||= File.basename(fname, '.*')
44
+ search_me = File.expand_path(File.join(File.dirname(fname), dir, '**', '*.rb'))
45
+ Dir.glob(search_me).sort.each {|rb| require rb }
46
+ end
47
+
48
+ @@app_file = lambda do
49
+ ignore = [
50
+ /lib\/twibot.*\.rb/, # Library
51
+ /\(.*\)/, # Generated code
52
+ /custom_require\.rb/ # RubyGems require
53
+ ]
54
+
55
+ path = caller.map { |line| line.split(/:\d/, 2).first }.find do |file|
56
+ next if ignore.any? { |pattern| file =~ pattern }
57
+ file
58
+ end
59
+
60
+ path || $0
61
+ end.call
62
+
63
+ #
64
+ # File name of the application file. Inspired by Sinatra
65
+ #
66
+ def self.app_file
67
+ @@app_file
68
+ end
69
+
70
+ #
71
+ # Runs application if application file is the script being executed
72
+ #
73
+ def self.run?
74
+ self.app_file == $0
75
+ end
76
+
77
+ end # module Twibot
78
+
79
+ Twitter::Client.configure do |config|
80
+ config.application_name = 'Twibot'
81
+ config.application_version = Twibot.version
82
+ config.application_url = 'http://github.com/cjohansen/twibot'
83
+ end
84
+
85
+ Twibot.require_all_libs_relative_to(__FILE__)
86
+
87
+ # EOF
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
@@ -0,0 +1,89 @@
1
+ require File.expand_path(File.join(File.dirname(__FILE__), 'test_helper')) unless defined?(Twibot)
2
+ require 'stringio'
3
+
4
+ class TestConfig < Test::Unit::TestCase
5
+ should "default configuration be a hash" do
6
+ assert_not_nil Twibot::Config::DEFAULT
7
+ assert Twibot::Config::DEFAULT.is_a?(Hash)
8
+ end
9
+
10
+ should "initialize with no options" do
11
+ assert_hashes_equal({}, Twibot::Config.new.settings)
12
+ end
13
+
14
+ should "return config from add" do
15
+ config = Twibot::Config.new
16
+ assert_equal config, config.add(Twibot::Config.new)
17
+ end
18
+
19
+ should "alias add to <<" do
20
+ config = Twibot::Config.new
21
+ assert config.respond_to?(:<<)
22
+ assert config << Twibot::Config.new
23
+ end
24
+
25
+ should "mirror method_missing as config getters" do
26
+ config = Twibot::Config.default << Twibot::Config.new
27
+ assert_equal Twibot::Config::DEFAULT[:min_interval], config.min_interval
28
+ assert_equal Twibot::Config::DEFAULT[:login], config.login
29
+ end
30
+
31
+ should "mirror missing methods as config setters" do
32
+ config = Twibot::Config.default << Twibot::Config.new
33
+ assert_equal Twibot::Config::DEFAULT[:min_interval], config.min_interval
34
+
35
+ val = config.min_interval
36
+ config.min_interval = val + 5
37
+ assert_not_equal Twibot::Config::DEFAULT[:min_interval], config.min_interval
38
+ assert_equal val + 5, config.min_interval
39
+ end
40
+
41
+ should "not override default hash" do
42
+ config = Twibot::Config.default
43
+ hash = Twibot::Config::DEFAULT
44
+
45
+ config.min_interval = 0
46
+ config.max_interval = 0
47
+
48
+ assert_hashes_not_equal Twibot::Config::DEFAULT, config.to_hash
49
+ assert_hashes_equal hash, Twibot::Config::DEFAULT
50
+ end
51
+
52
+ should "return merged configuration from to_hash" do
53
+ config = Twibot::Config.new
54
+ config.min_interval = 10
55
+ config.max_interval = 10
56
+
57
+ config2 = Twibot::Config.new({})
58
+ config2.min_interval = 1
59
+ config << config2
60
+ options = config.to_hash
61
+
62
+ assert_equal 10, options[:max_interval]
63
+ assert_equal 1, options[:min_interval]
64
+ end
65
+ end
66
+
67
+ class TestCliConfig < Test::Unit::TestCase
68
+ should "configure from options" do
69
+ config = Twibot::CliConfig.new %w{--min-interval 10 --max-interval 15}
70
+ assert_equal 10, config.min_interval
71
+ assert_equal 15, config.max_interval
72
+ end
73
+ end
74
+
75
+ class TestFileConfig < Test::Unit::TestCase
76
+ should "subclass config for file config" do
77
+ assert Twibot::FileConfig.new(StringIO.new).is_a?(Twibot::Config)
78
+ end
79
+
80
+ should "read settings from stream" do
81
+ config = Twibot::FileConfig.new(StringIO.new <<-YAML)
82
+ min_interval: 10
83
+ max_interval: 20
84
+ YAML
85
+
86
+ assert_equal 10, config.min_interval
87
+ assert_equal 20, config.max_interval
88
+ end
89
+ end