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.
- data/.yardoc/checksums +7 -7
- data/.yardoc/objects/root.dat +0 -0
- data/README.md +9 -4
- data/doc/Safubot.html +103 -1
- data/doc/Safubot/Bot.html +511 -224
- data/doc/Safubot/Evented.html +2 -3
- data/doc/Safubot/KnownUser.html +5 -5
- data/doc/Safubot/Log.html +37 -10
- data/doc/Safubot/Query.html +34 -7
- data/doc/Safubot/Request.html +3 -82
- data/doc/Safubot/Response.html +1 -1
- data/doc/Safubot/Test.html +76 -13
- data/doc/Safubot/Twitter.html +14 -2
- data/doc/Safubot/Twitter/Bot.html +441 -105
- data/doc/Safubot/Twitter/DirectMessage.html +16 -16
- data/doc/Safubot/Twitter/Tweet.html +31 -31
- data/doc/Safubot/XMPP.html +14 -2
- data/doc/Safubot/XMPP/Bot.html +324 -61
- data/doc/Safubot/XMPP/Message.html +13 -13
- data/doc/_index.html +1 -1
- data/doc/file.README.html +43 -5
- data/doc/index.html +43 -5
- data/doc/method_list.html +155 -83
- data/doc/top-level-namespace.html +1 -1
- data/lib/safubot/bot.rb +35 -16
- data/lib/safubot/evented.rb +1 -1
- data/lib/safubot/log.rb +2 -0
- data/lib/safubot/test_helper.rb +8 -1
- data/lib/safubot/twitter.rb +52 -21
- data/lib/safubot/version.rb +1 -1
- data/lib/safubot/xmpp.rb +34 -15
- data/safubot.gemspec +3 -3
- metadata +29 -29
data/lib/safubot/bot.rb
CHANGED
@@ -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
|
-
|
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
|
-
|
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
|
-
|
137
|
-
|
138
|
-
|
139
|
-
|
140
|
-
|
141
|
-
|
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
|
167
|
+
# Shuts down the streaming processes.
|
149
168
|
def stop
|
150
169
|
@twitter.stop if @twitter
|
151
170
|
@xmpp.stop if @xmpp
|
data/lib/safubot/evented.rb
CHANGED
@@ -7,7 +7,7 @@ module Safubot
|
|
7
7
|
#
|
8
8
|
# { :evid => Symbol, :id => Symbol, :repeat => Boolean, :proc => Proc }
|
9
9
|
#
|
10
|
-
# See
|
10
|
+
# See Evented#on and Evented#once for sugar.
|
11
11
|
def bind(evid, handler)
|
12
12
|
@handlers ||= {}
|
13
13
|
@handlers[evid] ||= {}
|
data/lib/safubot/log.rb
CHANGED
@@ -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)
|
data/lib/safubot/test_helper.rb
CHANGED
@@ -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)
|
data/lib/safubot/twitter.rb
CHANGED
@@ -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
|
-
#
|
226
|
-
def
|
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
|
-
@
|
243
|
-
|
244
|
-
|
245
|
-
|
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
|
-
|
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
|
-
|
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
|
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
|
-
|
268
|
-
#
|
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,
|
data/lib/safubot/version.rb
CHANGED
data/lib/safubot/xmpp.rb
CHANGED
@@ -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
|
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
|
-
|
109
|
-
|
110
|
-
|
111
|
-
|
112
|
-
|
113
|
-
|
114
|
-
|
115
|
-
|
116
|
-
|
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
|
-
|
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
|
-
@
|
127
|
-
|
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)
|
data/safubot.gemspec
CHANGED
@@ -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 = "
|
12
|
-
s.summary = "A
|
13
|
-
s.description = "A
|
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
|
|