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.
@@ -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