UNPKG

7.85 kBtext/coffeescriptView Raw
1{Robot, Adapter, EnterMessage, LeaveMessage, TopicMessage} = require 'brobbot'
2{SlackTextMessage, SlackRawMessage, SlackBotMessage} = require './message'
3{SlackRawListener, SlackBotListener} = require './listener'
4
5SlackClient = require 'slack-client'
6Util = require 'util'
7Q = require 'q'
8
9class SlackBot extends Adapter
10 @MAX_MESSAGE_LENGTH: 4000
11 @MIN_MESSAGE_LENGTH: 1
12
13 constructor: (robot) ->
14 @robot = robot
15 @readyDefer = Q.defer()
16 @ready = @readyDefer.promise
17
18 run: ->
19 # Take our options from the environment, and set otherwise suitable defaults
20 options =
21 token: process.env.BROBBOT_SLACK_TOKEN
22 autoReconnect: true
23 autoMark: true
24
25 return @robot.logger.error "No services token provided to Brobbot" unless options.token
26 return @robot.logger.error "v2 services token provided, please follow the upgrade instructions" unless (options.token.substring(0, 5) == 'xoxb-')
27
28 @options = options
29
30 # Create our slack client object
31 @client = new SlackClient options.token, options.autoReconnect, options.autoMark
32
33 # Setup event handlers
34 @client.on 'error', @.error
35 @client.on 'loggedIn', @.loggedIn
36 @client.on 'open', @.open
37 @client.on 'close', @.clientClose
38 @client.on 'message', @.message
39 @client.on 'userChange', @.userChange
40
41 # Start logging in
42 @client.login()
43
44 error: (error) =>
45 @robot.logger.error "Received error #{JSON.stringify error}"
46 @robot.logger.error error.stack
47 @robot.logger.error "Exiting in 1 second"
48 setTimeout process.exit.bind(process, 1), 1000
49
50 loggedIn: (self, team) =>
51 @robot.logger.info "Logged in as #{self.name} of #{team.name}, but not yet connected"
52
53 # store a copy of our own user data
54 @self = self
55
56 # Provide our name to Brobbot
57 @robot.name = self.name
58
59 for id, user of @client.users
60 @userChange user
61
62 userChange: (user) =>
63 newUser = {id: user.id, name: user.name, real_name: user.real_name, email_address: user.profile.email}
64
65 @robot.brain.addUser(newUser).then =>
66 @robot.brain.userForId user.id
67
68 open: =>
69 @robot.logger.info 'Slack client now connected'
70
71 # Tell Brobbot we're connected so it can load scripts
72 @readyDefer.resolve()
73
74 clientClose: =>
75 @robot.logger.info 'Slack client closed'
76 @client.removeListener 'error', @.error
77 @client.removeListener 'loggedIn', @.loggedIn
78 @client.removeListener 'open', @.open
79 @client.removeListener 'close', @.clientClose
80 @client.removeListener 'message', @.message
81 process.exit 0
82
83 message: (msg) =>
84 # Ignore our own messages
85 return if msg.user == @self.id
86
87 channel = @client.getChannelGroupOrDMByID msg.channel if msg.channel
88
89 if msg.hidden or (not msg.text and not msg.attachments) or msg.subtype is 'bot_message' or not msg.user or not channel
90 # use a raw message, so scripts that care can still see these things
91
92 if msg.user
93 getUser = @robot.brain.userForId msg.user
94 else
95 # We need to fake a user because, at the very least, CatchAllMessage
96 # expects it to be there.
97 user = {}
98 user.name = msg.username if msg.username?
99 getUser = Q user
100
101 getUser.then (user) =>
102 user.room = channel.name if channel
103
104 rawText = msg.getBody()
105 text = @removeFormatting rawText
106
107 if msg.subtype is 'bot_message'
108 @robot.logger.debug "Received bot message: '#{text}' in channel: #{channel?.name}, from: #{user?.name}"
109 @receive new SlackBotMessage user, text, rawText, msg
110 else
111 @robot.logger.debug "Received raw message (subtype: #{msg.subtype})"
112 @receive new SlackRawMessage user, text, rawText, msg
113
114 else
115 # Process the user into a full brobbot user
116 @robot.brain.userForId(msg.user).then (user) =>
117 user.room = channel.name
118
119 # Test for enter/leave messages
120 if msg.subtype is 'channel_join' or msg.subtype is 'group_join'
121 @robot.logger.debug "#{user.name} has joined #{channel.name}"
122 @receive new EnterMessage user
123
124 else if msg.subtype is 'channel_leave' or msg.subtype is 'group_leave'
125 @robot.logger.debug "#{user.name} has left #{channel.name}"
126 @receive new LeaveMessage user
127
128 else if msg.subtype is 'channel_topic' or msg.subtype is 'group_topic'
129 @robot.logger.debug "#{user.name} set the topic in #{channel.name} to #{msg.topic}"
130 @receive new TopicMessage user, msg.topic, msg.ts
131
132 else
133 # Build message text to respond to, including all attachments
134 rawText = msg.getBody()
135 text = @removeFormatting rawText
136
137 @robot.logger.debug "Received message: '#{text}' in channel: #{channel.name}, from: #{user.name}"
138
139 # If this is a DM, pretend it was addressed to us
140 if msg.getChannelType() == 'DM'
141 text = "#{@robot.name} #{text}"
142
143 @receive new SlackTextMessage user, text, rawText, msg
144
145 removeFormatting: (text) ->
146 # https://api.slack.com/docs/formatting
147 text = text.replace ///
148 < # opening angle bracket
149 ([@#!])? # link type
150 ([^>|]+) # link
151 (?:\| # start of |label (optional)
152 ([^>]+) # label
153 )? # end of label
154 > # closing angle bracket
155 ///g, (m, type, link, label) =>
156
157 switch type
158
159 when '@'
160 if label then return label
161 user = @client.getUserByID link
162 if user
163 return "@#{user.name}"
164
165 when '#'
166 if label then return label
167 channel = @client.getChannelByID link
168 if channel
169 return "\##{channel.name}"
170
171 when '!'
172 if link in ['channel','group','everyone']
173 return "@#{link}"
174
175 else
176 link = link.replace /^mailto:/, ''
177 if label and -1 == link.indexOf label
178 "#{label} (#{link})"
179 else
180 link
181 text = text.replace /&lt;/g, '<'
182 text = text.replace /&gt;/g, '>'
183 text = text.replace /&amp;/g, '&'
184 text
185
186 send: (envelope, messages...) ->
187 channel = @client.getChannelGroupOrDMByName envelope.room
188
189 for msg in messages
190 continue if msg.length < SlackBot.MIN_MESSAGE_LENGTH
191
192 @robot.logger.debug "Sending to #{envelope.room}: #{msg}"
193
194 if msg.length <= SlackBot.MAX_MESSAGE_LENGTH
195 channel.send msg
196
197 # If message is greater than MAX_MESSAGE_LENGTH, split it into multiple messages
198 else
199 submessages = []
200
201 while msg.length > 0
202 if msg.length <= SlackBot.MAX_MESSAGE_LENGTH
203 submessages.push msg
204 msg = ''
205
206 else
207 # Split message at last line break, if it exists
208 maxSizeChunk = msg.substring(0, SlackBot.MAX_MESSAGE_LENGTH)
209
210 lastLineBreak = maxSizeChunk.lastIndexOf('\n')
211 lastWordBreak = maxSizeChunk.match(/\W\w+$/)?.index
212
213 breakIndex = if lastLineBreak > -1
214 lastLineBreak
215 else if lastWordBreak
216 lastWordBreak
217 else
218 SlackBot.MAX_MESSAGE_LENGTH
219
220 submessages.push msg.substring(0, breakIndex)
221
222 # Skip char if split on line or word break
223 breakIndex++ if breakIndex isnt SlackBot.MAX_MESSAGE_LENGTH
224
225 msg = msg.substring(breakIndex, msg.length)
226
227 channel.send m for m in submessages
228
229 reply: (envelope, messages...) ->
230 @robot.logger.debug "Sending reply"
231
232 for msg in messages
233 # TODO: Don't prefix username if replying in DM
234 @send envelope, "#{envelope.user.name}: #{msg}"
235
236 topic: (envelope, strings...) ->
237 channel = @client.getChannelGroupOrDMByName envelope.room
238 channel.setTopic strings.join "\n"
239
240module.exports = SlackBot