safubot 0.0.3 → 0.0.4

Sign up to get free protection for your applications and to get access to all the features.
data/lib/safubot/bot.rb CHANGED
@@ -8,19 +8,51 @@ module Safubot
8
8
  "#{e.inspect}\n#{e.backtrace.join("\n\t")}"
9
9
  end
10
10
 
11
+ ###
12
+ # An EmbeddedDocument for storing processing/dispatch errors.
13
+ class Problem
14
+ include MongoMapper::EmbeddedDocument
15
+ key :when, Time
16
+ key :error, String # Exception#to_s
17
+ key :type, String # Exception class
18
+ key :backtrace, Array, :default => nil
19
+ end
20
+
21
+ ##
22
+ # A mixin adding Problem handling.
23
+ module Problematic
24
+ ##
25
+ # Adds a timestamped Problem to the list.
26
+ # @param e Exception from which the Problem is derived.
27
+ def add_problem(e)
28
+ problem = Problem.new(:error => e.to_s, :type => e.class.to_s,
29
+ :when => Time.now, :backtrace => e.backtrace)
30
+ self.problems.push(problem)
31
+ end
32
+
33
+ ##
34
+ # Fetches the most recent Problem.
35
+ def last_problem
36
+ self.problems.sort { |x,y| x.when <=> y.when }.last
37
+ end
38
+ end
39
+
40
+
11
41
  ##
12
42
  # Defines elements of the input queue, agnostic of the transfer medium.
13
43
  # May be extended by service-specific modules.
14
44
  class Request
15
45
  include MongoMapper::Document
46
+ include Problematic
16
47
  safe
17
- key :errors, Hash # Errors encountered during processing.
48
+ key :processing, Boolean, :default => false # Currently-processing lock.
18
49
  key :processed, Boolean, :default => false # Have we processed this request?
19
50
  key :success, Boolean, :default => false # Did we *successfully* process this request?
20
51
  key :text, String # The actual content.
21
52
  belongs_to :source, :polymorphic => true # The concrete medium-specific source.
22
- belongs_to :user, :polymorphic => true
53
+ belongs_to :user, :class_name => "Safubot::KnownUser"
23
54
  many :responses, :class_name => "Safubot::Response"
55
+ many :problems # Hopefully not that many ;)
24
56
  timestamps!
25
57
  end
26
58
 
@@ -29,11 +61,13 @@ module Safubot
29
61
  # May be extended by service-specific modules.
30
62
  class Response
31
63
  include MongoMapper::Document
64
+ include Problematic
32
65
  safe
33
- key :errors, Hash # Errors encountered during dispatch by timestamp.
66
+ key :dispatching, Boolean, :default => false # Dispatch lock.
34
67
  key :dispatched, Boolean, :default => false
35
68
  key :text, String
36
- belongs_to :request, :polymorphic => true
69
+ belongs_to :request, :class_name => "Safubot::Request"
70
+ many :problems
37
71
  timestamps!
38
72
  end
39
73
 
@@ -47,27 +81,54 @@ module Safubot
47
81
  attr_reader :opts, :twitter, :xmpp
48
82
 
49
83
  ##
50
- # Records an error and emits a corresponding :request_error event.
84
+ # Records an error in processing and emits a corresponding :request_error event.
51
85
  # @param req The Request for which the error was encountered.
52
86
  # @param e The caught Exception.
53
87
  def request_error(req, e)
54
- Log.error "Error processing #{req.source.class} '#{req.text}': #{e}\n#{e.backtrace.join("\n\t")}"
55
- req.errors[Time.now] = e
88
+ Log.error "Error processing #{req.source.class} '#{req.text}': #{error_report(e)}"
89
+ req.add_problem(e)
56
90
  req.save
57
91
  emit(:request_error, req, e)
58
92
  end
59
93
 
94
+ ##
95
+ # Records an error in dispatch and emits a corresponding :dispatch_error event.
96
+ # @param resp The Response for which the error was encountered.
97
+ # @param e The caught Exception.
98
+ def dispatch_error(resp, e)
99
+ Log.error "Error dispatching #{resp.request.source.class} '#{resp.text}': #{error_report(e)}"
100
+ resp.add_problem(e)
101
+ resp.save
102
+ emit(:dispatch_error, resp, e)
103
+ end
104
+
60
105
  ##
61
106
  # Processes an individual request (synchronously).
62
107
  # @param req An unprocessed Request.
63
108
  def process_request(req)
109
+ req.reload
110
+ if req.processed
111
+ Log.debug "Request '#{req.text}' has already been processed, ignoring."
112
+ return
113
+ elsif req.processing
114
+ Log.debug "Request '#{req.text}' is currently in processing, ignoring."
115
+ return
116
+ end
117
+
64
118
  begin
119
+ req.processing = true
120
+ req.save
65
121
  emit(:request, req)
66
122
  rescue Exception => e
67
123
  request_error(req, e)
124
+ else
125
+ req.success = true
68
126
  ensure
69
- req.processed = true
70
- req.save
127
+ #if Safubot::mode == :production
128
+ req.processing = false
129
+ req.processed = true
130
+ req.save
131
+ #end
71
132
  end
72
133
  end
73
134
 
@@ -75,8 +136,26 @@ module Safubot
75
136
  # Performs appropriate dispatch operation for response type.
76
137
  # @param resp An undispatched Response.
77
138
  def dispatch(resp)
139
+ resp.reload
140
+ if resp.dispatched
141
+ Log.debug "Response '#{resp.text}' has already been dispatched, ignoring."
142
+ return
143
+ elsif resp.dispatching
144
+ Log.debug "Response '#{resp.text}' is already in dispatch, ignoring."
145
+ return
146
+ elsif resp.problems.length > 10
147
+ Log.debug "Response '#{resp.text}' encountered more than ten dispatch errors, ignoring."
148
+ return
149
+ elsif !resp.problems.empty? && (Time.now - resp.last_problem.when) < 1.minute
150
+ Log.debug "Response '#{resp.text}' encountered a dispatch error <1 minute ago, ignoring."
151
+ return
152
+ end
153
+
78
154
  begin
79
155
  source = resp.request.source
156
+ resp.dispatching = true
157
+ resp.save
158
+
80
159
  if Safubot::mode != :production
81
160
  Log.info "#{source.class} Response to #{source.username}: #{resp.text}"
82
161
  else
@@ -87,15 +166,15 @@ module Safubot
87
166
  else
88
167
  raise NotImplementedError, "Don't know how to send response to a #{source.class}!"
89
168
  end
90
- end
91
169
 
92
- resp.dispatched = true
93
- resp.save
170
+ resp.dispatched = true
171
+ resp.save
172
+ end
94
173
  rescue Exception => e
95
- Log.error "Error dispatching #{source.class} '#{resp.text}': #{e}"
96
- resp.errors[Time.now] = e
174
+ dispatch_error(resp, e)
175
+ ensure
176
+ resp.dispatching = false
97
177
  resp.save
98
- emit(:dispatch_error, resp, e)
99
178
  end
100
179
  end
101
180
 
@@ -112,18 +191,12 @@ module Safubot
112
191
  end
113
192
 
114
193
  ##
115
- # Adds a response to the queue.
194
+ # Adds a response to the queue and dispatches it.
116
195
  # @param req Request to respond to.
117
196
  # @param text Contents of the response.
118
197
  def respond(req, text)
119
198
  Log.info("#{req.user.name}: #{req.text}\nsafubot: #{text}")
120
- Response.create(:request => req, :text => text)
121
- end
122
-
123
- # Respond + push
124
- def respond_now(req, text)
125
- respond(req, text)
126
- push
199
+ dispatch(Response.create(:request => req, :text => text))
127
200
  end
128
201
 
129
202
  # Dispatches all undispatched Responses.
@@ -142,7 +215,6 @@ module Safubot
142
215
  rescue Exception => e
143
216
  request_error(req, e)
144
217
  end
145
-
146
218
  push
147
219
  end
148
220
  end
@@ -174,7 +246,8 @@ module Safubot
174
246
  def enable_twitter(opts={})
175
247
  @twitter = Twitter::Bot.new(opts)
176
248
  @twitter.on(:request) do |req|
177
- process; push
249
+ process_request(req)
250
+ req.responses.where(:dispatched => false).map(&method(:dispatch))
178
251
  end
179
252
  end
180
253
 
@@ -183,7 +256,8 @@ module Safubot
183
256
  defaults = { :jid => nil, :password => nil }
184
257
  @xmpp = XMPP::Bot.new(defaults.merge(options))
185
258
  @xmpp.on(:request) do |req|
186
- process; push
259
+ process_request(req)
260
+ req.responses.where(:dispatched => false).map(&method(:dispatch))
187
261
  end
188
262
  end
189
263
 
@@ -52,6 +52,10 @@ module Safubot
52
52
  def make_request
53
53
  self.request || Request.create(:user => self.user, :text => self.text, :source => self)
54
54
  end
55
+
56
+ def username
57
+ self.user.name
58
+ end
55
59
  end
56
60
 
57
61
  class Bot
@@ -170,7 +170,7 @@ module Safubot
170
170
  if source.is_a?(DirectMessage)
171
171
  @client.direct_message_create(source.raw['sender']['screen_name'], resp.text)
172
172
  elsif source.is_a?(Tweet)
173
- reply("#{source.header_mentions.join(' ')} #{resp.text}")
173
+ reply source, resp.text
174
174
  else
175
175
  raise NotImplementedError, "Don't know how to send response to a #{req.source.class}!"
176
176
  end
@@ -188,7 +188,7 @@ module Safubot
188
188
  # @param message A raw JSON-derived direct message.
189
189
  def handle_message(message)
190
190
  return if message.sender.screen_name == @username
191
- DirectMessage.from(message).make_request
191
+ handle_request(DirectMessage.from(message).make_request)
192
192
  end
193
193
 
194
194
  ##
@@ -198,7 +198,7 @@ module Safubot
198
198
  def handle_tweet(status)
199
199
  return if status.user.screen_name == @username
200
200
  if status.text.match(/@#{@username}/i)
201
- Tweet.from(status).make_request
201
+ handle_request(Tweet.from(status).make_request)
202
202
  else
203
203
  emit(:timeline, Tweet.from(status))
204
204
  end
@@ -239,8 +239,7 @@ module Safubot
239
239
  @stream = TweetStream::Client.new(@opts)
240
240
 
241
241
  @stream.on_direct_message do |message|
242
- req = handle_message(message)
243
- handle_request(req) if req.is_a? Request
242
+ handle_message(message)
244
243
  end
245
244
 
246
245
  @stream.on_error do |err|
@@ -253,6 +252,7 @@ module Safubot
253
252
 
254
253
  @stream.on_inited do
255
254
  Log.info("TweetStream client is online at @#{@username} :3")
255
+ emit(:ready)
256
256
  end
257
257
  end
258
258
 
@@ -260,8 +260,7 @@ module Safubot
260
260
  def run_stream
261
261
  begin
262
262
  @stream.userstream do |status|
263
- req = handle_tweet(status)
264
- handle_request(req) if req.is_a? Request
263
+ handle_tweet(status)
265
264
  end
266
265
  rescue Exception => e
267
266
  if e.is_a?(Interrupt) || e.is_a?(SignalException)
@@ -1,3 +1,3 @@
1
1
  module Safubot
2
- VERSION = "0.0.3"
2
+ VERSION = "0.0.4"
3
3
  end
data/lib/safubot/xmpp.rb CHANGED
@@ -113,11 +113,13 @@ module Safubot
113
113
  # Runs the Blather client.
114
114
  def run_blather
115
115
  begin
116
- EM::run { @client.run }
116
+ EM::run {
117
+ @client.run
118
+ }
117
119
  rescue Exception => e
118
120
  if e.is_a?(Interrupt) || e.is_a?(SignalException)
119
121
  stop
120
- else
122
+ elsif @state == :running
121
123
  Log.error "XMPP client exited unexpectedly: #{error_report(e)}"
122
124
  Log.error "Restarting XMPP client in 5 seconds."
123
125
  sleep 5; init_blather; run_blather
@@ -155,7 +157,11 @@ module Safubot
155
157
 
156
158
  # Dispatch a Response via XMPP.
157
159
  def send(resp)
158
- tell(resp.request.source.from, resp.text)
160
+ if @state == :running
161
+ tell(resp.request.source.from, resp.text)
162
+ else
163
+ on(:ready) { send(resp) }
164
+ end
159
165
  end
160
166
 
161
167
  def initialize(opts)
data/safubot.gemspec CHANGED
@@ -19,6 +19,7 @@ Gem::Specification.new do |s|
19
19
  s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
20
20
  s.require_paths = ["lib"]
21
21
 
22
+ s.add_dependency "activesupport", "= 3.1.0" # mongo_mapper seems to need this without specifying it.
22
23
  s.add_dependency "eventmachine", "~> 0.12.10"
23
24
  s.add_dependency "bson_ext", "~> 1.4.0"
24
25
  s.add_dependency "mongo_mapper", "~> 0.10.1"
metadata CHANGED
@@ -1,144 +1,159 @@
1
- --- !ruby/object:Gem::Specification
1
+ --- !ruby/object:Gem::Specification
2
2
  name: safubot
3
- version: !ruby/object:Gem::Version
4
- version: 0.0.3
3
+ version: !ruby/object:Gem::Version
5
4
  prerelease:
5
+ version: 0.0.4
6
6
  platform: ruby
7
- authors:
7
+ authors:
8
8
  - Jaiden Mispy
9
9
  autorequire:
10
10
  bindir: bin
11
11
  cert_chain: []
12
- date: 2011-11-29 00:00:00.000000000Z
13
- dependencies:
14
- - !ruby/object:Gem::Dependency
12
+
13
+ date: 2011-12-01 00:00:00 Z
14
+ dependencies:
15
+ - !ruby/object:Gem::Dependency
16
+ name: activesupport
17
+ prerelease: false
18
+ requirement: &id001 !ruby/object:Gem::Requirement
19
+ none: false
20
+ requirements:
21
+ - - "="
22
+ - !ruby/object:Gem::Version
23
+ version: 3.1.0
24
+ type: :runtime
25
+ version_requirements: *id001
26
+ - !ruby/object:Gem::Dependency
15
27
  name: eventmachine
16
- requirement: &24360180 !ruby/object:Gem::Requirement
28
+ prerelease: false
29
+ requirement: &id002 !ruby/object:Gem::Requirement
17
30
  none: false
18
- requirements:
31
+ requirements:
19
32
  - - ~>
20
- - !ruby/object:Gem::Version
33
+ - !ruby/object:Gem::Version
21
34
  version: 0.12.10
22
35
  type: :runtime
23
- prerelease: false
24
- version_requirements: *24360180
25
- - !ruby/object:Gem::Dependency
36
+ version_requirements: *id002
37
+ - !ruby/object:Gem::Dependency
26
38
  name: bson_ext
27
- requirement: &24349720 !ruby/object:Gem::Requirement
39
+ prerelease: false
40
+ requirement: &id003 !ruby/object:Gem::Requirement
28
41
  none: false
29
- requirements:
42
+ requirements:
30
43
  - - ~>
31
- - !ruby/object:Gem::Version
44
+ - !ruby/object:Gem::Version
32
45
  version: 1.4.0
33
46
  type: :runtime
34
- prerelease: false
35
- version_requirements: *24349720
36
- - !ruby/object:Gem::Dependency
47
+ version_requirements: *id003
48
+ - !ruby/object:Gem::Dependency
37
49
  name: mongo_mapper
38
- requirement: &24349000 !ruby/object:Gem::Requirement
50
+ prerelease: false
51
+ requirement: &id004 !ruby/object:Gem::Requirement
39
52
  none: false
40
- requirements:
53
+ requirements:
41
54
  - - ~>
42
- - !ruby/object:Gem::Version
55
+ - !ruby/object:Gem::Version
43
56
  version: 0.10.1
44
57
  type: :runtime
45
- prerelease: false
46
- version_requirements: *24349000
47
- - !ruby/object:Gem::Dependency
58
+ version_requirements: *id004
59
+ - !ruby/object:Gem::Dependency
48
60
  name: twitter
49
- requirement: &24348220 !ruby/object:Gem::Requirement
61
+ prerelease: false
62
+ requirement: &id005 !ruby/object:Gem::Requirement
50
63
  none: false
51
- requirements:
64
+ requirements:
52
65
  - - ~>
53
- - !ruby/object:Gem::Version
66
+ - !ruby/object:Gem::Version
54
67
  version: 2.0.0
55
68
  type: :runtime
56
- prerelease: false
57
- version_requirements: *24348220
58
- - !ruby/object:Gem::Dependency
69
+ version_requirements: *id005
70
+ - !ruby/object:Gem::Dependency
59
71
  name: tweetstream
60
- requirement: &24347440 !ruby/object:Gem::Requirement
72
+ prerelease: false
73
+ requirement: &id006 !ruby/object:Gem::Requirement
61
74
  none: false
62
- requirements:
75
+ requirements:
63
76
  - - ~>
64
- - !ruby/object:Gem::Version
77
+ - !ruby/object:Gem::Version
65
78
  version: 1.1.3
66
79
  type: :runtime
67
- prerelease: false
68
- version_requirements: *24347440
69
- - !ruby/object:Gem::Dependency
80
+ version_requirements: *id006
81
+ - !ruby/object:Gem::Dependency
70
82
  name: blather
71
- requirement: &24346340 !ruby/object:Gem::Requirement
83
+ prerelease: false
84
+ requirement: &id007 !ruby/object:Gem::Requirement
72
85
  none: false
73
- requirements:
86
+ requirements:
74
87
  - - ~>
75
- - !ruby/object:Gem::Version
88
+ - !ruby/object:Gem::Version
76
89
  version: 0.5.8
77
90
  type: :runtime
78
- prerelease: false
79
- version_requirements: *24346340
80
- - !ruby/object:Gem::Dependency
91
+ version_requirements: *id007
92
+ - !ruby/object:Gem::Dependency
81
93
  name: rake
82
- requirement: &24345860 !ruby/object:Gem::Requirement
94
+ prerelease: false
95
+ requirement: &id008 !ruby/object:Gem::Requirement
83
96
  none: false
84
- requirements:
85
- - - ! '>='
86
- - !ruby/object:Gem::Version
87
- version: '0'
97
+ requirements:
98
+ - - ">="
99
+ - !ruby/object:Gem::Version
100
+ version: "0"
88
101
  type: :development
89
- prerelease: false
90
- version_requirements: *24345860
91
- - !ruby/object:Gem::Dependency
102
+ version_requirements: *id008
103
+ - !ruby/object:Gem::Dependency
92
104
  name: rspec
93
- requirement: &24344780 !ruby/object:Gem::Requirement
105
+ prerelease: false
106
+ requirement: &id009 !ruby/object:Gem::Requirement
94
107
  none: false
95
- requirements:
96
- - - ! '>='
97
- - !ruby/object:Gem::Version
98
- version: '0'
108
+ requirements:
109
+ - - ">="
110
+ - !ruby/object:Gem::Version
111
+ version: "0"
99
112
  type: :development
100
- prerelease: false
101
- version_requirements: *24344780
102
- - !ruby/object:Gem::Dependency
113
+ version_requirements: *id009
114
+ - !ruby/object:Gem::Dependency
103
115
  name: yard
104
- requirement: &24343320 !ruby/object:Gem::Requirement
116
+ prerelease: false
117
+ requirement: &id010 !ruby/object:Gem::Requirement
105
118
  none: false
106
- requirements:
107
- - - ! '>='
108
- - !ruby/object:Gem::Version
109
- version: '0'
119
+ requirements:
120
+ - - ">="
121
+ - !ruby/object:Gem::Version
122
+ version: "0"
110
123
  type: :development
111
- prerelease: false
112
- version_requirements: *24343320
113
- - !ruby/object:Gem::Dependency
124
+ version_requirements: *id010
125
+ - !ruby/object:Gem::Dependency
114
126
  name: bundler
115
- requirement: &24341920 !ruby/object:Gem::Requirement
127
+ prerelease: false
128
+ requirement: &id011 !ruby/object:Gem::Requirement
116
129
  none: false
117
- requirements:
118
- - - ! '>='
119
- - !ruby/object:Gem::Version
120
- version: '0'
130
+ requirements:
131
+ - - ">="
132
+ - !ruby/object:Gem::Version
133
+ version: "0"
121
134
  type: :development
122
- prerelease: false
123
- version_requirements: *24341920
124
- - !ruby/object:Gem::Dependency
135
+ version_requirements: *id011
136
+ - !ruby/object:Gem::Dependency
125
137
  name: wirble
126
- requirement: &24331220 !ruby/object:Gem::Requirement
138
+ prerelease: false
139
+ requirement: &id012 !ruby/object:Gem::Requirement
127
140
  none: false
128
- requirements:
129
- - - ! '>='
130
- - !ruby/object:Gem::Version
131
- version: '0'
141
+ requirements:
142
+ - - ">="
143
+ - !ruby/object:Gem::Version
144
+ version: "0"
132
145
  type: :development
133
- prerelease: false
134
- version_requirements: *24331220
146
+ version_requirements: *id012
135
147
  description: A friendly event-driven chatbot framework. Supports Twitter and XMPP.
136
- email:
148
+ email:
137
149
  - ^_^@mispy.me
138
150
  executables: []
151
+
139
152
  extensions: []
153
+
140
154
  extra_rdoc_files: []
141
- files:
155
+
156
+ files:
142
157
  - .gitignore
143
158
  - .yardoc/checksums
144
159
  - .yardoc/objects/root.dat
@@ -152,6 +167,8 @@ files:
152
167
  - doc/Safubot/Evented.html
153
168
  - doc/Safubot/KnownUser.html
154
169
  - doc/Safubot/Log.html
170
+ - doc/Safubot/Problem.html
171
+ - doc/Safubot/Problematic.html
155
172
  - doc/Safubot/Query.html
156
173
  - doc/Safubot/Request.html
157
174
  - doc/Safubot/Response.html
@@ -196,44 +213,33 @@ files:
196
213
  - spec/safubot/known_user_spec.rb
197
214
  - spec/safubot/twitter_spec.rb
198
215
  - spec/safubot/xmpp_spec.rb
199
- homepage: ''
216
+ homepage: ""
200
217
  licenses: []
218
+
201
219
  post_install_message:
202
220
  rdoc_options: []
203
- require_paths:
221
+
222
+ require_paths:
204
223
  - lib
205
- required_ruby_version: !ruby/object:Gem::Requirement
224
+ required_ruby_version: !ruby/object:Gem::Requirement
206
225
  none: false
207
- requirements:
208
- - - ! '>='
209
- - !ruby/object:Gem::Version
210
- version: '0'
211
- segments:
212
- - 0
213
- hash: 401033843839902587
214
- required_rubygems_version: !ruby/object:Gem::Requirement
226
+ requirements:
227
+ - - ">="
228
+ - !ruby/object:Gem::Version
229
+ version: "0"
230
+ required_rubygems_version: !ruby/object:Gem::Requirement
215
231
  none: false
216
- requirements:
217
- - - ! '>='
218
- - !ruby/object:Gem::Version
219
- version: '0'
220
- segments:
221
- - 0
222
- hash: 401033843839902587
232
+ requirements:
233
+ - - ">="
234
+ - !ruby/object:Gem::Version
235
+ version: "0"
223
236
  requirements: []
237
+
224
238
  rubyforge_project: safubot
225
- rubygems_version: 1.8.10
239
+ rubygems_version: 1.8.11
226
240
  signing_key:
227
241
  specification_version: 3
228
242
  summary: A friendly event-driven chatbot framework. Supports Twitter and XMPP.
229
- test_files:
230
- - spec/fixtures/twitter/direct_message
231
- - spec/fixtures/twitter/tweet
232
- - spec/fixtures/xmpp/message
233
- - spec/helper.rb
234
- - spec/safubot/bot_spec.rb
235
- - spec/safubot/evented_spec.rb
236
- - spec/safubot/known_user_spec.rb
237
- - spec/safubot/twitter_spec.rb
238
- - spec/safubot/xmpp_spec.rb
243
+ test_files: []
244
+
239
245
  has_rdoc: