mangrove-botkit
Version:
Building blocks for Building Bots
474 lines (364 loc) • 16.5 kB
JavaScript
var Botkit = require(__dirname + '/CoreBot.js');
var request = require('request');
var url = require('url');
var crypto = require('crypto');
function Sparkbot(configuration) {
// Create a core botkit bot
var controller = Botkit(configuration || {});
if (!controller.config.ciscospark_access_token) {
throw new Error('ciscospark_access_token required to create controller');
} else {
controller.api = require('ciscospark').init({
credentials: {
authorization: {
access_token: controller.config.ciscospark_access_token
}
}
});
if (!controller.api) {
throw new Error('Could not create Cisco Spark API');
}
controller.api.people.get('me').then(function(identity) {
console.log('Cisco Spark: My identity is', identity);
controller.identity = identity;
}).catch(function(err) {
throw new Error(err);
});
}
if (!controller.config.public_address) {
throw new Error('public_address parameter required to receive webhooks');
} else {
var endpoint = url.parse(controller.config.public_address);
if (!endpoint.hostname) {
throw new Error('Could not determine hostname of public address: ' + controller.config.public_address);
} else if (endpoint.protocol != 'https:') {
throw new Error('Please specify an SSL-enabled url for your public address: ' + controller.config.public_address);
} else {
controller.config.public_address = endpoint.hostname + (endpoint.port ? ':' + endpoint.port : '');
}
}
if (!controller.config.secret) {
console.log('WARNING: No secret specified. Source of incoming webhooks will not be validated. https://developer.ciscospark.com/webhooks-explained.html#auth');
// throw new Error('secret parameter required to secure webhooks');
}
controller.resetWebhookSubscriptions = function() {
controller.api.webhooks.list().then(function(list) {
for (var i = 0; i < list.items.length; i++) {
controller.api.webhooks.remove(list.items[i]).then(function(res) {
console.log('Removed subscription: ' + list.items[i].name);
}).catch(function(err) {
console.log('Error removing subscription:', err);
});
}
});
};
// set up a web route for receiving outgoing webhooks and/or slash commands
controller.createWebhookEndpoints = function(webserver, bot, cb) {
var webhook_name = controller.config.webhook_name || 'Botkit Firehose';
controller.log(
'** Serving webhook endpoints for Cisco Spark Platform at: ' +
'http://' + controller.config.hostname + ':' + controller.config.port + '/ciscospark/receive');
webserver.post('/ciscospark/receive', function(req, res) {
res.sendStatus(200);
controller.handleWebhookPayload(req, res, bot);
});
var list = controller.api.webhooks.list().then(function(list) {
var hook_id = null;
for (var i = 0; i < list.items.length; i++) {
if (list.items[i].name == webhook_name) {
hook_id = list.items[i].id;
}
}
var hook_url = 'https://' + controller.config.public_address + '/ciscospark/receive';
console.log('Cisco Spark: incoming webhook url is ', hook_url);
if (hook_id) {
controller.api.webhooks.update({
id: hook_id,
resource: 'all',
targetUrl: hook_url,
event: 'all',
secret: controller.config.secret,
name: webhook_name,
}).then(function(res) {
console.log('Cisco Spark: SUCCESSFULLY UPDATED CISCO SPARK WEBHOOKS');
if (cb) cb();
}).catch(function(err) {
console.log('FAILED TO REGISTER WEBHOOK', err);
throw new Error(err);
});
} else {
controller.api.webhooks.create({
resource: 'all',
targetUrl: hook_url,
event: 'all',
secret: controller.config.secret,
name: webhook_name,
}).then(function(res) {
console.log('Cisco Spark: SUCCESSFULLY REGISTERED CISCO SPARK WEBHOOKS');
if (cb) cb();
}).catch(function(err) {
console.log('FAILED TO REGISTER WEBHOOK', err);
throw new Error(err);
});
}
});
};
controller.middleware.ingest.use(function limitUsers(bot, message, res, next) {
if (controller.config.limit_to_org) {
console.log('limit to org', controller.config.limit_to_org, message.raw_message.orgId);
if (!message.raw_message.orgId || message.raw_message.orgId != controller.config.limit_to_org) {
// this message is from a user outside of the proscribed org
console.log('WARNING: this message is from a user outside of the proscribed org', controller.config.limit_to_org);
return false;
}
}
if (controller.config.limit_to_domain) {
var domains = [];
if (typeof(controller.config.limit_to_domain) == 'string') {
domains = [controller.config.limit_to_domain];
} else {
domains = controller.config.limit_to_domain;
}
var allowed = false;
for (var d = 0; d < domains.length; d++) {
if (message.user.toLowerCase().indexOf(domains[d]) >= 0) {
allowed = true;
}
}
if (!allowed) {
console.log('WARNING: this message came from a domain that is outside of the allowed list', controller.config.limit_to_domain);
// this message came from a domain that is outside of the allowed list.
return false;
}
}
next();
});
controller.middleware.normalize.use(function getDecryptedMessage(bot, message, next) {
if (message.resource == 'messages' && message.event == 'created') {
controller.api.messages.get(message.data).then(function(decrypted_message) {
message.user = decrypted_message.personEmail;
message.channel = decrypted_message.roomId;
message.text = decrypted_message.text;
message.html = decrypted_message.html;
message.id = decrypted_message.id;
// remove @mentions of the bot from the source text before we ingest it
if (message.html) {
// strip the mention & HTML from the message
var pattern = new RegExp('^(\<p\>)?\<spark\-mention .*?data\-object\-id\=\"' + controller.identity.id + '\".*\>.*?\<\/spark\-mention\>', 'im');
if (!message.html.match(pattern)) {
var encoded_id = controller.identity.id;
var decoded = new Buffer(encoded_id, 'base64').toString('ascii');
// this should look like ciscospark://us/PEOPLE/<id string>
var matches;
if (matches = decoded.match(/ciscospark\:\/\/.*\/(.*)/im)) {
pattern = new RegExp('^(\<p\>)?\<spark\-mention .*?data\-object\-id\=\"' + matches[1] + '\".*\>.*?\<\/spark\-mention\>', 'im');
}
}
var action = message.html.replace(pattern, '');
// strip the remaining HTML tags
action = action.replace(/\<.*?\>/img, '');
// strip remaining whitespace
action = action.trim();
// replace the message text with the the HTML version
message.text = action;
} else {
var pattern = new RegExp('^' + controller.identity.displayName + '\\s+', 'i');
if (message.text) {
message.text = message.text.replace(pattern, '');
}
}
next();
}).catch(function(err) {
console.error('Could not get message', err);
});
} else {
next();
}
});
controller.middleware.normalize.use(function handleEvents(bot, message, next) {
if (message.resource != 'messages' || message.event != 'created') {
var event = message.resource + '.' + message.event;
message.user = message.data.personEmail;
message.channel = message.data.roomId;
message.id = message.data.id;
message.type = event;
switch (event) {
case 'memberships.deleted':
if (message.user === controller.identity.emails[0]) {
message.type = 'bot_space_leave';
} else {
message.type = 'user_space_leave';
}
break;
case 'memberships.created':
if (message.user === controller.identity.emails[0]) {
message.type = 'bot_space_join';
} else {
message.type = 'user_space_join';
}
break;
}
}
next();
});
controller.middleware.categorize.use(function(bot, message, next) {
// further categorize messages
if (message.type == 'message_received') {
if (message.user === controller.identity.emails[0]) {
message.type = 'self_message';
} else if (message.raw_message.data.roomType == 'direct') {
message.type = 'direct_message';
} else {
message.type = 'direct_mention';
}
}
next();
});
controller.middleware.format.use(function(bot, message, platform_message, next) {
// clone the incoming message
for (var k in message) {
platform_message[k] = message[k];
}
// mutate the message into proper spark format
platform_message.roomId = message.channel;
delete platform_message.channel;
// delete reference to recipient
delete platform_message.to;
// default the markdown field to be the same as tex.
if (platform_message.text && !platform_message.markdown) {
platform_message.markdown = message.text;
}
next();
});
controller.handleWebhookPayload = function(req, res, bot) {
var payload = req.body;
if (controller.config.secret) {
var signature = req.headers['x-spark-signature'];
var hash = crypto.createHmac('sha1', controller.config.secret).update(JSON.stringify(payload)).digest('hex');
if (signature != hash) {
console.error('WARNING: Webhook received message with invalid signature. Potential malicious behavior!');
return false;
}
}
controller.ingest(bot, req.body, res);
};
// customize the bot definition, which will be used when new connections
// spawn!
controller.defineBot(function(botkit, config) {
var bot = {
type: 'ciscospark',
botkit: botkit,
config: config || {},
utterances: botkit.utterances,
};
/**
* Convenience method for creating a DM convo.
*/
bot.startPrivateConversation = function(message, cb) {
var message_options = {};
message_options.toPersonEmail = message.user;
botkit.startTask(bot, message_options, function(task, convo) {
convo.on('sent', function(sent_message) {
// update this convo so that future messages will match
// since the source message did not have this info in it.
convo.source_message.user = message_options.toPersonEmail;
convo.source_message.channel = sent_message.roomId;
convo.context.user = convo.source_message.user;
convo.context.channel = convo.source_message.channel;
});
cb(null, convo);
});
};
/**
* Convenience method for creating a DM based on a personId instead of email
*/
bot.startPrivateConversationWithPersonId = function(personId, cb) {
controller.api.people.get(personId).then(function(identity) {
bot.startPrivateConversation({user: identity.emails[0]}, cb);
}).catch(function(err) {
cb(err);
});
};
/**
* Convenience method for creating a DM convo with the `actor`, not the sender
* this applies to events like channel joins, where the actor may be the user who sent the invite
*/
bot.startPrivateConversationWithActor = function(message, cb) {
bot.startPrivateConversationWithPersonId(message.raw_message.actorId, cb);
};
bot.send = function(message, cb) {
controller.api.messages.create(message).then(function(message) {
if (cb) cb(null, message);
}).catch(function(err) {
if (cb) cb(err);
});
};
bot.reply = function(src, resp, cb) {
var msg = {};
if (typeof(resp) == 'string') {
msg.text = resp;
} else {
msg = resp;
}
if (src.channel) {
msg.channel = src.channel;
} else if (src.toPersonEmail) {
msg.toPersonEmail = src.toPersonEmail;
} else if (src.toPersonId) {
msg.toPersonId = src.toPersonId;
}
msg.to = src.user;
bot.say(msg, cb);
};
bot.findConversation = function(message, cb) {
botkit.debug('CUSTOM FIND CONVO', message.user, message.channel);
for (var t = 0; t < botkit.tasks.length; t++) {
for (var c = 0; c < botkit.tasks[t].convos.length; c++) {
if (
botkit.tasks[t].convos[c].isActive() &&
botkit.tasks[t].convos[c].source_message.user == message.user &&
botkit.tasks[t].convos[c].source_message.channel == message.channel
) {
botkit.debug('FOUND EXISTING CONVO!');
cb(botkit.tasks[t].convos[c]);
return;
}
}
}
cb();
};
bot.retrieveFileInfo = function(url, cb) {
request.head({
url: url,
headers: {
'Authorization': 'Bearer ' + controller.config.ciscospark_access_token
},
}, function(err, response, body) {
if (!err) {
var obj = response.headers;
if (obj['content-disposition']) {
obj.filename = obj['content-disposition'].replace(/.*filename=\"(.*)\".*/gi, '$1');
}
cb(null, obj);
} else {
cb(err);
}
});
};
bot.retrieveFile = function(url, cb) {
request({
url: url,
headers: {
'Authorization': 'Bearer ' + controller.config.ciscospark_access_token
},
encoding: 'binary',
}, function(err, response, body) {
cb(err, body);
});
};
return bot;
});
controller.startTicking();
return controller;
}
module.exports = Sparkbot;