forked from hubotio/hubot
-
Notifications
You must be signed in to change notification settings - Fork 0
/
robot.coffee
380 lines (330 loc) · 11.5 KB
/
robot.coffee
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
Fs = require 'fs'
Log = require 'log'
Path = require 'path'
Url = require 'url'
Brain = require './brain'
User = require './user'
HUBOT_DEFAULT_ADAPTERS = [ "campfire", "shell" ]
class Robot
# Robots receive messages from a chat source (Campfire, irc, etc), and
# dispatch them to matching listeners.
#
# path - String directory full of Hubot scripts to load.
constructor: (adapterPath, adapter, name = "Hubot") ->
@name = name
@brain = new Brain
@alias = false
@adapter = null
@commands = []
@Response = Robot.Response
@listeners = []
@loadPaths = []
@enableSlash = false
@logger = new Log process.env.HUBOT_LOG_LEVEL or "info"
@loadAdapter adapterPath, adapter if adapter?
# Public: Adds a Listener that attempts to match incoming messages based on
# a Regex.
#
# regex - A Regex that determines if the callback should be called.
# callback - A Function that is called with a Response object.
#
# Returns nothing.
hear: (regex, callback) ->
@listeners.push new TextListener(@, regex, callback)
# Public: Adds a Listener that attempts to match incoming messages directed
# at the robot based on a Regex. All regexes treat patterns like they begin
# with a '^'
#
# regex - A Regex that determines if the callback should be called.
# callback - A Function that is called with a Response object.
#
# Returns nothing.
respond: (regex, callback) ->
re = regex.toString().split("/")
re.shift() # remove empty first item
modifiers = re.pop() # pop off modifiers
if re[0] and re[0][0] is "^"
@logger.warning "Anchors don't work well with respond, perhaps you want to use 'hear'"
@logger.warning "The regex in question was #{regex.toString()}"
pattern = re.join("/") # combine the pattern back again
if @alias
alias = @alias.replace(/[-[\]{}()*+?.,\\^$|#\s]/g, "\\$&") # escape alias for regexp
newRegex = new RegExp("^(?:#{alias}[:,]?|#{@name}[:,]?)\\s*(?:#{pattern})", modifiers)
else
newRegex = new RegExp("^#{@name}[:,]?\\s*(?:#{pattern})", modifiers)
@logger.debug newRegex.toString()
@listeners.push new TextListener(@, newRegex, callback)
# Public: Adds a Listener that triggers when anyone enters the room.
#
# callback - A Function that is called with a Response object.
#
# Returns nothing.
enter: (callback) ->
@listeners.push new Listener(@, ((msg) -> msg instanceof Robot.EnterMessage), callback)
# Public: Adds a Listener that triggers when anyone leaves the room.
#
# callback - A Function that is called with a Response object.
#
# Returns nothing.
leave: (callback) ->
@listeners.push new Listener(@, ((msg) -> msg instanceof Robot.LeaveMessage), callback)
# Public: Passes the given message to any interested Listeners.
#
# message - A Robot.Message instance.
#
# Returns nothing.
receive: (message) ->
for lst in @listeners
try
lst.call message
catch ex
@logger.error "Unable to call the listener: #{ex}"
# Public: Loads every script in the given path.
#
# path - A String path on the filesystem.
#
# Returns nothing.
load: (path) ->
@logger.info "Loading scripts from #{path}"
Path.exists path, (exists) =>
if exists
@loadPaths.push path
for file in Fs.readdirSync(path)
@loadFile path, file
# Public: Loads a file in path
#
# path - A String path on the filesystem.
# file - A String filename in path on the filesystem.
#
# Returns nothing.
loadFile: (path, file) ->
ext = Path.extname file
full = Path.join path, Path.basename(file, ext)
if ext is '.coffee' or ext is '.js'
try
require(full) @
@parseHelp "#{path}/#{file}"
catch err
@logger.error "#{err}"
loadHubotScripts: (path, scripts) ->
@logger.info "Loading hubot-scripts from #{path}"
for script in scripts
@loadFile path, script
# Load the adapter Hubot is going to use.
#
# path - A String of the path to adapter if local.
# adapter - A String of the adapter name to use.
#
# Returns nothing.
loadAdapter: (path, adapter) ->
@logger.info "Loading adapter #{adapter}"
try
path = if adapter in HUBOT_DEFAULT_ADAPTERS
"#{path}/#{adapter}"
else
"hubot-#{adapter}"
@adapter = require("#{path}").use(@)
catch err
@logger.error "Cannot load adapter #{adapter}, try installing the package"
# Public: Help Commands for Running Scripts
#
# Returns an array of help commands for running scripts
helpCommands: ->
@commands.sort()
# Private: load help info from a loaded script
#
# path - The path to the file on disk
#
# Returns nothing
parseHelp: (path) ->
Fs.readFile path, "utf-8", (err, body) =>
throw err if err?
for i, line of body.split("\n")
break if !(line[0] == '#' or line.substr(0, 2) == '//')
continue if !line.match('-')
@commands.push line[2..line.length]
# Public: A helper send function which delegates to the adapter's send
# function.
#
# user - A User instance.
# strings - One or more Strings for each message to send.
send: (user, strings...) ->
@adapter.send user, strings...
# Public: A helper reply function which delegates to the adapter's reply
# function.
#
# user - A User instance.
# strings - One or more Strings for each message to send.
reply: (user, strings...) ->
@adapter.reply user, strings...
# Public: Get an Array of User objects stored in the brain.
users: ->
@brain.data.users
# Public: Get a User object given a unique identifier.
userForId: (id, options) ->
user = @brain.data.users[id]
unless user
user = new User id, options
@brain.data.users[id] = user
user
# Public: Get a User object given a name.
userForName: (name) ->
result = null
lowerName = name.toLowerCase()
for k of (@brain.data.users or { })
if @brain.data.users[k]['name'].toLowerCase() is lowerName
result = @brain.data.users[k]
result
# Public: Get all users whose names match fuzzyName. Currently, match
# means 'starts with', but this could be extended to match initials,
# nicknames, etc.
#
usersForRawFuzzyName: (fuzzyName) ->
lowerFuzzyName = fuzzyName.toLowerCase()
user for key, user of (@brain.data.users or {}) when (
user.name.toLowerCase().lastIndexOf(lowerFuzzyName, 0) == 0)
# Public: If fuzzyName is an exact match for a user, returns an array with
# just that user. Otherwise, returns an array of all users for which
# fuzzyName is a raw fuzzy match (see usersForRawFuzzyName).
#
usersForFuzzyName: (fuzzyName) ->
matchedUsers = @usersForRawFuzzyName(fuzzyName)
lowerFuzzyName = fuzzyName.toLowerCase()
# We can scan matchedUsers rather than all users since usersForRawFuzzyName
# will include exact matches
for user in matchedUsers
return [user] if user.name.toLowerCase() is lowerFuzzyName
matchedUsers
run: ->
@adapter.run()
class Robot.Message
# Represents an incoming message from the chat.
#
# user - A User instance that sent the message.
constructor: (@user) ->
class Robot.TextMessage extends Robot.Message
# Represents an incoming message from the chat.
#
# user - A User instance that sent the message.
# text - The String message contents.
constructor: (@user, @text) ->
super @user
# Determines if the message matches the given regex.
#
# regex - The Regex to check.
#
# Returns a Match object or null.
match: (regex) ->
@text.match regex
# Represents an incoming user entrance notification.
#
# user - A User instance for the user who entered.
class Robot.EnterMessage extends Robot.Message
# Represents an incoming user exit notification.
#
# user - A User instance for the user who left.
class Robot.LeaveMessage extends Robot.Message
class Listener
# Listeners receive every message from the chat source and decide if they
# want to act on it.
#
# robot - The current Robot instance.
# matcher - The Function that determines if this listener should trigger the
# callback.
# callback - The Function that is triggered if the incoming message matches.
constructor: (@robot, @matcher, @callback) ->
# Public: Determines if the listener likes the content of the message. If
# so, a Response built from the given Message is passed to the Listener
# callback.
#
# message - a Robot.Message instance.
#
# Returns nothing.
call: (message) ->
if match = @matcher message
@callback new @robot.Response(@robot, message, match)
class TextListener extends Listener
# TextListeners receive every message from the chat source and decide if they want
# to act on it.
#
# robot - The current Robot instance.
# regex - The Regex that determines if this listener should trigger the
# callback.
# callback - The Function that is triggered if the incoming message matches.
constructor: (@robot, @regex, @callback) ->
@matcher = (message) =>
if message instanceof Robot.TextMessage
message.match @regex
class Robot.Response
# Public: Responses are sent to matching listeners. Messages know about the
# content and user that made the original message, and how to reply back to
# them.
#
# robot - The current Robot instance.
# message - The current Robot.Message instance.
# match - The Match object from the successful Regex match.
constructor: (@robot, @message, @match) ->
# Public: Posts a message back to the chat source
#
# strings - One or more strings to be posted. The order of these strings
# should be kept intact.
#
# Returns nothing.
send: (strings...) ->
@robot.adapter.send @message.user, strings...
# Public: Posts a topic changing message
#
# strings - One or more strings to set as the topic of the
# room the bot is in.
#
# Returns nothing.
topic: (strings...) ->
@robot.adapter.topic @message.user, strings...
# Public: Posts a message mentioning the current user.
#
# strings - One or more strings to be posted. The order of these strings
# should be kept intact.
#
# Returns nothing.
reply: (strings...) ->
@robot.adapter.reply @message.user, strings...
# Public: Picks a random item from the given items.
#
# items - An Array of items (usually Strings).
#
# Returns a random item.
random: (items) ->
items[ Math.floor(Math.random() * items.length) ]
# Public: Creates a scoped http client with chainable methods for
# modifying the request. This doesn't actually make a request though.
# Once your request is assembled, you can call `get()`/`post()`/etc to
# send the request.
#
# url - String URL to access.
#
# Examples:
#
# res.http("http://example.com")
# # set a single header
# .header('Authorization', 'bearer abcdef')
#
# # set multiple headers
# .headers(Authorization: 'bearer abcdef', Accept: 'application/json')
#
# # add URI query parameters
# .query(a: 1, b: 'foo & bar')
#
# # make the actual request
# .get() (err, res, body) ->
# console.log body
#
# # or, you can POST data
# .post(data) (err, res, body) ->
# console.log body
#
# Returns a ScopedClient instance.
http: (url) ->
@httpClient.create(url)
HttpClient = require 'scoped-http-client'
Robot.Response::httpClient = HttpClient
module.exports = Robot