safubot 0.0.2 → 0.0.3

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