safubot 0.0.2 → 0.0.3

Sign up to get free protection for your applications and to get access to all the features.
@@ -94,7 +94,7 @@
94
94
  </div>
95
95
 
96
96
  <div id="footer">
97
- Generated on Mon Nov 28 20:20:04 2011 by
97
+ Generated on Tue Nov 29 11:44:04 2011 by
98
98
  <a href="http://yardoc.org" title="Yay! A Ruby Documentation Tool" target="_parent">yard</a>
99
99
  0.7.3 (ruby-1.9.2).
100
100
  </div>
@@ -1,10 +1,14 @@
1
1
  #!/usr/bin/env ruby
2
2
 
3
3
  module Safubot
4
+ ##
5
+ # Pretty-printing of Exceptions and their backtraces.
6
+ # @param e An Exception to print.
4
7
  def error_report(e)
5
8
  "#{e.inspect}\n#{e.backtrace.join("\n\t")}"
6
9
  end
7
10
 
11
+ ##
8
12
  # Defines elements of the input queue, agnostic of the transfer medium.
9
13
  # May be extended by service-specific modules.
10
14
  class Request
@@ -18,11 +22,9 @@ module Safubot
18
22
  belongs_to :user, :polymorphic => true
19
23
  many :responses, :class_name => "Safubot::Response"
20
24
  timestamps!
21
-
22
- attr_accessor :callback # If a callback is set, responses will be sent to it instead of Response.
23
-
24
25
  end
25
26
 
27
+ ##
26
28
  # Defines elements of the output queue, agnostic of the transfer medium.
27
29
  # May be extended by service-specific modules.
28
30
  class Response
@@ -35,12 +37,19 @@ module Safubot
35
37
  timestamps!
36
38
  end
37
39
 
40
+ ##
41
+ # The main event-processing class. You are encouraged to
42
+ # inherit from this class when building your own bot, but
43
+ # delegation is also entirely feasible.
38
44
  class Bot
39
45
  include Evented
40
46
 
41
47
  attr_reader :opts, :twitter, :xmpp
42
48
 
49
+ ##
43
50
  # Records an error and emits a corresponding :request_error event.
51
+ # @param req The Request for which the error was encountered.
52
+ # @param e The caught Exception.
44
53
  def request_error(req, e)
45
54
  Log.error "Error processing #{req.source.class} '#{req.text}': #{e}\n#{e.backtrace.join("\n\t")}"
46
55
  req.errors[Time.now] = e
@@ -48,7 +57,9 @@ module Safubot
48
57
  emit(:request_error, req, e)
49
58
  end
50
59
 
60
+ ##
51
61
  # Processes an individual request (synchronously).
62
+ # @param req An unprocessed Request.
52
63
  def process_request(req)
53
64
  begin
54
65
  emit(:request, req)
@@ -60,7 +71,9 @@ module Safubot
60
71
  end
61
72
  end
62
73
 
74
+ ##
63
75
  # Performs appropriate dispatch operation for response type.
76
+ # @param resp An undispatched Response.
64
77
  def dispatch(resp)
65
78
  begin
66
79
  source = resp.request.source
@@ -98,6 +111,7 @@ module Safubot
98
111
  end
99
112
  end
100
113
 
114
+ ##
101
115
  # Adds a response to the queue.
102
116
  # @param req Request to respond to.
103
117
  # @param text Contents of the response.
@@ -117,11 +131,12 @@ module Safubot
117
131
  Response.where(:dispatched => false).each(&method(:dispatch))
118
132
  end
119
133
 
120
- # Wraps EM::defer with error handling and response pushing for the given request.
134
+ ##
135
+ # Wraps Thread.new with error handling and response pushing for the given request.
121
136
  # @param req The Request being processed.
122
137
  # @param blk The operation to be performed in a separate thread.
123
138
  def concurrently(req, &blk)
124
- EM::defer do
139
+ Thread.new do
125
140
  begin
126
141
  blk.call
127
142
  rescue Exception => e
@@ -132,20 +147,24 @@ module Safubot
132
147
  end
133
148
  end
134
149
 
150
+ # Runs an initial request-processing loop and then forks the streaming processes.
151
+ def run_nowait
152
+ pull; process; push
153
+ @twitter.run if @twitter
154
+ @xmpp.run if @xmpp
155
+ end
156
+
157
+ # Calls run_nowait and then waits for all child processes.
135
158
  def run
136
- #EventMachine::run do
137
- pull; process; push
138
- @twitter.run if @twitter
139
- @xmpp.run if @xmpp
140
- begin
141
- Process.waitall
142
- rescue Interrupt
143
- stop
144
- end
145
- #end
159
+ run_nowait
160
+ begin
161
+ Process.waitall
162
+ rescue Interrupt
163
+ stop
164
+ end
146
165
  end
147
166
 
148
- # Shuts down the event loop.
167
+ # Shuts down the streaming processes.
149
168
  def stop
150
169
  @twitter.stop if @twitter
151
170
  @xmpp.stop if @xmpp
@@ -7,7 +7,7 @@ module Safubot
7
7
  #
8
8
  # { :evid => Symbol, :id => Symbol, :repeat => Boolean, :proc => Proc }
9
9
  #
10
- # See {on}[rdoc-ref:Safubot::Evented#on] and {once}[rdoc-ref:Safubot::Evented#once] for sugar.
10
+ # See Evented#on and Evented#once for sugar.
11
11
  def bind(evid, handler)
12
12
  @handlers ||= {}
13
13
  @handlers[evid] ||= {}
@@ -1,6 +1,7 @@
1
1
  require 'logger'
2
2
 
3
3
  module Safubot
4
+ # Log is a simple wrapper for stdout and file-writing Logger instances.
4
5
  module Log
5
6
  class << self
6
7
  attr_reader :path
@@ -11,6 +12,7 @@ module Safubot
11
12
  @filelog = Logger.new(str)
12
13
  end
13
14
 
15
+ # Dispatch method calls to Logger objects.
14
16
  def method_missing(method, *args)
15
17
  @stdlog ||= Logger.new(STDOUT)
16
18
  @stdlog.send(method, *args)
@@ -15,8 +15,10 @@ module Safubot
15
15
  end
16
16
  end
17
17
 
18
+ # Helpful methods for testing either via RSpec or IRB.
18
19
  module Test
19
20
  class << self
21
+ # Switches to the "safubot_testing" database and clears all data.
20
22
  def clean_environment
21
23
  MongoMapper.database = "safubot_testing"
22
24
  Request.destroy_all
@@ -27,14 +29,16 @@ module Safubot
27
29
  $bot = Safubot::Bot.new(:database => "safubot_testing")
28
30
  end
29
31
 
32
+ # Creates a Query-sourced testing Request.
33
+ # @param text The body of the Request.
30
34
  def request(text)
31
35
  Query.create(:user => KnownUser.by_name('testing'), :text => text).make_request
32
36
  end
33
37
  end
34
38
  end
35
39
 
40
+ # Generic mediumless Request source.
36
41
  class Query
37
- # Generic mediumless Request source.
38
42
  include MongoMapper::Document
39
43
  safe
40
44
 
@@ -44,6 +48,7 @@ module Safubot
44
48
 
45
49
  one :request, :class_name => "Safubot::Request"
46
50
 
51
+ # Find or create a Request sourced from this Query.
47
52
  def make_request
48
53
  self.request || Request.create(:user => self.user, :text => self.text, :source => self)
49
54
  end
@@ -51,6 +56,8 @@ module Safubot
51
56
 
52
57
  class Bot
53
58
  # A generic "respond immediately" interface.
59
+ # @param text The body of a Request to process.
60
+ # @param user The user sending the request. Defaults to KnownUser.by_name('testing')
54
61
  def answer(text, user=nil)
55
62
  user ||= KnownUser.by_name('testing')
56
63
  query = Query.create(:user => user, :text => text)
@@ -34,12 +34,13 @@ module Safubot
34
34
  end
35
35
  end
36
36
 
37
- # Extend the Request class with Twitter-related source_type scoping.
38
37
  class Request
38
+ # Extend the Request class with Twitter-related source_type scoping.
39
39
  scope :tweet, :source_type => "Safubot::Twitter::Tweet"
40
40
  scope :dm, :source_type => "Safubot::Twitter::DirectMessage"
41
41
  end
42
42
 
43
+ # XMPP-specific functionality.
43
44
  module Twitter
44
45
  # Tweet is both a Request source and general-purpose tweet storage.
45
46
  # Mentions are automatically made into Requests, but not timeline tweets.
@@ -54,12 +55,14 @@ module Safubot
54
55
  one :request, :as => :source, :class_name => "Safubot::Request"
55
56
 
56
57
  class << self
58
+ ##
57
59
  # Quickly look up a Tweet.
58
60
  # @param id The Twitter id.
59
61
  def [](id)
60
62
  Tweet.where('raw.id' => id).first
61
63
  end
62
64
 
65
+ ##
63
66
  # Find or create a Tweet.
64
67
  # @param status An appropriate ::Twitter or ::TweetStream object.
65
68
  def from(status)
@@ -123,6 +126,7 @@ module Safubot
123
126
  one :request, :as => :source, :class_name => "Safubot::Request"
124
127
 
125
128
  class << self
129
+ ##
126
130
  # Find or create a DirectMessage.
127
131
  # @param message An appropriate ::Twitter or ::TweetStream object.
128
132
  def from(message)
@@ -158,7 +162,9 @@ module Safubot
158
162
 
159
163
  attr_reader :username, :client, :opts, :stream, :pid
160
164
 
165
+ ##
161
166
  # Sends a Twitter-sourced Response to the appropriate target.
167
+ # @param resp Response to send.
162
168
  def send(resp)
163
169
  source = resp.request.source
164
170
  if source.is_a?(DirectMessage)
@@ -170,19 +176,25 @@ module Safubot
170
176
  end
171
177
  end
172
178
 
179
+ ##
173
180
  # Emit a request event unless the request is already processed.
181
+ # @param req Request to handle.
174
182
  def handle_request(req)
175
183
  emit(:request, req) unless req.nil? || req.processed
176
184
  end
177
185
 
186
+ ##
178
187
  # Stores a DM and creates a matching Request as needed.
188
+ # @param message A raw JSON-derived direct message.
179
189
  def handle_message(message)
180
190
  return if message.sender.screen_name == @username
181
191
  DirectMessage.from(message).make_request
182
192
  end
183
193
 
194
+ ##
184
195
  # Stores a tweet. If this tweet is directed at us, create a matching Request.
185
196
  # Otherwise, emit a :timeline event.
197
+ # @param status A raw JSON-derived tweet.
186
198
  def handle_tweet(status)
187
199
  return if status.user.screen_name == @username
188
200
  if status.text.match(/@#{@username}/i)
@@ -222,8 +234,8 @@ module Safubot
222
234
  end
223
235
  end
224
236
 
225
- # Starts our TweetStream client running.
226
- def run
237
+ # Initializes the TweetStream client.
238
+ def init_stream
227
239
  @stream = TweetStream::Client.new(@opts)
228
240
 
229
241
  @stream.on_direct_message do |message|
@@ -239,33 +251,52 @@ module Safubot
239
251
  end
240
252
  end
241
253
 
242
- @pid = Process.fork do
243
- begin
244
- @stream.userstream do |status|
245
- req = handle_tweet(status)
246
- handle_request(req) if req.is_a? Request
247
- end
248
- rescue Exception => e
249
- unless e.is_a? Interrupt
250
- Log.error "TweetStream client exited unexpectedly: #{error_report(e)}"
251
- Log.error "Restarting TweetStream client in 5 seconds."
252
- sleep 5; run
253
- end
254
- end
254
+ @stream.on_inited do
255
+ Log.info("TweetStream client is online at @#{@username} :3")
256
+ end
257
+ end
255
258
 
256
- Log.info("TweetStream client shutdown complete.")
259
+ # Runs the TweetStream client.
260
+ def run_stream
261
+ begin
262
+ @stream.userstream do |status|
263
+ req = handle_tweet(status)
264
+ handle_request(req) if req.is_a? Request
265
+ end
266
+ rescue Exception => e
267
+ if e.is_a?(Interrupt) || e.is_a?(SignalException)
268
+ stop
269
+ else
270
+ Log.error "TweetStream client exited unexpectedly: #{error_report(e)}"
271
+ Log.error "Restarting TweetStream client in 5 seconds."
272
+ sleep 5; init_stream; run_stream
273
+ end
257
274
  end
275
+ end
258
276
 
259
- Log.info("TweetStream client is online at @#{@username} :3")
277
+ # Starts our TweetStream client running in a new process.
278
+ def run
279
+ @pid = Process.fork do
280
+ Signal.trap("TERM") { stop }
281
+ init_stream
282
+ run_stream
283
+ end
260
284
  end
261
285
 
262
286
  # Shut down the TweetStream client.
263
287
  def stop
264
- @stream.stop
288
+ if @stream
289
+ @stream.stop
290
+ @stream = nil
291
+ Log.info("TweetStream client shutdown complete.")
292
+ else
293
+ Process.kill("TERM", @pid) if @pid
294
+ end
265
295
  end
266
296
 
267
- # Options are passed straight through to ::TweetStream and ::Twitter,
268
- # but the :username is ours and important.
297
+ ##
298
+ # @param options These are passed straight through to ::TweetStream and
299
+ # ::Twitter, but the :username is ours and important.
269
300
  def initialize(options={})
270
301
  defaults = { :username => nil,
271
302
  :consumer_key => nil, :consumer_secret => nil,
@@ -1,3 +1,3 @@
1
1
  module Safubot
2
- VERSION = "0.0.2"
2
+ VERSION = "0.0.3"
3
3
  end
@@ -16,8 +16,8 @@ module Safubot
16
16
  end
17
17
  end
18
18
 
19
- # XMPP-specific extensions to Request.
20
19
  class Request
20
+ # XMPP-specific extensions to Request.
21
21
  scope :xmpp, :source_type => "Safubot::XMPP::Message"
22
22
  end
23
23
 
@@ -75,7 +75,7 @@ module Safubot
75
75
  attr_reader :jid, :client, :state, :pid
76
76
 
77
77
  # Sets our Blather::Client event processor running.
78
- def run
78
+ def init_blather
79
79
  @client = Blather::Client.setup(@jid, @password)
80
80
 
81
81
  @client.register_handler(:ready) do
@@ -95,6 +95,7 @@ module Safubot
95
95
  end
96
96
 
97
97
  @client.register_handler(:disconnected) do
98
+ sleep 1 # HACK (Mispy): Give the state a chance to change when we're stopped.
98
99
  if @state == :running
99
100
  Log.warn("XMPP disconnected; attempting reconnection in 5 seconds.")
100
101
  sleep 5; @client.connect
@@ -105,26 +106,44 @@ module Safubot
105
106
  Log.error "Unhandled Blather error: #{error_report(e)}"
106
107
  end
107
108
 
108
- @pid = Process.fork do
109
- begin
110
- EM::run { @client.run }
111
- rescue Exception => e
112
- unless e.is_a? Interrupt
113
- Log.error "XMPP client exited unexpectedly: #{error_report(e)}"
114
- Log.error "Restarting XMPP client in 5 seconds."
115
- sleep 5; run
116
- end
109
+
110
+ @state = :running
111
+ end
112
+
113
+ # Runs the Blather client.
114
+ def run_blather
115
+ begin
116
+ EM::run { @client.run }
117
+ rescue Exception => e
118
+ if e.is_a?(Interrupt) || e.is_a?(SignalException)
119
+ stop
120
+ else
121
+ Log.error "XMPP client exited unexpectedly: #{error_report(e)}"
122
+ Log.error "Restarting XMPP client in 5 seconds."
123
+ sleep 5; init_blather; run_blather
117
124
  end
118
- Log.info "XMPP client shutdown complete."
119
125
  end
126
+ end
120
127
 
121
- @state = :running
128
+ # Starts our Blather client running in a new process.
129
+ def run
130
+ @pid = Process.fork do
131
+ Signal.trap("TERM") { stop }
132
+ init_blather
133
+ run_blather
134
+ end
122
135
  end
123
136
 
124
137
  # Shuts down the Blather client.
125
138
  def stop
126
- @state = :stopped
127
- @client.close
139
+ if @client
140
+ @state = :stopped
141
+ @client.close
142
+ @client = nil
143
+ Log.info "XMPP client shutdown complete."
144
+ elsif @pid
145
+ Process.kill("TERM", @pid)
146
+ end
128
147
  end
129
148
 
130
149
  def tell(jid, text)
@@ -8,9 +8,9 @@ Gem::Specification.new do |s|
8
8
  s.platform = Gem::Platform::RUBY
9
9
  s.authors = ["Jaiden Mispy"]
10
10
  s.email = ["^_^@mispy.me"]
11
- s.homepage = "http://safubot.mispy.me"
12
- s.summary = "A modular, evented chatbot framework. Supports Twitter and XMPP."
13
- s.description = "A modular, evented chatbot framework. Supports Twitter and XMPP."
11
+ s.homepage = ""
12
+ s.summary = "A friendly event-driven chatbot framework. Supports Twitter and XMPP."
13
+ s.description = "A friendly event-driven chatbot framework. Supports Twitter and XMPP."
14
14
 
15
15
  s.rubyforge_project = "safubot"
16
16