Permalink
Newer
Older
100644 306 lines (270 sloc) 8.94 KB
1
Fs = require 'fs'
2
Url = require 'url'
3
Path = require 'path'
4
Redis = require 'redis'
Aug 27, 2011
5
6
class Robot
7
# Robots receive messages from a chat source (Campfire, irc, etc), and
8
# dispatch them to matching listeners.
9
#
10
# path - String directory full of Hubot scripts to load.
11
constructor: (path, name = "Hubot") ->
12
@name = name
13
@brain = new Robot.Brain()
14
@commands = []
15
@Response = Robot.Response
16
@listeners = []
17
@loadPaths = []
18
@enableSlash = false
Aug 27, 2011
19
if path then @load path
20
21
# Public: Adds a Listener that attempts to match incoming messages based on
22
# a Regex.
23
#
24
# regex - A Regex that determines if the callback should be called.
25
# callback - A Function that is called with a Response object.
26
#
27
# Returns nothing.
Aug 27, 2011
28
hear: (regex, callback) ->
29
@listeners.push new Listener(@, regex, callback)
30
Tom Bell
Oct 26, 2011
31
# Public: Adds a Listener that attempts to match incoming messages directed
32
# at the robot based on a Regex. All regexes treat patterns like they begin
33
# with a '^'
34
#
35
# regex - A Regex that determines if the callback should be called.
36
# callback - A Function that is called with a Response object.
37
#
38
# Returns nothing.
39
respond: (regex, callback) ->
40
re = regex.toString().split("/")
41
re.shift() # remove empty first item
42
modifiers = re.pop() # pop off modifiers
43
44
if re[0] and re[0][0] is "^"
45
console.log "\nWARNING: Anchors don't work well with respond, perhaps you want to use 'hear'"
46
console.log "WARNING: The regex in question was #{regex.toString()}\n"
47
48
pattern = re.join("/") # combine the pattern back again
49
if @enableSlash
50
newRegex = new RegExp("^(?:\/|#{@name}:?)\\s*#{pattern}", modifiers)
52
newRegex = new RegExp("^#{@name}:?\\s*#{pattern}", modifiers)
53
54
console.log newRegex.toString()
55
@listeners.push new Listener(@, newRegex, callback)
56
57
# Public: Passes the given message to any interested Listeners.
58
#
59
# message - A Robot.Message instance.
60
#
61
# Returns nothing.
Aug 27, 2011
62
receive: (message) ->
63
listener.call message for listener in @listeners
Aug 27, 2011
64
65
# Public: Loads every script in the given path.
66
#
67
# path - A String path on the filesystem.
68
#
69
# Returns nothing.
Aug 27, 2011
70
load: (path) ->
71
Path.exists path, (exists) =>
72
if exists
73
@loadPaths.push path
74
for file in Fs.readdirSync(path)
75
@loadFile path, file
76
77
# Public: Loads a file in path
78
#
79
# path - A String path on the filesystem.
80
# file - A String filename in path on the filesystem.
81
#
82
# Returns nothing.
83
loadFile: (path, file) ->
84
ext = Path.extname file
85
full = Path.join path, Path.basename(file, ext)
86
if ext is '.coffee' or ext is '.js'
87
require(full) @
88
@parseHelp "#{path}/#{file}"
89
90
# Public: Help Commands for Running Scripts
91
#
92
# Returns an array of help commands for running scripts
93
#
94
helpCommands: () ->
95
@commands.sort()
96
97
# Private: load help info from a loaded script
98
#
99
# path - The path to the file on disk
100
#
101
# Returns nothing
102
parseHelp: (path) ->
103
Fs.readFile path, "utf-8", (err, body) =>
104
throw err if err
105
for i, line of body.split("\n")
106
break if line[0] != '#'
107
continue if !line.match('-')
108
@commands.push line[2..line.length]
Aug 27, 2011
109
110
# Public: Raw method for sending data back to the chat source. Extend this.
111
#
112
# user - A Robot.User instance.
113
# strings - One or more Strings for each message to send.
Aug 27, 2011
114
send: (user, strings...) ->
115
116
# Public: Raw method for building a reply and sending it back to the chat
117
# source. Extend this.
118
#
119
# user - A Robot.User instance.
120
# strings - One or more Strings for each reply to send.
Aug 27, 2011
121
reply: (user, strings...) ->
122
123
# Public: Raw method for invoking the bot to run
Aug 27, 2011
124
# Extend this.
Aug 27, 2011
126
127
users: () ->
128
@brain.data.users
129
130
# Public: Get a User object given a unique identifier
131
#
132
userForId: (id, options) ->
133
user = @brain.data.users[id]
134
unless user
135
user = new Robot.User id, options
136
@brain.data.users[id] = user
137
user
138
139
# Public: Get a User object given a name
140
#
141
userForName: (name) ->
142
result = null
143
lowerName = name.toLowerCase()
144
for k of (@brain.data.users or { })
145
if @brain.data.users[k]['name'].toLowerCase() is lowerName
146
result = @brain.data.users[k]
147
result
148
149
class Robot.User
150
# Represents a participating user in the chat.
151
#
152
# id - A unique ID for the user.
153
# options - An optional Hash of key, value pairs for this user.
154
constructor: (@id, options = { }) ->
155
for k of (options or { })
156
@[k] = options[k]
157
158
class Robot.Brain
159
# Represents somewhat persistent storage for the robot.
160
#
161
# Returns a new Brain that's trying to connect to redis
162
#
163
# Previously persisted data is loaded on a successful connection
164
#
165
# Redis connects to a environmental variable REDISTOGO_URL or
166
# fallsback to localhost
167
constructor: () ->
168
@data =
169
users: { }
170
Oct 13, 2011
171
info = Url.parse process.env.REDISTOGO_URL || 'redis://localhost:6379'
172
@client = Redis.createClient(info.port, info.hostname)
173
174
if info.auth
175
@client.auth info.auth.split(":")[1]
176
177
@client.on "error", (err) ->
178
console.log "Error #{err}"
179
@client.on "connect", () =>
Oct 11, 2011
180
console.log "Successfully connected to Redis"
181
@client.get "hubot:storage", (err, reply) =>
182
throw err if err
183
@mergeData JSON.parse reply.toString() if reply
184
185
setInterval =>
186
data = JSON.stringify @data
187
@client.set "hubot:storage", data, (err, reply) ->
188
# console.log "Saved #{reply.toString()}"
189
, 5000
190
191
# Merge keys loaded from redis against the in memory representation
192
#
193
# Returns nothing
194
#
195
# Caveats: Deeply nested structures don't merge well
196
mergeData: (data) ->
197
for k of (data or { })
198
@data[k] = data[k]
199
200
class Robot.Message
201
# Represents an incoming message from the chat.
202
#
203
# user - A Robot.User instance that sent the message.
204
# text - The String message contents.
205
constructor: (@user, @text) ->
207
# Determines if the message matches the given regex.
208
#
209
# regex - The Regex to check.
210
#
211
# Returns a Match object or null.
212
match: (regex) ->
213
@text.match regex
214
Aug 27, 2011
215
class Listener
Tom Bell
Oct 26, 2011
216
# Listeners receive every message from the chat source and decide if they
217
# want to act on it.
218
#
219
# robot - The current Robot instance.
220
# regex - The Regex that determines if this listener should trigger the
221
# callback.
222
# callback - The Function that is triggered if the incoming message matches.
Aug 27, 2011
223
constructor: (@robot, @regex, @callback) ->
224
225
# Public: Determines if the listener likes the content of the message. If
226
# so, a Response built from the given Message is passed to the Listener
227
# callback.
228
#
229
# message - a Robot.Message instance.
230
#
231
# Returns nothing.
Aug 27, 2011
232
call: (message) ->
233
if match = message.match @regex
Aug 29, 2011
234
@callback new @robot.Response(@robot, message, match)
Aug 27, 2011
235
Aug 29, 2011
236
class Robot.Response
237
# Public: Responses are sent to matching listeners. Messages know about the
238
# content and user that made the original message, and how to reply back to
239
# them.
240
#
241
# robot - The current Robot instance.
242
# message - The current Robot.Message instance.
243
# match - The Match object from the successful Regex match.
Aug 27, 2011
244
constructor: (@robot, @message, @match) ->
245
246
# Public: Posts a message back to the chat source
Aug 27, 2011
247
#
248
# strings - One or more strings to be posted. The order of these strings
249
# should be kept intact.
250
#
251
# Returns nothing.
252
send: (strings...) ->
253
@robot.send @message.user, strings...
Aug 27, 2011
254
255
# Public: Posts a message mentioning the current user.
Aug 27, 2011
256
#
257
# strings - One or more strings to be posted. The order of these strings
258
# should be kept intact.
259
#
260
# Returns nothing.
261
reply: (strings...) ->
262
@robot.reply @message.user, strings...
Aug 27, 2011
263
264
# Public: Picks a random item from the given items.
265
#
266
# items - An Array of items (usually Strings).
267
#
268
# Returns a random item.
Aug 27, 2011
269
random: (items) ->
270
items[ Math.floor(Math.random() * items.length) ]
271
272
# Public: Creates a scoped http client with chainable methods for
273
# modifying the request. This doesn't actually make a request though.
274
# Once your request is assembled, you can call `get()`/`post()`/etc to
275
# send the request.
276
#
277
# url - String URL to access.
278
#
279
# Examples:
281
# res.http("http://example.com")
282
# # set a single header
283
# .header('Authorization', 'bearer abcdef')
284
#
285
# # set multiple headers
286
# .headers(Authorization: 'bearer abcdef', Accept: 'application/json')
287
#
288
# # add URI query parameters
289
# .query(a: 1, b: 'foo & bar')
290
#
291
# # make the actual request
292
# .get() (err, res, body) ->
293
# console.log body
294
#
295
# # or, you can POST data
296
# .post(data) (err, res, body) ->
297
# console.log body
298
#
299
# Returns a ScopedClient instance.
300
http: (url) ->
301
@httpClient.create(url)
302
Aug 29, 2011
303
Robot.Response.prototype.httpClient = require 'scoped-http-client'
Aug 27, 2011
304
305
module.exports = Robot