mangrove-botkit
Version:
Building blocks for Building Bots
544 lines (406 loc) • 17.5 kB
JavaScript
var Botkit = require(__dirname + '/CoreBot.js');
var express = require('express');
var bodyParser = require('body-parser');
var querystring = require('querystring');
var request = require('requestretry');
var clone = require('clone');
var async = require('async');
var TeamsAPI = require(__dirname + '/TeamsAPI.js');
function TeamsBot(configuration) {
var controller = Botkit(configuration || {});
controller.api = TeamsAPI(configuration || {});
controller.api.getToken(function(err) {
if (err) {
// this is a fatal error - could not create a Teams API client
throw new Error(err);
}
});
controller.defineBot(function(botkit, config) {
var bot = {
type: 'teams',
botkit: botkit,
config: config || {},
utterances: botkit.utterances
};
bot.startConversation = function(message, cb) {
botkit.startConversation(this, message, cb);
};
bot.createConversation = function(message, cb) {
botkit.createConversation(this, message, cb);
};
bot.channelLink = function(channel_info) {
return '<a href="https://teams.microsoft.com/l/channel/' + channel_info.id + '/' + channel_info.name + '">' + channel_info.name + '</a>';
};
bot.startPrivateConversation = function(message, cb) {
bot.createPrivateConversation(message, function(err, new_convo) {
if (err) {
cb(err);
} else {
new_convo.activate();
cb(null, new_convo);
}
});
};
bot.createPrivateConversation = function(message, cb) {
bot.openPrivateConvo(message, function(err, new_convo) {
if (err) {
cb(err);
} else {
message.raw_message.conversation = new_convo;
bot.createConversation(message, cb);
}
});
};
bot.openPrivateConvo = function(src, cb) {
var data = {
bot: src.recipient,
members: [src.raw_message.from],
channelData: src.channelData,
};
bot.api.createConversation(data, cb);
};
bot.openConvo = function(src, members, cb) {
var data = {
isGroup: true,
bot: src.recipient,
members: members,
channelData: src.channelData,
};
bot.api.createConveration(data, cb);
};
bot.send = function(message, cb) {
bot.api.addMessageToConversation(message.conversation.id, message, cb);
};
bot.replyWithActivity = function(src, message, cb) {
var data = {
type: 'message',
recipient: src.raw_message.from,
from: src.raw_message.recipient,
conversation: src.conversation,
channelData: {
notification: {
alert: true
}
},
text: message.text,
summary: message.summary,
attachments: message.attachments || null,
attachmentLayout: message.attachmentLayout || 'list',
};
bot.api.addMessageToConversation(src.conversation.id, data, cb);
};
bot.replyToComposeExtension = function(src, attachments, cb) {
// attachments will be an array of attachments
// need to wrap it in necessary stuff
var resp = {
composeExtension: {
type: 'result',
attachmentLayout: 'list',
attachments: attachments,
}
};
src.http_res.send(resp);
if (cb) {
cb();
}
};
bot.replyInThread = function(src, resp, cb) {
// can't clone theis, not needed for this type of messages.
delete(src.http_res);
var copy = clone(src);
// make sure this does NOT include the activity id
copy.raw_message.conversation = src.raw_message.channelData.channel;
bot.reply(copy, resp, cb);
};
bot.reply = function(src, resp, cb) {
if (src.type === 'composeExtension') {
bot.replyToComposeExtension(src, resp, cb);
}
if (typeof resp == 'string') {
resp = {
text: resp
};
}
resp.serviceUrl = src.raw_message.serviceUrl;
resp.from = src.raw_message.recipient;
resp.recipient = src.raw_message.from;
resp.to = src.user;
resp.channel = src.channel;
resp.conversation = src.raw_message.conversation;
bot.say(resp, 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.debug('FOUND EXISTING CONVO!');
cb(botkit.tasks[t].convos[c]);
return;
}
}
}
cb();
};
/* helper functions for creating attachments */
bot.createAttachment = function(type, title, subtitle, text, images, buttons, tap) {
var obj = {
content: (typeof(title) === 'object') ? title : {
title: title || null,
subtitle: subtitle || null,
text: text || null,
buttons: buttons || [],
images: images || [],
tap: tap || null,
},
contentType: 'application/vnd.microsoft.card.' + type,
title: function(v) {
this.content.title = v;
return this;
},
subtitle: function(v) {
this.content.subtitle = v;
return this;
},
text: function(v) {
this.content.text = v;
return this;
},
button: function(type, title, payload) {
if (!this.content.buttons) {
this.content.buttons = [];
}
var button_obj = (typeof(type) === 'object') ? type : {
type: type,
title: title,
payload: payload,
};
this.content.buttons.push(button_obj);
return this;
},
image: function(url, alt) {
if (!this.content.images) {
this.content.images = [];
}
var img_obj = (typeof(url) === 'object') ? type : {
url: url,
alt: alt || null
};
this.content.images.push(img_obj);
return this;
},
tap: function(type, title, payload) {
var tap_action = (typeof(type) === 'object') ? type : {
type: type,
title: title,
payload: payload,
};
this.content.tap = tap_action;
return this;
},
asString: function() {
return JSON.stringify(this, null, 2);
}
};
return obj;
};
bot.createHero = function(title, subtitle, text, buttons, images, tap) {
return bot.createAttachment('hero', title, subtitle, text, buttons, images, tap);
};
bot.createThumbnail = function(title, subtitle, text, buttons, images, tap) {
return bot.createAttachment('thumbnail', title, subtitle, text, buttons, images, tap);
};
return bot;
});
controller.createWebhookEndpoints = function() {
controller.webserver.post('/teams/receive', function(req, res) {
var message = req.body;
var options = {
serviceUrl: message.serviceUrl,
};
if (message.channelData && message.channelData.team && message.channelData.team.id) {
options.team = message.channelData.team.id;
}
var bot = controller.spawn(options);
if (message.recipient) {
bot.identity = message.recipient;
}
controller.ingest(bot, message, res);
});
};
controller.middleware.spawn.use(function(bot, next) {
if (!bot.config.serviceUrl) {
throw new Error('Cannot spawn a bot without a serviceUrl in the configuration');
}
// set up the teams api client
bot.api = TeamsAPI({
clientId: controller.config.clientId,
clientSecret: controller.config.clientSecret,
token: controller.config.token,
serviceUrl: bot.config.serviceUrl,
team: bot.config.team,
});
next();
});
controller.middleware.ingest.use(function(bot, message, res, next) {
res.status(200);
if (message.name != 'composeExtension/query') {
// send a result back immediately
res.send('');
}
message.http_res = res;
next();
});
controller.middleware.normalize.use(function(bot, message, next) {
message.user = message.raw_message.from.id;
message.channel = message.raw_message.conversation.id;
next();
});
controller.middleware.categorize.use(function(bot, message, next) {
if (message.type === 'invoke' && message.name === 'composeExtension/query') {
message.type = 'composeExtension';
// teams only supports a single parameter, it either exists or doesn't!
message.text = message.value.parameters[0].value;
}
next();
});
controller.middleware.categorize.use(function(bot, message, next) {
if (message.type == 'conversationUpdate') {
if (message.raw_message.membersAdded) {
// replies to these end up in the right place
for (var m = 0; m < message.raw_message.membersAdded.length; m++) {
// clone the message
// and copy this member into the from list
delete(message.http_res); // <-- that can't be cloned safely
var copy = clone(message);
copy.from = message.raw_message.membersAdded[m];
copy.user = copy.from.id;
if (copy.user == message.raw_message.recipient.id) {
copy.type = 'bot_channel_join';
} else {
copy.type = 'user_channel_join';
}
// restart the categorize process for the newly cloned messages
controller.categorize(bot, copy);
}
} else if (message.raw_message.membersRemoved) {
// replies to these end up in the right place
for (var m = 0; m < message.raw_message.membersRemoved.length; m++) {
// clone the message
// and copy this member into the from list
delete(message.http_res); // <-- that can't be cloned safely
var copy = clone(message);
copy.from = message.raw_message.membersRemoved[m];
copy.user = copy.from.id;
if (copy.user == message.raw_message.recipient.id) {
copy.type = 'bot_channel_leave';
} else {
copy.type = 'user_channel_leave';
}
// restart the categorize process for the newly cloned messages
controller.categorize(bot, copy);
}
next();
} else if (message.raw_message.channelData && message.raw_message.channelData.eventType) {
// channelCreated
// channelDeleted
// channelRenamed
// teamRenamed
message.type = message.raw_message.channelData.eventType;
// replies to these end up in general
next();
}
} else {
next();
}
});
controller.middleware.categorize.use(function(bot, message, next) {
if (message.type == 'message') message.type = 'message_received';
if (!message.conversation.isGroup && message.type == 'message_received') {
message.type = 'direct_message';
} else if (message.conversation.isGroup && message.type == 'message_received') {
// start by setting this to a mention, meaning that the bot's name was _somewhere_ in the string
message.type = 'mention';
// check to see if this is a direct mention ,meaning bot was mentioned at start of string
for (var e = 0; e < message.entities.length; e++) {
var entity = message.entities[0];
if (entity.type == 'mention' && message.text) {
var pattern = new RegExp(message.recipient.id);
if (entity.mentioned.id.match(pattern)) {
var clean = new RegExp('^' + entity.text + '\\s+');
if (message.text.match(clean)) {
message.text = message.text.replace(clean, '');
message.type = 'direct_mention';
}
}
}
}
}
next();
});
// This middleware looks for Slack-style user mentions in a message
// <@USERID> and translates them into Microsoft Teams style mentions
// which look like <at>@User Name</at> and have a matching row in the
// message.entities field.
controller.middleware.send.use(function(bot, message, next) {
var matches;
var uniques = [];
// extract all the <@USERID> patterns
if (matches = message.text.match(/\<\@(.*?)\>/igm)) {
// get a set of UNIQUE mentions - since the lookup of profile data is expensive
for (var m = 0; m < matches.length; m++) {
if (uniques.indexOf(matches[m]) == -1) {
uniques.push(matches[m]);
}
}
// loop over each mention
async.each(uniques, function(match, next_match) {
var uid = match.replace(/^\<\@/, '').replace(/\>$/, '');
// use the teams API to load the latest profile information for the user
bot.api.getUserById(message.channel, uid, function(err, user_profile) {
// if user is valid, replace the Slack-style mention and append to entities list
if (user_profile) {
var pattern = new RegExp('<@' + uid + '>', 'g');
message.text = message.text.replace(pattern, '<at>@' + user_profile.name + '</at>');
if (!message.entities) {
message.entities = [];
}
message.entities.push({
type: 'mention',
mentioned: {
id: uid,
name: user_profile.name,
},
text: '<at>@' + user_profile.name + '</at>',
});
}
next_match();
});
}, function() {
// we've processed all the matches, continue
next();
});
} else {
// if there were no matches, continue
next();
}
});
controller.middleware.format.use(function(bot, message, platform_message, next) {
platform_message.type = 'message';
platform_message.recipient = message.recipient;
platform_message.from = message.from;
platform_message.text = message.text;
platform_message.textFormat = 'markdown';
platform_message.entities = message.entities;
platform_message.attachments = message.attachments || null;
platform_message.attachmentLayout = message.attachmentLayout || 'list';
platform_message.conversation = message.conversation;
next();
});
controller.startTicking();
return controller;
}
module.exports = TeamsBot;