twibot 0.1.6 → 0.1.7
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 +14 -1
- data/Readme.rdoc +1 -1
- data/lib/twibot.rb +1 -1
- data/lib/twibot/bot.rb +67 -33
- data/lib/twibot/config.rb +2 -0
- data/lib/twibot/macros.rb +10 -1
- data/test/test_bot.rb +125 -10
- metadata +2 -2
data/History.txt
CHANGED
@@ -1,6 +1,19 @@
|
|
1
|
+
== 0.1.7 / 2009-06-01
|
2
|
+
|
3
|
+
* New feature - choose how Twibot processes incoming tweets on startup
|
4
|
+
(process all, process new [old behaviour], or process from a given ID)
|
5
|
+
Bodaniel Jeanes
|
6
|
+
* Substantially improved error handling. Now survives all common network
|
7
|
+
stability issues
|
8
|
+
* Added a host configuration option. The host name is displayed along all
|
9
|
+
output from Twibot. Currently Twitter4R does nothing with this option,
|
10
|
+
Twibot knowing about it should make it easier to put Twibot/Twitter4R on
|
11
|
+
other services like Laconica instances
|
12
|
+
|
1
13
|
== 0.1.6 / 2009-04-13
|
2
14
|
|
3
|
-
* Fixed configure block not actually working for username and password
|
15
|
+
* Fixed configure block not actually working for username and password
|
16
|
+
Bodaniel Jeanes
|
4
17
|
* Minor updates in tests
|
5
18
|
|
6
19
|
== 0.1.5 / 2009-04-12
|
data/Readme.rdoc
CHANGED
@@ -23,7 +23,7 @@ bots, heavily inspired by Sinatra.
|
|
23
23
|
# Respond to @replies if they come from the right crowd
|
24
24
|
#
|
25
25
|
reply :from => [:cjno, :irbno] do |message, params|
|
26
|
-
|
26
|
+
post_reply message, "I agree"
|
27
27
|
end
|
28
28
|
|
29
29
|
# Listen in and log tweets
|
data/lib/twibot.rb
CHANGED
@@ -7,7 +7,7 @@ require File.join(File.dirname(__FILE__), 'hash')
|
|
7
7
|
module Twibot
|
8
8
|
|
9
9
|
# :stopdoc:
|
10
|
-
VERSION = '0.1.
|
10
|
+
VERSION = '0.1.7'
|
11
11
|
LIBPATH = ::File.expand_path(::File.dirname(__FILE__)) + ::File::SEPARATOR
|
12
12
|
PATH = ::File.dirname(LIBPATH) + ::File::SEPARATOR
|
13
13
|
# :startdoc:
|
data/lib/twibot/bot.rb
CHANGED
@@ -34,36 +34,43 @@ module Twibot
|
|
34
34
|
end
|
35
35
|
|
36
36
|
def twitter
|
37
|
-
@twitter ||= Twitter::Client.new
|
37
|
+
@twitter ||= Twitter::Client.new(:login => config[:login],
|
38
|
+
:password => config[:password],
|
39
|
+
:host => config[:host])
|
38
40
|
end
|
39
41
|
|
40
42
|
#
|
41
43
|
# Run application
|
42
44
|
#
|
43
45
|
def run!
|
44
|
-
puts "Twibot #{Twibot::VERSION} imposing as @#{login}"
|
46
|
+
puts "Twibot #{Twibot::VERSION} imposing as @#{login} on #{config[:host]}"
|
45
47
|
|
46
48
|
trap(:INT) do
|
47
49
|
puts "\nAnd it's a wrap. See ya soon!"
|
48
50
|
exit
|
49
51
|
end
|
50
52
|
|
51
|
-
|
52
|
-
|
53
|
-
|
53
|
+
case config[:process]
|
54
|
+
when :all, nil
|
55
|
+
# do nothing so it will fetch ALL
|
56
|
+
when :new
|
57
|
+
# Make sure we don't process messages and tweets received prior to bot launch
|
58
|
+
messages = twitter.messages(:received, { :count => 1 })
|
59
|
+
processed[:message] = messages.first.id if messages.length > 0
|
54
60
|
|
55
|
-
|
56
|
-
|
61
|
+
handle_tweets = !handlers.nil? && handlers[:tweet].length + handlers[:reply].length > 0
|
62
|
+
tweets = []
|
57
63
|
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
log.error("Failed to connect to Twitter. It's likely down for a bit:")
|
62
|
-
log.error(e.to_s)
|
63
|
-
end
|
64
|
+
sandbox do
|
65
|
+
tweets = handle_tweets ? twitter.timeline_for(config[:timeline_for], { :count => 1 }) : []
|
66
|
+
end
|
64
67
|
|
65
|
-
|
66
|
-
|
68
|
+
processed[:tweet] = tweets.first.id if tweets.length > 0
|
69
|
+
processed[:reply] = tweets.first.id if tweets.length > 0
|
70
|
+
when Numeric, /\d+/ # a tweet ID to start from
|
71
|
+
processed[:tweet] = processed[:reply] = processed[:message] = config[:process]
|
72
|
+
else abort "Unknown process option #{config[:process]}, aborting..."
|
73
|
+
end
|
67
74
|
|
68
75
|
poll
|
69
76
|
end
|
@@ -84,7 +91,7 @@ module Twibot
|
|
84
91
|
|
85
92
|
interval = message_count > 0 ? min_interval : [interval + step, max].min
|
86
93
|
|
87
|
-
log.debug "
|
94
|
+
log.debug "#{config[:host]} sleeping for #{interval}s"
|
88
95
|
sleep interval
|
89
96
|
end
|
90
97
|
end
|
@@ -97,12 +104,9 @@ module Twibot
|
|
97
104
|
return false unless handlers[type].length > 0
|
98
105
|
options = {}
|
99
106
|
options[:since_id] = processed[type] if processed[type]
|
100
|
-
|
107
|
+
|
108
|
+
sandbox(0) do
|
101
109
|
dispatch_messages(type, twitter.messages(:received, options), %w{message messages})
|
102
|
-
rescue Twitter::RESTError => e
|
103
|
-
log.error("Failed to connect to Twitter. It's likely down for a bit:")
|
104
|
-
log.error(e.to_s)
|
105
|
-
0
|
106
110
|
end
|
107
111
|
end
|
108
112
|
|
@@ -114,12 +118,9 @@ module Twibot
|
|
114
118
|
return false unless handlers[type].length > 0
|
115
119
|
options = {}
|
116
120
|
options[:since_id] = processed[type] if processed[type]
|
117
|
-
|
121
|
+
|
122
|
+
sandbox(0) do
|
118
123
|
dispatch_messages(type, twitter.timeline_for(config.to_hash[:timeline_for] || :public, options), %w{tweet tweets})
|
119
|
-
rescue Twitter::RESTError => e
|
120
|
-
log.error("Failed to connect to Twitter. It's likely down for a bit:")
|
121
|
-
log.error(e.to_s)
|
122
|
-
0
|
123
124
|
end
|
124
125
|
end
|
125
126
|
|
@@ -131,14 +132,10 @@ module Twibot
|
|
131
132
|
return false unless handlers[type].length > 0
|
132
133
|
options = {}
|
133
134
|
options[:since_id] = processed[type] if processed[type]
|
134
|
-
|
135
|
+
|
136
|
+
sandbox(0) do
|
135
137
|
dispatch_messages(type, twitter.status(:replies, options), %w{reply replies})
|
136
|
-
rescue Twitter::RESTError => e
|
137
|
-
log.error("Failed to connect to Twitter. It's likely down for a bit:")
|
138
|
-
log.error(e.to_s)
|
139
|
-
0
|
140
138
|
end
|
141
|
-
|
142
139
|
end
|
143
140
|
|
144
141
|
#
|
@@ -150,7 +147,7 @@ module Twibot
|
|
150
147
|
processed[type] = messages.first.id if messages.length > 0
|
151
148
|
|
152
149
|
num = messages.length
|
153
|
-
log.info "Received #{num} #{num == 1 ? labels[0] : labels[1]}"
|
150
|
+
log.info "#{config[:host]}: Received #{num} #{num == 1 ? labels[0] : labels[1]}"
|
154
151
|
num
|
155
152
|
end
|
156
153
|
|
@@ -216,6 +213,43 @@ Unable to continue without login and password. Do one of the following:
|
|
216
213
|
|
217
214
|
@conf
|
218
215
|
end
|
216
|
+
|
217
|
+
#
|
218
|
+
# Takes a block and executes it in a sandboxed network environment. It
|
219
|
+
# catches and logs most common network connectivity and timeout errors.
|
220
|
+
#
|
221
|
+
# The method takes an optional parameter. If set, this value will be
|
222
|
+
# returned in case an error was raised.
|
223
|
+
#
|
224
|
+
def sandbox(return_value = nil)
|
225
|
+
begin
|
226
|
+
return_value = yield
|
227
|
+
rescue Twitter::RESTError => e
|
228
|
+
log.error("Failed to connect to Twitter. It's likely down for a bit:")
|
229
|
+
log.error(e.to_s)
|
230
|
+
rescue Errno::ECONNRESET => e
|
231
|
+
log.error("Connection was reset")
|
232
|
+
log.error(e.to_s)
|
233
|
+
rescue Timeout::Error => e
|
234
|
+
log.error("Timeout")
|
235
|
+
log.error(e.to_s)
|
236
|
+
rescue EOFError => e
|
237
|
+
log.error(e.to_s)
|
238
|
+
rescue Errno::ETIMEDOUT => e
|
239
|
+
log.error("Timeout")
|
240
|
+
log.error(e.to_s)
|
241
|
+
rescue JSON::ParserError => e
|
242
|
+
log.error("JSON Parsing error")
|
243
|
+
log.error(e.to_s)
|
244
|
+
rescue OpenSSL::SSL::SSLError => e
|
245
|
+
log.error("SSL error")
|
246
|
+
log.error(e.to_s)
|
247
|
+
rescue SystemStackError => e
|
248
|
+
log.error(e.to_s)
|
249
|
+
end
|
250
|
+
|
251
|
+
return return_value
|
252
|
+
end
|
219
253
|
end
|
220
254
|
end
|
221
255
|
|
data/lib/twibot/config.rb
CHANGED
@@ -20,6 +20,7 @@ module Twibot
|
|
20
20
|
attr_reader :settings
|
21
21
|
|
22
22
|
DEFAULT = {
|
23
|
+
:host => "twitter.com",
|
23
24
|
:min_interval => 30,
|
24
25
|
:max_interval => 300,
|
25
26
|
:interval_step => 10,
|
@@ -27,6 +28,7 @@ module Twibot
|
|
27
28
|
:log_file => nil,
|
28
29
|
:login => nil,
|
29
30
|
:password => nil,
|
31
|
+
:process => :new,
|
30
32
|
:prompt => false,
|
31
33
|
:daemonize => false,
|
32
34
|
:include_friends => false,
|
data/lib/twibot/macros.rb
CHANGED
@@ -37,7 +37,16 @@ module Twibot
|
|
37
37
|
puts message
|
38
38
|
client.status(:post, message)
|
39
39
|
end
|
40
|
-
|
40
|
+
|
41
|
+
def post_reply(status, msg)
|
42
|
+
text = msg.respond_to?(:text) ? msg.text : msg
|
43
|
+
reply_to_screen_name = status.user.screen_name
|
44
|
+
reply_to_status_id = status.id
|
45
|
+
message = "@#{reply_to_screen_name} #{text}"
|
46
|
+
puts message
|
47
|
+
client.status(:reply, message, reply_to_status_id)
|
48
|
+
end
|
49
|
+
|
41
50
|
def run?
|
42
51
|
!@@bot.nil?
|
43
52
|
end
|
data/test/test_bot.rb
CHANGED
@@ -55,10 +55,68 @@ class TestBot < Test::Unit::TestCase
|
|
55
55
|
assert !bot.receive_tweets
|
56
56
|
end
|
57
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
|
+
|
58
116
|
should "receive message" do
|
59
117
|
bot = Twibot::Bot.new(Twibot::Config.new(:log_level => "error"))
|
60
118
|
bot.add_handler(:message, Twibot::Handler.new)
|
61
|
-
|
119
|
+
bot.twitter.expects(:messages).with(:received, {}).returns([twitter_message("cjno", "Hei der!")])
|
62
120
|
|
63
121
|
assert bot.receive_messages
|
64
122
|
end
|
@@ -66,17 +124,17 @@ class TestBot < Test::Unit::TestCase
|
|
66
124
|
should "remember last received message" do
|
67
125
|
bot = Twibot::Bot.new(Twibot::Config.new(:log_level => "error"))
|
68
126
|
bot.add_handler(:message, Twibot::Handler.new)
|
69
|
-
|
127
|
+
bot.twitter.expects(:messages).with(:received, {}).returns([twitter_message("cjno", "Hei der!")])
|
70
128
|
assert_equal 1, bot.receive_messages
|
71
129
|
|
72
|
-
|
130
|
+
bot.twitter.expects(:messages).with(:received, { :since_id => 1 }).returns([])
|
73
131
|
assert_equal 0, bot.receive_messages
|
74
132
|
end
|
75
133
|
|
76
134
|
should "receive tweet" do
|
77
135
|
bot = Twibot::Bot.new(Twibot::Config.new(:log_level => "error"))
|
78
136
|
bot.add_handler(:tweet, Twibot::Handler.new)
|
79
|
-
|
137
|
+
bot.twitter.expects(:timeline_for).with(:public, {}).returns([tweet("cjno", "Hei der!")])
|
80
138
|
|
81
139
|
assert_equal 1, bot.receive_tweets
|
82
140
|
end
|
@@ -84,7 +142,7 @@ class TestBot < Test::Unit::TestCase
|
|
84
142
|
should "receive friend tweets if configured" do
|
85
143
|
bot = Twibot::Bot.new(Twibot::Config.new({:log_level => "error", :timeline_for => :friends}))
|
86
144
|
bot.add_handler(:tweet, Twibot::Handler.new)
|
87
|
-
|
145
|
+
bot.twitter.expects(:timeline_for).with(:friends, {}).returns([tweet("cjno", "Hei der!")])
|
88
146
|
|
89
147
|
assert_equal 1, bot.receive_tweets
|
90
148
|
end
|
@@ -92,17 +150,17 @@ class TestBot < Test::Unit::TestCase
|
|
92
150
|
should "remember received tweets" do
|
93
151
|
bot = Twibot::Bot.new(Twibot::Config.new(:log_level => "error"))
|
94
152
|
bot.add_handler(:tweet, Twibot::Handler.new)
|
95
|
-
|
153
|
+
bot.twitter.expects(:timeline_for).with(:public, {}).returns([tweet("cjno", "Hei der!")])
|
96
154
|
assert_equal 1, bot.receive_tweets
|
97
155
|
|
98
|
-
|
156
|
+
bot.twitter.expects(:timeline_for).with(:public, { :since_id => 1 }).returns([])
|
99
157
|
assert_equal 0, bot.receive_tweets
|
100
158
|
end
|
101
159
|
|
102
160
|
should "receive reply when tweet starts with login" do
|
103
161
|
bot = Twibot::Bot.new(Twibot::Config.new(:log_level => "error", :login => "irbno"))
|
104
162
|
bot.add_handler(:reply, Twibot::Handler.new)
|
105
|
-
|
163
|
+
bot.twitter.expects(:status).with(:replies, {}).returns([tweet("cjno", "@irbno Hei der!")])
|
106
164
|
|
107
165
|
assert_equal 1, bot.receive_replies
|
108
166
|
end
|
@@ -110,10 +168,10 @@ class TestBot < Test::Unit::TestCase
|
|
110
168
|
should "remember received replies" do
|
111
169
|
bot = Twibot::Bot.new(Twibot::Config.new(:log_level => "error", :login => "irbno"))
|
112
170
|
bot.add_handler(:reply, Twibot::Handler.new)
|
113
|
-
|
171
|
+
bot.twitter.expects(:status).with(:replies, {}).returns([tweet("cjno", "@irbno Hei der!")])
|
114
172
|
assert_equal 1, bot.receive_replies
|
115
173
|
|
116
|
-
|
174
|
+
bot.twitter.expects(:status).with(:replies, { :since_id => 1 }).returns([])
|
117
175
|
assert_equal 0, bot.receive_replies
|
118
176
|
end
|
119
177
|
|
@@ -121,6 +179,41 @@ class TestBot < Test::Unit::TestCase
|
|
121
179
|
bot = Twibot::Bot.new(Twibot::Config.default)
|
122
180
|
assert_equal :public, bot.instance_eval { @config.to_hash[:timeline_for] }
|
123
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
|
124
217
|
end
|
125
218
|
|
126
219
|
class TestBotMacros < Test::Unit::TestCase
|
@@ -149,6 +242,28 @@ class TestBotMacros < Test::Unit::TestCase
|
|
149
242
|
assert respond_to?(:twitter)
|
150
243
|
assert respond_to?(:client)
|
151
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
|
152
267
|
end
|
153
268
|
|
154
269
|
class TestBotHandlers < Test::Unit::TestCase
|
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: twibot
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.1.
|
4
|
+
version: 0.1.7
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Christian Johansen
|
@@ -9,7 +9,7 @@ autorequire:
|
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
11
|
|
12
|
-
date: 2009-
|
12
|
+
date: 2009-06-01 00:00:00 +02:00
|
13
13
|
default_executable:
|
14
14
|
dependencies:
|
15
15
|
- !ruby/object:Gem::Dependency
|