bottender
Version:
A framework for building conversational user interfaces.
301 lines • 12.1 kB
JavaScript
"use strict";
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
return new (P || (P = Promise))(function (resolve, reject) {
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
step((generator = generator.apply(thisArg, _arguments || [])).next());
});
};
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
const crypto_1 = __importDefault(require("crypto"));
const invariant_1 = __importDefault(require("invariant"));
const p_props_1 = __importDefault(require("p-props"));
const warning_1 = __importDefault(require("warning"));
const messaging_api_slack_1 = require("messaging-api-slack");
const messaging_api_common_1 = require("messaging-api-common");
const SlackContext_1 = __importDefault(require("./SlackContext"));
const SlackEvent_1 = __importDefault(require("./SlackEvent"));
class SlackConnector {
constructor(options) {
const { verificationToken, skipLegacyProfile, includeBotMessages, signingSecret, } = options;
if ('client' in options) {
this._client = options.client;
}
else {
const { accessToken, origin } = options;
invariant_1.default(accessToken, 'Slack access token is required. Please make sure you have filled it correctly in `bottender.config.js` or `.env` file.');
this._client = new messaging_api_slack_1.SlackOAuthClient({
accessToken,
origin,
});
}
this._signingSecret = signingSecret || '';
this._verificationToken = verificationToken || '';
if (!this._signingSecret) {
if (!this._verificationToken) {
warning_1.default(false, 'Both `signingSecret` and `verificationToken` is not set. Will bypass Slack event verification.\nPass in `signingSecret` to perform Slack event verification.');
}
else {
warning_1.default(false, "It's deprecated to use `verificationToken` here, use `signingSecret` instead.");
}
}
this._skipLegacyProfile =
typeof skipLegacyProfile === 'boolean' ? skipLegacyProfile : true;
this._includeBotMessages = includeBotMessages || false;
}
_getRawEventFromRequest(body) {
if ('event' in body) {
return body.event;
}
if (body.payload && typeof body.payload === 'string') {
const payload = messaging_api_common_1.camelcaseKeysDeep(JSON.parse(body.payload));
if (payload.type === 'interactive_message') {
return payload;
}
return payload;
}
return body;
}
_isBotEventRequest(body) {
const rawEvent = this._getRawEventFromRequest(body);
return !!(rawEvent.botId ||
('subtype' in rawEvent && rawEvent.subtype === 'bot_message'));
}
get platform() {
return 'slack';
}
get client() {
return this._client;
}
_getChannelId(rawEvent) {
if (rawEvent.channel &&
typeof rawEvent.channel === 'object' &&
rawEvent.channel.id) {
return rawEvent.channel.id;
}
if (rawEvent.channelId) {
return rawEvent.channelId;
}
if (rawEvent.view && rawEvent.view.privateMetadata) {
const channelId = JSON.parse(rawEvent.view.privateMetadata).channelId;
if (channelId) {
return channelId;
}
}
if (rawEvent.item &&
typeof rawEvent.item === 'object' &&
typeof rawEvent.item.channel === 'string') {
return rawEvent.item.channel;
}
return (rawEvent.channel || rawEvent.channelId);
}
_getUserId(rawEvent) {
var _a;
if (rawEvent.type === 'interactive_message' ||
rawEvent.type === 'block_actions' ||
rawEvent.type === 'view_submission' ||
rawEvent.type === 'view_closed') {
return rawEvent.user.id;
}
return (rawEvent.user ||
rawEvent.userId || ((_a = rawEvent.user) === null || _a === void 0 ? void 0 : _a.id));
}
getUniqueSessionKey(body) {
const rawEvent = this._getRawEventFromRequest(body);
return this._getChannelId(rawEvent) || this._getUserId(rawEvent);
}
updateSession(session, body) {
return __awaiter(this, void 0, void 0, function* () {
if (this._isBotEventRequest(body)) {
return;
}
const rawEvent = this._getRawEventFromRequest(body);
const userId = this._getUserId(rawEvent);
const channelId = this._getChannelId(rawEvent);
if (typeof session.user === 'object' &&
session.user &&
session.user.id &&
session.user.id === userId) {
return;
}
if (!userId) {
return;
}
if (this._skipLegacyProfile) {
session.user = {
id: userId,
_updatedAt: new Date().toISOString(),
};
session.channel = {
id: channelId,
_updatedAt: new Date().toISOString(),
};
Object.freeze(session.user);
Object.defineProperty(session, 'user', {
configurable: false,
enumerable: true,
writable: false,
value: session.user,
});
Object.freeze(session.channel);
Object.defineProperty(session, 'channel', {
configurable: false,
enumerable: true,
writable: false,
value: session.channel,
});
return;
}
const promises = {
sender: this._client.getUserInfo(userId),
};
if (!session.channel ||
(session.channel.members &&
Array.isArray(session.channel.members) &&
session.channel.members.indexOf(userId) < 0)) {
if (channelId) {
promises.channel = this._client.getConversationInfo(channelId);
promises.channelMembers =
this._client.getAllConversationMembers(channelId);
}
}
if (!session.team ||
(session.team.members &&
Array.isArray(session.team.members) &&
session.team.members.indexOf(userId) < 0)) {
promises.allUsers = this._client.getAllUserList();
}
const results = yield p_props_1.default(promises);
session.user = Object.assign({ id: userId, _updatedAt: new Date().toISOString() }, results.sender);
Object.freeze(session.user);
Object.defineProperty(session, 'user', {
configurable: false,
enumerable: true,
writable: false,
value: session.user,
});
if (promises.channel) {
session.channel = Object.assign(Object.assign({}, results.channel), { members: results.channelMembers, _updatedAt: new Date().toISOString() });
Object.freeze(session.channel);
Object.defineProperty(session, 'channel', {
configurable: false,
enumerable: true,
writable: false,
value: session.channel,
});
}
if (promises.allUsers) {
session.team = {
members: results.allUsers,
_updatedAt: new Date().toISOString(),
};
Object.freeze(session.team);
Object.defineProperty(session, 'team', {
configurable: false,
enumerable: true,
writable: false,
value: session.team,
});
}
});
}
mapRequestToEvents(body) {
const rawEvent = this._getRawEventFromRequest(body);
if (!this._includeBotMessages && this._isBotEventRequest(body)) {
return [];
}
return [new SlackEvent_1.default(rawEvent)];
}
createContext(params) {
return new SlackContext_1.default(Object.assign(Object.assign({}, params), { client: this._client }));
}
verifySignature(tokenFromBody) {
const bufferFromBot = Buffer.from(this._verificationToken);
const bufferFromBody = Buffer.from(tokenFromBody);
if (bufferFromBot.length !== bufferFromBody.length) {
return false;
}
return crypto_1.default.timingSafeEqual(bufferFromBot, bufferFromBody);
}
verifySignatureBySigningSecret({ rawBody, signature, timestamp, }) {
const FIVE_MINUTES_IN_MILLISECONDS = 5 * 1000 * 60;
if (Math.abs(Date.now() - Number(timestamp) * 1000) >
FIVE_MINUTES_IN_MILLISECONDS) {
return false;
}
const SIGNATURE_VERSION = 'v0';
const signatureBaseString = `${SIGNATURE_VERSION}:${timestamp}:${rawBody}`;
const digest = crypto_1.default
.createHmac('sha256', this._signingSecret)
.update(signatureBaseString, 'utf8')
.digest('hex');
const calculatedSignature = `${SIGNATURE_VERSION}=${digest}`;
return crypto_1.default.timingSafeEqual(Buffer.from(signature, 'utf8'), Buffer.from(calculatedSignature, 'utf8'));
}
preprocess({ method, headers, body, rawBody, }) {
if (method.toLowerCase() !== 'post') {
return {
shouldNext: true,
};
}
const timestamp = headers['x-slack-request-timestamp'];
const signature = headers['x-slack-signature'];
if (this._signingSecret &&
!this.verifySignatureBySigningSecret({
rawBody,
timestamp,
signature,
})) {
const error = {
message: 'Slack Signing Secret Validation Failed!',
request: {
body,
},
};
return {
shouldNext: false,
response: {
status: 400,
body: { error },
},
};
}
const token = !body.token && body.payload && typeof body.payload === 'string'
? JSON.parse(body.payload).token
: body.token;
if (this._verificationToken && !this.verifySignature(token)) {
const error = {
message: 'Slack Verification Token Validation Failed!',
request: {
body,
},
};
return {
shouldNext: false,
response: {
status: 400,
body: { error },
},
};
}
if (body.type === 'url_verification') {
return {
shouldNext: false,
response: {
status: 200,
body: body.challenge,
},
};
}
return {
shouldNext: true,
};
}
}
exports.default = SlackConnector;
//# sourceMappingURL=SlackConnector.js.map