relu-core
Version:
615 lines (614 loc) • 26.4 kB
JavaScript
;
var OpenIdMetadata_1 = require("./OpenIdMetadata");
var utils = require("../utils");
var logger = require("../logger");
var consts = require("../consts");
var request = require("request");
var async = require("async");
var jwt = require("jsonwebtoken");
var zlib = require("zlib");
var urlJoin = require("url-join");
var pjson = require('../../package.json');
var MAX_DATA_LENGTH = 65000;
var USER_AGENT = "Microsoft-BotFramework/3.1 (BotBuilder Node.js/" + pjson.version + ")";
var ChatConnector = (function () {
function ChatConnector(settings) {
if (settings === void 0) { settings = {}; }
this.settings = settings;
if (!this.settings.endpoint) {
this.settings.endpoint = {
refreshEndpoint: 'https://login.microsoftonline.com/botframework.com/oauth2/v2.0/token',
refreshScope: 'https://api.botframework.com/.default',
botConnectorOpenIdMetadata: this.settings.openIdMetadata || 'https://login.botframework.com/v1/.well-known/openidconfiguration',
botConnectorIssuer: 'https://api.botframework.com',
botConnectorAudience: this.settings.appId,
msaOpenIdMetadata: 'https://login.microsoftonline.com/common/v2.0/.well-known/openid-configuration',
msaIssuer: 'https://sts.windows.net/72f988bf-86f1-41af-91ab-2d7cd011db47/',
msaAudience: 'https://graph.microsoft.com',
emulatorOpenIdMetadata: 'https://login.microsoftonline.com/botframework.com/v2.0/.well-known/openid-configuration',
emulatorAudience: 'https://sts.windows.net/d6d49420-f39b-4df7-a1dc-d59a935871db/',
emulatorIssuer: this.settings.appId,
stateEndpoint: this.settings.stateEndpoint || 'https://state.botframework.com'
};
}
this.botConnectorOpenIdMetadata = new OpenIdMetadata_1.OpenIdMetadata(this.settings.endpoint.botConnectorOpenIdMetadata);
this.msaOpenIdMetadata = new OpenIdMetadata_1.OpenIdMetadata(this.settings.endpoint.msaOpenIdMetadata);
this.emulatorOpenIdMetadata = new OpenIdMetadata_1.OpenIdMetadata(this.settings.endpoint.emulatorOpenIdMetadata);
}
ChatConnector.prototype.listen = function () {
var _this = this;
return function (req, res) {
if (req.body) {
_this.verifyBotFramework(req, res);
}
else {
var requestData = '';
req.on('data', function (chunk) {
requestData += chunk;
});
req.on('end', function () {
req.body = JSON.parse(requestData);
_this.verifyBotFramework(req, res);
});
}
};
};
ChatConnector.prototype.verifyBotFramework = function (req, res) {
var _this = this;
var token;
var isEmulator = req.body['channelId'] === 'emulator';
var authHeaderValue = req.headers ? req.headers['authorization'] || req.headers['Authorization'] : null;
if (authHeaderValue) {
var auth = authHeaderValue.trim().split(' ');
if (auth.length == 2 && auth[0].toLowerCase() == 'bearer') {
token = auth[1];
}
}
if (token) {
req.body['useAuth'] = true;
var decoded = jwt.decode(token, { complete: true });
var verifyOptions;
var openIdMetadata;
if (isEmulator && decoded.payload.iss == this.settings.endpoint.msaIssuer) {
openIdMetadata = this.msaOpenIdMetadata;
verifyOptions = {
issuer: this.settings.endpoint.msaIssuer,
audience: this.settings.endpoint.msaAudience,
clockTolerance: 300
};
}
else if (isEmulator && decoded.payload.iss == this.settings.endpoint.emulatorIssuer) {
openIdMetadata = this.emulatorOpenIdMetadata;
verifyOptions = {
issuer: this.settings.endpoint.emulatorIssuer,
audience: this.settings.endpoint.emulatorAudience,
clockTolerance: 300
};
}
else {
openIdMetadata = this.botConnectorOpenIdMetadata;
verifyOptions = {
issuer: this.settings.endpoint.botConnectorIssuer,
audience: this.settings.endpoint.botConnectorAudience,
clockTolerance: 300
};
}
if (isEmulator && decoded.payload.appid != this.settings.appId) {
logger.error('ChatConnector: receive - invalid token. Requested by unexpected app ID.');
res.status(403);
res.end();
return;
}
openIdMetadata.getKey(decoded.header.kid, function (key) {
if (key) {
try {
jwt.verify(token, key, verifyOptions);
}
catch (err) {
logger.error('ChatConnector: receive - invalid token. Check bot\'s app ID & Password.');
res.status(403);
res.end();
return;
}
_this.dispatch(req.body, res);
}
else {
logger.error('ChatConnector: receive - invalid signing key or OpenId metadata document.');
res.status(500);
res.end();
return;
}
});
}
else if (isEmulator && !this.settings.appId && !this.settings.appPassword) {
logger.warn(req.body, 'ChatConnector: receive - emulator running without security enabled.');
req.body['useAuth'] = false;
this.dispatch(req.body, res);
}
else {
logger.error('ChatConnector: receive - no security token sent.');
res.status(401);
res.end();
}
};
ChatConnector.prototype.onEvent = function (handler) {
this.onEventHandler = handler;
};
ChatConnector.prototype.onInvoke = function (handler) {
this.onInvokeHandler = handler;
};
ChatConnector.prototype.send = function (messages, done) {
var _this = this;
async.eachSeries(messages, function (msg, cb) {
try {
if (msg.address && msg.address.serviceUrl) {
_this.postMessage(msg, cb);
}
else {
logger.error('ChatConnector: send - message is missing address or serviceUrl.');
cb(new Error('Message missing address or serviceUrl.'));
}
}
catch (e) {
cb(e);
}
}, done);
};
ChatConnector.prototype.startConversation = function (address, done) {
if (address && address.user && address.bot && address.serviceUrl) {
var options = {
method: 'POST',
url: urlJoin(address.serviceUrl, '/v3/conversations'),
body: {
bot: address.bot,
members: [address.user]
},
json: true
};
this.authenticatedRequest(options, function (err, response, body) {
var adr;
if (!err) {
try {
var obj = typeof body === 'string' ? JSON.parse(body) : body;
if (obj && obj.hasOwnProperty('id')) {
adr = utils.clone(address);
adr.conversation = { id: obj['id'] };
if (adr.id) {
delete adr.id;
}
}
else {
err = new Error('Failed to start conversation: no conversation ID returned.');
}
}
catch (e) {
err = e instanceof Error ? e : new Error(e.toString());
}
}
if (err) {
logger.error('ChatConnector: startConversation - error starting conversation.');
}
done(err, adr);
});
}
else {
logger.error('ChatConnector: startConversation - address is invalid.');
done(new Error('Invalid address.'));
}
};
ChatConnector.prototype.getData = function (context, callback) {
var _this = this;
try {
var root = this.getStoragePath(context.address);
var list = [];
if (context.userId) {
if (context.persistUserData) {
list.push({
field: 'userData',
url: root + '/users/' + encodeURIComponent(context.userId)
});
}
if (context.conversationId) {
list.push({
field: 'privateConversationData',
url: root + '/conversations/' + encodeURIComponent(context.conversationId) +
'/users/' + encodeURIComponent(context.userId)
});
}
}
if (context.persistConversationData && context.conversationId) {
list.push({
field: 'conversationData',
url: root + '/conversations/' + encodeURIComponent(context.conversationId)
});
}
var data = {};
async.each(list, function (entry, cb) {
var options = {
method: 'GET',
url: entry.url,
json: true
};
_this.authenticatedRequest(options, function (err, response, body) {
if (!err && body) {
var botData = body.data ? body.data : {};
if (typeof botData === 'string') {
zlib.gunzip(new Buffer(botData, 'base64'), function (err, result) {
if (!err) {
try {
var txt = result.toString();
data[entry.field + 'Hash'] = txt;
data[entry.field] = JSON.parse(txt);
}
catch (e) {
err = e;
}
}
cb(err);
});
}
else {
try {
data[entry.field + 'Hash'] = JSON.stringify(botData);
data[entry.field] = botData;
}
catch (e) {
err = e;
}
cb(err);
}
}
else {
cb(err);
}
});
}, function (err) {
if (!err) {
callback(null, data);
}
else {
var m = err.toString();
callback(err instanceof Error ? err : new Error(m), null);
}
});
}
catch (e) {
callback(e instanceof Error ? e : new Error(e.toString()), null);
}
};
ChatConnector.prototype.saveData = function (context, data, callback) {
var _this = this;
var list = [];
function addWrite(field, botData, url) {
var hashKey = field + 'Hash';
var hash = JSON.stringify(botData);
if (!data[hashKey] || hash !== data[hashKey]) {
data[hashKey] = hash;
list.push({ botData: botData, url: url, hash: hash });
}
}
try {
var root = this.getStoragePath(context.address);
if (context.userId) {
if (context.persistUserData) {
addWrite('userData', data.userData || {}, root + '/users/' + encodeURIComponent(context.userId));
}
if (context.conversationId) {
var url = root + '/conversations/' + encodeURIComponent(context.conversationId) +
'/users/' + encodeURIComponent(context.userId);
addWrite('privateConversationData', data.privateConversationData || {}, url);
}
}
if (context.persistConversationData && context.conversationId) {
addWrite('conversationData', data.conversationData || {}, root + '/conversations/' + encodeURIComponent(context.conversationId));
}
async.each(list, function (entry, cb) {
if (_this.settings.gzipData) {
zlib.gzip(entry.hash, function (err, result) {
if (!err && result.length > MAX_DATA_LENGTH) {
err = new Error("Data of " + result.length + " bytes gzipped exceeds the " + MAX_DATA_LENGTH + " byte limit. Can't post to: " + entry.url);
err.code = consts.Errors.EMSGSIZE;
}
if (!err) {
var options = {
method: 'POST',
url: entry.url,
body: { eTag: '*', data: result.toString('base64') },
json: true
};
_this.authenticatedRequest(options, function (err, response, body) {
cb(err);
});
}
else {
cb(err);
}
});
}
else if (entry.hash.length < MAX_DATA_LENGTH) {
var options = {
method: 'POST',
url: entry.url,
body: { eTag: '*', data: entry.botData },
json: true
};
_this.authenticatedRequest(options, function (err, response, body) {
cb(err);
});
}
else {
var err = new Error("Data of " + entry.hash.length + " bytes exceeds the " + MAX_DATA_LENGTH + " byte limit. Consider setting connectors gzipData option. Can't post to: " + entry.url);
err.code = consts.Errors.EMSGSIZE;
cb(err);
}
}, function (err) {
if (callback) {
if (!err) {
callback(null);
}
else {
var m = err.toString();
callback(err instanceof Error ? err : new Error(m));
}
}
});
}
catch (e) {
if (callback) {
var err = e instanceof Error ? e : new Error(e.toString());
err.code = consts.Errors.EBADMSG;
callback(err);
}
}
};
ChatConnector.prototype.dispatch = function (msg, res) {
try {
this.prepIncomingMessage(msg);
logger.info(msg, 'ChatConnector: message received.');
if (this.isInvoke(msg)) {
this.onInvokeHandler(msg, function (err, body, status) {
if (err) {
res.status(500);
res.end();
logger.error('Received error from invoke handler: ', err.message || '');
}
else {
res.send(status || 200, body);
}
});
}
else {
this.onEventHandler([msg]);
res.status(202);
res.end();
}
}
catch (e) {
console.error(e instanceof Error ? e.stack : e.toString());
res.status(500);
res.end();
}
};
ChatConnector.prototype.isInvoke = function (message) {
return (message && message.type && message.type.toLowerCase() == consts.invokeType);
};
ChatConnector.prototype.postMessage = function (msg, cb) {
logger.info(address, 'ChatConnector: sending message.');
this.prepOutgoingMessage(msg);
var address = msg.address;
msg['from'] = address.bot;
msg['recipient'] = address.user;
delete msg.address;
var path = '/v3/conversations/' + encodeURIComponent(address.conversation.id) + '/activities';
if (address.id && address.channelId !== 'skype') {
path += '/' + encodeURIComponent(address.id);
}
var options = {
method: 'POST',
url: urlJoin(address.serviceUrl, path),
body: msg,
json: true
};
if (address.useAuth) {
this.authenticatedRequest(options, function (err, response, body) { return cb(err); });
}
else {
this.addUserAgent(options);
request(options, function (err, response, body) {
if (!err && response.statusCode >= 400) {
var txt = "Request to '" + options.url + "' failed: [" + response.statusCode + "] " + response.statusMessage;
err = new Error(txt);
}
cb(err);
});
}
};
ChatConnector.prototype.authenticatedRequest = function (options, callback, refresh) {
var _this = this;
if (refresh === void 0) { refresh = false; }
if (refresh) {
this.accessToken = null;
}
this.addAccessToken(options, function (err) {
if (!err) {
request(options, function (err, response, body) {
if (!err) {
switch (response.statusCode) {
case 401:
case 403:
if (!refresh) {
_this.authenticatedRequest(options, callback, true);
}
else {
callback(null, response, body);
}
break;
default:
if (response.statusCode < 400) {
callback(null, response, body);
}
else {
var txt = "Request to '" + options.url + "' failed: [" + response.statusCode + "] " + response.statusMessage;
callback(new Error(txt), response, null);
}
break;
}
}
else {
callback(err, null, null);
}
});
}
else {
callback(err, null, null);
}
});
};
ChatConnector.prototype.getAccessToken = function (cb) {
var _this = this;
if (!this.accessToken || new Date().getTime() >= this.accessTokenExpires) {
var opt = {
method: 'POST',
url: this.settings.endpoint.refreshEndpoint,
form: {
grant_type: 'client_credentials',
client_id: this.settings.appId,
client_secret: this.settings.appPassword,
scope: this.settings.endpoint.refreshScope
}
};
request(opt, function (err, response, body) {
if (!err) {
if (body && response.statusCode < 300) {
var oauthResponse = JSON.parse(body);
_this.accessToken = oauthResponse.access_token;
_this.accessTokenExpires = new Date().getTime() + ((oauthResponse.expires_in - 300) * 1000);
cb(null, _this.accessToken);
}
else {
cb(new Error('Refresh access token failed with status code: ' + response.statusCode), null);
}
}
else {
cb(err, null);
}
});
}
else {
cb(null, this.accessToken);
}
};
ChatConnector.prototype.addUserAgent = function (options) {
if (options.headers == null) {
options.headers = {};
}
options.headers['User-Agent'] = USER_AGENT;
};
ChatConnector.prototype.addAccessToken = function (options, cb) {
this.addUserAgent(options);
if (this.settings.appId && this.settings.appPassword) {
this.getAccessToken(function (err, token) {
if (!err && token) {
options.headers = {
'Authorization': 'Bearer ' + token
};
cb(null);
}
else {
cb(err);
}
});
}
else {
cb(null);
}
};
ChatConnector.prototype.getStoragePath = function (address) {
var path;
switch (address.channelId) {
case 'emulator':
if (address.serviceUrl) {
path = address.serviceUrl;
}
else {
throw new Error('ChatConnector.getStoragePath() missing address.serviceUrl.');
}
break;
default:
path = this.settings.endpoint.stateEndpoint;
break;
}
return path + '/v3/botstate/' + encodeURIComponent(address.channelId);
};
ChatConnector.prototype.prepIncomingMessage = function (msg) {
utils.moveFieldsTo(msg, msg, {
'locale': 'textLocale',
'channelData': 'sourceEvent'
});
msg.text = msg.text || '';
msg.attachments = msg.attachments || [];
msg.entities = msg.entities || [];
var address = {};
utils.moveFieldsTo(msg, address, toAddress);
msg.address = address;
msg.source = address.channelId;
if (msg.source == 'facebook' && msg.sourceEvent && msg.sourceEvent.message && msg.sourceEvent.message.quick_reply) {
msg.text = msg.sourceEvent.message.quick_reply.payload;
}
};
ChatConnector.prototype.prepOutgoingMessage = function (msg) {
if (msg.attachments) {
var attachments = [];
for (var i = 0; i < msg.attachments.length; i++) {
var a = msg.attachments[i];
switch (a.contentType) {
case 'application/vnd.microsoft.keyboard':
if (msg.address.channelId == 'facebook') {
msg.sourceEvent = { quick_replies: [] };
a.content.buttons.forEach(function (action) {
switch (action.type) {
case 'imBack':
case 'postBack':
msg.sourceEvent.quick_replies.push({
content_type: 'text',
title: action.title,
payload: action.value
});
break;
default:
logger.warn(msg, "Invalid keyboard '%s' button sent to facebook.", action.type);
break;
}
});
}
else {
a.contentType = 'application/vnd.microsoft.card.hero';
attachments.push(a);
}
break;
default:
attachments.push(a);
break;
}
}
msg.attachments = attachments;
}
utils.moveFieldsTo(msg, msg, {
'textLocale': 'locale',
'sourceEvent': 'channelData'
});
delete msg.agent;
delete msg.source;
};
return ChatConnector;
}());
exports.ChatConnector = ChatConnector;
var toAddress = {
'id': 'id',
'channelId': 'channelId',
'from': 'user',
'conversation': 'conversation',
'recipient': 'bot',
'serviceUrl': 'serviceUrl',
'useAuth': 'useAuth'
};