safubot 0.0.3 → 0.0.4

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/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: