From c5094d1a15a67cdc941cf21dcc5916cb97fefd77 Mon Sep 17 00:00:00 2001 From: perissology Date: Fri, 12 Jan 2018 14:51:54 -0800 Subject: [PATCH 01/55] add support for send msgs as bot user --- lib/AdminCommands.js | 11 +++++++++-- lib/BridgedRoom.js | 35 +++++++++++++++++++++++++++-------- lib/Main.js | 12 ++++++++++++ 3 files changed, 48 insertions(+), 10 deletions(-) diff --git a/lib/AdminCommands.js b/lib/AdminCommands.js index bd355c16..cf9bd0b4 100644 --- a/lib/AdminCommands.js +++ b/lib/AdminCommands.js @@ -131,9 +131,13 @@ adminCommands.link = new AdminCommand({ required: true, }, webhook_url: { - description: "Slack webhook URL", + description: "Slack webhook URL. Used with slack outgoing hooks integration", aliases: ['u'], }, + slack_token: { + description: "Slack bot user token. Used with slack bot user & Events api", + aliases: ['t'], + }, }, func: function(main, opts, args, respond) { return main.actionLink({ @@ -141,10 +145,13 @@ adminCommands.link = new AdminCommand({ slack_channel_name: opts.channel, slack_channel_id: opts.channel_id, slack_webhook_uri: opts.webhook_url, + slack_bot_token: opts.slack_token, }).then( (room) => { respond("Room is now " + room.getStatus()); - respond("Inbound URL is " + main.getInboundUrlForRoom(room)); + if (room.getSlackWebhookUri()) { + respond("Inbound URL is " + main.getInboundUrlForRoom(room)); + } }, (e) => { respond("Cannot link - " + e ) } ); diff --git a/lib/BridgedRoom.js b/lib/BridgedRoom.js index 6420edda..56cbd644 100644 --- a/lib/BridgedRoom.js +++ b/lib/BridgedRoom.js @@ -20,6 +20,7 @@ function BridgedRoom(main, opts) { this._slack_channel_name = opts.slack_channel_name; this._slack_channel_id = opts.slack_channel_id; this._slack_webhook_uri = opts.slack_webhook_uri; + this._slack_bot_token = opts.slack_bot_token; this._access_token = opts.access_token; this._access_scopes = opts.access_scopes; @@ -30,13 +31,13 @@ function BridgedRoom(main, opts) { }; BridgedRoom.prototype.getStatus = function() { - if (!this._slack_webhook_uri) { + if (!this._slack_webhook_uri && !this._slack_bot_token) { return "pending-params"; } if (!this._slack_channel_name) { return "pending-name"; } - if (!this._access_token) { + if (!this._access_token && !this._slack_bot_token) { return "ready-no-token"; } return "ready"; @@ -65,7 +66,7 @@ BridgedRoom.prototype.getSlackWebhookUri = function() { }; BridgedRoom.prototype.getAccessToken = function() { - return this._access_token; + return this._access_token || this._slack_bot_token; }; BridgedRoom.prototype.getMatrixRoomId = function() { @@ -92,7 +93,13 @@ BridgedRoom.prototype.updateSlackWebhookUri = function(slack_webhook_uri) { this._slack_webhook_uri = slack_webhook_uri; }; +BridgedRoom.prototype.updateSlackBotToken = function(slack_bot_token) { + if (this._slack_bot_token !== slack_bot_token) this._dirty = true; + this._slack_bot_token = slack_bot_token; +}; + BridgedRoom.prototype.updateAccessToken = function(token, scopes) { + console.log('updateAccessToken ->', token, scopes); if (this._access_token === token && this._access_scopes.sort().join(",") === scopes.sort().join(",")) return; @@ -108,6 +115,7 @@ BridgedRoom.fromEntry = function(main, entry) { slack_channel_id: entry.remote.id, slack_channel_name: entry.remote.name, slack_webhook_uri: entry.remote.webhook_uri, + slack_bot_token: entry.remote.slack_bot_token, access_token: entry.remote.access_token, access_scopes: entry.remote.access_scopes, }; @@ -123,6 +131,7 @@ BridgedRoom.prototype.toEntry = function() { id: this._slack_channel_id, name: this._slack_channel_name, webhook_uri: this._slack_webhook_uri, + slack_bot_token: this._slack_bot_token, access_token: this._access_token, access_scopes: this._access_scopes, }, @@ -137,18 +146,28 @@ BridgedRoom.prototype.toEntry = function() { }; BridgedRoom.prototype.onMatrixMessage = function(message) { - if (!this._slack_webhook_uri) return Promise.resolve(); + if (!this._slack_webhook_uri || !this._slack_bot_token) return Promise.resolve(); return this._main.getOrCreateMatrixUser(message.user_id).then((user) => { var body = substitutions.matrixToSlack(message, this._main); + var uri = (this._slack_bot_token) ? "https://slack.com/api/chat.postMessage" : this._slack_webhook_uri; + var sendMessageParams = { method: "POST", json: true, - uri: this._slack_webhook_uri, - body: body + uri: uri, + body: body, }; + if (this._slack_bot_token) { + sendMessageParams.headers = { + Authorization: 'Bearer ' + this._slack_bot_token + }; + sendMessageParams.body.as_user = false; + sendMessageParams.body.channel = this._slack_channel_id; + } + sendMessageParams.body.username = user.getDisplaynameForRoom(message.room_id); var avatar_url = user.getAvatarUrlForRoom(message.room_id); @@ -162,8 +181,8 @@ BridgedRoom.prototype.onMatrixMessage = function(message) { return rp(sendMessageParams).then((res) => { this._main.incCounter("sent_messages", {side: "remote"}); - if (!res) { - console.log("HTTP Error: %s", res); + if (!res || (this._slack_bot_token && !res.ok)) { + console.log("HTTP Error: ", res); } }); }); diff --git a/lib/Main.js b/lib/Main.js index 009eae59..68ba609c 100644 --- a/lib/Main.js +++ b/lib/Main.js @@ -521,6 +521,18 @@ Main.prototype.actionLink = function(opts) { room.updateSlackWebhookUri(opts.slack_webhook_uri); } + if (opts.slack_bot_token) { + room.updateSlackBotToken(opts.slack_bot_token); + } + + if (opts.slack_channel_id) { + room.updateSlackChannelId(opts.slack_channel_id); + } + + // if (opts.slack_channel_name) { + // room.updateSlackChannelName(opts.slack_channel_name); + // } + if (room.isDirty()) { this.putRoomToStore(room); } From 2139a7c21a36da0f555cca1e1eada4367d8ad454 Mon Sep 17 00:00:00 2001 From: perissology Date: Sat, 13 Jan 2018 09:06:34 -0800 Subject: [PATCH 02/55] got basic slack event bridging --- README.md | 31 +++ lib/BridgedRoom.js | 94 +++++++- lib/Main.js | 29 ++- lib/SlackEventHandler.js | 447 +++++++++++++++++++++++++++++++++++++++ lib/SlackGhost.js | 66 ++++-- lib/SlackHookHandler.js | 12 +- 6 files changed, 655 insertions(+), 24 deletions(-) create mode 100644 lib/SlackEventHandler.js diff --git a/README.md b/README.md index fa484849..91a1afe0 100644 --- a/README.md +++ b/README.md @@ -90,6 +90,37 @@ The bridge itself should now be running. To actually use it, you will need to configure some linked channels. +Slack Bot Setup +--------------- + +1. add a custom app to your slack team/workspace by visiting https://api.slack.com/apps + and clicking on `Create New App`. + +2. Name the app & select the team/workspace this app will belong to. + +3. Click on `bot users` and add a new bot user. We will use this account to bridge the + the rooms. + +4. Click on `Event Subscriptions` and enable them. At this point, the bridge needs to be + started as slack will do some verification of the request rul. The request url should be + `https://$HOST:$SLACK_PORT"`. Then add the following events and save: + + Bot User Events: + + - team_domain_change + - message.channels + +5. Click on `Install App` and `Install App to Workspace`. Note the `Bot User OAuth Access Token` + as you will need it whenever you link a room. + +6. You will need to invite the bot user you created above to each slack channel you would like + to bridge. + + ``` + /invite @bot-user-name + + ``` + Provisioning ------------ diff --git a/lib/BridgedRoom.js b/lib/BridgedRoom.js index 56cbd644..d038e98a 100644 --- a/lib/BridgedRoom.js +++ b/lib/BridgedRoom.js @@ -21,6 +21,10 @@ function BridgedRoom(main, opts) { this._slack_channel_id = opts.slack_channel_id; this._slack_webhook_uri = opts.slack_webhook_uri; this._slack_bot_token = opts.slack_bot_token; + this._slack_team_domain = opts.slack_team_domain; + this._slack_team_id = opts.slack_team_id; + this._slack_user_id = opts.slack_user_id; + this._slack_bot_id = opts.slack_bot_id; this._access_token = opts.access_token; this._access_scopes = opts.access_scopes; @@ -73,6 +77,18 @@ BridgedRoom.prototype.getMatrixRoomId = function() { return this._matrix_room_id; }; +BridgedRoom.prototype.getSlackTeamDomain = function() { + return this._slack_team_domain; +}; + +BridgedRoom.prototype.getSlackTeamId = function() { + return this._slack_team_id; +}; + +BridgedRoom.prototype.getSlackBotId = function() { + return this._slack_bot_id; +}; + BridgedRoom.prototype.updateInboundId = function(inbound_id) { if (this._inbound_id !== inbound_id) this._dirty = true; this._inbound_id = inbound_id; @@ -98,6 +114,11 @@ BridgedRoom.prototype.updateSlackBotToken = function(slack_bot_token) { this._slack_bot_token = slack_bot_token; }; +BridgedRoom.prototype.updateSlackTeamDomain = function(domain) { + if (this._slack_team_domain !== domain) this._dirty = true; + this._slack_team_domain = domain; +}; + BridgedRoom.prototype.updateAccessToken = function(token, scopes) { console.log('updateAccessToken ->', token, scopes); if (this._access_token === token && @@ -116,6 +137,10 @@ BridgedRoom.fromEntry = function(main, entry) { slack_channel_name: entry.remote.name, slack_webhook_uri: entry.remote.webhook_uri, slack_bot_token: entry.remote.slack_bot_token, + slack_team_domain: entry.remote.slack_team_domain, + slack_team_id: entry.remote.slack_team_id, + slack_user_id: entry.remote.slack_user_id, + slack_bot_id: entry.remote.slack_bot_id, access_token: entry.remote.access_token, access_scopes: entry.remote.access_scopes, }; @@ -132,6 +157,10 @@ BridgedRoom.prototype.toEntry = function() { name: this._slack_channel_name, webhook_uri: this._slack_webhook_uri, slack_bot_token: this._slack_bot_token, + slack_team_domain: this._slack_team_domain, + slack_team_id: this._slack_team_id, + slack_user_id: this._slack_user_id, + slack_bot_id: this._slack_bot_id, access_token: this._access_token, access_scopes: this._access_scopes, }, @@ -146,7 +175,7 @@ BridgedRoom.prototype.toEntry = function() { }; BridgedRoom.prototype.onMatrixMessage = function(message) { - if (!this._slack_webhook_uri || !this._slack_bot_token) return Promise.resolve(); + if (!this._slack_webhook_uri && !this._slack_bot_token) return Promise.resolve(); return this._main.getOrCreateMatrixUser(message.user_id).then((user) => { var body = substitutions.matrixToSlack(message, this._main); @@ -319,4 +348,67 @@ BridgedRoom.prototype.getMatrixATime = function() { return this._matrixAtime; }; +BridgedRoom.prototype.lookupAndSetTeamInfo = function() { + if (!this._slack_bot_token) return Promise.resolve(); + + return rp({ + uri: 'https://slack.com/api/team.info', + qs: { + token: this._slack_bot_token + }, + json: true, + }).then((response) => { + if (!response.team) return; + + if (!this._slack_team_domain !== response.team.domain) { + this._slack_team_domain = response.team.domain; + this._dirty = true; + } + + if (!this._slack_team_id !== response.team.id) { + this._slack_team_id = response.team.id; + this._dirty = true; + } + + return; + }); +}; + +BridgedRoom.prototype.lookupAndSetUserInfo = function() { + if (!this._slack_bot_token) return Promise.resolve(); + + return rp({ + uri: 'https://slack.com/api/auth.test', + qs: { + token: this._slack_bot_token + }, + json: true, + }).then((response) => { + if (!response.user_id) return; + + if (!this._slack_user_id !== response.user_id) { + this._slack_user_id = response.user_id; + this._dirty = true; + } + + return rp({ + uri: 'https://slack.com/api/users.info', + qs: { + token: this._slack_bot_token, + user: response.user_id, + }, + json: true, + }); + }).then((response) => { + if (!response.user && response.user.profile) return Promise.resolve(); + + if (!this._slack_bot_id !== response.user.profile.bot_id) { + this._slack_bot_id = response.user.profile.bot_id; + this._dirty = true; + } + + return; + }); +}; + module.exports = BridgedRoom; diff --git a/lib/Main.js b/lib/Main.js index 68ba609c..cc5c9568 100644 --- a/lib/Main.js +++ b/lib/Main.js @@ -40,6 +40,7 @@ function Main(config) { this._rooms = []; this._roomsBySlackChannelId = {}; + this._roomsBySlackTeamId = {}; this._roomsByMatrixRoomId = {}; this._roomsByInboundId = {}; @@ -193,6 +194,7 @@ Main.prototype.getGhostForSlackMessage = function(message) { // Slack ghost IDs need to be constructed from user IDs, not usernames, // because users can change their names + // TODO it the team_domain is changed, we will recreate all users // TODO(paul): Steal MatrixIdTemplate from matrix-appservice-gitter var user_id = [ "@", this._config.username_prefix, message.team_domain.toLowerCase(), @@ -258,6 +260,16 @@ Main.prototype.addBridgedRoom = function(room) { var inbound_id = room.getInboundId(); if (inbound_id) this._roomsByInboundId[inbound_id] = room; + + var team_id = room.getSlackTeamId(); + if (team_id) { + var rooms = this._roomsBySlackTeamId[team_id]; + if (!rooms) { + this._roomsBySlackTeamId[team_id] = [ room ]; + } else { + rooms.push(room); + } + } }; Main.prototype.removeBridgedRoom = function(room) { @@ -274,6 +286,10 @@ Main.prototype.getRoomBySlackChannelId = function(channel_id) { return this._roomsBySlackChannelId[channel_id]; }; +Main.prototype.getRoomsBySlackTeamId = function(team_id) { + return this._roomsBySlackTeamId[team_id] || []; +}; + Main.prototype.getRoomBySlackChannelName = function(channel_name) { // TODO(paul): this gets inefficient for long lists for(var i = 0; i < this._rooms.length; i++) { @@ -505,6 +521,7 @@ Main.prototype.actionLink = function(opts) { var room = this.getRoomByMatrixRoomId(matrix_room_id); + var isNew = false; if (!room) { var inbound_id = this.genInboundId(); @@ -512,7 +529,7 @@ Main.prototype.actionLink = function(opts) { inbound_id: inbound_id, matrix_room_id: matrix_room_id, }); - this.addBridgedRoom(room); + isNew = true; this._roomsByMatrixRoomId[matrix_room_id] = room; this._stateStorage.trackRoom(matrix_room_id); } @@ -523,6 +540,14 @@ Main.prototype.actionLink = function(opts) { if (opts.slack_bot_token) { room.updateSlackBotToken(opts.slack_bot_token); + Promise.all([ room.lookupAndSetTeamInfo(), room.lookupAndSetUserInfo() ]) + .then(() => { + this.addBridgedRoom(room); + + if (room.isDirty()) { + this.putRoomToStore(room); + } + }); } if (opts.slack_channel_id) { @@ -533,6 +558,8 @@ Main.prototype.actionLink = function(opts) { // room.updateSlackChannelName(opts.slack_channel_name); // } + if (isNew) this.addBridgedRoom(room); + if (room.isDirty()) { this.putRoomToStore(room); } diff --git a/lib/SlackEventHandler.js b/lib/SlackEventHandler.js new file mode 100644 index 00000000..c5403462 --- /dev/null +++ b/lib/SlackEventHandler.js @@ -0,0 +1,447 @@ +"use strict"; + +var substitutions = require("./substitutions"); +var rp = require('request-promise'); +var qs = require("querystring"); +var Promise = require('bluebird'); +var promiseWhile = require("./promiseWhile"); + +var PRESERVE_KEYS = [ + "team_domain", "team_id", + "channel_name", "channel_id", + "user_name", "user_id", +]; + +var UnknownEvent = function() {}; +var UnknownChannel = function(channel) { this.channel = channel; }; + +/** + * @constructor + * @param {Main} main the toplevel bridge instance through which to + * communicate with matrix. + */ +function SlackEventHandler(main) { + this._main = main; +} + + +/** + * Handles a slack event request. + * + * @param {Object} params HTTP body of the event request, as a JSON-parsed dictionary. + * @param {string} params.team_id The unique identifier for the workspace/team where this event occurred. + * @param {Object} params.event Slack event object + * @param {string} params.event.type Slack event type + * @param {string} params.type type of callback we are receiving. typically event_callback + * or url_verification. + // * @param {string} timestamp Timestamp when message was received, in seconds + // * formatted as a float. + */ +SlackEventHandler.prototype.handle = function(params, response) { + try { + console.log("Received slack event:", JSON.stringify(params)); + + var main = this._main; + + var endTimer = main.startTimer("remote_request_seconds"); + + // respond to event url challenges + if (params.type === 'url_verification') { + response.writeHead(200, {"Content-Type": "application/json"}); + response.write(JSON.stringify({challenge: params.challenge})); + response.end(); + return; + } + + // var room = main.getRoomBySlackChannelId(params.event.channel); + + var result; + switch (params.event.type) { + case 'message': + result = this.handleMessageEvent(params); + break; + case 'channel_rename': + result = this.handleChannelRenameEvent(params); + break; + case 'team_domain_change': + result = this.handleDomainChangeEvent(params); + break; + case 'file_comment_added': + result = Promise.resolve(); + break; + default: + result = Promise.reject(new UnknownEvent()); + } + + result + .then(() => endTimer({outcome: "success"})) + .catch((e) => { + if (e instanceof UnknownChannel) { + console.log("Ignoring message from unrecognised slack channel id : %s (%s)", + e.channel, params.team_id); + main.incCounter("received_messages", {side: "remote"}); + + endTimer({outcome: "dropped"}); + return; + } else if (e instanceof UnknownEvent) { + endTimer({outcome: "dropped"}); + } else { + endTimer({outcome: "fail"}); + } + + console.log("Failed: ", e); + } + ); + + response.writeHead(200, {"Content-Type": "application/json"}); + response.end(); + } catch (e) { + console.log("Oops - SlackEventHandler failed:", e); + + // return 200 so slack doesn't keep sending the event + response.writeHead(200, {"Content-Type": "text/plain"}); + response.end(); + } +}; + +SlackEventHandler.prototype.replaceChannelIdsWithNames = function(message, token) { + var main = this._main; + + // match all channelIds + var testForName = message.text.match(/<#(\w+)\|?\w*?>/g); + var iteration = 0; + var matches = 0; + if (testForName && testForName.length) { + matches = testForName.length; + } + return promiseWhile(function() { + // Do this until there are no more channel ID matches + return iteration < matches; + }, function() { + // foreach channelId, pull out the ID + // (if this is an emote msg, the format is <#ID|name>, but in normal msgs it's just <#ID> + var id = testForName[iteration].match(/<#(\w+)\|?\w*?>/)[1]; + var channelsInfoApiParams = { + uri: 'https://slack.com/api/channels.info', + qs: { + token: token, + channel: id + }, + json: true + }; + main.incRemoteCallCounter("channels.info"); + return rp(channelsInfoApiParams).then((response) => { + if (response && response.channel && response.channel.name) { + console.log("channels.info: " + id + " mapped to " + response.channel.name); + message.text = message.text.replace(/<#(\w+)\|?\w*?>/, "#" + response.channel.name); + } + else { + console.log("channels.info returned no result for " + id); + } + iteration++; + }).catch((err) => { + console.log("Caught error " + err); + }); + }).then(() => { + // Notice we can chain it because it's a Promise, + // this will run after completion of the promiseWhile Promise! + return message; + }); +}; + +SlackEventHandler.prototype.replaceUserIdsWithNames = function(message, token) { + var main = this._main; + + // match all userIds + var testForName = message.text.match(/<@(\w+)\|?\w*?>/g); + var iteration = 0; + var matches = 0; + if (testForName && testForName.length) { + matches = testForName.length; + } + return promiseWhile(() => { + // Condition for stopping + return iteration < matches; + }, function() { + // foreach userId, pull out the ID + // (if this is an emote msg, the format is <@ID|nick>, but in normal msgs it's just <@ID> + var id = testForName[iteration].match(/<@(\w+)\|?\w*?>/)[1]; + var channelsInfoApiParams = { + uri: 'https://slack.com/api/users.info', + qs: { + token: token, + user: id + }, + json: true + }; + main.incRemoteCallCounter("users.info"); + return rp(channelsInfoApiParams).then((response) => { + if (response && response.user && response.user.name) { + console.log("users.info: " + id + " mapped to " + response.user.name); + message.text = message.text.replace(/<@(\w+)\|?\w*?>/, response.user.name); + } + else { + console.log("users.info returned no result for " + id); + } + iteration++; + }).catch((err) => { + console.log("Caught error " + err); + }); + }).then(() => { + // Notice we can chain it because it's a Promise, + // this will run after completion of the promiseWhile Promise! + return message; + }); +}; + +/** + * 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. + */ +//SlackEventHandler.prototype.lookupAndSendMessage = +SlackEventHandler.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) { + console.log("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... + console.log("Really unlucky, got multiple messages at same" + + " microsecond, dropping:" + response); + return undefined; + } + var message = response.messages[0]; + console.log("Looked up message from history as " + JSON.stringify(message)); + + if (message.subtype === "file_share" && shouldFetchContent(message.file)) { + return this.fetchFileContent(message.file, token).then((content) => { + message.file._content = content; + return message; + }); + } + return message; + }); +} + +// Return true if we ought to fetch the content of the given file object +function shouldFetchContent(file) { + if (!file) return false; + if (file.mimetype && file.mimetype.indexOf("image/") === 0) return true; + return false; +} + +/** + * Enables public sharing on the given file object then fetches its content. + * + * @param {Object} file A slack 'message.file' data object + * @param {string} token A slack API token that has 'files:write:user' scope + * @return {Promise} A Promise of file contents + */ +SlackEventHandler.prototype.fetchFileContent = function(file, token) { + this._main.incRemoteCallCounter("files.sharedPublicURL"); + return rp({ + method: 'POST', + form : { + file: file.id, + token: token, + }, + uri: "https://slack.com/api/files.sharedPublicURL", + json: true + }).then((response) => { + if (!response || !response.file || !response.file.permalink_public) { + console.log("Could not find sharedPublichURL: " + JSON.stringify(response)); + return undefined; + } + + var pub_secret = file.permalink_public.match(/https?:\/\/slack-files.com\/[^-]*-[^-]*-(.*)/); + var public_file_url = file.permalink_public; + // try to get direct link to image + if (pub_secret != undefined && pub_secret.length > 0) { + public_file_url = file.url_private + "?pub_secret=" + pub_secret[1]; + } + + return rp({ + uri: public_file_url, + resolveWithFullResponse: true, + encoding: null + }); + }).then((response) => { + var content = response.body; + console.log("Successfully fetched file " + file.id + + " content (" + content.length + " bytes)"); + return content; + }); +} + + +/** + * + * @param room + * @param params + */ +SlackEventHandler.prototype.handleEvent = function(room, params) { + var main = this._main; + + // TODO set bot_user_id on room on link. authed_user isn't the correct check + // 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") { + if (params.event.user in params.authed_user) { + return Promise.resolve(); + } + + // Only count received messages that aren't self-reflections + main.incCounter("received_messages", {side: "remote"}); + + +}; + +/** + * Attempts to handle the `team_domain_change` event. + * + * @param {Object} params The event request emitted. + * @param {Object} params.team_id The slack team_id for the event. + * @param {string} params.event.domain The new team domain. + */ +SlackEventHandler.prototype.handleDomainChangeEvent = function(params) { + this._main.getRoomsBySlackTeamId(params.team_id).forEach(room => { + room.updateSlackTeamDomain(params.event.domain); + if (room.isDirty()) { + this._main.putRoomToStore(room); + } + }); + return Promise.resolve(); +}; + +/** + * Attempts to handle the `channel_rename` event. + * + * @param {Object} params The event request emitted. + * @param {string} params.event.id The slack channel id + * @param {string} params.event.name The new name + */ +SlackEventHandler.prototype.handleChannelRenameEvent = function(params) { + //TODO test me. and do we even need this? doesn't appear to be used anymore + var room = this._main.getRoomBySlackChannelId(params.event.channel.id); + if (!room) throw new UnknownChannel(params.event.channel.id); + + var channel_name = room.getSlackTeamDomain() + ".#" + params.name; + room.updateSlackChannelName(channel_name); + if (room.isDirty()) { + this._main.putRoomToStore(room); + } + return Promise.resolve(); +}; + +/** + * Attempts to handle the `message` event. + * + * 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 The event request emitted. + * @param {string} params.event.user 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.event.text Text contents of the message, if a text message. + * @param {string} params.event.channel The slack channel id + * @param {string} params.event.ts The unique (per-channel) timestamp + */ +SlackEventHandler.prototype.handleMessageEvent = function(params) { + var room = this._main.getRoomBySlackChannelId(params.event.channel); + if (!room) throw new UnknownChannel(params.event.channel); + + if (params.event.subtype === 'bot_message' && params.event.bot_id === room.getSlackBotId()) { + return Promise.resolve(); + } + + // Only count received messages that aren't self-reflections + this._main.incCounter("received_messages", {side: "remote"}); + + var token = room.getAccessToken(); + + var text = params.event.text; + 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. + console.log("no slack token for " + room.getSlackTeamDomain() || room.getSlackChannelId()); + + if (text) { + return room.onSlackMessage({ + text, + user_id: params.event.user, + team_domain: room.getSlackTeamDomain() || room.getSlackTeamId() + }); + } + return Promise.resolve(); + } + + 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(); + } + + // TODO update this. I think it only involves file_share messages + // return this.lookupMessage(params.event.channel, params.event.ts, token).then((msg) => { + // console.log('msg ->', msg); + // if(undefined == msg) { + // msg = params; + // } + + // Restore the original parameters, because we've forgotten a lot of + // them by now + // "team_domain", "team_id", + // "channel_name", "channel_id", + // "user_name", "user_id", + // PRESERVE_KEYS.forEach((k) => msg[k] = params[k]); + + var msg = Object.assign({}, params.event, { + user_id: params.event.user, + team_domain: room.getSlackTeamDomain() || room.getSlackTeamId() + }); + console.log(msg); + return room.onSlackMessage(msg); + // 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); + // }); +}; + +module.exports = SlackEventHandler; diff --git a/lib/SlackGhost.js b/lib/SlackGhost.js index f053b700..661b6af5 100644 --- a/lib/SlackGhost.js +++ b/lib/SlackGhost.js @@ -3,8 +3,8 @@ var rp = require('request-promise'); var slackdown = require('slackdown'); -// How long in msec to cache avatar URL lookups from slack -var AVATAR_CACHE_TIMEOUT = 10 * 60 * 1000; // 10 minutes +// How long in msec to cache user infor lookups from slack +var USER_CACHE_TIMEOUT = 10 * 60 * 1000; // 10 minutes function SlackGhost(opts) { this._main = opts.main; @@ -57,20 +57,50 @@ SlackGhost.prototype.update = function(message, room) { SlackGhost.prototype.updateDisplayname = function(message, room) { var display_name = message.user_name; - if (!display_name) return Promise.resolve(); - if (this._display_name === display_name) return Promise.resolve(); - return this.getIntent().setDisplayName(display_name).then(() => { - this._display_name = display_name; - return this._main.putUserToStore(this); - }); + var getDisplayName = (!display_name) ? + this.lookupUserInfo(message.user_id, room.getAccessToken()) + .then(user => { + if (user && user.profile) { + return user.profile.display_name || user.profile.real_name; + } + return undefined; + }) : + Promise.resolve(display_name); + + return getDisplayName.then(display_name => { + if (!display_name) return Promise.resolve(); + if (this._display_name === display_name) return Promise.resolve(); + + return this.getIntent().setDisplayName(display_name).then(() => { + this._display_name = display_name; + return this._main.putUserToStore(this); + }); + }) }; SlackGhost.prototype.lookupAvatarUrl = function(user_id, token) { - if (this._avatar_url_cache) return Promise.resolve(this._avatar_url_cache); + return this.lookupUserInfo(user_id, token).then((user) => { + if (!user || !user.profile) return; + var profile = user.profile; + + // Pick the original image if we can, otherwise pick the largest image + // that is defined + var avatar_url = profile.image_original || + profile.image_1024 || profile.image_512 || profile.image_192 || + profile.image_72 || profile.image_48; + + return avatar_url; + }); +}; + +SlackGhost.prototype.lookupUserInfo = function(user_id, token) { + if (this._user_info_cache) return Promise.resolve(this._user_info_cache); + if (this._loading_user) return this._loading_user; + if (!token) return Promise.resolve(); this._main.incRemoteCallCounter("users.info"); - return rp({ + this._loading_user = rp({ uri: 'https://slack.com/api/users.info', qs: { token: token, @@ -79,19 +109,15 @@ SlackGhost.prototype.lookupAvatarUrl = function(user_id, token) { json: true, }).then((response) => { if (!response.user || !response.user.profile) return; - var profile = response.user.profile; - // Pick the original image if we can, otherwise pick the largest image - // that is defined - var avatar_url = profile.image_original || - profile.image_1024 || profile.image_512 || profile.image_192 || - profile.image_72 || profile.image_48; - - this._avatar_url_cache = avatar_url; - setTimeout(() => { this._avatar_url_cache = null }, AVATAR_CACHE_TIMEOUT); + this._user_info_cache = response.user; + setTimeout(() => { this._user_info_cache = null }, USER_CACHE_TIMEOUT); - return avatar_url; + delete this._loading_user; + return response.user; }); + + return this._loading_user; }; SlackGhost.prototype.updateAvatar = function(message, room) { diff --git a/lib/SlackHookHandler.js b/lib/SlackHookHandler.js index 8efbb104..c5cc7a83 100644 --- a/lib/SlackHookHandler.js +++ b/lib/SlackHookHandler.js @@ -1,6 +1,7 @@ "use strict"; var substitutions = require("./substitutions"); +var SlackEventHandler = require('./SlackEventHandler'); var rp = require('request-promise'); var qs = require("querystring"); var Promise = require('bluebird'); @@ -19,6 +20,7 @@ var PRESERVE_KEYS = [ */ function SlackHookHandler(main) { this._main = main; + this.eventHandler = new SlackEventHandler(main); } /** @@ -53,9 +55,15 @@ SlackHookHandler.prototype.startAndListen = function(port, tls_config) { }); request.on("end", () => { - var params = qs.parse(body); + var isEvent = request.headers['content-type'] === 'application/json' && request.method === 'POST'; try { - this.handle(request.method, request.url, params, response); + 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) { console.log("Oops - SlackHookHandler failed:", e); From 579776198ed2e17c9c8818632802dd127184de4c Mon Sep 17 00:00:00 2001 From: perissology Date: Sat, 13 Jan 2018 09:17:34 -0800 Subject: [PATCH 03/55] update readme --- README.md | 67 +++++++++++++++++++++++++++++++++++++++---------------- 1 file changed, 48 insertions(+), 19 deletions(-) diff --git a/README.md b/README.md index 91a1afe0..96c5c004 100644 --- a/README.md +++ b/README.md @@ -90,8 +90,21 @@ The bridge itself should now be running. To actually use it, you will need to configure some linked channels. -Slack Bot Setup ---------------- +Provisioning +------------ + +This bridge allows linking together pairs of Matrix rooms and Slack channels, +relaying messages said by people in one side into the other. To create a link +first the individual Matrix room and Slack channel need to be created, and then +a command needs to be issued in the administration console room to add the link +to the bridge's database. + +There are 2 ways to bridge a room. The recommended way uses the newer slack events api +and bot users. This allows you to link as many channels as you would like with only +1 slack integration. The legacy way uses incoming/outgoing webhooks, and requires +2 slack integrations per channel to be bridged. + +### Recommended 1. add a custom app to your slack team/workspace by visiting https://api.slack.com/apps and clicking on `Create New App`. @@ -113,23 +126,39 @@ Slack Bot Setup 5. Click on `Install App` and `Install App to Workspace`. Note the `Bot User OAuth Access Token` as you will need it whenever you link a room. -6. You will need to invite the bot user you created above to each slack channel you would like - to bridge. - - ``` - /invite @bot-user-name +6. For each channel you would like to bridge, perform the following steps: - ``` - - -Provisioning ------------- + 1. Create a Matrix room in the usual manner for your client. Take a note of its + Matrix room ID - it will look something like `!aBcDeF:example.com`. + + 2. invite the bot user to the slack channel you would like to bridge. + + ``` + /invite @bot-user-name -This bridge allows linking together pairs of Matrix rooms and Slack channels, -relaying messages said by people in one side into the other. To create a link -first the individual Matrix room and Slack channel need to be created, and then -a command needs to be issued in the administration console room to add the link -to the bridge's database. + ``` + + You will also need to determine the "channel ID" that Slack uses to identify + the channel. Unfortunately, it is not easily obtained from the Slack UI. The + easiest way to do this is to send a message from Slack to the bridge; the + bridge will log the channel ID as part of the unrecognised message output. + You can then take note of the `channel_id` field. + + 2. Issue a ``link`` command in the administration control room with these + collected values as arguments: + + ``` + link --channel_id CHANNELID --room !the-matrix:room.id --slack_token xoxb-xxxxxxxxxx-xxxxxxxxxxxxxxxxxxxx + ``` + + These arguments can be shortened to single-letter forms: + + ``` + link -I CHANNELID -R !the-matrix:room.id -t xoxb-xxxxxxxxxx-xxxxxxxxxxxxxxxxxxxx + ``` + + +### Legacy 1. Create a Matrix room in the usual manner for your client. Take a note of its Matrix room ID - it will look something like `!aBcDeF:example.com`. @@ -154,13 +183,13 @@ to the bridge's database. collected values as arguments: ``` - link --channel CHANNELID --room !the-matrix:room.id --token THETOKEN --webhook_uri http://the.webhook/uri + link --channel_id CHANNELID --room !the-matrix:room.id --webhook_url https://hooks.slack.com/services/ABC/DEF/123 ``` These arguments can be shortened to single-letter forms: ``` - link -c CHANNELID -r !the-matrix:room.id -t THETOKEN -u http://the.webhook/uri + link -I CHANNELID -R !the-matrix:room.id -u https://hooks.slack.com/services/ABC/DEF/123 ``` See also https://github.com/matrix-org/matrix-appservice-bridge/blob/master/HOWTO.md for the general theory of all this :) From 86353f213fa190b504e9884a1c05760fc6084f73 Mon Sep 17 00:00:00 2001 From: perissology Date: Sat, 13 Jan 2018 15:37:16 -0800 Subject: [PATCH 04/55] better file support & fix room registration bug - abstracted out a BaseSlackHandler class which the SlackEventHandler & SlackHookHandler extend. - fix bug with room linking - enable slack msg transformations in SlackEventHandler - provide better file bridging support from slack -> matrix --- README.md | 39 +++-- lib/AdminCommands.js | 8 +- lib/BaseSlackHandler.js | 172 ++++++++++++++++++++++ lib/BridgedRoom.js | 60 +++++++- lib/Main.js | 24 +-- lib/SlackEventHandler.js | 309 ++++++--------------------------------- lib/SlackHookHandler.js | 164 +++------------------ lib/substitutions.js | 20 ++- 8 files changed, 354 insertions(+), 442 deletions(-) create mode 100644 lib/BaseSlackHandler.js diff --git a/README.md b/README.md index 96c5c004..46bd8890 100644 --- a/README.md +++ b/README.md @@ -122,11 +122,23 @@ and bot users. This allows you to link as many channels as you would like with o - team_domain_change - message.channels + - message.groups (if you want to bridge private channels) -5. Click on `Install App` and `Install App to Workspace`. Note the `Bot User OAuth Access Token` - as you will need it whenever you link a room. +5. Skip this step if you do not want to bridge files. + Click on `OAuth & Permissions` and add the following scopes: + + - files:write:user + + Note: media uploaded to matrix is currently not permissioned, and anyone with the link + can access the file. In order to make slack files visible to matrix users, this bridge + will set make the slack file visible to anyone with the url (including files in private channels). + The current behavior in slack is that files are only accessible to authenticated users. + +6. Click on `Install App` and `Install App to Workspace`. Note the access tokens show. + You will need the `Bot User OAuth Access Token` and if you want to bridge files, the + `OAuth Access Token` whenever you link a room. -6. For each channel you would like to bridge, perform the following steps: +7. For each channel you would like to bridge, perform the following steps: 1. Create a Matrix room in the usual manner for your client. Take a note of its Matrix room ID - it will look something like `!aBcDeF:example.com`. @@ -147,15 +159,22 @@ and bot users. This allows you to link as many channels as you would like with o 2. Issue a ``link`` command in the administration control room with these collected values as arguments: - ``` - link --channel_id CHANNELID --room !the-matrix:room.id --slack_token xoxb-xxxxxxxxxx-xxxxxxxxxxxxxxxxxxxx - ``` + with file bridging: + + ``` + link --channel_id CHANNELID --room !the-matrix:room.id --slack_bot_token xoxb-xxxxxxxxxx-xxxxxxxxxxxxxxxxxxxx --slack_user_token xoxp-xxxxxxxx-xxxxxxxxx-xxxxxxxx-xxxxxxxx + ``` + without file bridging: + + ``` + link --channel_id CHANNELID --room !the-matrix:room.id --slack_bot_token xoxb-xxxxxxxxxx-xxxxxxxxxxxxxxxxxxxx + ``` - These arguments can be shortened to single-letter forms: + These arguments can be shortened to single-letter forms: - ``` - link -I CHANNELID -R !the-matrix:room.id -t xoxb-xxxxxxxxxx-xxxxxxxxxxxxxxxxxxxx - ``` + ``` + link -I CHANNELID -R !the-matrix:room.id -t xoxb-xxxxxxxxxx-xxxxxxxxxxxxxxxxxxxx + ``` ### Legacy diff --git a/lib/AdminCommands.js b/lib/AdminCommands.js index cf9bd0b4..ed0888d1 100644 --- a/lib/AdminCommands.js +++ b/lib/AdminCommands.js @@ -134,10 +134,13 @@ adminCommands.link = new AdminCommand({ description: "Slack webhook URL. Used with slack outgoing hooks integration", aliases: ['u'], }, - slack_token: { + slack_bot_token: { description: "Slack bot user token. Used with slack bot user & Events api", aliases: ['t'], }, + slack_user_token: { + description: "Slack user token. Used to bridge files", + } }, func: function(main, opts, args, respond) { return main.actionLink({ @@ -145,7 +148,8 @@ adminCommands.link = new AdminCommand({ slack_channel_name: opts.channel, slack_channel_id: opts.channel_id, slack_webhook_uri: opts.webhook_url, - slack_bot_token: opts.slack_token, + slack_bot_token: opts.slack_bot_token, + slack_user_token: opts.slack_user_token, }).then( (room) => { respond("Room is now " + room.getStatus()); diff --git a/lib/BaseSlackHandler.js b/lib/BaseSlackHandler.js new file mode 100644 index 00000000..6a515082 --- /dev/null +++ b/lib/BaseSlackHandler.js @@ -0,0 +1,172 @@ +"use strict"; + +var rp = require('request-promise'); +var Promise = require('bluebird'); +var promiseWhile = require("./promiseWhile"); +var getSlackFileUrl = require("./substitutions").getSlackFileUrl; + +/** + * @constructor + * @param {Main} main the toplevel bridge instance through which to + * communicate with matrix. + */ +function BaseSlackHandler(main) { + this._main = main; +} + +BaseSlackHandler.prototype.replaceChannelIdsWithNames = function (message, token) { + var main = this._main; + + // match all channelIds + var testForName = message.text.match(/<#(\w+)\|?\w*?>/g); + var iteration = 0; + var matches = 0; + if (testForName && testForName.length) { + matches = testForName.length; + } + return promiseWhile(function () { + // Do this until there are no more channel ID matches + return iteration < matches; + }, function () { + // foreach channelId, pull out the ID + // (if this is an emote msg, the format is <#ID|name>, but in normal msgs it's just <#ID> + var id = testForName[iteration].match(/<#(\w+)\|?\w*?>/)[1]; + var channelsInfoApiParams = { + uri: 'https://slack.com/api/channels.info', + qs: { + token: token, + channel: id + }, + json: true + }; + main.incRemoteCallCounter("channels.info"); + return rp(channelsInfoApiParams).then((response) => { + if (response && response.channel && response.channel.name) { + console.log("channels.info: " + id + " mapped to " + response.channel.name); + message.text = message.text.replace(/<#(\w+)\|?\w*?>/, "#" + response.channel.name); + } + else { + console.log("channels.info returned no result for " + id); + } + iteration++; + }).catch((err) => { + console.log("Caught error " + err); + }); + }).then(() => { + // Notice we can chain it because it's a Promise, + // this will run after completion of the promiseWhile Promise! + return message; + }); +}; + +BaseSlackHandler.prototype.replaceUserIdsWithNames = function (message, token) { + var main = this._main; + + // match all userIds + var testForName = message.text.match(/<@(\w+)\|?\w*?>/g); + var iteration = 0; + var matches = 0; + if (testForName && testForName.length) { + matches = testForName.length; + } + return promiseWhile(() => { + // Condition for stopping + return iteration < matches; + }, function () { + // foreach userId, pull out the ID + // (if this is an emote msg, the format is <@ID|nick>, but in normal msgs it's just <@ID> + var id = testForName[iteration].match(/<@(\w+)\|?\w*?>/)[1]; + var channelsInfoApiParams = { + uri: 'https://slack.com/api/users.info', + qs: { + token: token, + user: id + }, + json: true + }; + main.incRemoteCallCounter("users.info"); + return rp(channelsInfoApiParams).then((response) => { + if (response && response.user && response.user.profile) { + var name = response.user.profile.display_name || response.user.profile.real_name; + console.log("users.info: " + id + " mapped to " + name); + message.text = message.text.replace(/<@(\w+)\|?\w*?>/, name); + } + else { + console.log("users.info returned no result for " + id); + } + iteration++; + }).catch((err) => { + console.log("Caught error " + err); + }); + }).then(() => { + // Notice we can chain it because it's a Promise, + // this will run after completion of the promiseWhile Promise! + return message; + }); +}; + +// Return true if we ought to fetch the content of the given file object +BaseSlackHandler.prototype.shouldFetchContent = function (file) { + if (!file) return false; + if (file.mimetype && file.mimetype.indexOf("image/") === 0) return true; + return false; +} + +/** + * Enables public sharing on the given file object. then fetches its content. + * + * @param {Object} file A slack 'message.file' data object + * @param {string} token A slack API token that has 'files:write:user' scope + * @return {Promise} A Promise of the updated slack file data object + */ +BaseSlackHandler.prototype.enablePublicSharing = function (file, token) { + if (file.public_url_shared) return Promise.resolve(file); + + this._main.incRemoteCallCounter("files.sharedPublicURL"); + return rp({ + method: 'POST', + form: { + file: file.id, + token: token, + }, + uri: "https://slack.com/api/files.sharedPublicURL", + json: true + }).then((response) => { + if (!response || !response.file || !response.file.permalink_public) { + console.log("Could not find sharedPublichURL: " + JSON.stringify(response)); + return undefined; + } + + return response.file; + }); +} + +/** + * Fetchs the file at a given url. + * + * @param {Object} file A slack 'message.file' data object + * @return {Promise} A Promise of file contents + */ +BaseSlackHandler.prototype.fetchFileContent = function (file, token) { + if (!file) return Promise.resolve(); + + // if (file.is_public) { + var url = getSlackFileUrl(file); + if (!url) url = file.permalink_public; + // } else { + // url = file.url_private + // } + + return rp({ + uri: url, + resolveWithFullResponse: true, + encoding: null + }).then((response) => { + var content = response.body; + console.log("Successfully fetched file " + file.id + + " content (" + content.length + " bytes)"); + return content; + }); +}; + +module.exports = BaseSlackHandler; diff --git a/lib/BridgedRoom.js b/lib/BridgedRoom.js index d038e98a..30c81c63 100644 --- a/lib/BridgedRoom.js +++ b/lib/BridgedRoom.js @@ -21,6 +21,7 @@ function BridgedRoom(main, opts) { this._slack_channel_id = opts.slack_channel_id; this._slack_webhook_uri = opts.slack_webhook_uri; this._slack_bot_token = opts.slack_bot_token; + this._slack_user_token = opts.slack_user_token; this._slack_team_domain = opts.slack_team_domain; this._slack_team_id = opts.slack_team_id; this._slack_user_id = opts.slack_user_id; @@ -89,6 +90,10 @@ BridgedRoom.prototype.getSlackBotId = function() { return this._slack_bot_id; }; +BridgedRoom.prototype.getSlackUserToken = function() { + return this._slack_user_token; +}; + BridgedRoom.prototype.updateInboundId = function(inbound_id) { if (this._inbound_id !== inbound_id) this._dirty = true; this._inbound_id = inbound_id; @@ -114,6 +119,11 @@ BridgedRoom.prototype.updateSlackBotToken = function(slack_bot_token) { this._slack_bot_token = slack_bot_token; }; +BridgedRoom.prototype.updateSlackUserToken = function(slack_user_token) { + if (this._slack_user_token !== slack_user_token) this._dirty = true; + this._slack_user_token = slack_user_token; +}; + BridgedRoom.prototype.updateSlackTeamDomain = function(domain) { if (this._slack_team_domain !== domain) this._dirty = true; this._slack_team_domain = domain; @@ -137,6 +147,7 @@ BridgedRoom.fromEntry = function(main, entry) { slack_channel_name: entry.remote.name, slack_webhook_uri: entry.remote.webhook_uri, slack_bot_token: entry.remote.slack_bot_token, + slack_user_token: entry.remote.slack_user_token, slack_team_domain: entry.remote.slack_team_domain, slack_team_id: entry.remote.slack_team_id, slack_user_id: entry.remote.slack_user_id, @@ -157,6 +168,7 @@ BridgedRoom.prototype.toEntry = function() { name: this._slack_channel_name, webhook_uri: this._slack_webhook_uri, slack_bot_token: this._slack_bot_token, + slack_user_token: this._slack_user_token, slack_team_domain: this._slack_team_domain, slack_team_id: this._slack_team_id, slack_user_id: this._slack_user_id, @@ -233,7 +245,7 @@ BridgedRoom.prototype._handleSlackMessage = function(message, ghost) { var subtype = message.subtype; - if (!subtype) { + if (!subtype || subtype === "bot_message") { var text = substitutions.slackToMatrix(message.text); return ghost.sendText(roomID, text); } @@ -245,12 +257,12 @@ BridgedRoom.prototype._handleSlackMessage = function(message, ghost) { return ghost.sendMessage(roomID, message); } else if (subtype === "file_comment") { - var text = substitutions.slackToMatrix(message.text); + var text = substitutions.slackToMatrix(message.text, message.file); return ghost.sendText(roomID, text); } else if (subtype === "file_share") { if (!message.file) { - console.log("Ignoring non-text non-image message: " + res); + console.log("Ignoring missing file message: " + message); return; } if (message.file._content) { @@ -264,14 +276,20 @@ BridgedRoom.prototype._handleSlackMessage = function(message, ghost) { // no URL returned from media repo; abort return undefined; } - var matrixMessage = slackImageToMatrixImage(message.file, content_uri); + var matrixMessage = slackFileToMatrixMessage(message.file, content_uri); return ghost.sendMessage(roomID, matrixMessage); }).finally(() => { - var text = substitutions.slackToMatrix( - message.file.initial_comment.comment - ); - return ghost.sendText(roomID, text); + if (message.file.initial_comment) { + var text = substitutions.slackToMatrix( + message.file.initial_comment.comment + ); + return ghost.sendText(roomID, text); + } }); + } else { + // post a msg with the link + var text = substitutions.slackToMatrix(message.text, message.file); + return ghost.sendText(roomID, text); } } else { @@ -340,6 +358,31 @@ var slackImageToMatrixImage = function(file, url) { return message; }; +/** + * Converts a slack file upload to a matrix file upload event. + * + * @param {Object} file The slack file object. + * @return {Object} Matrix event content, as per https://matrix.org/docs/spec/#m-file + */ +var slackFileToMatrixMessage = function(file, url) { + if (file.mimetype && file.mimetype.indexOf("image/") === 0) { + return slackImageToMatrixImage(file, url); + } + + var message = { + msgtype: "m.file", + url: url, + body: file.title, + info: { + mimetype: file.mimetype + } + }; + if (file.size) { + message.info.size = file.size; + } + return message; +}; + BridgedRoom.prototype.getRemoteATime = function() { return this._slackAtime; }; @@ -384,6 +427,7 @@ BridgedRoom.prototype.lookupAndSetUserInfo = function() { }, json: true, }).then((response) => { + console.log(response); if (!response.user_id) return; if (!this._slack_user_id !== response.user_id) { diff --git a/lib/Main.js b/lib/Main.js index cc5c9568..6f0fa804 100644 --- a/lib/Main.js +++ b/lib/Main.js @@ -538,26 +538,30 @@ Main.prototype.actionLink = function(opts) { room.updateSlackWebhookUri(opts.slack_webhook_uri); } + if (opts.slack_channel_id) { + room.updateSlackChannelId(opts.slack_channel_id); + } + + if (opts.slack_user_token) { + room.updateSlackUserToken(opts.slack_user_token); + } + if (opts.slack_bot_token) { room.updateSlackBotToken(opts.slack_bot_token); - Promise.all([ room.lookupAndSetTeamInfo(), room.lookupAndSetUserInfo() ]) + return Promise.all([ room.lookupAndSetTeamInfo(), room.lookupAndSetUserInfo() ]) .then(() => { - this.addBridgedRoom(room); + if (isNew) { + this.addBridgedRoom(room); + } if (room.isDirty()) { this.putRoomToStore(room); } - }); - } - if (opts.slack_channel_id) { - room.updateSlackChannelId(opts.slack_channel_id); + return room; + }); } - // if (opts.slack_channel_name) { - // room.updateSlackChannelName(opts.slack_channel_name); - // } - if (isNew) this.addBridgedRoom(room); if (room.isDirty()) { diff --git a/lib/SlackEventHandler.js b/lib/SlackEventHandler.js index c5403462..6a3346ac 100644 --- a/lib/SlackEventHandler.js +++ b/lib/SlackEventHandler.js @@ -1,19 +1,14 @@ "use strict"; -var substitutions = require("./substitutions"); -var rp = require('request-promise'); -var qs = require("querystring"); +var BaseSlackHandler = require('./BaseSlackHandler'); var Promise = require('bluebird'); -var promiseWhile = require("./promiseWhile"); +var util = require("util"); -var PRESERVE_KEYS = [ - "team_domain", "team_id", - "channel_name", "channel_id", - "user_name", "user_id", -]; - -var UnknownEvent = function() {}; -var UnknownChannel = function(channel) { this.channel = channel; }; +var UnknownEvent = function () { +}; +var UnknownChannel = function (channel) { + this.channel = channel; +}; /** * @constructor @@ -24,6 +19,7 @@ function SlackEventHandler(main) { this._main = main; } +util.inherits(SlackEventHandler, BaseSlackHandler); /** * Handles a slack event request. @@ -37,7 +33,7 @@ function SlackEventHandler(main) { // * @param {string} timestamp Timestamp when message was received, in seconds // * formatted as a float. */ -SlackEventHandler.prototype.handle = function(params, response) { +SlackEventHandler.prototype.handle = function (params, response) { try { console.log("Received slack event:", JSON.stringify(params)); @@ -104,230 +100,6 @@ SlackEventHandler.prototype.handle = function(params, response) { } }; -SlackEventHandler.prototype.replaceChannelIdsWithNames = function(message, token) { - var main = this._main; - - // match all channelIds - var testForName = message.text.match(/<#(\w+)\|?\w*?>/g); - var iteration = 0; - var matches = 0; - if (testForName && testForName.length) { - matches = testForName.length; - } - return promiseWhile(function() { - // Do this until there are no more channel ID matches - return iteration < matches; - }, function() { - // foreach channelId, pull out the ID - // (if this is an emote msg, the format is <#ID|name>, but in normal msgs it's just <#ID> - var id = testForName[iteration].match(/<#(\w+)\|?\w*?>/)[1]; - var channelsInfoApiParams = { - uri: 'https://slack.com/api/channels.info', - qs: { - token: token, - channel: id - }, - json: true - }; - main.incRemoteCallCounter("channels.info"); - return rp(channelsInfoApiParams).then((response) => { - if (response && response.channel && response.channel.name) { - console.log("channels.info: " + id + " mapped to " + response.channel.name); - message.text = message.text.replace(/<#(\w+)\|?\w*?>/, "#" + response.channel.name); - } - else { - console.log("channels.info returned no result for " + id); - } - iteration++; - }).catch((err) => { - console.log("Caught error " + err); - }); - }).then(() => { - // Notice we can chain it because it's a Promise, - // this will run after completion of the promiseWhile Promise! - return message; - }); -}; - -SlackEventHandler.prototype.replaceUserIdsWithNames = function(message, token) { - var main = this._main; - - // match all userIds - var testForName = message.text.match(/<@(\w+)\|?\w*?>/g); - var iteration = 0; - var matches = 0; - if (testForName && testForName.length) { - matches = testForName.length; - } - return promiseWhile(() => { - // Condition for stopping - return iteration < matches; - }, function() { - // foreach userId, pull out the ID - // (if this is an emote msg, the format is <@ID|nick>, but in normal msgs it's just <@ID> - var id = testForName[iteration].match(/<@(\w+)\|?\w*?>/)[1]; - var channelsInfoApiParams = { - uri: 'https://slack.com/api/users.info', - qs: { - token: token, - user: id - }, - json: true - }; - main.incRemoteCallCounter("users.info"); - return rp(channelsInfoApiParams).then((response) => { - if (response && response.user && response.user.name) { - console.log("users.info: " + id + " mapped to " + response.user.name); - message.text = message.text.replace(/<@(\w+)\|?\w*?>/, response.user.name); - } - else { - console.log("users.info returned no result for " + id); - } - iteration++; - }).catch((err) => { - console.log("Caught error " + err); - }); - }).then(() => { - // Notice we can chain it because it's a Promise, - // this will run after completion of the promiseWhile Promise! - return message; - }); -}; - -/** - * 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. - */ -//SlackEventHandler.prototype.lookupAndSendMessage = -SlackEventHandler.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) { - console.log("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... - console.log("Really unlucky, got multiple messages at same" + - " microsecond, dropping:" + response); - return undefined; - } - var message = response.messages[0]; - console.log("Looked up message from history as " + JSON.stringify(message)); - - if (message.subtype === "file_share" && shouldFetchContent(message.file)) { - return this.fetchFileContent(message.file, token).then((content) => { - message.file._content = content; - return message; - }); - } - return message; - }); -} - -// Return true if we ought to fetch the content of the given file object -function shouldFetchContent(file) { - if (!file) return false; - if (file.mimetype && file.mimetype.indexOf("image/") === 0) return true; - return false; -} - -/** - * Enables public sharing on the given file object then fetches its content. - * - * @param {Object} file A slack 'message.file' data object - * @param {string} token A slack API token that has 'files:write:user' scope - * @return {Promise} A Promise of file contents - */ -SlackEventHandler.prototype.fetchFileContent = function(file, token) { - this._main.incRemoteCallCounter("files.sharedPublicURL"); - return rp({ - method: 'POST', - form : { - file: file.id, - token: token, - }, - uri: "https://slack.com/api/files.sharedPublicURL", - json: true - }).then((response) => { - if (!response || !response.file || !response.file.permalink_public) { - console.log("Could not find sharedPublichURL: " + JSON.stringify(response)); - return undefined; - } - - var pub_secret = file.permalink_public.match(/https?:\/\/slack-files.com\/[^-]*-[^-]*-(.*)/); - var public_file_url = file.permalink_public; - // try to get direct link to image - if (pub_secret != undefined && pub_secret.length > 0) { - public_file_url = file.url_private + "?pub_secret=" + pub_secret[1]; - } - - return rp({ - uri: public_file_url, - resolveWithFullResponse: true, - encoding: null - }); - }).then((response) => { - var content = response.body; - console.log("Successfully fetched file " + file.id + - " content (" + content.length + " bytes)"); - return content; - }); -} - - -/** - * - * @param room - * @param params - */ -SlackEventHandler.prototype.handleEvent = function(room, params) { - var main = this._main; - - // TODO set bot_user_id on room on link. authed_user isn't the correct check - // 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") { - if (params.event.user in params.authed_user) { - return Promise.resolve(); - } - - // Only count received messages that aren't self-reflections - main.incCounter("received_messages", {side: "remote"}); - - -}; - /** * Attempts to handle the `team_domain_change` event. * @@ -335,7 +107,7 @@ SlackEventHandler.prototype.handleEvent = function(room, params) { * @param {Object} params.team_id The slack team_id for the event. * @param {string} params.event.domain The new team domain. */ -SlackEventHandler.prototype.handleDomainChangeEvent = function(params) { +SlackEventHandler.prototype.handleDomainChangeEvent = function (params) { this._main.getRoomsBySlackTeamId(params.team_id).forEach(room => { room.updateSlackTeamDomain(params.event.domain); if (room.isDirty()) { @@ -352,7 +124,7 @@ SlackEventHandler.prototype.handleDomainChangeEvent = function(params) { * @param {string} params.event.id The slack channel id * @param {string} params.event.name The new name */ -SlackEventHandler.prototype.handleChannelRenameEvent = function(params) { +SlackEventHandler.prototype.handleChannelRenameEvent = function (params) { //TODO test me. and do we even need this? doesn't appear to be used anymore var room = this._main.getRoomBySlackChannelId(params.event.channel.id); if (!room) throw new UnknownChannel(params.event.channel.id); @@ -378,7 +150,7 @@ SlackEventHandler.prototype.handleChannelRenameEvent = function(params) { * @param {string} params.event.channel The slack channel id * @param {string} params.event.ts The unique (per-channel) timestamp */ -SlackEventHandler.prototype.handleMessageEvent = function(params) { +SlackEventHandler.prototype.handleMessageEvent = function (params) { var room = this._main.getRoomBySlackChannelId(params.event.channel); if (!room) throw new UnknownChannel(params.event.channel); @@ -412,36 +184,43 @@ SlackEventHandler.prototype.handleMessageEvent = function(params) { // 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); + console.log('SlackEventHandler text === undefined'); return Promise.resolve(); } - // TODO update this. I think it only involves file_share messages - // return this.lookupMessage(params.event.channel, params.event.ts, token).then((msg) => { - // console.log('msg ->', msg); - // if(undefined == msg) { - // msg = params; - // } + var result; + if (params.event.subtype === "file_share") { + // TODO check is_public when matrix supports authenticated media https://github.com/matrix-org/matrix-doc/issues/701 + // we need a user token to be able to enablePublicSharing + if (room.getSlackUserToken()) { + result = this.enablePublicSharing(params.event.file, room.getSlackUserToken()) + .then((file) => { + params.event.file = file; + + return this.fetchFileContent(params.event.file, token) + .then((content) => { + params.event.file._content = content; + }); + }) + } + } else { + result = Promise.resolve(); + } - // Restore the original parameters, because we've forgotten a lot of - // them by now - // "team_domain", "team_id", - // "channel_name", "channel_id", - // "user_name", "user_id", - // PRESERVE_KEYS.forEach((k) => msg[k] = params[k]); + return result.then(() => { + var msg = Object.assign({}, params.event, { + user_id: params.event.user || params.event.bot_id, + team_domain: room.getSlackTeamDomain() || room.getSlackTeamId() + }); - var msg = Object.assign({}, params.event, { - user_id: params.event.user, - team_domain: room.getSlackTeamDomain() || room.getSlackTeamId() - }); - console.log(msg); - return room.onSlackMessage(msg); - // 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); - // }); + if (msg.subtype === 'file_comment') { + msg.user_id = msg.comment.user; + } + + return msg; + }).then((msg) => this.replaceChannelIdsWithNames(msg, token)) + .then((msg) => this.replaceUserIdsWithNames(msg, token)) + .then((msg) => room.onSlackMessage(msg)); }; module.exports = SlackEventHandler; diff --git a/lib/SlackHookHandler.js b/lib/SlackHookHandler.js index c5cc7a83..587afeaf 100644 --- a/lib/SlackHookHandler.js +++ b/lib/SlackHookHandler.js @@ -2,10 +2,12 @@ var substitutions = require("./substitutions"); var SlackEventHandler = require('./SlackEventHandler'); +var BaseSlackHandler = require('./BaseSlackHandler'); var rp = require('request-promise'); var qs = require("querystring"); var Promise = require('bluebird'); var promiseWhile = require("./promiseWhile"); +var util = require("util"); var PRESERVE_KEYS = [ "team_domain", "team_id", @@ -23,6 +25,8 @@ function SlackHookHandler(main) { this.eventHandler = new SlackEventHandler(main); } +util.inherits(SlackHookHandler, BaseSlackHandler); + /** * Starts the hook server listening on the given port and (optional) TLS * configuration. @@ -279,96 +283,6 @@ SlackHookHandler.prototype.handleAuthorize = function(room, params) { ); }; -SlackHookHandler.prototype.replaceChannelIdsWithNames = function(message, token) { - var main = this._main; - - // match all channelIds - var testForName = message.text.match(/<#(\w+)\|?\w*?>/g); - var iteration = 0; - var matches = 0; - if (testForName && testForName.length) { - matches = testForName.length; - } - return promiseWhile(function() { - // Do this until there are no more channel ID matches - return iteration < matches; - }, function() { - // foreach channelId, pull out the ID - // (if this is an emote msg, the format is <#ID|name>, but in normal msgs it's just <#ID> - var id = testForName[iteration].match(/<#(\w+)\|?\w*?>/)[1]; - var channelsInfoApiParams = { - uri: 'https://slack.com/api/channels.info', - qs: { - token: token, - channel: id - }, - json: true - }; - main.incRemoteCallCounter("channels.info"); - return rp(channelsInfoApiParams).then((response) => { - if (response && response.channel && response.channel.name) { - console.log("channels.info: " + id + " mapped to " + response.channel.name); - message.text = message.text.replace(/<#(\w+)\|?\w*?>/, "#" + response.channel.name); - } - else { - console.log("channels.info returned no result for " + id); - } - iteration++; - }).catch((err) => { - console.log("Caught error " + err); - }); - }).then(() => { - // Notice we can chain it because it's a Promise, - // this will run after completion of the promiseWhile Promise! - return message; - }); -}; - -SlackHookHandler.prototype.replaceUserIdsWithNames = function(message, token) { - var main = this._main; - - // match all userIds - var testForName = message.text.match(/<@(\w+)\|?\w*?>/g); - var iteration = 0; - var matches = 0; - if (testForName && testForName.length) { - matches = testForName.length; - } - return promiseWhile(() => { - // Condition for stopping - return iteration < matches; - }, function() { - // foreach userId, pull out the ID - // (if this is an emote msg, the format is <@ID|nick>, but in normal msgs it's just <@ID> - var id = testForName[iteration].match(/<@(\w+)\|?\w*?>/)[1]; - var channelsInfoApiParams = { - uri: 'https://slack.com/api/users.info', - qs: { - token: token, - user: id - }, - json: true - }; - main.incRemoteCallCounter("users.info"); - return rp(channelsInfoApiParams).then((response) => { - if (response && response.user && response.user.name) { - console.log("users.info: " + id + " mapped to " + response.user.name); - message.text = message.text.replace(/<@(\w+)\|?\w*?>/, response.user.name); - } - else { - console.log("users.info returned no result for " + id); - } - iteration++; - }).catch((err) => { - console.log("Caught error " + err); - }); - }).then(() => { - // Notice we can chain it because it's a Promise, - // this will run after completion of the promiseWhile Promise! - return message; - }); -}; - /** * Attempts to handle a message received from a slack webhook request. * @@ -417,63 +331,23 @@ SlackHookHandler.prototype.lookupMessage = function(channelID, timestamp, token) var message = response.messages[0]; console.log("Looked up message from history as " + JSON.stringify(message)); - if (message.subtype === "file_share" && shouldFetchContent(message.file)) { - return this.fetchFileContent(message.file, token).then((content) => { - message.file._content = content; - return message; - }); - } - return message; - }); -} - -// Return true if we ought to fetch the content of the given file object -function shouldFetchContent(file) { - if (!file) return false; - if (file.mimetype && file.mimetype.indexOf("image/") === 0) return true; - return false; -} - -/** - * Enables public sharing on the given file object then fetches its content. - * - * @param {Object} file A slack 'message.file' data object - * @param {string} token A slack API token that has 'files:write:user' scope - * @return {Promise} A Promise of file contents - */ -SlackHookHandler.prototype.fetchFileContent = function(file, token) { - this._main.incRemoteCallCounter("files.sharedPublicURL"); - return rp({ - method: 'POST', - form : { - file: file.id, - token: token, - }, - uri: "https://slack.com/api/files.sharedPublicURL", - json: true - }).then((response) => { - if (!response || !response.file || !response.file.permalink_public) { - console.log("Could not find sharedPublichURL: " + JSON.stringify(response)); - return undefined; - } + if (message.subtype === "file_share" && this.shouldFetchContent(message.file)) { + return this.enablePublicSharing(message.file, token) + .then((file) => { + message.file = file; + + if (this.shouldFetchContent(message.file)) { + return this.fetchFileContent(message.file, token) + .then((content) => { + message.file._content = content; + return message; + }); + } - var pub_secret = file.permalink_public.match(/https?:\/\/slack-files.com\/[^-]*-[^-]*-(.*)/); - var public_file_url = file.permalink_public; - // try to get direct link to image - if (pub_secret != undefined && pub_secret.length > 0) { - public_file_url = file.url_private + "?pub_secret=" + pub_secret[1]; + return message; + }); } - - return rp({ - uri: public_file_url, - resolveWithFullResponse: true, - encoding: null - }); - }).then((response) => { - var content = response.body; - console.log("Successfully fetched file " + file.id + - " content (" + content.length + " bytes)"); - return content; + return message; }); } diff --git a/lib/substitutions.js b/lib/substitutions.js index e59418f9..34386f58 100644 --- a/lib/substitutions.js +++ b/lib/substitutions.js @@ -51,8 +51,9 @@ function replaceAll(string, old, replacement) { * of a Slack message appear like the text of a Matrix message. * * @param {string} string the text, in Slack's format. + * @param {Object} file options slack file object */ -var slackToMatrix = function(body) { +var slackToMatrix = function(body, file) { console.log("running substitutions on: " + body); if (undefined != body) { @@ -73,6 +74,12 @@ var slackToMatrix = function(body) { // so we need to strip <> from the link body = body.replace(/<(https?:\/\/[^>]+?)>/g, '$1'); + // if we have a file, attempt to get the direct link to the file + if (file && file.public_url_shared) { + var url = getSlackFileUrl(file); + body = body.replace(file.permalink, url); + } + // attempt to match any text inside colons to emoji, e.g. :smiley: // we use replace to run a function on each match, replacing matches in buf // (returning the match to body doesn't work, hence this approach) @@ -89,6 +96,14 @@ var slackToMatrix = function(body) { return buf; }; +var getSlackFileUrl = function(file) { + var pub_secret = file.permalink_public.match(/https?:\/\/slack-files.com\/[^-]*-[^-]*-(.*)/); + // try to get direct link to the file + if (pub_secret !== undefined && pub_secret.length > 0) { + return file.url_private + "?pub_secret=" + pub_secret[1]; + } +}; + /** * Performs any escaping, unescaping, or substituting required to make the text * of a Matrix message appear like the text of a Slack message. @@ -137,5 +152,6 @@ var matrixToSlack = function(event, main) { module.exports = { matrixToSlack: matrixToSlack, - slackToMatrix: slackToMatrix + slackToMatrix: slackToMatrix, + getSlackFileUrl: getSlackFileUrl }; From a241862ed24d3b732641a955d6da0a89a87d90a0 Mon Sep 17 00:00:00 2001 From: perissology Date: Sat, 13 Jan 2018 16:09:11 -0800 Subject: [PATCH 05/55] fix bug when failed to enablePublicSharing --- lib/SlackEventHandler.js | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/lib/SlackEventHandler.js b/lib/SlackEventHandler.js index 6a3346ac..655e2c27 100644 --- a/lib/SlackEventHandler.js +++ b/lib/SlackEventHandler.js @@ -189,13 +189,15 @@ SlackEventHandler.prototype.handleMessageEvent = function (params) { } var result; - if (params.event.subtype === "file_share") { + if (params.event.subtype === "file_share" && params.event.file) { // TODO check is_public when matrix supports authenticated media https://github.com/matrix-org/matrix-doc/issues/701 // we need a user token to be able to enablePublicSharing if (room.getSlackUserToken()) { result = this.enablePublicSharing(params.event.file, room.getSlackUserToken()) .then((file) => { - params.event.file = file; + if (file) { + params.event.file = file; + } return this.fetchFileContent(params.event.file, token) .then((content) => { From d050846a8ea5676555b2c0b03cc98b2a7df8ad04 Mon Sep 17 00:00:00 2001 From: perissology Date: Mon, 15 Jan 2018 11:54:26 -0800 Subject: [PATCH 06/55] small slack event handler refactor --- lib/Main.js | 1 + lib/SlackEventHandler.js | 48 ++++++++++++++++++---------------------- 2 files changed, 22 insertions(+), 27 deletions(-) diff --git a/lib/Main.js b/lib/Main.js index 6f0fa804..44df4fda 100644 --- a/lib/Main.js +++ b/lib/Main.js @@ -397,6 +397,7 @@ Main.prototype.listRoomsFor = function(intent, state) { }; Main.prototype.onMatrixEvent = function(ev) { + console.log("Received matrix event:", JSON.stringify(ev)); // simple de-dup var recents = this._recentMatrixEventIds; for (var i = 0; i < recents.length; i++) { diff --git a/lib/SlackEventHandler.js b/lib/SlackEventHandler.js index 655e2c27..64ccd2a7 100644 --- a/lib/SlackEventHandler.js +++ b/lib/SlackEventHandler.js @@ -163,24 +163,28 @@ SlackEventHandler.prototype.handleMessageEvent = function (params) { var token = room.getAccessToken(); - var text = params.event.text; + var msg = Object.assign({}, params.event, { + user_id: params.event.user || params.event.bot_id, + team_domain: room.getSlackTeamDomain() || room.getSlackTeamId(), + channel_id: params.event.channel + }); + + var processMsg = msg.text || msg.subtype === 'message_deleted'; + + if (msg.subtype === 'file_comment') { + msg.user_id = msg.comment.user; + } + 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. console.log("no slack token for " + room.getSlackTeamDomain() || room.getSlackChannelId()); - if (text) { - return room.onSlackMessage({ - text, - user_id: params.event.user, - team_domain: room.getSlackTeamDomain() || room.getSlackTeamId() - }); - } - return Promise.resolve(); + return (processMsg) ? room.onSlackMessage(msg) : Promise.resolve(); } - if (undefined == text) { + if (!processMsg) { // 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); @@ -189,19 +193,19 @@ SlackEventHandler.prototype.handleMessageEvent = function (params) { } var result; - if (params.event.subtype === "file_share" && params.event.file) { + if (msg.subtype === "file_share" && msg.file) { // TODO check is_public when matrix supports authenticated media https://github.com/matrix-org/matrix-doc/issues/701 // we need a user token to be able to enablePublicSharing if (room.getSlackUserToken()) { - result = this.enablePublicSharing(params.event.file, room.getSlackUserToken()) + result = this.enablePublicSharing(msg.file, room.getSlackUserToken()) .then((file) => { if (file) { - params.event.file = file; + msg.file = file; } - return this.fetchFileContent(params.event.file, token) + return this.fetchFileContent(msg.file, token) .then((content) => { - params.event.file._content = content; + msg.file._content = content; }); }) } @@ -209,18 +213,8 @@ SlackEventHandler.prototype.handleMessageEvent = function (params) { result = Promise.resolve(); } - return result.then(() => { - var msg = Object.assign({}, params.event, { - user_id: params.event.user || params.event.bot_id, - team_domain: room.getSlackTeamDomain() || room.getSlackTeamId() - }); - - if (msg.subtype === 'file_comment') { - msg.user_id = msg.comment.user; - } - - return msg; - }).then((msg) => this.replaceChannelIdsWithNames(msg, token)) + return result.then(() => msg) + .then((msg) => this.replaceChannelIdsWithNames(msg, token)) .then((msg) => this.replaceUserIdsWithNames(msg, token)) .then((msg) => room.onSlackMessage(msg)); }; From 9c692b3100e93ade72b627d552a92d17c0538db2 Mon Sep 17 00:00:00 2001 From: perissology Date: Mon, 15 Jan 2018 20:17:29 -0800 Subject: [PATCH 07/55] a bit more cleanup --- README.md | 16 +++++++--------- lib/BridgedRoom.js | 4 ---- lib/Main.js | 11 +++-------- lib/SlackEventHandler.js | 23 ++++++++--------------- lib/SlackGhost.js | 15 ++++++++------- lib/SlackHookHandler.js | 2 ++ 6 files changed, 28 insertions(+), 43 deletions(-) diff --git a/README.md b/README.md index 46bd8890..96cbebef 100644 --- a/README.md +++ b/README.md @@ -106,7 +106,7 @@ and bot users. This allows you to link as many channels as you would like with o ### Recommended -1. add a custom app to your slack team/workspace by visiting https://api.slack.com/apps +1. Add a custom app to your slack team/workspace by visiting https://api.slack.com/apps and clicking on `Create New App`. 2. Name the app & select the team/workspace this app will belong to. @@ -129,10 +129,11 @@ and bot users. This allows you to link as many channels as you would like with o - files:write:user - Note: media uploaded to matrix is currently not permissioned, and anyone with the link - can access the file. In order to make slack files visible to matrix users, this bridge - will set make the slack file visible to anyone with the url (including files in private channels). - The current behavior in slack is that files are only accessible to authenticated users. + Note: any media uploaded to matrix is currently accessible by anyone who knows the url. + In order to make slack files visible to matrix users, this bridge will make slack files + visible to anyone with the url (including files in private channels). This is different + then the current behavior in slack, which only allows authenticated access to media + posted in private channels. 6. Click on `Install App` and `Install App to Workspace`. Note the access tokens show. You will need the `Bot User OAuth Access Token` and if you want to bridge files, the @@ -151,10 +152,7 @@ and bot users. This allows you to link as many channels as you would like with o ``` You will also need to determine the "channel ID" that Slack uses to identify - the channel. Unfortunately, it is not easily obtained from the Slack UI. The - easiest way to do this is to send a message from Slack to the bridge; the - bridge will log the channel ID as part of the unrecognised message output. - You can then take note of the `channel_id` field. + the channel, which can be found in the url `https://XXX.slack.com/messages//`. 2. Issue a ``link`` command in the administration control room with these collected values as arguments: diff --git a/lib/BridgedRoom.js b/lib/BridgedRoom.js index 30c81c63..ce45dc9a 100644 --- a/lib/BridgedRoom.js +++ b/lib/BridgedRoom.js @@ -412,8 +412,6 @@ BridgedRoom.prototype.lookupAndSetTeamInfo = function() { this._slack_team_id = response.team.id; this._dirty = true; } - - return; }); }; @@ -450,8 +448,6 @@ BridgedRoom.prototype.lookupAndSetUserInfo = function() { this._slack_bot_id = response.user.profile.bot_id; this._dirty = true; } - - return; }); }; diff --git a/lib/Main.js b/lib/Main.js index 44df4fda..0ce15dcd 100644 --- a/lib/Main.js +++ b/lib/Main.js @@ -194,7 +194,7 @@ Main.prototype.getGhostForSlackMessage = function(message) { // Slack ghost IDs need to be constructed from user IDs, not usernames, // because users can change their names - // TODO it the team_domain is changed, we will recreate all users + // TODO if the team_domain is changed, we will recreate all users. // TODO(paul): Steal MatrixIdTemplate from matrix-appservice-gitter var user_id = [ "@", this._config.username_prefix, message.team_domain.toLowerCase(), @@ -397,7 +397,6 @@ Main.prototype.listRoomsFor = function(intent, state) { }; Main.prototype.onMatrixEvent = function(ev) { - console.log("Received matrix event:", JSON.stringify(ev)); // simple de-dup var recents = this._recentMatrixEventIds; for (var i = 0; i < recents.length; i++) { @@ -551,13 +550,9 @@ Main.prototype.actionLink = function(opts) { room.updateSlackBotToken(opts.slack_bot_token); return Promise.all([ room.lookupAndSetTeamInfo(), room.lookupAndSetUserInfo() ]) .then(() => { - if (isNew) { - this.addBridgedRoom(room); - } + if (isNew) this.addBridgedRoom(room); - if (room.isDirty()) { - this.putRoomToStore(room); - } + if (room.isDirty()) this.putRoomToStore(room); return room; }); diff --git a/lib/SlackEventHandler.js b/lib/SlackEventHandler.js index 64ccd2a7..67e16605 100644 --- a/lib/SlackEventHandler.js +++ b/lib/SlackEventHandler.js @@ -30,8 +30,6 @@ util.inherits(SlackEventHandler, BaseSlackHandler); * @param {string} params.event.type Slack event type * @param {string} params.type type of callback we are receiving. typically event_callback * or url_verification. - // * @param {string} timestamp Timestamp when message was received, in seconds - // * formatted as a float. */ SlackEventHandler.prototype.handle = function (params, response) { try { @@ -49,8 +47,6 @@ SlackEventHandler.prototype.handle = function (params, response) { return; } - // var room = main.getRoomBySlackChannelId(params.event.channel); - var result; switch (params.event.type) { case 'message': @@ -69,8 +65,7 @@ SlackEventHandler.prototype.handle = function (params, response) { result = Promise.reject(new UnknownEvent()); } - result - .then(() => endTimer({outcome: "success"})) + result.then(() => endTimer({outcome: "success"})) .catch((e) => { if (e instanceof UnknownChannel) { console.log("Ignoring message from unrecognised slack channel id : %s (%s)", @@ -88,16 +83,14 @@ SlackEventHandler.prototype.handle = function (params, response) { console.log("Failed: ", e); } ); - - response.writeHead(200, {"Content-Type": "application/json"}); - response.end(); } catch (e) { console.log("Oops - SlackEventHandler failed:", e); - - // return 200 so slack doesn't keep sending the event - response.writeHead(200, {"Content-Type": "text/plain"}); - response.end(); } + + // return 200 so slack doesn't keep sending the event + response.writeHead(200, {"Content-Type": "text/plain"}); + response.end(); + }; /** @@ -145,7 +138,6 @@ SlackEventHandler.prototype.handleChannelRenameEvent = function (params) { * * @param {Object} params The event request emitted. * @param {string} params.event.user 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.event.text Text contents of the message, if a text message. * @param {string} params.event.channel The slack channel id * @param {string} params.event.ts The unique (per-channel) timestamp @@ -154,7 +146,8 @@ SlackEventHandler.prototype.handleMessageEvent = function (params) { var room = this._main.getRoomBySlackChannelId(params.event.channel); if (!room) throw new UnknownChannel(params.event.channel); - if (params.event.subtype === 'bot_message' && params.event.bot_id === room.getSlackBotId()) { + if (params.event.subtype === 'bot_message' && + (!room.getSlackBotId() || params.event.bot_id === room.getSlackBotId())) { return Promise.resolve(); } diff --git a/lib/SlackGhost.js b/lib/SlackGhost.js index 661b6af5..2bb0b236 100644 --- a/lib/SlackGhost.js +++ b/lib/SlackGhost.js @@ -58,19 +58,20 @@ SlackGhost.prototype.update = function(message, room) { SlackGhost.prototype.updateDisplayname = function(message, room) { var display_name = message.user_name; - var getDisplayName = (!display_name) ? - this.lookupUserInfo(message.user_id, room.getAccessToken()) + var getDisplayName; + if (!display_name) { + getDisplayName = this.lookupUserInfo(message.user_id, room.getAccessToken()) .then(user => { if (user && user.profile) { return user.profile.display_name || user.profile.real_name; } - return undefined; - }) : - Promise.resolve(display_name); + }); + } else { + getDisplayName = Promise.resolve(display_name); + } return getDisplayName.then(display_name => { - if (!display_name) return Promise.resolve(); - if (this._display_name === display_name) return Promise.resolve(); + if (!display_name || this._display_name === display_name) return Promise.resolve(); return this.getIntent().setDisplayName(display_name).then(() => { this._display_name = display_name; diff --git a/lib/SlackHookHandler.js b/lib/SlackHookHandler.js index 587afeaf..2612602f 100644 --- a/lib/SlackHookHandler.js +++ b/lib/SlackHookHandler.js @@ -59,6 +59,8 @@ SlackHookHandler.prototype.startAndListen = function(port, tls_config) { }); request.on("end", () => { + // if isEvent === true, this was an event emitted from the slack Event API + // https://api.slack.com/events-api var isEvent = request.headers['content-type'] === 'application/json' && request.method === 'POST'; try { if (isEvent) { From 64b1f8d8e902f0036b8d0e08de03e5299b6cb44b Mon Sep 17 00:00:00 2001 From: perissology Date: Tue, 16 Jan 2018 05:28:30 -0800 Subject: [PATCH 08/55] be consistent w/ returns --- lib/BridgedRoom.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/BridgedRoom.js b/lib/BridgedRoom.js index ce45dc9a..6bd0298b 100644 --- a/lib/BridgedRoom.js +++ b/lib/BridgedRoom.js @@ -442,7 +442,7 @@ BridgedRoom.prototype.lookupAndSetUserInfo = function() { json: true, }); }).then((response) => { - if (!response.user && response.user.profile) return Promise.resolve(); + if (!response.user && response.user.profile) return; if (!this._slack_bot_id !== response.user.profile.bot_id) { this._slack_bot_id = response.user.profile.bot_id; From beb4aaccab3dbc1580dd2eba2543a9d0e3ff0872 Mon Sep 17 00:00:00 2001 From: perissology Date: Wed, 17 Jan 2018 17:32:34 -0800 Subject: [PATCH 09/55] fix bug when inviting bot with a user on another HS --- lib/Main.js | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/lib/Main.js b/lib/Main.js index 0ce15dcd..d3730ef6 100644 --- a/lib/Main.js +++ b/lib/Main.js @@ -422,8 +422,11 @@ Main.prototype.onMatrixEvent = function(ev) { // A membership event about myself var membership = ev.content.membership; if (membership === "invite") { - // Automatically accept all invitations - this.getBotIntent().join(ev.room_id); + // Automatically accept all invitations, but wait 30 seconds before doing it. + // this was throwing an error when the bot was invited to a room by a user on + // a different homeserver ({"errcode":"M_FORBIDDEN","error":"You are not invited to this room."}) + Promise.delay(30 * 1000).then(() => this.getBotIntent().join(ev.room_id)); + } endTimer({outcome: "success"}); From 6be1c789a3f305ef577f3b41df5e2d324653f860 Mon Sep 17 00:00:00 2001 From: perissology Date: Wed, 24 Jan 2018 15:15:25 -0800 Subject: [PATCH 10/55] fix some typos in if statements --- lib/BridgedRoom.js | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/lib/BridgedRoom.js b/lib/BridgedRoom.js index 6bd0298b..30e0f341 100644 --- a/lib/BridgedRoom.js +++ b/lib/BridgedRoom.js @@ -403,12 +403,12 @@ BridgedRoom.prototype.lookupAndSetTeamInfo = function() { }).then((response) => { if (!response.team) return; - if (!this._slack_team_domain !== response.team.domain) { + if (this._slack_team_domain !== response.team.domain) { this._slack_team_domain = response.team.domain; this._dirty = true; } - if (!this._slack_team_id !== response.team.id) { + if (this._slack_team_id !== response.team.id) { this._slack_team_id = response.team.id; this._dirty = true; } @@ -428,7 +428,7 @@ BridgedRoom.prototype.lookupAndSetUserInfo = function() { console.log(response); if (!response.user_id) return; - if (!this._slack_user_id !== response.user_id) { + if (this._slack_user_id !== response.user_id) { this._slack_user_id = response.user_id; this._dirty = true; } @@ -442,9 +442,9 @@ BridgedRoom.prototype.lookupAndSetUserInfo = function() { json: true, }); }).then((response) => { - if (!response.user && response.user.profile) return; + if (!response.user || !response.user.profile) return; - if (!this._slack_bot_id !== response.user.profile.bot_id) { + if (this._slack_bot_id !== response.user.profile.bot_id) { this._slack_bot_id = response.user.profile.bot_id; this._dirty = true; } From 8b808cfa5958c17c9bbbe38109b6769f1700af2a Mon Sep 17 00:00:00 2001 From: Will Hunt Date: Mon, 24 Sep 2018 14:58:50 +0100 Subject: [PATCH 11/55] Slack should be capitalised --- README.md | 56 +++++++++++++++++++++++++++---------------------------- 1 file changed, 28 insertions(+), 28 deletions(-) diff --git a/README.md b/README.md index 96cbebef..8419cf80 100644 --- a/README.md +++ b/README.md @@ -99,83 +99,83 @@ first the individual Matrix room and Slack channel need to be created, and then a command needs to be issued in the administration console room to add the link to the bridge's database. -There are 2 ways to bridge a room. The recommended way uses the newer slack events api +There are 2 ways to bridge a room. The recommended way uses the newer Slack events api and bot users. This allows you to link as many channels as you would like with only -1 slack integration. The legacy way uses incoming/outgoing webhooks, and requires -2 slack integrations per channel to be bridged. +1 Slack integration. The legacy way uses incoming/outgoing webhooks, and requires +2 Slack integrations per channel to be bridged. -### Recommended +### Recommended - Events API -1. Add a custom app to your slack team/workspace by visiting https://api.slack.com/apps +1. Add a custom app to your Slack team/workspace by visiting https://api.slack.com/apps and clicking on `Create New App`. - + 2. Name the app & select the team/workspace this app will belong to. 3. Click on `bot users` and add a new bot user. We will use this account to bridge the the rooms. - + 4. Click on `Event Subscriptions` and enable them. At this point, the bridge needs to be - started as slack will do some verification of the request rul. The request url should be + started as Slack will do some verification of the request rul. The request url should be `https://$HOST:$SLACK_PORT"`. Then add the following events and save: - + Bot User Events: - + - team_domain_change - message.channels - message.groups (if you want to bridge private channels) - + 5. Skip this step if you do not want to bridge files. Click on `OAuth & Permissions` and add the following scopes: - files:write:user - + Note: any media uploaded to matrix is currently accessible by anyone who knows the url. - In order to make slack files visible to matrix users, this bridge will make slack files + In order to make Slack files visible to matrix users, this bridge will make Slack files visible to anyone with the url (including files in private channels). This is different - then the current behavior in slack, which only allows authenticated access to media + then the current behavior in Slack, which only allows authenticated access to media posted in private channels. - + 6. Click on `Install App` and `Install App to Workspace`. Note the access tokens show. You will need the `Bot User OAuth Access Token` and if you want to bridge files, the `OAuth Access Token` whenever you link a room. - + 7. For each channel you would like to bridge, perform the following steps: 1. Create a Matrix room in the usual manner for your client. Take a note of its Matrix room ID - it will look something like `!aBcDeF:example.com`. - - 2. invite the bot user to the slack channel you would like to bridge. - + + 2. invite the bot user to the Slack channel you would like to bridge. + ``` /invite @bot-user-name - ``` - + ``` + You will also need to determine the "channel ID" that Slack uses to identify the channel, which can be found in the url `https://XXX.slack.com/messages//`. - + 2. Issue a ``link`` command in the administration control room with these collected values as arguments: - + with file bridging: - + ``` link --channel_id CHANNELID --room !the-matrix:room.id --slack_bot_token xoxb-xxxxxxxxxx-xxxxxxxxxxxxxxxxxxxx --slack_user_token xoxp-xxxxxxxx-xxxxxxxxx-xxxxxxxx-xxxxxxxx ``` without file bridging: - + ``` link --channel_id CHANNELID --room !the-matrix:room.id --slack_bot_token xoxb-xxxxxxxxxx-xxxxxxxxxxxxxxxxxxxx ``` - + These arguments can be shortened to single-letter forms: - + ``` link -I CHANNELID -R !the-matrix:room.id -t xoxb-xxxxxxxxxx-xxxxxxxxxxxxxxxxxxxx ``` -### Legacy +### Legacy - Webhooks 1. Create a Matrix room in the usual manner for your client. Take a note of its Matrix room ID - it will look something like `!aBcDeF:example.com`. From cff4c093e78b9e9c67822ea86a4356fcbce83046 Mon Sep 17 00:00:00 2001 From: Will Hunt Date: Mon, 24 Sep 2018 14:59:07 +0100 Subject: [PATCH 12/55] Use const for imports and constants --- lib/BaseSlackHandler.js | 8 ++++---- lib/SlackEventHandler.js | 10 +++++----- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/lib/BaseSlackHandler.js b/lib/BaseSlackHandler.js index 6a515082..40c81023 100644 --- a/lib/BaseSlackHandler.js +++ b/lib/BaseSlackHandler.js @@ -1,9 +1,9 @@ "use strict"; -var rp = require('request-promise'); -var Promise = require('bluebird'); -var promiseWhile = require("./promiseWhile"); -var getSlackFileUrl = require("./substitutions").getSlackFileUrl; +const rp = require('request-promise'); +const Promise = require('bluebird'); +const promiseWhile = require("./promiseWhile"); +const getSlackFileUrl = require("./substitutions").getSlackFileUrl; /** * @constructor diff --git a/lib/SlackEventHandler.js b/lib/SlackEventHandler.js index 67e16605..4353857d 100644 --- a/lib/SlackEventHandler.js +++ b/lib/SlackEventHandler.js @@ -1,12 +1,12 @@ "use strict"; -var BaseSlackHandler = require('./BaseSlackHandler'); -var Promise = require('bluebird'); -var util = require("util"); +const BaseSlackHandler = require('./BaseSlackHandler'); +const Promise = require('bluebird'); +const util = require("util"); -var UnknownEvent = function () { +const UnknownEvent = function () { }; -var UnknownChannel = function (channel) { +const UnknownChannel = function (channel) { this.channel = channel; }; From 191d8575750160d0721d38f2265cc2e9327389e5 Mon Sep 17 00:00:00 2001 From: Will Hunt Date: Mon, 24 Sep 2018 14:59:38 +0100 Subject: [PATCH 13/55] Don't stick a delay in because the homeserver is racy --- lib/Main.js | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/lib/Main.js b/lib/Main.js index 563d16e8..3336d251 100644 --- a/lib/Main.js +++ b/lib/Main.js @@ -415,11 +415,9 @@ Main.prototype.onMatrixEvent = function(ev) { // A membership event about myself var membership = ev.content.membership; if (membership === "invite") { - // Automatically accept all invitations, but wait 30 seconds before doing it. - // this was throwing an error when the bot was invited to a room by a user on - // a different homeserver ({"errcode":"M_FORBIDDEN","error":"You are not invited to this room."}) - Promise.delay(30 * 1000).then(() => this.getBotIntent().join(ev.room_id)); - + // NOTE: This can race and fail if the invite goes down the AS stream + // before the homeserver believes we can actually join the room. + this.getBotIntent().join(ev.room_id); } endTimer({outcome: "success"}); From 74a794dd12d8d995453f7823b05fad6575b3d057 Mon Sep 17 00:00:00 2001 From: Will Hunt Date: Mon, 24 Sep 2018 15:00:02 +0100 Subject: [PATCH 14/55] Can't imagine only webhooks are allowed to be !ok --- lib/BridgedRoom.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/BridgedRoom.js b/lib/BridgedRoom.js index 30e0f341..9608a12d 100644 --- a/lib/BridgedRoom.js +++ b/lib/BridgedRoom.js @@ -200,11 +200,11 @@ BridgedRoom.prototype.onMatrixMessage = function(message) { uri: uri, body: body, }; - if (this._slack_bot_token) { sendMessageParams.headers = { Authorization: 'Bearer ' + this._slack_bot_token }; + // See https://api.slack.com/methods/chat.postMessage#authorship sendMessageParams.body.as_user = false; sendMessageParams.body.channel = this._slack_channel_id; } @@ -222,7 +222,7 @@ BridgedRoom.prototype.onMatrixMessage = function(message) { return rp(sendMessageParams).then((res) => { this._main.incCounter("sent_messages", {side: "remote"}); - if (!res || (this._slack_bot_token && !res.ok)) { + if (!res || !res.ok) { console.log("HTTP Error: ", res); } }); From 83be7146b6c464584e29d04f3e2374b1d1c8f45d Mon Sep 17 00:00:00 2001 From: Will Hunt Date: Mon, 24 Sep 2018 16:05:00 +0100 Subject: [PATCH 15/55] team_domain is no more, attempt to extract it via an API call. --- lib/Main.js | 78 ++++++++++++++++++++++++++++++++++++----------------- 1 file changed, 54 insertions(+), 24 deletions(-) diff --git a/lib/Main.js b/lib/Main.js index 3336d251..85fd4727 100644 --- a/lib/Main.js +++ b/lib/Main.js @@ -22,6 +22,7 @@ function Main(config) { var self = this; this._config = config; + this._teams = Map(); //team_id => team. if (config.oauth2) { this._oauth2 = new OAuth2({ @@ -192,6 +193,31 @@ Main.prototype.getBotIntent = function() { return this._bridge.getIntent(); }; +Main.prototype.getTeamDomainForMessage = function(message) { + if !message.team_domain) { + return message.team_domain; + } + if (this._teams.has(message.team_id) { + return this._teams.get(message.team_id).domain; + } + + const room = main.getRoomBySlackChannelId(message.channel); + + var channelsInfoApiParams = { + uri: 'https://slack.com/api/team.info', + qs: { + token: room.getAccessToken() + }, + json: true + }; + main.incRemoteCallCounter("team.info"); + return rp(channelsInfoApiParams).then((response) => { + console.log("Got new team:", response); + this._teams.set(message.team_id, response); + return response.domain; + }); +} + // Returns a Promise of a SlackGhost Main.prototype.getGhostForSlackMessage = function(message) { // Slack ghost IDs need to be constructed from user IDs, not usernames, @@ -199,35 +225,39 @@ Main.prototype.getGhostForSlackMessage = function(message) { // TODO if the team_domain is changed, we will recreate all users. // TODO(paul): Steal MatrixIdTemplate from matrix-appservice-gitter - var user_id = [ - "@", this._config.username_prefix, message.team_domain.toLowerCase(), - "_", message.user_id.toUpperCase(), ":", this._config.homeserver.server_name - ].join(""); - - if (this._ghostsByUserId[user_id]) { - return Promise.resolve(this._ghostsByUserId[user_id]); - } - var intent = this._bridge.getIntent(user_id); + // team_domain is gone, so we have to actually get the domain from a friendly object. + this.getTeamDomainForMessage(message).then((team_domain) => { + const user_id = [ + "@", this._config.username_prefix, team_domain.toLowerCase(), + "_", message.user_id.toUpperCase(), ":", this._config.homeserver.server_name + ].join(""); - var store = this.getUserStore(); - return store.select({id: user_id}).then((entries) => { - var ghost; - if (entries.length) { - ghost = SlackGhost.fromEntry(this, entries[0], intent); + if (this._ghostsByUserId[user_id]) { + return Promise.resolve(this._ghostsByUserId[user_id]); } - else { - ghost = new SlackGhost({ - main: this, - user_id: user_id, - intent: intent, - }); - this.putUserToStore(ghost); - } + const intent = this._bridge.getIntent(user_id); + const store = this.getUserStore(); + + return store.select({id: user_id}).then((entries) => { + var ghost; + if (entries.length) { + ghost = SlackGhost.fromEntry(this, entries[0], intent); + } + else { + ghost = new SlackGhost({ + main: this, - this._ghostsByUserId[user_id] = ghost; - return ghost; + user_id: user_id, + intent: intent, + }); + this.putUserToStore(ghost); + } + + this._ghostsByUserId[user_id] = ghost; + return ghost; + }); }); }; From bdfc5b2bdaffe271dd4ad718262262ffe7901ece Mon Sep 17 00:00:00 2001 From: Will Hunt Date: Mon, 24 Sep 2018 20:44:42 +0100 Subject: [PATCH 16/55] Missing ( --- lib/Main.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/Main.js b/lib/Main.js index 85fd4727..05a4c3db 100644 --- a/lib/Main.js +++ b/lib/Main.js @@ -194,10 +194,10 @@ Main.prototype.getBotIntent = function() { }; Main.prototype.getTeamDomainForMessage = function(message) { - if !message.team_domain) { + if (!message.team_domain) { return message.team_domain; } - if (this._teams.has(message.team_id) { + if (this._teams.has(message.team_id)) { return this._teams.get(message.team_id).domain; } From 1392c31049cf2d0c8cefa5fb05183415481c9157 Mon Sep 17 00:00:00 2001 From: Will Hunt Date: Mon, 24 Sep 2018 20:44:59 +0100 Subject: [PATCH 17/55] Add extra scope required --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index 8419cf80..05199655 100644 --- a/README.md +++ b/README.md @@ -122,6 +122,7 @@ and bot users. This allows you to link as many channels as you would like with o - team_domain_change - message.channels + - chat:write:bot - message.groups (if you want to bridge private channels) 5. Skip this step if you do not want to bridge files. From c5e474a648b5f6ee88681330489eb815d98fc930 Mon Sep 17 00:00:00 2001 From: Will Hunt Date: Mon, 24 Sep 2018 20:45:49 +0100 Subject: [PATCH 18/55] new --- lib/Main.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/Main.js b/lib/Main.js index 05a4c3db..aaa4280f 100644 --- a/lib/Main.js +++ b/lib/Main.js @@ -22,7 +22,7 @@ function Main(config) { var self = this; this._config = config; - this._teams = Map(); //team_id => team. + this._teams = new Map(); //team_id => team. if (config.oauth2) { this._oauth2 = new OAuth2({ From ad0180eafa9d1eab96fb09edf9074bbd7bef7112 Mon Sep 17 00:00:00 2001 From: Will Hunt Date: Mon, 24 Sep 2018 20:48:13 +0100 Subject: [PATCH 19/55] Import rp --- lib/Main.js | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/lib/Main.js b/lib/Main.js index aaa4280f..e08e6f54 100644 --- a/lib/Main.js +++ b/lib/Main.js @@ -17,6 +17,7 @@ var OAuth2 = require("./OAuth2"); var Provisioning = require("./Provisioning"); var randomstring = require("randomstring"); +const rp = require('request-promise'); function Main(config) { var self = this; @@ -403,10 +404,10 @@ Main.prototype.drainAndLeaveMatrixRoom = function(roomId) { // (or a default of "join") in Main.prototype.listRoomsFor = function(intent, state) { if (!intent) intent = this.getBotIntent(); - if (state && state !== "join") { + if (state && state !== "join") { return Promise.reject("listRoomsFor only supports 'join'"); } - + // This endpoint sucks because we can only get joined rooms. // Previously we filtered a /sync but obviously that was broken too. // -- Halfy @@ -687,7 +688,7 @@ Main.prototype.run = function(port) { // Give the bridge a little while to start up, and then clean up pending // invites // TODO: This is currently unsupported as we used to /sync, and there - // is no endpoint to fetch pending invites. + // is no endpoint to fetch pending invites. /*Promise.delay(30 * 1000).then(() => { console.log("Accepting pending invites"); From edb4ee6b7dff500672f590ec3d03fffe6ed7c91e Mon Sep 17 00:00:00 2001 From: Will Hunt Date: Mon, 24 Sep 2018 20:49:54 +0100 Subject: [PATCH 20/55] Return a promise --- lib/Main.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/lib/Main.js b/lib/Main.js index e08e6f54..1901f271 100644 --- a/lib/Main.js +++ b/lib/Main.js @@ -195,11 +195,11 @@ Main.prototype.getBotIntent = function() { }; Main.prototype.getTeamDomainForMessage = function(message) { - if (!message.team_domain) { - return message.team_domain; + if (message.team_domain) { + return Promise.resolve(message.team_domain); } if (this._teams.has(message.team_id)) { - return this._teams.get(message.team_id).domain; + return Promise.resolve(this._teams.get(message.team_id).domain); } const room = main.getRoomBySlackChannelId(message.channel); From 9e9f5a29d04c2c40bf51526a2ff8f63f43f3b1fe Mon Sep 17 00:00:00 2001 From: Will Hunt Date: Mon, 24 Sep 2018 20:50:55 +0100 Subject: [PATCH 21/55] main -> this --- lib/Main.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/Main.js b/lib/Main.js index 1901f271..7a189fb9 100644 --- a/lib/Main.js +++ b/lib/Main.js @@ -202,7 +202,7 @@ Main.prototype.getTeamDomainForMessage = function(message) { return Promise.resolve(this._teams.get(message.team_id).domain); } - const room = main.getRoomBySlackChannelId(message.channel); + const room = this.getRoomBySlackChannelId(message.channel); var channelsInfoApiParams = { uri: 'https://slack.com/api/team.info', @@ -211,7 +211,7 @@ Main.prototype.getTeamDomainForMessage = function(message) { }, json: true }; - main.incRemoteCallCounter("team.info"); + this.incRemoteCallCounter("team.info"); return rp(channelsInfoApiParams).then((response) => { console.log("Got new team:", response); this._teams.set(message.team_id, response); From bc54e4eed51d39247ee84375ac539fc5b57226c2 Mon Sep 17 00:00:00 2001 From: Will Hunt Date: Mon, 24 Sep 2018 21:02:02 +0100 Subject: [PATCH 22/55] Handle the promise --- lib/Main.js | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/lib/Main.js b/lib/Main.js index 7a189fb9..6670bd2c 100644 --- a/lib/Main.js +++ b/lib/Main.js @@ -213,9 +213,13 @@ Main.prototype.getTeamDomainForMessage = function(message) { }; this.incRemoteCallCounter("team.info"); return rp(channelsInfoApiParams).then((response) => { + if (!response.ok) { + console.error(`Trying to fetch the ${message.team_id} team.`, response); + return Promise.resolve(); + } console.log("Got new team:", response); - this._teams.set(message.team_id, response); - return response.domain; + this._teams.set(message.team_id, response.team); + return response.team.domain; }); } From 79632cbdfb62b2df58c6d4aef3b2f877e0b376a7 Mon Sep 17 00:00:00 2001 From: Will Hunt Date: Mon, 24 Sep 2018 21:03:13 +0100 Subject: [PATCH 23/55] Return the promise --- lib/Main.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/Main.js b/lib/Main.js index 6670bd2c..47b08114 100644 --- a/lib/Main.js +++ b/lib/Main.js @@ -232,7 +232,7 @@ Main.prototype.getGhostForSlackMessage = function(message) { // TODO(paul): Steal MatrixIdTemplate from matrix-appservice-gitter // team_domain is gone, so we have to actually get the domain from a friendly object. - this.getTeamDomainForMessage(message).then((team_domain) => { + return this.getTeamDomainForMessage(message).then((team_domain) => { const user_id = [ "@", this._config.username_prefix, team_domain.toLowerCase(), "_", message.user_id.toUpperCase(), ":", this._config.homeserver.server_name From a61575be1d09e8216660db7a5084a4b748914985 Mon Sep 17 00:00:00 2001 From: Will Hunt Date: Mon, 24 Sep 2018 21:30:32 +0100 Subject: [PATCH 24/55] Log when trying to update a users profile --- lib/SlackGhost.js | 1 + 1 file changed, 1 insertion(+) diff --git a/lib/SlackGhost.js b/lib/SlackGhost.js index c61801ab..59741684 100644 --- a/lib/SlackGhost.js +++ b/lib/SlackGhost.js @@ -45,6 +45,7 @@ SlackGhost.prototype.getIntent = function() { }; SlackGhost.prototype.update = function(message, room) { + console.log("Updating user information for " + message.user_name); return Promise.all([ this.updateDisplayname(message, room).catch((e) => { console.log("Failed to update ghost displayname:", e); From 091233abeaa6a625b23f7d659a557d42e18a84b8 Mon Sep 17 00:00:00 2001 From: Will Hunt Date: Mon, 24 Sep 2018 21:34:44 +0100 Subject: [PATCH 25/55] Log if failed to get user profile --- lib/SlackGhost.js | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/lib/SlackGhost.js b/lib/SlackGhost.js index 59741684..b6c249bf 100644 --- a/lib/SlackGhost.js +++ b/lib/SlackGhost.js @@ -45,7 +45,7 @@ SlackGhost.prototype.getIntent = function() { }; SlackGhost.prototype.update = function(message, room) { - console.log("Updating user information for " + message.user_name); + console.log("Updating user information for " + message.user_id); return Promise.all([ this.updateDisplayname(message, room).catch((e) => { console.log("Failed to update ghost displayname:", e); @@ -110,7 +110,10 @@ SlackGhost.prototype.lookupUserInfo = function(user_id, token) { }, json: true, }).then((response) => { - if (!response.user || !response.user.profile) return; + if (!response.user || !response.user.profile) { + console.error("Failed to get user profile", response); + return; + }; this._user_info_cache = response.user; setTimeout(() => { this._user_info_cache = null }, USER_CACHE_TIMEOUT); From c4799797a5a1e5593d7202c0115af98462b5591f Mon Sep 17 00:00:00 2001 From: Will Hunt Date: Mon, 24 Sep 2018 21:55:11 +0100 Subject: [PATCH 26/55] Add additional scope dependencies --- README.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/README.md b/README.md index 05199655..5664dea1 100644 --- a/README.md +++ b/README.md @@ -124,6 +124,8 @@ and bot users. This allows you to link as many channels as you would like with o - message.channels - chat:write:bot - message.groups (if you want to bridge private channels) + - users:read + - team.info 5. Skip this step if you do not want to bridge files. Click on `OAuth & Permissions` and add the following scopes: From b17c1d05018e78e4690a885d5a6aa427d381947b Mon Sep 17 00:00:00 2001 From: Stuart Mumford Date: Wed, 26 Sep 2018 10:46:22 +0100 Subject: [PATCH 27/55] Fix corrext substitution of < > and & in formatted bodies Signed-off-by: Stuart Mumford --- lib/SlackGhost.js | 3 ++- lib/substitutions.js | 9 ++++++++- 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/lib/SlackGhost.js b/lib/SlackGhost.js index b6c249bf..6ae29ddc 100644 --- a/lib/SlackGhost.js +++ b/lib/SlackGhost.js @@ -2,6 +2,7 @@ var rp = require('request-promise'); var slackdown = require('Slackdown'); +var substitutions = require("./substitutions"); // How long in msec to cache user infor lookups from slack var USER_CACHE_TIMEOUT = 10 * 60 * 1000; // 10 minutes @@ -157,7 +158,7 @@ SlackGhost.prototype.sendText = function(room_id, text) { const content = { body: text, msgtype: "m.text", - formatted_body: slackdown.parse(text), + formatted_body: slackdown.parse(substitutions.htmlEscape(text)), format: "org.matrix.custom.html" }; return this.getIntent().sendMessage(room_id, content).then(() => { diff --git a/lib/substitutions.js b/lib/substitutions.js index 18931f50..d92a9b7d 100644 --- a/lib/substitutions.js +++ b/lib/substitutions.js @@ -159,8 +159,15 @@ var matrixToSlack = function(event, main) { return ret; }; +var htmlEscape = function(s) { + return s.replace(/&/g, "&") + .replace(//g, ">"); +}; + module.exports = { matrixToSlack: matrixToSlack, slackToMatrix: slackToMatrix, - getSlackFileUrl: getSlackFileUrl + getSlackFileUrl: getSlackFileUrl, + htmlEscape: htmlEscape }; From f4f4db20b2aa8d6997f380e415dc8ae833309a31 Mon Sep 17 00:00:00 2001 From: Stuart Mumford Date: Wed, 26 Sep 2018 16:21:32 +0100 Subject: [PATCH 28/55] Fix file bridging with the new slack files Signed-off-by: Stuart Mumford --- lib/BridgedRoom.js | 36 ++++++++++++------------------------ lib/SlackEventHandler.js | 4 ++-- lib/SlackGhost.js | 33 +++++++++++++++++++++++++++++++++ 3 files changed, 47 insertions(+), 26 deletions(-) diff --git a/lib/BridgedRoom.js b/lib/BridgedRoom.js index 9608a12d..46a37fac 100644 --- a/lib/BridgedRoom.js +++ b/lib/BridgedRoom.js @@ -260,36 +260,24 @@ BridgedRoom.prototype._handleSlackMessage = function(message, ghost) { var text = substitutions.slackToMatrix(message.text, message.file); return ghost.sendText(roomID, text); } - else if (subtype === "file_share") { - if (!message.file) { - console.log("Ignoring missing file message: " + message); - return; - } - if (message.file._content) { - // TODO: Currently Matrix lacks a way to upload a "captioned image", - // so we just send a separate `m.image` and `m.text` message - // See https://github.com/matrix-org/matrix-doc/issues/906 - - // upload to media repo; get media repo URL back - return ghost.uploadContent(message.file).then((content_uri) => { - if(undefined == content_uri) { - // no URL returned from media repo; abort - return undefined; - } - var matrixMessage = slackFileToMatrixMessage(message.file, content_uri); + else if (message.files != undefined) { + for (var i = 0; i < message.files.length; i++) { + var file = message.files[i]; + + return ghost.uploadContentFromURI(file, file.url_private, this._slack_bot_token).then((content_uri) => { + var matrixMessage = slackFileToMatrixMessage(file, content_uri); return ghost.sendMessage(roomID, matrixMessage); - }).finally(() => { - if (message.file.initial_comment) { + }).then(() => { + // TODO: Currently Matrix lacks a way to upload a "captioned image", + // so we just send a separate `m.image` and `m.text` message + // See https://github.com/matrix-org/matrix-doc/issues/906 + if (message.text) { var text = substitutions.slackToMatrix( - message.file.initial_comment.comment + message.text ); return ghost.sendText(roomID, text); } }); - } else { - // post a msg with the link - var text = substitutions.slackToMatrix(message.text, message.file); - return ghost.sendText(roomID, text); } } else { diff --git a/lib/SlackEventHandler.js b/lib/SlackEventHandler.js index 4353857d..00d7809f 100644 --- a/lib/SlackEventHandler.js +++ b/lib/SlackEventHandler.js @@ -162,7 +162,7 @@ SlackEventHandler.prototype.handleMessageEvent = function (params) { channel_id: params.event.channel }); - var processMsg = msg.text || msg.subtype === 'message_deleted'; + var processMsg = msg.text || msg.subtype === 'message_deleted' || msg.files != undefined; if (msg.subtype === 'file_comment') { msg.user_id = msg.comment.user; @@ -181,7 +181,7 @@ SlackEventHandler.prototype.handleMessageEvent = function (params) { // 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); - console.log('SlackEventHandler text === undefined'); + console.log("Did not understand event. " + JSON.stringify(message)); return Promise.resolve(); } diff --git a/lib/SlackGhost.js b/lib/SlackGhost.js index 6ae29ddc..53ac5ba3 100644 --- a/lib/SlackGhost.js +++ b/lib/SlackGhost.js @@ -1,5 +1,7 @@ "use strict"; +var url = require('url'); +var https = require('https'); var rp = require('request-promise'); var slackdown = require('Slackdown'); var substitutions = require("./substitutions"); @@ -172,6 +174,37 @@ SlackGhost.prototype.sendMessage = function(room_id, msg) { }); }; +SlackGhost.prototype.uploadContentFromURI = function(file, uri, token) { + return new Promise((resolve, reject) => { + var options = url.parse(uri); + options.headers = { + Authorization: 'Bearer ' + token + }; + const req = https.get(options, (res) => { + let buffer = Buffer.alloc(0); + + res.on("data", (d) => { + buffer = Buffer.concat([buffer, d]); + }); + + res.on("end", () => { + resolve(buffer); + }); + }); + req.on("error", (err) => { + reject("Failed to download"); + }); + }).then((buffer) => { + file._content = buffer; + return this.uploadContent(file); + }).then((contentUri) => { + return contentUri; + }).catch((reason) => { + console.log("UploadContent", "Failed to upload content:\n%s", reason); + throw reason; + }); +} + SlackGhost.prototype.uploadContent = function(file) { return this.getIntent().getClient().uploadContent({ stream: new Buffer(file._content, "binary"), From 8b7b69524eeaf13efeefeac53ed05c032976ae8c Mon Sep 17 00:00:00 2001 From: Stuart Mumford Date: Wed, 26 Sep 2018 18:57:22 +0100 Subject: [PATCH 29/55] Strip out language specifier on the code tags Signed-off-by: Stuart Mumford --- lib/substitutions.js | 3 +++ 1 file changed, 3 insertions(+) diff --git a/lib/substitutions.js b/lib/substitutions.js index d92a9b7d..ce33458a 100644 --- a/lib/substitutions.js +++ b/lib/substitutions.js @@ -138,6 +138,9 @@ var matrixToSlack = function(event, main) { // convert @room to @channel string = string.replace("@room", "@channel"); + // Strip out any language specifier on the code tags, as they are not supported by slack. + string = string.replace(/```[\w*]+\n/g, "```\n"); + // the link_names flag means that writing @username will act as a mention in slack var ret = { username: event.user_id, From 11a2999f65dbf9b4947c3ebd0c36c5c1c7538b05 Mon Sep 17 00:00:00 2001 From: Will Hunt Date: Thu, 27 Sep 2018 21:11:43 +0100 Subject: [PATCH 30/55] Support all media types of attachments. --- lib/BridgedRoom.js | 126 +++++++++++++++++++++++++++++++++------- lib/SlackHookHandler.js | 28 ++++----- 2 files changed, 118 insertions(+), 36 deletions(-) diff --git a/lib/BridgedRoom.js b/lib/BridgedRoom.js index 46a37fac..42708d0a 100644 --- a/lib/BridgedRoom.js +++ b/lib/BridgedRoom.js @@ -260,13 +260,33 @@ BridgedRoom.prototype._handleSlackMessage = function(message, ghost) { var text = substitutions.slackToMatrix(message.text, message.file); return ghost.sendText(roomID, text); } - else if (message.files != undefined) { + else if (message.files) { for (var i = 0; i < message.files.length; i++) { var file = message.files[i]; - - return ghost.uploadContentFromURI(file, file.url_private, this._slack_bot_token).then((content_uri) => { - var matrixMessage = slackFileToMatrixMessage(file, content_uri); - return ghost.sendMessage(roomID, matrixMessage); + // We also need to upload the thumbnail + let thumbnail_promise = Promise.resolve(); + // Slack ain't a believer in consistency. + const thumb_uri = file.thumb_360 || file.thumb_video; + if (thumb_uri) { + const thumbnail_promise = ghost.uploadContentFromURI( + { + // Yes, we hardcode jpeg. Slack always use em. + title: `${file.name}_thumb.jpeg`, + mimetype: "image/jpeg", + }, + thumb_uri, + this._slack_bot_token + ); + } + let content_uri = ""; + return ghost.uploadContentFromURI(file, file.url_private, this._slack_bot_token).then((file_content_uri) => { + content_uri = file_content_uri; + return thumbnail_promise; + }).then((thumb_content_uri) => { + return ghost.sendMessage( + roomID, + slackFileToMatrixMessage(file, content_uri, thumb_content_uri) + ); }).then(() => { // TODO: Currently Matrix lacks a way to upload a "captioned image", // so we just send a separate `m.image` and `m.text` message @@ -301,10 +321,8 @@ BridgedRoom.prototype.leaveGhosts = function(ghosts) { * Converts a slack image attachment to a matrix image event. * * @param {Object} file The slack image attachment file object. - * @param {string} file.url URL of the file. * @param {string} file.title alt-text for the file. * @param {string} file.mimetype mime-type of the file. - * @param {?integer} file.size size of the file in bytes. * @param {?integer} file.original_w width of the file if an image, in pixels. * @param {?integer} file.original_h height of the file if an image, in pixels. * @param {?string} file.thumb_360 URL of a 360 pixel wide thumbnail of the @@ -313,9 +331,11 @@ BridgedRoom.prototype.leaveGhosts = function(ghosts) { * wide thumbnail of the file, if an image. * @param {?integer} file.thumb_360_h height of the thumbnail of the 36 pixel * wide thumbnail of the file, if an image. + * @param {string} url The matrix file mxc. + * @param {?string} thumbnail_url The matrix thumbnail mxc. * @return {Object} Matrix event content, as per https://matrix.org/docs/spec/#m-image */ -var slackImageToMatrixImage = function(file, url) { +const slackImageToMatrixImage = function(file, url, thumbnail_url) { var message = { msgtype: "m.image", url: url, @@ -324,17 +344,17 @@ var slackImageToMatrixImage = function(file, url) { mimetype: file.mimetype } }; + if (file.original_w) { message.info.w = file.original_w; } + if (file.original_h) { message.info.h = file.original_h; } - if (file.size) { - message.info.size = file.size; - } - if (false && file.thumb_360) { - message.thumbnail_url = file.thumb_360; + + if (thumbnail_url) { + message.thumbnail_url = thumbnail_url; message.thumbnail_info = {}; if (file.thumb_360_w) { message.thumbnail_info.w = file.thumb_360_w; @@ -346,18 +366,87 @@ var slackImageToMatrixImage = function(file, url) { return message; }; +/** + * Converts a slack video attachment to a matrix video event. + * + * @param {Object} file The slack video attachment file object. + * @param {string} file.title alt-text for the file. + * @param {string} file.mimetype mime-type of the file. + * @param {?integer} file.original_w width of the file if an image, in pixels. + * @param {?integer} file.original_h height of the file if an image, in pixels. + * @param {string} url The matrix file mxc. + * @param {?string} thumbnail_url The matrix thumbnail mxc. + * @return {Object} Matrix event content, as per https://matrix.org/docs/spec/client_server/r0.4.0.html#m-video + */ +const slackImageToMatrixVideo = function(file, url, thumbnail_url) { + var message = { + msgtype: "m.video", + url: url, + body: file.title, + info: { + mimetype: file.mimetype + } + }; + + if (file.original_w) { + message.info.w = file.original_w; + } + + if (file.original_h) { + message.info.h = file.original_h; + } + + if (thumbnail_url) { + message.thumbnail_url = thumbnail_url; + // Slack don't tell us the thumbnail size for videos. Boo + } + + return message; +}; + +/** + * Converts a slack audio attachment to a matrix audio event. + * + * @param {Object} file The slack audio attachment file object. + * @param {string} file.title alt-text for the file. + * @param {string} file.mimetype mime-type of the file. + * @param {string} url The matrix file mxc. + * @return {Object} Matrix event content, as per https://matrix.org/docs/spec/client_server/r0.4.0.html#m-audio + */ +const slackImageToMatrixAudio = function(file, url) { + return { + msgtype: "m.audio", + url: url, + body: file.title, + info: { + mimetype: file.mimetype + } + }; +}; /** * Converts a slack file upload to a matrix file upload event. * * @param {Object} file The slack file object. + * @param {string} url The matrix file mxc. + * @param {?string} thumbnail_url The matrix thumbnail mxc. * @return {Object} Matrix event content, as per https://matrix.org/docs/spec/#m-file */ -var slackFileToMatrixMessage = function(file, url) { - if (file.mimetype && file.mimetype.indexOf("image/") === 0) { - return slackImageToMatrixImage(file, url); +const slackFileToMatrixMessage = function(file, url, thumbnail_url) { + if (file.size) { + message.info.size = file.size; } - var message = { + if (file.mimetype) { + if (file.mimetype.startsWith("image/")) { + return slackImageToMatrixImage(file, url, thumbnail_url); + } else if (file.mimetype.startsWith("video/")) { + return slackImageToMatrixVideo(file, url, thumbnail_url); + } else if (file.mimetype.startsWith("audio/")) { + return slackImageToMatrixAudio(file, url); + } + } + + const message = { msgtype: "m.file", url: url, body: file.title, @@ -365,9 +454,6 @@ var slackFileToMatrixMessage = function(file, url) { mimetype: file.mimetype } }; - if (file.size) { - message.info.size = file.size; - } return message; }; diff --git a/lib/SlackHookHandler.js b/lib/SlackHookHandler.js index 2612602f..058a27f4 100644 --- a/lib/SlackHookHandler.js +++ b/lib/SlackHookHandler.js @@ -333,23 +333,19 @@ SlackHookHandler.prototype.lookupMessage = function(channelID, timestamp, token) var message = response.messages[0]; console.log("Looked up message from history as " + JSON.stringify(message)); - if (message.subtype === "file_share" && this.shouldFetchContent(message.file)) { - return this.enablePublicSharing(message.file, token) - .then((file) => { - message.file = file; - - if (this.shouldFetchContent(message.file)) { - return this.fetchFileContent(message.file, token) - .then((content) => { - message.file._content = content; - return message; - }); - } - - return message; - }); + if (messages.subtype !== "file_share") { + return message; } - 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) => { + console.error("Failed to get file content: ", err); + }); }); } From 45b2608e630572cc7699d83e1001a3d864af6ce3 Mon Sep 17 00:00:00 2001 From: Will Hunt Date: Thu, 27 Sep 2018 21:16:58 +0100 Subject: [PATCH 31/55] Add size to all file types --- lib/BridgedRoom.js | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/lib/BridgedRoom.js b/lib/BridgedRoom.js index 42708d0a..874a051e 100644 --- a/lib/BridgedRoom.js +++ b/lib/BridgedRoom.js @@ -341,7 +341,8 @@ const slackImageToMatrixImage = function(file, url, thumbnail_url) { url: url, body: file.title, info: { - mimetype: file.mimetype + mimetype: file.mimetype, + size: file.size, } }; @@ -384,7 +385,8 @@ const slackImageToMatrixVideo = function(file, url, thumbnail_url) { url: url, body: file.title, info: { - mimetype: file.mimetype + mimetype: file.mimetype, + size: file.size, } }; @@ -419,7 +421,8 @@ const slackImageToMatrixAudio = function(file, url) { url: url, body: file.title, info: { - mimetype: file.mimetype + mimetype: file.mimetype, + size: file.size, } }; }; @@ -432,10 +435,6 @@ const slackImageToMatrixAudio = function(file, url) { * @return {Object} Matrix event content, as per https://matrix.org/docs/spec/#m-file */ const slackFileToMatrixMessage = function(file, url, thumbnail_url) { - if (file.size) { - message.info.size = file.size; - } - if (file.mimetype) { if (file.mimetype.startsWith("image/")) { return slackImageToMatrixImage(file, url, thumbnail_url); @@ -451,7 +450,8 @@ const slackFileToMatrixMessage = function(file, url, thumbnail_url) { url: url, body: file.title, info: { - mimetype: file.mimetype + mimetype: file.mimetype, + size: file.size, } }; return message; From b0fe94de7f51c9df7c4f28b96eaf17e1d0266113 Mon Sep 17 00:00:00 2001 From: Will Hunt Date: Thu, 27 Sep 2018 21:17:48 +0100 Subject: [PATCH 32/55] Document size --- lib/BridgedRoom.js | 3 +++ 1 file changed, 3 insertions(+) diff --git a/lib/BridgedRoom.js b/lib/BridgedRoom.js index 874a051e..f639d68d 100644 --- a/lib/BridgedRoom.js +++ b/lib/BridgedRoom.js @@ -321,6 +321,7 @@ BridgedRoom.prototype.leaveGhosts = function(ghosts) { * Converts a slack image attachment to a matrix image event. * * @param {Object} file The slack image attachment file object. + * @param {?integer} file.size size of the file in bytes. * @param {string} file.title alt-text for the file. * @param {string} file.mimetype mime-type of the file. * @param {?integer} file.original_w width of the file if an image, in pixels. @@ -371,6 +372,7 @@ const slackImageToMatrixImage = function(file, url, thumbnail_url) { * Converts a slack video attachment to a matrix video event. * * @param {Object} file The slack video attachment file object. + * @param {?integer} file.size size of the file in bytes. * @param {string} file.title alt-text for the file. * @param {string} file.mimetype mime-type of the file. * @param {?integer} file.original_w width of the file if an image, in pixels. @@ -410,6 +412,7 @@ const slackImageToMatrixVideo = function(file, url, thumbnail_url) { * Converts a slack audio attachment to a matrix audio event. * * @param {Object} file The slack audio attachment file object. + * @param {?integer} file.size size of the file in bytes. * @param {string} file.title alt-text for the file. * @param {string} file.mimetype mime-type of the file. * @param {string} url The matrix file mxc. From 44e3b7db9c0268b66f1c36fb15f731c8a789d199 Mon Sep 17 00:00:00 2001 From: Will Hunt Date: Thu, 27 Sep 2018 21:41:50 +0100 Subject: [PATCH 33/55] Let's try using request promise --- lib/SlackGhost.js | 25 ++++++------------------- 1 file changed, 6 insertions(+), 19 deletions(-) diff --git a/lib/SlackGhost.js b/lib/SlackGhost.js index 53ac5ba3..84e324b5 100644 --- a/lib/SlackGhost.js +++ b/lib/SlackGhost.js @@ -175,25 +175,12 @@ SlackGhost.prototype.sendMessage = function(room_id, msg) { }; SlackGhost.prototype.uploadContentFromURI = function(file, uri, token) { - return new Promise((resolve, reject) => { - var options = url.parse(uri); - options.headers = { - Authorization: 'Bearer ' + token - }; - const req = https.get(options, (res) => { - let buffer = Buffer.alloc(0); - - res.on("data", (d) => { - buffer = Buffer.concat([buffer, d]); - }); - - res.on("end", () => { - resolve(buffer); - }); - }); - req.on("error", (err) => { - reject("Failed to download"); - }); + return rp({ + uri: uri, + qs: { + token: token, + }, + encoding: null, // Because we expect a binary }).then((buffer) => { file._content = buffer; return this.uploadContent(file); From 1ffd7b00fdc6a29636451b3874180aec01f5d306 Mon Sep 17 00:00:00 2001 From: Will Hunt Date: Thu, 27 Sep 2018 21:59:31 +0100 Subject: [PATCH 34/55] Try setting the header directly --- lib/SlackGhost.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/lib/SlackGhost.js b/lib/SlackGhost.js index 84e324b5..938fa29e 100644 --- a/lib/SlackGhost.js +++ b/lib/SlackGhost.js @@ -177,8 +177,8 @@ SlackGhost.prototype.sendMessage = function(room_id, msg) { SlackGhost.prototype.uploadContentFromURI = function(file, uri, token) { return rp({ uri: uri, - qs: { - token: token, + headers:{ + Authorization: 'Bearer ' + this._slack_bot_token }, encoding: null, // Because we expect a binary }).then((buffer) => { @@ -190,7 +190,7 @@ SlackGhost.prototype.uploadContentFromURI = function(file, uri, token) { console.log("UploadContent", "Failed to upload content:\n%s", reason); throw reason; }); -} +}; SlackGhost.prototype.uploadContent = function(file) { return this.getIntent().getClient().uploadContent({ From 0fbd4d71052bb353e2d015da0c5081f28a404674 Mon Sep 17 00:00:00 2001 From: Will Hunt Date: Thu, 27 Sep 2018 22:02:23 +0100 Subject: [PATCH 35/55] Oops --- lib/SlackGhost.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/SlackGhost.js b/lib/SlackGhost.js index 938fa29e..77ecabfd 100644 --- a/lib/SlackGhost.js +++ b/lib/SlackGhost.js @@ -177,8 +177,8 @@ SlackGhost.prototype.sendMessage = function(room_id, msg) { SlackGhost.prototype.uploadContentFromURI = function(file, uri, token) { return rp({ uri: uri, - headers:{ - Authorization: 'Bearer ' + this._slack_bot_token + headers: { + Authorization: `Bearer ${token}`, }, encoding: null, // Because we expect a binary }).then((buffer) => { From c5ca926ba8eeb44e050e1158e121dac00b1e394e Mon Sep 17 00:00:00 2001 From: Will Hunt Date: Thu, 27 Sep 2018 22:06:42 +0100 Subject: [PATCH 36/55] Thumbnails --- lib/BridgedRoom.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/lib/BridgedRoom.js b/lib/BridgedRoom.js index f639d68d..5399e859 100644 --- a/lib/BridgedRoom.js +++ b/lib/BridgedRoom.js @@ -262,13 +262,13 @@ BridgedRoom.prototype._handleSlackMessage = function(message, ghost) { } else if (message.files) { for (var i = 0; i < message.files.length; i++) { - var file = message.files[i]; + const file = message.files[i]; // We also need to upload the thumbnail let thumbnail_promise = Promise.resolve(); // Slack ain't a believer in consistency. - const thumb_uri = file.thumb_360 || file.thumb_video; + const thumb_uri = file.thumb_video || file.thumb_360; if (thumb_uri) { - const thumbnail_promise = ghost.uploadContentFromURI( + thumbnail_promise = ghost.uploadContentFromURI( { // Yes, we hardcode jpeg. Slack always use em. title: `${file.name}_thumb.jpeg`, From 1ae5daa1162058db521a7e53d95e63bd00c8cd7e Mon Sep 17 00:00:00 2001 From: Will Hunt Date: Thu, 27 Sep 2018 22:14:10 +0100 Subject: [PATCH 37/55] indenting --- lib/AdminCommands.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/AdminCommands.js b/lib/AdminCommands.js index ed0888d1..fab5d3d1 100644 --- a/lib/AdminCommands.js +++ b/lib/AdminCommands.js @@ -154,7 +154,7 @@ adminCommands.link = new AdminCommand({ (room) => { respond("Room is now " + room.getStatus()); if (room.getSlackWebhookUri()) { - respond("Inbound URL is " + main.getInboundUrlForRoom(room)); + respond("Inbound URL is " + main.getInboundUrlForRoom(room)); } }, (e) => { respond("Cannot link - " + e ) } From c567927de38800800f1550a0da3fd989f9f5b870 Mon Sep 17 00:00:00 2001 From: Will Hunt Date: Thu, 27 Sep 2018 22:14:26 +0100 Subject: [PATCH 38/55] We want to fetch all content --- lib/BaseSlackHandler.js | 7 ------- 1 file changed, 7 deletions(-) diff --git a/lib/BaseSlackHandler.js b/lib/BaseSlackHandler.js index 40c81023..2558468f 100644 --- a/lib/BaseSlackHandler.js +++ b/lib/BaseSlackHandler.js @@ -105,13 +105,6 @@ BaseSlackHandler.prototype.replaceUserIdsWithNames = function (message, token) { }); }; -// Return true if we ought to fetch the content of the given file object -BaseSlackHandler.prototype.shouldFetchContent = function (file) { - if (!file) return false; - if (file.mimetype && file.mimetype.indexOf("image/") === 0) return true; - return false; -} - /** * Enables public sharing on the given file object. then fetches its content. * From 5ec13ef259ee06dade630060ae326dd342963e14 Mon Sep 17 00:00:00 2001 From: Will Hunt Date: Thu, 27 Sep 2018 22:14:37 +0100 Subject: [PATCH 39/55] Make some regexs constants --- lib/BaseSlackHandler.js | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/lib/BaseSlackHandler.js b/lib/BaseSlackHandler.js index 2558468f..501f0edb 100644 --- a/lib/BaseSlackHandler.js +++ b/lib/BaseSlackHandler.js @@ -5,6 +5,11 @@ const Promise = require('bluebird'); const promiseWhile = require("./promiseWhile"); const getSlackFileUrl = require("./substitutions").getSlackFileUrl; +const CHANNEL_ID_REGEX = /<#(\w+)\|?\w*?>/g; + +// (if this is an emote msg, the format is <@ID|nick>, but in normal msgs it's just <@ID> +const USER_ID_REGEX = /<@(\w+)\|?\w*?>/g; + /** * @constructor * @param {Main} main the toplevel bridge instance through which to @@ -18,7 +23,7 @@ BaseSlackHandler.prototype.replaceChannelIdsWithNames = function (message, token var main = this._main; // match all channelIds - var testForName = message.text.match(/<#(\w+)\|?\w*?>/g); + var testForName = message.text.match(CHANNEL_ID_REGEX); var iteration = 0; var matches = 0; if (testForName && testForName.length) { @@ -53,7 +58,7 @@ BaseSlackHandler.prototype.replaceChannelIdsWithNames = function (message, token console.log("Caught error " + err); }); }).then(() => { - // Notice we can chain it because it's a Promise, + // Notice we can chain it because it's a Promise, // this will run after completion of the promiseWhile Promise! return message; }); @@ -63,7 +68,7 @@ BaseSlackHandler.prototype.replaceUserIdsWithNames = function (message, token) { var main = this._main; // match all userIds - var testForName = message.text.match(/<@(\w+)\|?\w*?>/g); + var testForName = message.text.match(USER_ID_REGEX); var iteration = 0; var matches = 0; if (testForName && testForName.length) { @@ -73,8 +78,7 @@ BaseSlackHandler.prototype.replaceUserIdsWithNames = function (message, token) { // Condition for stopping return iteration < matches; }, function () { - // foreach userId, pull out the ID - // (if this is an emote msg, the format is <@ID|nick>, but in normal msgs it's just <@ID> + // foreach userId, pull out the ID var id = testForName[iteration].match(/<@(\w+)\|?\w*?>/)[1]; var channelsInfoApiParams = { uri: 'https://slack.com/api/users.info', @@ -99,7 +103,7 @@ BaseSlackHandler.prototype.replaceUserIdsWithNames = function (message, token) { console.log("Caught error " + err); }); }).then(() => { - // Notice we can chain it because it's a Promise, + // Notice we can chain it because it's a Promise, // this will run after completion of the promiseWhile Promise! return message; }); From 3f70865b4275dfd0b11592e11d37dcc28c30a385 Mon Sep 17 00:00:00 2001 From: Will Hunt Date: Thu, 27 Sep 2018 22:14:56 +0100 Subject: [PATCH 40/55] Tidy readme up --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 5664dea1..0b9f4fd9 100644 --- a/README.md +++ b/README.md @@ -136,7 +136,8 @@ and bot users. This allows you to link as many channels as you would like with o In order to make Slack files visible to matrix users, this bridge will make Slack files visible to anyone with the url (including files in private channels). This is different then the current behavior in Slack, which only allows authenticated access to media - posted in private channels. + posted in private channels. See [MSC701](https://github.com/matrix-org/matrix-doc/issues/701) + for details. 6. Click on `Install App` and `Install App to Workspace`. Note the access tokens show. You will need the `Bot User OAuth Access Token` and if you want to bridge files, the @@ -151,7 +152,6 @@ and bot users. This allows you to link as many channels as you would like with o ``` /invite @bot-user-name - ``` You will also need to determine the "channel ID" that Slack uses to identify From 24d94dd453d87b64019a4a5b6fe3ca0805c9a0c8 Mon Sep 17 00:00:00 2001 From: Will Hunt Date: Thu, 27 Sep 2018 22:29:36 +0100 Subject: [PATCH 41/55] Responses to feedback --- lib/BaseSlackHandler.js | 10 ++--- lib/BridgedRoom.js | 16 +++---- lib/Main.js | 8 ++-- lib/SlackEventHandler.js | 4 +- lib/SlackGhost.js | 2 +- lib/SlackHookHandler.js | 91 ++++++++++++++++++++-------------------- 6 files changed, 66 insertions(+), 65 deletions(-) diff --git a/lib/BaseSlackHandler.js b/lib/BaseSlackHandler.js index 501f0edb..4478ddf9 100644 --- a/lib/BaseSlackHandler.js +++ b/lib/BaseSlackHandler.js @@ -147,12 +147,10 @@ BaseSlackHandler.prototype.enablePublicSharing = function (file, token) { BaseSlackHandler.prototype.fetchFileContent = function (file, token) { if (!file) return Promise.resolve(); - // if (file.is_public) { - var url = getSlackFileUrl(file); - if (!url) url = file.permalink_public; - // } else { - // url = file.url_private - // } + const url = getSlackFileUrl(file) || file.permalink_public; + if (!url) { + return Promise,reject("File doesn't have any URLs we can use."); + } return rp({ uri: url, diff --git a/lib/BridgedRoom.js b/lib/BridgedRoom.js index 5399e859..ee74b6f0 100644 --- a/lib/BridgedRoom.js +++ b/lib/BridgedRoom.js @@ -79,7 +79,7 @@ BridgedRoom.prototype.getMatrixRoomId = function() { }; BridgedRoom.prototype.getSlackTeamDomain = function() { - return this._slack_team_domain; + return this._slack_team_domain; }; BridgedRoom.prototype.getSlackTeamId = function() { @@ -115,8 +115,8 @@ BridgedRoom.prototype.updateSlackWebhookUri = function(slack_webhook_uri) { }; BridgedRoom.prototype.updateSlackBotToken = function(slack_bot_token) { - if (this._slack_bot_token !== slack_bot_token) this._dirty = true; - this._slack_bot_token = slack_bot_token; + if (this._slack_bot_token !== slack_bot_token) this._dirty = true; + this._slack_bot_token = slack_bot_token; }; BridgedRoom.prototype.updateSlackUserToken = function(slack_user_token) { @@ -125,12 +125,12 @@ BridgedRoom.prototype.updateSlackUserToken = function(slack_user_token) { }; BridgedRoom.prototype.updateSlackTeamDomain = function(domain) { - if (this._slack_team_domain !== domain) this._dirty = true; - this._slack_team_domain = domain; + if (this._slack_team_domain !== domain) this._dirty = true; + this._slack_team_domain = domain; }; BridgedRoom.prototype.updateAccessToken = function(token, scopes) { - console.log('updateAccessToken ->', token, scopes); + console.log('updateAccessToken ->', token, scopes); if (this._access_token === token && this._access_scopes.sort().join(",") === scopes.sort().join(",")) return; @@ -468,7 +468,7 @@ BridgedRoom.prototype.getMatrixATime = function() { return this._matrixAtime; }; -BridgedRoom.prototype.lookupAndSetTeamInfo = function() { +BridgedRoom.prototype.refreshTeamInfo = function() { if (!this._slack_bot_token) return Promise.resolve(); return rp({ @@ -492,7 +492,7 @@ BridgedRoom.prototype.lookupAndSetTeamInfo = function() { }); }; -BridgedRoom.prototype.lookupAndSetUserInfo = function() { +BridgedRoom.prototype.refreshUserInfo = function() { if (!this._slack_bot_token) return Promise.resolve(); return rp({ diff --git a/lib/Main.js b/lib/Main.js index 47b08114..e5b7f17e 100644 --- a/lib/Main.js +++ b/lib/Main.js @@ -199,7 +199,7 @@ Main.prototype.getTeamDomainForMessage = function(message) { return Promise.resolve(message.team_domain); } if (this._teams.has(message.team_id)) { - return Promise.resolve(this._teams.get(message.team_id).domain); + return Promise.resolve(this._teams.get(message.team_id).domain); } const room = this.getRoomBySlackChannelId(message.channel); @@ -304,7 +304,8 @@ Main.prototype.addBridgedRoom = function(room) { var rooms = this._roomsBySlackTeamId[team_id]; if (!rooms) { this._roomsBySlackTeamId[team_id] = [ room ]; - } else { + } + else { rooms.push(room); } } @@ -450,6 +451,7 @@ Main.prototype.onMatrixEvent = function(ev) { // A membership event about myself var membership = ev.content.membership; if (membership === "invite") { + // Automatically accept all invitations // NOTE: This can race and fail if the invite goes down the AS stream // before the homeserver believes we can actually join the room. this.getBotIntent().join(ev.room_id); @@ -596,7 +598,7 @@ Main.prototype.actionLink = function(opts) { if (opts.slack_bot_token) { room.updateSlackBotToken(opts.slack_bot_token); - return Promise.all([ room.lookupAndSetTeamInfo(), room.lookupAndSetUserInfo() ]) + return Promise.all([ room.refreshTeamInfo(), room.refreshUserInfo() ]) .then(() => { if (isNew) this.addBridgedRoom(room); diff --git a/lib/SlackEventHandler.js b/lib/SlackEventHandler.js index 00d7809f..7740c7cb 100644 --- a/lib/SlackEventHandler.js +++ b/lib/SlackEventHandler.js @@ -33,7 +33,7 @@ util.inherits(SlackEventHandler, BaseSlackHandler); */ SlackEventHandler.prototype.handle = function (params, response) { try { - console.log("Received slack event:", JSON.stringify(params)); + console.log("Received slack event:", params); var main = this._main; @@ -84,7 +84,7 @@ SlackEventHandler.prototype.handle = function (params, response) { } ); } catch (e) { - console.log("Oops - SlackEventHandler failed:", e); + console.error("SlackEventHandler failed:", e); } // return 200 so slack doesn't keep sending the event diff --git a/lib/SlackGhost.js b/lib/SlackGhost.js index 77ecabfd..15f757d7 100644 --- a/lib/SlackGhost.js +++ b/lib/SlackGhost.js @@ -6,7 +6,7 @@ var rp = require('request-promise'); var slackdown = require('Slackdown'); var substitutions = require("./substitutions"); -// How long in msec to cache user infor lookups from slack +// How long in milliseconds to cache user info lookups. var USER_CACHE_TIMEOUT = 10 * 60 * 1000; // 10 minutes function SlackGhost(opts) { diff --git a/lib/SlackHookHandler.js b/lib/SlackHookHandler.js index 058a27f4..65560e3b 100644 --- a/lib/SlackHookHandler.js +++ b/lib/SlackHookHandler.js @@ -1,15 +1,16 @@ "use strict"; -var substitutions = require("./substitutions"); -var SlackEventHandler = require('./SlackEventHandler'); -var BaseSlackHandler = require('./BaseSlackHandler'); -var rp = require('request-promise'); -var qs = require("querystring"); -var Promise = require('bluebird'); -var promiseWhile = require("./promiseWhile"); -var util = require("util"); - -var PRESERVE_KEYS = [ +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 PRESERVE_KEYS = [ "team_domain", "team_id", "channel_name", "channel_id", "user_name", "user_id", @@ -36,10 +37,9 @@ util.inherits(SlackHookHandler, BaseSlackHandler); * ready to accept requests */ SlackHookHandler.prototype.startAndListen = function(port, tls_config) { - var createServer; + let createServer; if (tls_config) { - var fs = require("fs"); - var tls_options = { + const tls_options = { key: fs.readFileSync(tls_config.key_file), cert: fs.readFileSync(tls_config.crt_file) }; @@ -52,43 +52,44 @@ SlackHookHandler.prototype.startAndListen = function(port, tls_config) { } return new Promise((resolve, reject) => { - createServer((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 - var 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) { - console.log("Oops - SlackHookHandler failed:", e); - - response.writeHead(500, {"Content-Type": "text/plain"}); - if (request.method !== "HEAD") { - response.write("Internal Server Error"); - } - response.end(); - } - }); - }).listen(port, () => { - var protocol = tls_config ? "https" : "http"; + createServer(this.onRequest.bind(this)).listen(port, () => { + const protocol = tls_config ? "https" : "http"; console.log("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) { + console.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. * @@ -105,7 +106,7 @@ SlackHookHandler.prototype.startAndListen = function(port, tls_config) { * formatted as a float. */ SlackHookHandler.prototype.handle = function(method, url, params, response) { - console.log("Received slack webhook " + method + " " + url + ": " + JSON.stringify(params)); + console.log("Received slack webhook " + method + " " + url + ": ", params); var main = this._main; From 8e856c3ca043a8696636d1e0dfda12e3993df4bf Mon Sep 17 00:00:00 2001 From: Will Hunt Date: Thu, 27 Sep 2018 22:37:54 +0100 Subject: [PATCH 42/55] indentation --- lib/Main.js | 2 +- lib/SlackEventHandler.js | 31 ++++++++++++++----------------- 2 files changed, 15 insertions(+), 18 deletions(-) diff --git a/lib/Main.js b/lib/Main.js index e5b7f17e..38eaaf29 100644 --- a/lib/Main.js +++ b/lib/Main.js @@ -451,7 +451,7 @@ Main.prototype.onMatrixEvent = function(ev) { // A membership event about myself var membership = ev.content.membership; if (membership === "invite") { - // Automatically accept all invitations + // Automatically accept all invitations // NOTE: This can race and fail if the invite goes down the AS stream // before the homeserver believes we can actually join the room. this.getBotIntent().join(ev.room_id); diff --git a/lib/SlackEventHandler.js b/lib/SlackEventHandler.js index 7740c7cb..81c969cc 100644 --- a/lib/SlackEventHandler.js +++ b/lib/SlackEventHandler.js @@ -66,23 +66,20 @@ SlackEventHandler.prototype.handle = function (params, response) { } result.then(() => endTimer({outcome: "success"})) - .catch((e) => { - if (e instanceof UnknownChannel) { - console.log("Ignoring message from unrecognised slack channel id : %s (%s)", - e.channel, params.team_id); - main.incCounter("received_messages", {side: "remote"}); - - endTimer({outcome: "dropped"}); - return; - } else if (e instanceof UnknownEvent) { - endTimer({outcome: "dropped"}); - } else { - endTimer({outcome: "fail"}); - } - - console.log("Failed: ", e); - } - ); + .catch((e) => { + if (e instanceof UnknownChannel) { + console.log("Ignoring message from unrecognised slack channel id : %s (%s)", + e.channel, params.team_id); + main.incCounter("received_messages", {side: "remote"}); + endTimer({outcome: "dropped"}); + return; + } else if (e instanceof UnknownEvent) { + endTimer({outcome: "dropped"}); + } else { + endTimer({outcome: "fail"}); + } + console.log("Failed: ", e); + }); } catch (e) { console.error("SlackEventHandler failed:", e); } From 1a36dc872b579c7a2c74476d395cf0f22bfba39c Mon Sep 17 00:00:00 2001 From: Will Hunt Date: Thu, 27 Sep 2018 22:38:02 +0100 Subject: [PATCH 43/55] Replace string regardless with ID if it fails. --- lib/BaseSlackHandler.js | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/lib/BaseSlackHandler.js b/lib/BaseSlackHandler.js index 4478ddf9..56245968 100644 --- a/lib/BaseSlackHandler.js +++ b/lib/BaseSlackHandler.js @@ -46,13 +46,15 @@ BaseSlackHandler.prototype.replaceChannelIdsWithNames = function (message, token }; main.incRemoteCallCounter("channels.info"); return rp(channelsInfoApiParams).then((response) => { + let name = id; if (response && response.channel && response.channel.name) { console.log("channels.info: " + id + " mapped to " + response.channel.name); - message.text = message.text.replace(/<#(\w+)\|?\w*?>/, "#" + response.channel.name); + name = response.channel.name; } else { console.log("channels.info returned no result for " + id); } + message.text = message.text.replace(/<#(\w+)\|?\w*?>/, "#" + name); iteration++; }).catch((err) => { console.log("Caught error " + err); @@ -90,14 +92,15 @@ BaseSlackHandler.prototype.replaceUserIdsWithNames = function (message, token) { }; main.incRemoteCallCounter("users.info"); return rp(channelsInfoApiParams).then((response) => { + let name = id; if (response && response.user && response.user.profile) { - var name = response.user.profile.display_name || response.user.profile.real_name; + name = response.user.profile.display_name || response.user.profile.real_name; console.log("users.info: " + id + " mapped to " + name); - message.text = message.text.replace(/<@(\w+)\|?\w*?>/, name); } else { console.log("users.info returned no result for " + id); } + message.text = message.text.replace(/<@(\w+)\|?\w*?>/, "#" + name); iteration++; }).catch((err) => { console.log("Caught error " + err); From 1f95220b11581c93f3d9bdfad093b6b258aaf9c2 Mon Sep 17 00:00:00 2001 From: Will Hunt Date: Thu, 27 Sep 2018 22:59:11 +0100 Subject: [PATCH 44/55] Move logic to correct function --- lib/BaseSlackHandler.js | 62 +++++++++++++++------- lib/SlackHookHandler.js | 115 ---------------------------------------- 2 files changed, 44 insertions(+), 133 deletions(-) diff --git a/lib/BaseSlackHandler.js b/lib/BaseSlackHandler.js index 56245968..8583acde 100644 --- a/lib/BaseSlackHandler.js +++ b/lib/BaseSlackHandler.js @@ -6,9 +6,11 @@ const promiseWhile = require("./promiseWhile"); const getSlackFileUrl = require("./substitutions").getSlackFileUrl; const CHANNEL_ID_REGEX = /<#(\w+)\|?\w*?>/g; +const CHANNEL_ID_REGEX_FIRST = /<#(\w+)\|?\w*?>/; // (if this is an emote msg, the format is <@ID|nick>, but in normal msgs it's just <@ID> const USER_ID_REGEX = /<@(\w+)\|?\w*?>/g; +const USER_ID_REGEX_FIRST = /<@(\w+)\|?\w*?>/; /** * @constructor @@ -19,11 +21,11 @@ function BaseSlackHandler(main) { this._main = main; } -BaseSlackHandler.prototype.replaceChannelIdsWithNames = function (message, token) { +SlackHookHandler.prototype.replaceChannelIdsWithNames = function(message, token) { var main = this._main; // match all channelIds - var testForName = message.text.match(CHANNEL_ID_REGEX); + var testForName = message.text.matchCHANNEL_ID_REGEX); var iteration = 0; var matches = 0; if (testForName && testForName.length) { @@ -35,7 +37,7 @@ BaseSlackHandler.prototype.replaceChannelIdsWithNames = function (message, token }, function () { // foreach channelId, pull out the ID // (if this is an emote msg, the format is <#ID|name>, but in normal msgs it's just <#ID> - var id = testForName[iteration].match(/<#(\w+)\|?\w*?>/)[1]; + var id = testForName[iteration].match(CHANNEL_ID_REGEX_FIRST)[1]; var channelsInfoApiParams = { uri: 'https://slack.com/api/channels.info', qs: { @@ -49,16 +51,16 @@ BaseSlackHandler.prototype.replaceChannelIdsWithNames = function (message, token let name = id; if (response && response.channel && response.channel.name) { console.log("channels.info: " + id + " mapped to " + response.channel.name); - name = response.channel.name; + id = response.channel.name; } else { console.log("channels.info returned no result for " + id); } - message.text = message.text.replace(/<#(\w+)\|?\w*?>/, "#" + name); + message.text = message.text.replace(CHANNEL_ID_REGEX_FIRST, "#" + name); iteration++; - }).catch((err) => { - console.log("Caught error " + err); - }); + }).catch((err) => { + console.log("Caught error " + err); + }); }).then(() => { // Notice we can chain it because it's a Promise, // this will run after completion of the promiseWhile Promise! @@ -66,7 +68,7 @@ BaseSlackHandler.prototype.replaceChannelIdsWithNames = function (message, token }); }; -BaseSlackHandler.prototype.replaceUserIdsWithNames = function (message, token) { +SlackHookHandler.prototype.replaceUserIdsWithNames = function(message, token) { var main = this._main; // match all userIds @@ -81,7 +83,8 @@ BaseSlackHandler.prototype.replaceUserIdsWithNames = function (message, token) { return iteration < matches; }, function () { // foreach userId, pull out the ID - var id = testForName[iteration].match(/<@(\w+)\|?\w*?>/)[1]; + // (if this is an emote msg, the format is <@ID|nick>, but in normal msgs it's just <@ID> + var id = testForName[iteration].match(USER_ID_REGEX_FIRST)[1]; var channelsInfoApiParams = { uri: 'https://slack.com/api/users.info', qs: { @@ -92,19 +95,41 @@ BaseSlackHandler.prototype.replaceUserIdsWithNames = function (message, token) { }; main.incRemoteCallCounter("users.info"); return rp(channelsInfoApiParams).then((response) => { - let name = id; - if (response && response.user && response.user.profile) { - name = response.user.profile.display_name || response.user.profile.real_name; - console.log("users.info: " + id + " mapped to " + name); + const user_id = main.getUserId(id, message.team_domain); + if (response && response.user && response.user.name) { + console.log("users.info: " + id + " mapped to " + response.user.name); + + const pill = `[${response.user.name}](https://matrix.to/#/${user_id})`; + message.text = message.text.replace(USER_ID_REGEX_FIRST, response.user.name); + message.markdownText = message.markdownText.replace( + USER_ID_REGEX_FIRST, + pill + ); + return; } - else { - console.log("users.info returned no result for " + id); + console.log(`users.info returned no result for ${id} Response:`, response); + // Fallback to checking the user store. + var store = this.getUserStore(); + return store.select({id: user_id}); + }).then((result) => { + if (result === undefined) { + return; } - message.text = message.text.replace(/<@(\w+)\|?\w*?>/, "#" + name); - iteration++; + let name = user_id; + console.log(`${user_id} did ${result.length > 0 ? "not" : ""} an entry`); + if (result.length) { + // It's possible not to have a displayname set. + name = result[0].display_name || result[0].id; + } + message.text = message.text.replace(USER_ID_REGEX_FIRST, name); + message.markdownText = message.markdownText.replace( + USER_ID_REGEX_FIRST, + `[${name}](https://matrix.to/#/${user_id})` + ); }).catch((err) => { console.log("Caught error " + err); }); + iteration++; }).then(() => { // Notice we can chain it because it's a Promise, // this will run after completion of the promiseWhile Promise! @@ -112,6 +137,7 @@ BaseSlackHandler.prototype.replaceUserIdsWithNames = function (message, token) { }); }; + /** * Enables public sharing on the given file object. then fetches its content. * diff --git a/lib/SlackHookHandler.js b/lib/SlackHookHandler.js index 2e3ed9d2..2d387655 100644 --- a/lib/SlackHookHandler.js +++ b/lib/SlackHookHandler.js @@ -286,121 +286,6 @@ SlackHookHandler.prototype.handleAuthorize = function(room, params) { ); }; -SlackHookHandler.prototype.replaceChannelIdsWithNames = function(message, token) { - var main = this._main; - - // match all channelIds - var testForName = message.text.match(/<#(\w+)\|?\w*?>/g); - var iteration = 0; - var matches = 0; - if (testForName && testForName.length) { - matches = testForName.length; - } - return promiseWhile(function() { - // Do this until there are no more channel ID matches - return iteration < matches; - }, function() { - // foreach channelId, pull out the ID - // (if this is an emote msg, the format is <#ID|name>, but in normal msgs it's just <#ID> - var id = testForName[iteration].match(/<#(\w+)\|?\w*?>/)[1]; - var channelsInfoApiParams = { - uri: 'https://slack.com/api/channels.info', - qs: { - token: token, - channel: id - }, - json: true - }; - main.incRemoteCallCounter("channels.info"); - return rp(channelsInfoApiParams).then((response) => { - if (response && response.channel && response.channel.name) { - console.log("channels.info: " + id + " mapped to " + response.channel.name); - message.text = message.text.replace(/<#(\w+)\|?\w*?>/, "#" + response.channel.name); - } - else { - console.log("channels.info returned no result for " + id); - } - iteration++; - }).catch((err) => { - console.log("Caught error " + err); - }); - }).then(() => { - // Notice we can chain it because it's a Promise, - // this will run after completion of the promiseWhile Promise! - return message; - }); -}; - -SlackHookHandler.prototype.replaceUserIdsWithNames = function(message, token) { - var main = this._main; - - // match all userIds - var testForName = message.text.match(/<@(\w+)\|?\w*?>/g); - var iteration = 0; - var matches = 0; - if (testForName && testForName.length) { - matches = testForName.length; - } - return promiseWhile(() => { - // Condition for stopping - return iteration < matches; - }, function() { - // foreach userId, pull out the ID - // (if this is an emote msg, the format is <@ID|nick>, but in normal msgs it's just <@ID> - var id = testForName[iteration].match(/<@(\w+)\|?\w*?>/)[1]; - var channelsInfoApiParams = { - uri: 'https://slack.com/api/users.info', - qs: { - token: token, - user: id - }, - json: true - }; - main.incRemoteCallCounter("users.info"); - return rp(channelsInfoApiParams).then((response) => { - const user_id = main.getUserId(id, message.team_domain); - if (response && response.user && response.user.name) { - console.log("users.info: " + id + " mapped to " + response.user.name); - - const pill = `[${response.user.name}](https://matrix.to/#/${user_id})`; - message.text = message.text.replace(/<@(\w+)\|?\w*?>/, response.user.name); - message.markdownText = message.markdownText.replace( - /<@(\w+)\|?\w*?>/, - pill - ); - return; - } - console.log(`users.info returned no result for ${id} Response:`, response); - // Fallback to checking the user store. - var store = this.getUserStore(); - return store.select({id: user_id}); - }).then((result) => { - if (result === undefined) { - return; - } - console.log(`${user_id} did ${result.length > 0 ? "not" : ""} an entry`); - if (result.length) { - // It's possible not to have a displayname set. - const name = result[0].display_name || result[0].id; - - const pill = `[${name}](https://matrix.to/#/${user_id})`; - message.text = message.text.replace(/<@(\w+)\|?\w*?>/, name); - message.markdownText = message.markdownText.replace( - /<@(\w+)\|?\w*?>/, - pill - ); - } - }).catch((err) => { - console.log("Caught error " + err); - }); - iteration++; - }).then(() => { - // Notice we can chain it because it's a Promise, - // this will run after completion of the promiseWhile Promise! - return message; - }); -}; - /** * Attempts to handle a message received from a slack webhook request. * From f3534e866650f079297e1fecd630b576a40d201f Mon Sep 17 00:00:00 2001 From: Will Hunt Date: Thu, 27 Sep 2018 23:04:23 +0100 Subject: [PATCH 45/55] Fix typos --- lib/BaseSlackHandler.js | 6 +++--- lib/Main.js | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/lib/BaseSlackHandler.js b/lib/BaseSlackHandler.js index 8583acde..98d9344b 100644 --- a/lib/BaseSlackHandler.js +++ b/lib/BaseSlackHandler.js @@ -21,11 +21,11 @@ function BaseSlackHandler(main) { this._main = main; } -SlackHookHandler.prototype.replaceChannelIdsWithNames = function(message, token) { +BaseSlackHandler.prototype.replaceChannelIdsWithNames = function(message, token) { var main = this._main; // match all channelIds - var testForName = message.text.matchCHANNEL_ID_REGEX); + var testForName = message.text.match(CHANNEL_ID_REGEX); var iteration = 0; var matches = 0; if (testForName && testForName.length) { @@ -68,7 +68,7 @@ SlackHookHandler.prototype.replaceChannelIdsWithNames = function(message, token) }); }; -SlackHookHandler.prototype.replaceUserIdsWithNames = function(message, token) { +BaseSlackHandler.prototype.replaceUserIdsWithNames = function(message, token) { var main = this._main; // match all userIds diff --git a/lib/Main.js b/lib/Main.js index 8c2542e0..8d77d075 100644 --- a/lib/Main.js +++ b/lib/Main.js @@ -7,7 +7,7 @@ var Bridge = bridgeLib.Bridge; var Metrics = bridgeLib.PrometheusMetrics; var StateLookup = bridgeLib.StateLookup; -var SlackHookHandler = require("./"); +var SlackHookHandler = require("./SlackHookHandler"); var BridgedRoom = require("./BridgedRoom"); var SlackGhost = require("./SlackGhost"); var MatrixUser = require("./MatrixUser"); // NB: this is not bridgeLib.MatrixUser ! From b9142dfe62033eaa3c5ea4f54821c5de64d15ede Mon Sep 17 00:00:00 2001 From: Will Hunt Date: Thu, 27 Sep 2018 23:09:09 +0100 Subject: [PATCH 46/55] Properly replace channel --- lib/BaseSlackHandler.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/BaseSlackHandler.js b/lib/BaseSlackHandler.js index 98d9344b..b65b4f45 100644 --- a/lib/BaseSlackHandler.js +++ b/lib/BaseSlackHandler.js @@ -51,7 +51,7 @@ BaseSlackHandler.prototype.replaceChannelIdsWithNames = function(message, token) let name = id; if (response && response.channel && response.channel.name) { console.log("channels.info: " + id + " mapped to " + response.channel.name); - id = response.channel.name; + name = response.channel.name; } else { console.log("channels.info returned no result for " + id); From e0b1a577260eaa28250375d9bbf45c3287452eb9 Mon Sep 17 00:00:00 2001 From: Will Hunt Date: Thu, 27 Sep 2018 23:13:26 +0100 Subject: [PATCH 47/55] Reinstate getUserId --- lib/Main.js | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/lib/Main.js b/lib/Main.js index 8d77d075..50aeb14d 100644 --- a/lib/Main.js +++ b/lib/Main.js @@ -223,6 +223,14 @@ Main.prototype.getTeamDomainForMessage = function(message) { }); } +Main.prototype.getUserId = function(id, team_domain) { + return [ + "@", this._config.username_prefix, team_domain.toLowerCase(), + "_", id.toUpperCase(), ":", this._config.homeserver.server_name + ].join(""); + +} + // Returns a Promise of a SlackGhost Main.prototype.getGhostForSlackMessage = function(message) { // Slack ghost IDs need to be constructed from user IDs, not usernames, @@ -232,10 +240,10 @@ Main.prototype.getGhostForSlackMessage = function(message) { // team_domain is gone, so we have to actually get the domain from a friendly object. return this.getTeamDomainForMessage(message).then((team_domain) => { - const user_id = [ - "@", this._config.username_prefix, team_domain.toLowerCase(), - "_", message.user_id.toUpperCase(), ":", this._config.homeserver.server_name - ].join(""); + const user_id = this.getUserId( + message.user_id.toUpperCase(), + team_domain.toLowerCase() + ); if (this._ghostsByUserId[user_id]) { return Promise.resolve(this._ghostsByUserId[user_id]); From 0d6a25ab9fff99ee102de334d8863a239c980d2e Mon Sep 17 00:00:00 2001 From: Will Hunt Date: Thu, 27 Sep 2018 23:22:05 +0100 Subject: [PATCH 48/55] Get the team for users --- lib/BaseSlackHandler.js | 13 ++++++++++--- lib/Main.js | 4 ++++ 2 files changed, 14 insertions(+), 3 deletions(-) diff --git a/lib/BaseSlackHandler.js b/lib/BaseSlackHandler.js index b65b4f45..fcf6625c 100644 --- a/lib/BaseSlackHandler.js +++ b/lib/BaseSlackHandler.js @@ -85,7 +85,7 @@ BaseSlackHandler.prototype.replaceUserIdsWithNames = function(message, token) { // foreach userId, pull out the ID // (if this is an emote msg, the format is <@ID|nick>, but in normal msgs it's just <@ID> var id = testForName[iteration].match(USER_ID_REGEX_FIRST)[1]; - var channelsInfoApiParams = { + var usersInfoApiParams = { uri: 'https://slack.com/api/users.info', qs: { token: token, @@ -94,8 +94,15 @@ BaseSlackHandler.prototype.replaceUserIdsWithNames = function(message, token) { json: true }; main.incRemoteCallCounter("users.info"); - return rp(channelsInfoApiParams).then((response) => { - const user_id = main.getUserId(id, message.team_domain); + let response; + return rp(usersInfoApiParams).then((res) => { + // Technically the function only requires the team_id, so + // pass in the response to get user instead. + // Though obviously don't if the response was wrong. + response = res; + return this.getTeamDomainForMessage(res && res.user ? res.user : message); + }).then((team_domain) => { + const user_id = main.getUserId(id, team_domain); if (response && response.user && response.user.name) { console.log("users.info: " + id + " mapped to " + response.user.name); diff --git a/lib/Main.js b/lib/Main.js index 50aeb14d..13f3b391 100644 --- a/lib/Main.js +++ b/lib/Main.js @@ -198,6 +198,10 @@ Main.prototype.getTeamDomainForMessage = function(message) { if (message.team_domain) { return Promise.resolve(message.team_domain); } + if (!message.team_id) { + return Promise.reject("Cannot determine team, no id given."); + } + if (this._teams.has(message.team_id)) { return Promise.resolve(this._teams.get(message.team_id).domain); } From 197e50c44d4a0bc79205fc4ebecc4efa48a1a62b Mon Sep 17 00:00:00 2001 From: Will Hunt Date: Thu, 27 Sep 2018 23:33:58 +0100 Subject: [PATCH 49/55] More team rubbish --- lib/BaseSlackHandler.js | 2 +- lib/SlackEventHandler.js | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/lib/BaseSlackHandler.js b/lib/BaseSlackHandler.js index fcf6625c..d3df72bd 100644 --- a/lib/BaseSlackHandler.js +++ b/lib/BaseSlackHandler.js @@ -100,7 +100,7 @@ BaseSlackHandler.prototype.replaceUserIdsWithNames = function(message, token) { // pass in the response to get user instead. // Though obviously don't if the response was wrong. response = res; - return this.getTeamDomainForMessage(res && res.user ? res.user : message); + return main.getTeamDomainForMessage(res && res.user ? res.user : message); }).then((team_domain) => { const user_id = main.getUserId(id, team_domain); if (response && response.user && response.user.name) { diff --git a/lib/SlackEventHandler.js b/lib/SlackEventHandler.js index 81c969cc..b8e07475 100644 --- a/lib/SlackEventHandler.js +++ b/lib/SlackEventHandler.js @@ -156,6 +156,7 @@ SlackEventHandler.prototype.handleMessageEvent = function (params) { var msg = Object.assign({}, params.event, { user_id: params.event.user || params.event.bot_id, team_domain: room.getSlackTeamDomain() || room.getSlackTeamId(), + team_id: params.team_id, channel_id: params.event.channel }); From 73814f98c6f7abe980a96bae4142d7f91b7a625c Mon Sep 17 00:00:00 2001 From: Will Hunt Date: Thu, 27 Sep 2018 23:38:25 +0100 Subject: [PATCH 50/55] Supply channel to getTeamDomainForMessage --- lib/BaseSlackHandler.js | 7 ++++++- lib/Main.js | 3 +++ 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/lib/BaseSlackHandler.js b/lib/BaseSlackHandler.js index d3df72bd..adf632d5 100644 --- a/lib/BaseSlackHandler.js +++ b/lib/BaseSlackHandler.js @@ -100,7 +100,12 @@ BaseSlackHandler.prototype.replaceUserIdsWithNames = function(message, token) { // pass in the response to get user instead. // Though obviously don't if the response was wrong. response = res; - return main.getTeamDomainForMessage(res && res.user ? res.user : message); + return main.getTeamDomainForMessage( + { + team_id: (res && res.user ? res.user : message).team_id, + channel: message.channel, + } + ); }).then((team_domain) => { const user_id = main.getUserId(id, team_domain); if (response && response.user && response.user.name) { diff --git a/lib/Main.js b/lib/Main.js index 13f3b391..6d43884a 100644 --- a/lib/Main.js +++ b/lib/Main.js @@ -198,6 +198,9 @@ Main.prototype.getTeamDomainForMessage = function(message) { if (message.team_domain) { return Promise.resolve(message.team_domain); } + + console.log(message); + if (!message.team_id) { return Promise.reject("Cannot determine team, no id given."); } From e17eb34d3f7c63c11cef34a16746a3524c47ea7a Mon Sep 17 00:00:00 2001 From: Will Hunt Date: Thu, 27 Sep 2018 23:59:02 +0100 Subject: [PATCH 51/55] dupe text field to markdownText --- lib/Main.js | 4 +--- lib/SlackEventHandler.js | 2 ++ 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/lib/Main.js b/lib/Main.js index 6d43884a..71231a9e 100644 --- a/lib/Main.js +++ b/lib/Main.js @@ -198,9 +198,7 @@ Main.prototype.getTeamDomainForMessage = function(message) { if (message.team_domain) { return Promise.resolve(message.team_domain); } - - console.log(message); - + if (!message.team_id) { return Promise.reject("Cannot determine team, no id given."); } diff --git a/lib/SlackEventHandler.js b/lib/SlackEventHandler.js index b8e07475..dc622ee6 100644 --- a/lib/SlackEventHandler.js +++ b/lib/SlackEventHandler.js @@ -203,6 +203,8 @@ SlackEventHandler.prototype.handleMessageEvent = function (params) { } else { result = Promise.resolve(); } + + msg.markdownText = msg.text; return result.then(() => msg) .then((msg) => this.replaceChannelIdsWithNames(msg, token)) From ec88b12a4973574fc401935700a5fa2a0a621a12 Mon Sep 17 00:00:00 2001 From: Will Hunt Date: Fri, 28 Sep 2018 00:01:51 +0100 Subject: [PATCH 52/55] Count iterations --- lib/BaseSlackHandler.js | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/lib/BaseSlackHandler.js b/lib/BaseSlackHandler.js index adf632d5..48240f9e 100644 --- a/lib/BaseSlackHandler.js +++ b/lib/BaseSlackHandler.js @@ -95,6 +95,7 @@ BaseSlackHandler.prototype.replaceUserIdsWithNames = function(message, token) { }; main.incRemoteCallCounter("users.info"); let response; + iteration++; return rp(usersInfoApiParams).then((res) => { // Technically the function only requires the team_id, so // pass in the response to get user instead. @@ -110,7 +111,6 @@ BaseSlackHandler.prototype.replaceUserIdsWithNames = function(message, token) { const user_id = main.getUserId(id, team_domain); if (response && response.user && response.user.name) { console.log("users.info: " + id + " mapped to " + response.user.name); - const pill = `[${response.user.name}](https://matrix.to/#/${user_id})`; message.text = message.text.replace(USER_ID_REGEX_FIRST, response.user.name); message.markdownText = message.markdownText.replace( @@ -141,7 +141,6 @@ BaseSlackHandler.prototype.replaceUserIdsWithNames = function(message, token) { }).catch((err) => { console.log("Caught error " + err); }); - iteration++; }).then(() => { // Notice we can chain it because it's a Promise, // this will run after completion of the promiseWhile Promise! From 2cd84cdefd1d3e27a39f57b5d6f7a4021ac219c6 Mon Sep 17 00:00:00 2001 From: Will Hunt Date: Fri, 28 Sep 2018 00:21:20 +0100 Subject: [PATCH 53/55] Slack markdown is just wrong --- lib/BaseSlackHandler.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/BaseSlackHandler.js b/lib/BaseSlackHandler.js index 48240f9e..4b7d6244 100644 --- a/lib/BaseSlackHandler.js +++ b/lib/BaseSlackHandler.js @@ -111,7 +111,7 @@ BaseSlackHandler.prototype.replaceUserIdsWithNames = function(message, token) { const user_id = main.getUserId(id, team_domain); if (response && response.user && response.user.name) { console.log("users.info: " + id + " mapped to " + response.user.name); - const pill = `[${response.user.name}](https://matrix.to/#/${user_id})`; + const pill = ``; message.text = message.text.replace(USER_ID_REGEX_FIRST, response.user.name); message.markdownText = message.markdownText.replace( USER_ID_REGEX_FIRST, @@ -136,7 +136,7 @@ BaseSlackHandler.prototype.replaceUserIdsWithNames = function(message, token) { message.text = message.text.replace(USER_ID_REGEX_FIRST, name); message.markdownText = message.markdownText.replace( USER_ID_REGEX_FIRST, - `[${name}](https://matrix.to/#/${user_id})` + `` ); }).catch((err) => { console.log("Caught error " + err); From d1a5cbf6cd1c50d545e41d2bf272c14058b9050e Mon Sep 17 00:00:00 2001 From: Will Hunt Date: Fri, 5 Oct 2018 11:09:30 +0100 Subject: [PATCH 54/55] Use Logging for Slack Events --- lib/BaseSlackHandler.js | 19 ++++++++++--------- lib/BridgedRoom.js | 33 ++------------------------------- lib/Main.js | 2 +- lib/SlackEventHandler.js | 15 ++++++++------- lib/SlackGhost.js | 2 +- lib/SlackHookHandler.js | 2 +- 6 files changed, 23 insertions(+), 50 deletions(-) diff --git a/lib/BaseSlackHandler.js b/lib/BaseSlackHandler.js index 4b7d6244..91cbbb72 100644 --- a/lib/BaseSlackHandler.js +++ b/lib/BaseSlackHandler.js @@ -4,6 +4,7 @@ const rp = require('request-promise'); const Promise = require('bluebird'); const promiseWhile = require("./promiseWhile"); const getSlackFileUrl = require("./substitutions").getSlackFileUrl; +const log = require("matrix-appservice-bridge").Logging.get("BaseSlackHandler"); const CHANNEL_ID_REGEX = /<#(\w+)\|?\w*?>/g; const CHANNEL_ID_REGEX_FIRST = /<#(\w+)\|?\w*?>/; @@ -50,16 +51,16 @@ BaseSlackHandler.prototype.replaceChannelIdsWithNames = function(message, token) return rp(channelsInfoApiParams).then((response) => { let name = id; if (response && response.channel && response.channel.name) { - console.log("channels.info: " + id + " mapped to " + response.channel.name); + log.info("channels.info: " + id + " mapped to " + response.channel.name); name = response.channel.name; } else { - console.log("channels.info returned no result for " + id); + log.info("channels.info returned no result for " + id); } message.text = message.text.replace(CHANNEL_ID_REGEX_FIRST, "#" + name); iteration++; }).catch((err) => { - console.log("Caught error " + err); + log.error("Caught error handling channels.info:" + err); }); }).then(() => { // Notice we can chain it because it's a Promise, @@ -110,7 +111,7 @@ BaseSlackHandler.prototype.replaceUserIdsWithNames = function(message, token) { }).then((team_domain) => { const user_id = main.getUserId(id, team_domain); if (response && response.user && response.user.name) { - console.log("users.info: " + id + " mapped to " + response.user.name); + log.info("users.info: " + id + " mapped to " + response.user.name); const pill = ``; message.text = message.text.replace(USER_ID_REGEX_FIRST, response.user.name); message.markdownText = message.markdownText.replace( @@ -119,7 +120,7 @@ BaseSlackHandler.prototype.replaceUserIdsWithNames = function(message, token) { ); return; } - console.log(`users.info returned no result for ${id} Response:`, response); + log.warn(`users.info returned no result for ${id} Response:`, response); // Fallback to checking the user store. var store = this.getUserStore(); return store.select({id: user_id}); @@ -128,7 +129,7 @@ BaseSlackHandler.prototype.replaceUserIdsWithNames = function(message, token) { return; } let name = user_id; - console.log(`${user_id} did ${result.length > 0 ? "not" : ""} an entry`); + log.info(`${user_id} did ${result.length > 0 ? "not" : ""} an entry`); if (result.length) { // It's possible not to have a displayname set. name = result[0].display_name || result[0].id; @@ -139,7 +140,7 @@ BaseSlackHandler.prototype.replaceUserIdsWithNames = function(message, token) { `` ); }).catch((err) => { - console.log("Caught error " + err); + log.error("Caught error handing users.info: " + err); }); }).then(() => { // Notice we can chain it because it's a Promise, @@ -170,7 +171,7 @@ BaseSlackHandler.prototype.enablePublicSharing = function (file, token) { json: true }).then((response) => { if (!response || !response.file || !response.file.permalink_public) { - console.log("Could not find sharedPublichURL: " + JSON.stringify(response)); + log.warn("Could not find sharedPublicURL: " + JSON.stringify(response)); return undefined; } @@ -198,7 +199,7 @@ BaseSlackHandler.prototype.fetchFileContent = function (file, token) { encoding: null }).then((response) => { var content = response.body; - console.log("Successfully fetched file " + file.id + + log.debug("Successfully fetched file " + file.id + " content (" + content.length + " bytes)"); return content; }); diff --git a/lib/BridgedRoom.js b/lib/BridgedRoom.js index 78107169..eb9a9627 100644 --- a/lib/BridgedRoom.js +++ b/lib/BridgedRoom.js @@ -131,7 +131,7 @@ BridgedRoom.prototype.updateSlackTeamDomain = function(domain) { }; BridgedRoom.prototype.updateAccessToken = function(token, scopes) { - console.log('updateAccessToken ->', token, scopes); + log.info('updateAccessToken ->', token, scopes); if (this._access_token === token && this._access_scopes.sort().join(",") === scopes.sort().join(",")) return; @@ -223,13 +223,8 @@ BridgedRoom.prototype.onMatrixMessage = function(message) { return rp(sendMessageParams).then((res) => { this._main.incCounter("sent_messages", {side: "remote"}); -<<<<<<< HEAD - if (!res || !res.ok) { - console.log("HTTP Error: ", res); -======= if (!res) { log.error("Outgoing message HTTP error: %s", res); ->>>>>>> origin/develop } }); }); @@ -270,7 +265,6 @@ BridgedRoom.prototype._handleSlackMessage = function(message, ghost) { var text = substitutions.slackToMatrix(message.text, message.file); return ghost.sendText(roomID, text); } -<<<<<<< HEAD else if (message.files) { for (var i = 0; i < message.files.length; i++) { const file = message.files[i]; @@ -297,29 +291,6 @@ BridgedRoom.prototype._handleSlackMessage = function(message, ghost) { return ghost.sendMessage( roomID, slackFileToMatrixMessage(file, content_uri, thumb_content_uri) -======= - else if (subtype === "file_share") { - if (!message.file) { - log.warn("Ignoring non-text non-image message: " + res); - return; - } - if (message.file._content) { - // TODO: Currently Matrix lacks a way to upload a "captioned image", - // so we just send a separate `m.image` and `m.text` message - // See https://github.com/matrix-org/matrix-doc/issues/906 - - // upload to media repo; get media repo URL back - return ghost.uploadContent(message.file).then((content_uri) => { - if(undefined == content_uri) { - // no URL returned from media repo; abort - return undefined; - } - var matrixMessage = slackImageToMatrixImage(message.file, content_uri); - return ghost.sendMessage(roomID, matrixMessage); - }).finally(() => { - var text = substitutions.slackToMatrix( - message.file.initial_comment.comment ->>>>>>> origin/develop ); }).then(() => { // TODO: Currently Matrix lacks a way to upload a "captioned image", @@ -536,7 +507,7 @@ BridgedRoom.prototype.refreshUserInfo = function() { }, json: true, }).then((response) => { - console.log(response); + log.debug("auth.test res:", response); if (!response.user_id) return; if (this._slack_user_id !== response.user_id) { diff --git a/lib/Main.js b/lib/Main.js index 267fa7e6..146f48d8 100644 --- a/lib/Main.js +++ b/lib/Main.js @@ -216,7 +216,7 @@ Main.prototype.getTeamDomainForMessage = function(message) { this.incRemoteCallCounter("team.info"); return rp(channelsInfoApiParams).then((response) => { if (!response.ok) { - console.error(`Trying to fetch the ${message.team_id} team.`, response); + log.error(`Trying to fetch the ${message.team_id} team.`, response); return Promise.resolve(); } log.info("Got new team:", response); diff --git a/lib/SlackEventHandler.js b/lib/SlackEventHandler.js index dc622ee6..214363b6 100644 --- a/lib/SlackEventHandler.js +++ b/lib/SlackEventHandler.js @@ -3,6 +3,7 @@ const BaseSlackHandler = require('./BaseSlackHandler'); const Promise = require('bluebird'); const util = require("util"); +const log = require("matrix-appservice-bridge").Logging.get("SlackEventHandler"); const UnknownEvent = function () { }; @@ -33,7 +34,7 @@ util.inherits(SlackEventHandler, BaseSlackHandler); */ SlackEventHandler.prototype.handle = function (params, response) { try { - console.log("Received slack event:", params); + log.debug("Received slack event:", params); var main = this._main; @@ -68,7 +69,7 @@ SlackEventHandler.prototype.handle = function (params, response) { result.then(() => endTimer({outcome: "success"})) .catch((e) => { if (e instanceof UnknownChannel) { - console.log("Ignoring message from unrecognised slack channel id : %s (%s)", + log.warn("Ignoring message from unrecognised slack channel id : %s (%s)", e.channel, params.team_id); main.incCounter("received_messages", {side: "remote"}); endTimer({outcome: "dropped"}); @@ -78,10 +79,10 @@ SlackEventHandler.prototype.handle = function (params, response) { } else { endTimer({outcome: "fail"}); } - console.log("Failed: ", e); + log.error("Failed to handle slack event: ", e); }); } catch (e) { - console.error("SlackEventHandler failed:", e); + log.error("SlackEventHandler.handle failed:", e); } // return 200 so slack doesn't keep sending the event @@ -170,7 +171,7 @@ SlackEventHandler.prototype.handleMessageEvent = function (params) { // 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. - console.log("no slack token for " + room.getSlackTeamDomain() || room.getSlackChannelId()); + log.warn("no slack token for " + room.getSlackTeamDomain() || room.getSlackChannelId()); return (processMsg) ? room.onSlackMessage(msg) : Promise.resolve(); } @@ -179,7 +180,7 @@ SlackEventHandler.prototype.handleMessageEvent = function (params) { // 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); - console.log("Did not understand event. " + JSON.stringify(message)); + log.warn("Did not understand event: ", JSON.stringify(message)); return Promise.resolve(); } @@ -203,7 +204,7 @@ SlackEventHandler.prototype.handleMessageEvent = function (params) { } else { result = Promise.resolve(); } - + msg.markdownText = msg.text; return result.then(() => msg) diff --git a/lib/SlackGhost.js b/lib/SlackGhost.js index 1b7b7bb3..27227689 100644 --- a/lib/SlackGhost.js +++ b/lib/SlackGhost.js @@ -191,7 +191,7 @@ SlackGhost.prototype.uploadContentFromURI = function(file, uri, token) { }).then((contentUri) => { return contentUri; }).catch((reason) => { - console.log("UploadContent", "Failed to upload content:\n%s", reason); + log.error("Failed to upload content:\n%s", reason); throw reason; }); }; diff --git a/lib/SlackHookHandler.js b/lib/SlackHookHandler.js index f0c266a6..09313a7a 100644 --- a/lib/SlackHookHandler.js +++ b/lib/SlackHookHandler.js @@ -82,7 +82,7 @@ SlackHookHandler.prototype.onRequest = function(request, response) { } } catch (e) { - console.error("SlackHookHandler failed:", e); + log.error("SlackHookHandler failed:", e); response.writeHead(500, {"Content-Type": "text/plain"}); if (request.method !== "HEAD") { response.write("Internal Server Error"); From 87c6dad337746975fbdd26e825cab41bffaa8b7a Mon Sep 17 00:00:00 2001 From: Will Hunt Date: Fri, 5 Oct 2018 11:47:42 +0100 Subject: [PATCH 55/55] Touchups --- README.md | 4 ++-- lib/AdminCommands.js | 4 ++-- lib/BaseSlackHandler.js | 2 +- lib/BridgedRoom.js | 7 +++---- 4 files changed, 8 insertions(+), 9 deletions(-) diff --git a/README.md b/README.md index 0b9f4fd9..36d17e1c 100644 --- a/README.md +++ b/README.md @@ -148,7 +148,7 @@ and bot users. This allows you to link as many channels as you would like with o 1. Create a Matrix room in the usual manner for your client. Take a note of its Matrix room ID - it will look something like `!aBcDeF:example.com`. - 2. invite the bot user to the Slack channel you would like to bridge. + 2. Invite the bot user to the Slack channel you would like to bridge. ``` /invite @bot-user-name @@ -157,7 +157,7 @@ and bot users. This allows you to link as many channels as you would like with o You will also need to determine the "channel ID" that Slack uses to identify the channel, which can be found in the url `https://XXX.slack.com/messages//`. - 2. Issue a ``link`` command in the administration control room with these + 3. Issue a ``link`` command in the administration control room with these collected values as arguments: with file bridging: diff --git a/lib/AdminCommands.js b/lib/AdminCommands.js index bb0a0e27..f3d2e640 100644 --- a/lib/AdminCommands.js +++ b/lib/AdminCommands.js @@ -131,11 +131,11 @@ adminCommands.link = new AdminCommand({ required: true, }, webhook_url: { - description: "Slack webhook URL. Used with slack outgoing hooks integration", + description: "Slack webhook URL. Used with Slack outgoing hooks integration", aliases: ['u'], }, slack_bot_token: { - description: "Slack bot user token. Used with slack bot user & Events api", + description: "Slack bot user token. Used with Slack bot user & Events api", aliases: ['t'], }, slack_user_token: { diff --git a/lib/BaseSlackHandler.js b/lib/BaseSlackHandler.js index 91cbbb72..5e23e4c7 100644 --- a/lib/BaseSlackHandler.js +++ b/lib/BaseSlackHandler.js @@ -9,7 +9,7 @@ const log = require("matrix-appservice-bridge").Logging.get("BaseSlackHandler"); const CHANNEL_ID_REGEX = /<#(\w+)\|?\w*?>/g; const CHANNEL_ID_REGEX_FIRST = /<#(\w+)\|?\w*?>/; -// (if this is an emote msg, the format is <@ID|nick>, but in normal msgs it's just <@ID> +// (if the message is an emote, the format is <@ID|nick>, but in normal msgs it's just <@ID> const USER_ID_REGEX = /<@(\w+)\|?\w*?>/g; const USER_ID_REGEX_FIRST = /<@(\w+)\|?\w*?>/; diff --git a/lib/BridgedRoom.js b/lib/BridgedRoom.js index eb9a9627..3c600be6 100644 --- a/lib/BridgedRoom.js +++ b/lib/BridgedRoom.js @@ -284,7 +284,8 @@ BridgedRoom.prototype._handleSlackMessage = function(message, ghost) { ); } let content_uri = ""; - return ghost.uploadContentFromURI(file, file.url_private, this._slack_bot_token).then((file_content_uri) => { + return ghost.uploadContentFromURI(file, file.url_private, this._slack_bot_token) + .then((file_content_uri) => { content_uri = file_content_uri; return thumbnail_promise; }).then((thumb_content_uri) => { @@ -297,9 +298,7 @@ BridgedRoom.prototype._handleSlackMessage = function(message, ghost) { // so we just send a separate `m.image` and `m.text` message // See https://github.com/matrix-org/matrix-doc/issues/906 if (message.text) { - var text = substitutions.slackToMatrix( - message.text - ); + const text = substitutions.slackToMatrix(message.text); return ghost.sendText(roomID, text); } });