twibot 0.1.6 → 0.1.7

Sign up to get free protection for your applications and to get access to all the features.
@@ -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 (Bodaniel Jeanes)
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
@@ -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
- post_tweet "@#{message.sender.screen_name} I agree"
26
+ post_reply message, "I agree"
27
27
  end
28
28
 
29
29
  # Listen in and log tweets
@@ -7,7 +7,7 @@ require File.join(File.dirname(__FILE__), 'hash')
7
7
  module Twibot
8
8
 
9
9
  # :stopdoc:
10
- VERSION = '0.1.6'
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:
@@ -34,36 +34,43 @@ module Twibot
34
34
  end
35
35
 
36
36
  def twitter
37
- @twitter ||= Twitter::Client.new :login => config[:login], :password => config[:password]
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
- # Make sure we don't process messages and tweets received prior to bot launch
52
- messages = twitter.messages(:received, { :count => 1 })
53
- processed[:message] = messages.first.id if messages.length > 0
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
- handle_tweets = !@handlers.nil? && @handlers[:tweet].length + @handlers[:reply].length > 0
56
- tweets = []
61
+ handle_tweets = !handlers.nil? && handlers[:tweet].length + handlers[:reply].length > 0
62
+ tweets = []
57
63
 
58
- begin
59
- tweets = handle_tweets ? twitter.timeline_for(config[:timeline_for], { :count => 1 }) : []
60
- rescue Twitter::RESTError => e
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
- processed[:tweet] = tweets.first.id if tweets.length > 0
66
- processed[:reply] = tweets.first.id if tweets.length > 0
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 "Sleeping for #{interval}s"
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
- begin
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
- begin
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
- begin
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
 
@@ -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,
@@ -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
@@ -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
- Twitter::Client.any_instance.expects(:messages).with(:received, {}).returns([twitter_message("cjno", "Hei der!")])
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
- Twitter::Client.any_instance.expects(:messages).with(:received, {}).returns([twitter_message("cjno", "Hei der!")])
127
+ bot.twitter.expects(:messages).with(:received, {}).returns([twitter_message("cjno", "Hei der!")])
70
128
  assert_equal 1, bot.receive_messages
71
129
 
72
- Twitter::Client.any_instance.expects(:messages).with(:received, { :since_id => 1 }).returns([])
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
- Twitter::Client.any_instance.expects(:timeline_for).with(:public, {}).returns([tweet("cjno", "Hei der!")])
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
- Twitter::Client.any_instance.expects(:timeline_for).with(:friends, {}).returns([tweet("cjno", "Hei der!")])
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
- Twitter::Client.any_instance.expects(:timeline_for).with(:public, {}).returns([tweet("cjno", "Hei der!")])
153
+ bot.twitter.expects(:timeline_for).with(:public, {}).returns([tweet("cjno", "Hei der!")])
96
154
  assert_equal 1, bot.receive_tweets
97
155
 
98
- Twitter::Client.any_instance.expects(:timeline_for).with(:public, { :since_id => 1 }).returns([])
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
- Twitter::Client.any_instance.expects(:status).with(:replies, {}).returns([tweet("cjno", "@irbno Hei der!")])
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
- Twitter::Client.any_instance.expects(:status).with(:replies, {}).returns([tweet("cjno", "@irbno Hei der!")])
171
+ bot.twitter.expects(:status).with(:replies, {}).returns([tweet("cjno", "@irbno Hei der!")])
114
172
  assert_equal 1, bot.receive_replies
115
173
 
116
- Twitter::Client.any_instance.expects(:status).with(:replies, { :since_id => 1 }).returns([])
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.6
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-04-13 00:00:00 +02:00
12
+ date: 2009-06-01 00:00:00 +02:00
13
13
  default_executable:
14
14
  dependencies:
15
15
  - !ruby/object:Gem::Dependency