-
Notifications
You must be signed in to change notification settings - Fork 73
/
Copy pathSlackHookHandler.js
353 lines (311 loc) · 11.9 KB
/
SlackHookHandler.js
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
"use strict";
const substitutions = require("./substitutions");
const SlackEventHandler = require('./SlackEventHandler');
const BaseSlackHandler = require('./BaseSlackHandler');
const rp = require('request-promise');
const qs = require("querystring");
const Promise = require('bluebird');
const promiseWhile = require("./promiseWhile");
const util = require("util");
const fs = require("fs");
const log = require("matrix-appservice-bridge").Logging.get("SlackHookHandler");
const PRESERVE_KEYS = [
"team_domain", "team_id",
"channel_name", "channel_id",
"user_name", "user_id",
];
/**
* @constructor
* @param {Main} main the toplevel bridge instance through which to
* communicate with matrix.
*/
function SlackHookHandler(main) {
this._main = main;
this.eventHandler = new SlackEventHandler(main);
}
util.inherits(SlackHookHandler, BaseSlackHandler);
/**
* Starts the hook server listening on the given port and (optional) TLS
* configuration.
* @param {int} port The TCP port to listen on
* @param {?Object} tls_config Optional TLS configuration
* @return {Promise} Returns a Promise that will resolve when the server is
* ready to accept requests
*/
SlackHookHandler.prototype.startAndListen = function(port, tls_config) {
let createServer;
if (tls_config) {
const tls_options = {
key: fs.readFileSync(tls_config.key_file),
cert: fs.readFileSync(tls_config.crt_file)
};
createServer = function(cb) {
return require("https").createServer(tls_options, cb);
};
}
else {
createServer = require("http").createServer;
}
return new Promise((resolve, reject) => {
createServer(this.onRequest.bind(this)).listen(port, () => {
const protocol = tls_config ? "https" : "http";
log.info("Slack-side listening on port " + port + " over " + protocol);
resolve();
});
});
};
SlackHookHandler.prototype.onRequest = function(request, response) {
var body = "";
request.on("data", (chunk) => {
body += chunk;
});
request.on("end", () => {
// if isEvent === true, this was an event emitted from the slack Event API
// https://api.slack.com/events-api
const isEvent = request.headers['content-type'] === 'application/json' && request.method === 'POST';
try {
if (isEvent) {
var params = JSON.parse(body);
this.eventHandler.handle(params, response);
}
else {
var params = qs.parse(body);
this.handle(request.method, request.url, params, response);
}
}
catch (e) {
log.error("SlackHookHandler failed:", e);
response.writeHead(500, {"Content-Type": "text/plain"});
if (request.method !== "HEAD") {
response.write("Internal Server Error");
}
response.end();
}
});
}
/**
* Handles a slack webhook request.
*
* Sends a message to Matrix if it understands enough of the message to do so.
* Attempts to make the message as native-matrix feeling as it can.
*
* @param {Object} params HTTP body of the webhook request, as a JSON-parsed dictionary.
* @param {string} params.channel_id Slack channel ID receiving the message.
* @param {string} params.channel_name Slack channel name receiving the message.
* @param {string} params.user_id Slack user ID of user sending the message.
* @param {string} params.user_name Slack user name of the user sending the message.
* @param {?string} params.text Text contents of the message, if a text message.
* @param {string} timestamp Timestamp when message was received, in seconds
* formatted as a float.
*/
SlackHookHandler.prototype.handle = function(method, url, params, response) {
log.info("Received slack webhook " + method + " " + url + ": " + JSON.stringify(params));
var main = this._main;
var endTimer = main.startTimer("remote_request_seconds");
var result = url.match(/^\/(.{32})(?:\/(.*))?$/);
if (!result) {
log.warn("Ignoring message with bad slackhook URL " + url);
response.writeHead(200, {"Content-Type": "text/plain"});
response.end();
endTimer({outcome: "dropped"});
return;
}
var inbound_id = result[1];
var path = result[2] || "post";
// GET requests (e.g. authorize) have params in query string
if (method === "GET") {
result = path.match(/^([^?]+)(?:\?(.*))$/);
path = result[1];
params = qs.parse(result[2]);
}
var room = main.getRoomByInboundId(inbound_id);
if (!room) {
log.warn("Ignoring message from unrecognised inbound ID: %s (%s.#%s)",
inbound_id, params.team_domain, params.channel_name
);
main.incCounter("received_messages", {side: "remote"});
response.writeHead(200, {"Content-Type": "text/plain"});
response.end();
endTimer({outcome: "dropped"});
return;
}
if (method === "POST" && path === "post") {
this.handlePost(room, params).then(
() => endTimer({outcome: "success"}),
(e) => {
endTimer({outcome: "fail"});
log.error("handlePost failed: ", e);
}
);
response.writeHead(200, {"Content-Type": "application/json"});
response.end();
}
else if (method === "GET" && path === "authorize") {
this.handleAuthorize(room, params).then((result) => {
response.writeHead(result.code || 200, {"Content-Type": "text/html"});
response.write(result.html);
response.end();
endTimer({outcome: "success"});
});
}
else {
// TODO: Handle this
log.debug(`Got call to ${method}${path} that we can't handle`);
response.writeHead(200, {"Content-Type": "application/json"});
if (method !== "HEAD") {
response.write("{}");
}
response.end();
endTimer({outcome: "dropped"});
}
};
SlackHookHandler.prototype.handlePost = function(room, params) {
// We can't easily query the name of a channel from its ID, but we can
// infer its current name every time we receive a message, because slack
// tells us.
var channel_name = params.team_domain + ".#" + params.channel_name;
var main = this._main;
room.updateSlackChannelName(channel_name);
if (room.isDirty()) {
main.putRoomToStore(room);
}
// TODO(paul): This will reject every bot-posted message, both our own
// reflections and other messages from other bot integrations. It would
// be nice if we could distinguish the two by somehow learning our own
// 'bot_id' parameter.
// https://github.com/matrix-org/matrix-appservice-slack/issues/29
if (params.user_id === "USLACKBOT") {
return Promise.resolve();
}
// Only count received messages that aren't self-reflections
main.incCounter("received_messages", {side: "remote"});
var token = room.getAccessToken();
if (!token) {
// If we can't look up more details about the message
// (because we don't have a master token), but it has text,
// just send the message as text.
log.warn("no slack token for " + params.team_domain);
if (params.text) {
return room.onSlackMessage(params);
}
return Promise.resolve();
}
var text = params.text;
if (undefined == text) {
// TODO(paul): When I started looking at this code there was no lookupAndSendMessage()
// I wonder if this code path never gets called...?
// lookupAndSendMessage(params.channel_id, params.timestamp, intent, roomID, token);
return Promise.resolve();
}
return this.lookupMessage(params.channel_id, params.timestamp, token).then((msg) => {
if(undefined == msg) {
msg = params;
}
// Restore the original parameters, because we've forgotten a lot of
// them by now
PRESERVE_KEYS.forEach((k) => msg[k] = params[k]);
return this.replaceChannelIdsWithNames(msg, token);
}).then((msg) => {
return this.replaceUserIdsWithNames(msg, token);
}).then((msg) => {
// we can't use .finally here as it does not get the final value, see https://github.com/kriskowal/q/issues/589
return room.onSlackMessage(msg);
});
};
SlackHookHandler.prototype.handleAuthorize = function(room, params) {
var oauth2 = this._main.getOAuth2();
if (!oauth2) {
log.warn("Wasn't expecting to receive /authorize without OAuth2 configured");
return;
}
log.debug("Exchanging temporary code for full OAuth2 token for " + room.getInboundId());
return oauth2.exchangeCodeForToken({
code: params.code,
room: room,
}).then((result) => {
log.debug("Got a full OAuth2 token");
room.updateAccessToken(result.access_token, result.access_scopes);
return this._main.putRoomToStore(room);
}).then(
() => {
return {
html: `
<h2>Integration Successful!</h2>
<p>Your Matrix-Slack channel integration is now correctly authorized.</p>
`
};
},
(err) => {
return {
code: 403,
html: `
<h2>Integration Failed</h2>
<p>Unfortunately your channel integration did not go as expected...</p>
`
};
}
);
};
/**
* Attempts to handle a message received from a slack webhook request.
*
* The webhook request that we receive doesn't have enough information to richly
* represent the message in Matrix, so we look up more details.
*
* @param {string} channelID Slack channel ID.
* @param {string} timestamp Timestamp when message was received, in seconds
* formatted as a float.
* @param {Intent} intent Intent for sending messages as the relevant user.
* @param {string} roomID Matrix room ID associated with channelID.
*/
//SlackHookHandler.prototype.lookupAndSendMessage =
SlackHookHandler.prototype.lookupMessage = function(channelID, timestamp, token) {
// Look up all messages at the exact timestamp we received.
// This has microsecond granularity, so should return the message we want.
var params = {
method: 'POST',
form : {
channel: channelID,
latest: timestamp,
oldest: timestamp,
inclusive: "1",
token: token,
},
uri: "https://slack.com/api/channels.history",
json: true
};
this._main.incRemoteCallCounter("channels.history");
return rp(params).then((response) => {
if (!response || !response.messages || response.messages.length === 0) {
log.warn("Could not find history: " + response);
return undefined;
}
if (response.messages.length != 1) {
// Just laziness.
// If we get unlucky and two messages were sent at exactly the
// same microsecond, we could parse them all, filter by user,
// filter by whether they have attachments, and such, and pick
// the right message. But this is unlikely, and I'm lazy, so
// we'll just drop the message...
log.warn("Really unlucky, got multiple messages at same" +
" microsecond, dropping:" + response);
return undefined;
}
var message = response.messages[0];
log.debug("Looked up message from history as " + JSON.stringify(message));
if (messages.subtype !== "file_share") {
return message;
}
return this.enablePublicSharing(message.file, token)
.then((file) => {
message.file = file;
return this.fetchFileContent(message.file, token);
}).then((content) => {
message.file._content = content;
return message;
}).catch((err) => {
log.error("Failed to get file content: ", err);
});
});
}
module.exports = SlackHookHandler;