UNPKG

botbuilder-adapter-webex

Version:
544 lines 25.5 kB
"use strict"; /** * @module botbuilder-adapter-webex */ /** * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. */ 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()); }); }; Object.defineProperty(exports, "__esModule", { value: true }); exports.WebexAdapter = void 0; const botbuilder_1 = require("botbuilder"); const botworker_1 = require("./botworker"); const Webex = require("webex"); const url = require("url"); const crypto = require("crypto"); const Debug = require("debug"); const debug = Debug('botkit:webex'); /** * Connect [Botkit](https://www.npmjs.com/package/botkit) or [BotBuilder](https://www.npmjs.com/package/botbuilder) to Webex Teams. */ class WebexAdapter extends botbuilder_1.BotAdapter { /** * Create a Webex adapter. See [WebexAdapterOptions](#webexadapteroptions) for a full definition of the allowed parameters. * * Use with Botkit: *```javascript * const adapter = new WebexAdapter({ * access_token: process.env.ACCESS_TOKEN, // access token from https://developer.webex.com * public_address: process.env.PUBLIC_ADDRESS, // public url of this app https://myapp.com/ * secret: process.env.SECRET // webhook validation secret - you can define this yourself * }); * const controller = new Botkit({ * adapter: adapter, * // ... other configuration options * }); * ``` * * Use with BotBuilder: *```javascript * const adapter = new WebexAdapter({ * access_token: process.env.ACCESS_TOKEN, // access token from https://developer.webex.com * public_address: process.env.PUBLIC_ADDRESS, // public url of this app https://myapp.com/ * secret: process.env.SECRET // webhook validation secret - you can define this yourself * }); * * // set up restify... * const server = restify.createServer(); * server.use(restify.plugins.bodyParser()); * // register the webhook subscription to start receiving messages - Botkit does this automatically! * adapter.registerWebhookSubscription('/api/messages'); * // Load up the bot's identity, otherwise it won't know how to filter messages from itself * adapter.getIdentity(); * // create an endpoint for receiving messages * server.post('/api/messages', (req, res) => { * adapter.processActivity(req, res, async(context) => { * // do your bot logic here! * }); * }); * ``` * * @param options An object containing API credentials, a webhook verification token and other options */ constructor(config) { super(); /** * Name used by Botkit plugin loader * @ignore */ this.name = 'Webex Adapter'; /** * A customized BotWorker object that exposes additional utility methods. * @ignore */ this.botkit_worker = botworker_1.WebexBotWorker; this.options = Object.assign({}, config); if (!this.options.access_token) { const err = 'Missing required parameter `access_token`'; if (!this.options.enable_incomplete) { throw new Error(err); } else { console.error(err); } } else { this._api = Webex.init({ credentials: { authorization: { access_token: this.options.access_token } } }); if (!this._api) { const err = 'Could not create the Webex Teams API client'; if (!this.options.enable_incomplete) { throw new Error(err); } else { console.error(err); } } } if (!this.options.public_address) { const err = 'Missing required parameter `public_address`'; if (!this.options.enable_incomplete) { throw new Error(err); } else { console.error(err); } } else { const endpoint = new url.URL(this.options.public_address); if (!endpoint.hostname) { throw new Error('Could not determine hostname of public address: ' + this.options.public_address); } else { this.options.public_address = endpoint.hostname + (endpoint.port ? ':' + endpoint.port : ''); } } if (!this.options.secret) { console.warn('WARNING: No secret specified. Source of incoming webhooks will not be validated. https://developer.webex.com/webhooks-explained.html#auth'); } if (this.options.enable_incomplete) { const warning = [ '', '****************************************************************************************', '* WARNING: Your adapter may be running with an incomplete/unsafe configuration. *', '* - Ensure all required configuration options are present *', '* - Disable the "enable_incomplete" option! *', '****************************************************************************************', '' ]; console.warn(warning.join('\n')); } // Botkit Plugin additions this.middlewares = { spawn: [ (bot, next) => __awaiter(this, void 0, void 0, function* () { // make webex api directly available on a botkit instance. bot.api = this._api; next(); }) ] }; } /** * Load the bot's identity via the Webex API. * MUST be called by BotBuilder bots in order to filter messages sent by the bot. */ getIdentity() { return __awaiter(this, void 0, void 0, function* () { return new Promise((resolve, reject) => { if (this._api) { this._api.people.get('me').then((identity) => { debug('Webex: My identity is', identity); this._identity = identity; resolve(identity); }).catch((err) => { reject(err); }); } else { reject(new Error('No API client configured')); } }); }); } /** * Returns the identity of the bot, including {id, emails, displayName, created} and anything else from [this spec](https://webex.github.io/spark-js-sdk/api/#personobject) */ get identity() { return this._identity || {}; } /** * Botkit-only: Initialization function called automatically when used with Botkit. * * Calls registerWebhookSubscription() during bootup. * * Calls getIdentit() to load the bot's identity. */ init(botkit) { // when the bot is ready, register the webhook subscription with the Webex API botkit.addDep('webex-identity'); this.getIdentity().then(() => { botkit.completeDep('webex-identity'); }).catch((err) => { throw new Error(err); }); botkit.ready(() => { debug('Registering webhook subscription!'); botkit.adapter.registerWebhookSubscription(botkit.getConfig('webhook_uri')); }); } /** * Clear out and reset all the webhook subscriptions currently associated with this application. */ resetWebhookSubscriptions() { return __awaiter(this, void 0, void 0, function* () { return new Promise((resolve, reject) => { this._api.webhooks.list().then((list) => __awaiter(this, void 0, void 0, function* () { for (let i = 0; i < list.items.length; i++) { yield this._api.webhooks.remove(list.items[i]).catch(reject); } resolve(); })); }); }); } ; /** * Register a webhook subscription with Webex Teams to start receiving message events. * @param webhook_path the path of the webhook endpoint like `/api/messages` */ registerWebhookSubscription(webhook_path) { const webhook_name = this.options.webhook_name || 'Botkit Firehose'; this._api.webhooks.list().then((list) => { let hook_id = null; for (let i = 0; i < list.items.length; i++) { if (list.items[i].name === webhook_name) { hook_id = list.items[i].id; } } const hook_url = 'https://' + this.options.public_address + webhook_path; debug('Webex: incoming webhook url is ', hook_url); if (hook_id) { this._api.webhooks.update({ id: hook_id, resource: 'all', targetUrl: hook_url, event: 'all', secret: this.options.secret, name: webhook_name }).then(function () { debug('Webex: SUCCESSFULLY UPDATED WEBEX WEBHOOKS'); }).catch(function (err) { console.error('FAILED TO REGISTER WEBHOOK', err); throw new Error(err); }); } else { this._api.webhooks.create({ resource: 'all', targetUrl: hook_url, event: 'all', secret: this.options.secret, name: webhook_name }).then(function () { debug('Webex: SUCCESSFULLY REGISTERED WEBEX WEBHOOKS'); }).catch(function (err) { console.error('FAILED TO REGISTER WEBHOOK', err); throw new Error(err); }); } }).catch(function (err) { throw new Error(err); }); } /** * Register a webhook subscription with Webex Teams to start receiving message events. * @param webhook_path the path of the webhook endpoint like `/api/messages` */ registerAdaptiveCardWebhookSubscription(webhook_path) { const webhook_name = this.options.webhook_name || 'Botkit AttachmentActions'; this._api.webhooks.list().then((list) => { let hook_id = null; for (let i = 0; i < list.items.length; i++) { if (list.items[i].name === webhook_name) { hook_id = list.items[i].id; } } const hook_url = 'https://' + this.options.public_address + webhook_path; debug('Webex: incoming webhook url is ', hook_url); if (hook_id) { this._api.webhooks.update({ id: hook_id, resource: 'attachmentActions', targetUrl: hook_url, event: 'all', secret: this.options.secret, name: webhook_name }).then(function () { debug('Webex: SUCCESSFULLY UPDATED WEBEX WEBHOOKS'); }).catch(function (err) { console.error('FAILED TO REGISTER WEBHOOK', err); throw new Error(err); }); } else { this._api.webhooks.create({ resource: 'attachmentActions', targetUrl: hook_url, event: 'all', secret: this.options.secret, name: webhook_name }).then(function () { debug('Webex: SUCCESSFULLY REGISTERED WEBEX WEBHOOKS'); }).catch(function (err) { console.error('FAILED TO REGISTER WEBHOOK', err); throw new Error(err); }); } }).catch(function (err) { throw new Error(err); }); } /** * Standard BotBuilder adapter method to send a message from the bot to the messaging API. * [BotBuilder reference docs](https://docs.microsoft.com/en-us/javascript/api/botbuilder-core/botadapter?view=botbuilder-ts-latest#sendactivities). * @param context A TurnContext representing the current incoming message and environment. * @param activities An array of outgoing activities to be sent back to the messaging API. */ sendActivities(context, activities) { return __awaiter(this, void 0, void 0, function* () { const responses = []; for (let a = 0; a < activities.length; a++) { const activity = activities[a]; if (activity.type === botbuilder_1.ActivityTypes.Message) { debug('OUTGOING ACTIVITY', activity); // transform activity into the webex message format // https://developer.webex.com/docs/api/v1/messages/create-a-message const message = {}; if (activity.channelData && activity.channelData.files) { message.files = activity.channelData.files; } if (activity.text) { message.text = activity.text; } if (activity.channelData && activity.channelData.markdown) { message.markdown = activity.channelData.markdown; } if (activity.conversation && activity.conversation.id) { message.roomId = activity.conversation.id; } else if (!activity.conversation && activity.recipient.id) { message.toPersonId = activity.recipient.id; } else if (activity.channelData && activity.channelData.toPersonEmail) { message.toPersonEmail = activity.channelData.toPersonEmail; } if (activity.attachments) { message.attachments = activity.attachments; } else if (activity.channelData && activity.channelData.attachments) { message.attachments = activity.channelData.attachments; } // @ts-ignore ignore this webex specific field if (activity.conversation && activity.conversation.parentId) { // @ts-ignore ignore this webex specific field message.parentId = activity.conversation.parentId; } else if (activity.channelData && activity.channelData.parentId) { message.parentId = activity.channelData.parentId; } const response = yield this._api.messages.create(message); responses.push(response); } else { // If there are ever any non-message type events that need to be sent, do it here. debug('Unknown message type encountered in sendActivities: ', activity.type); } } return responses; }); } /** * Webex adapter does not support updateActivity. * @ignore */ // eslint-disable-next-line updateActivity(context, activity) { return __awaiter(this, void 0, void 0, function* () { debug('Webex adapter does not support updateActivity.'); }); } /** * Standard BotBuilder adapter method to delete a previous message. * [BotBuilder reference docs](https://docs.microsoft.com/en-us/javascript/api/botbuilder-core/botadapter?view=botbuilder-ts-latest#deleteactivity). * @param context A TurnContext representing the current incoming message and environment. (not used) * @param reference An object in the form `{activityId: <id of message to delete>, conversation: { id: <id of slack channel>}}` */ deleteActivity(context, reference) { return __awaiter(this, void 0, void 0, function* () { if (reference.activityId) { try { yield this._api.messages.remove({ id: reference.activityId }); } catch (err) { throw new Error(err); } } else { throw new Error('Cannot delete activity: reference is missing activityId'); } }); } /** * Standard BotBuilder adapter method for continuing an existing conversation based on a conversation reference. * [BotBuilder reference docs](https://docs.microsoft.com/en-us/javascript/api/botbuilder-core/botadapter?view=botbuilder-ts-latest#continueconversation) * @param reference A conversation reference to be applied to future messages. * @param logic A bot logic function that will perform continuing action in the form `async(context) => { ... }` */ continueConversation(reference, logic) { return __awaiter(this, void 0, void 0, function* () { const request = botbuilder_1.TurnContext.applyConversationReference({ type: 'event', name: 'continueConversation' }, reference, true); const context = new botbuilder_1.TurnContext(this, request); return this.runMiddleware(context, logic); }); } /** * Accept an incoming webhook request and convert it into a TurnContext which can be processed by the bot's logic. * @param req A request object from Restify or Express * @param res A response object from Restify or Express * @param logic A bot logic function in the form `async(context) => { ... }` */ processActivity(req, res, logic) { return __awaiter(this, void 0, void 0, function* () { res.status(200); res.end(); const payload = req.body; let activity; if (this.options.secret) { const signature = req.headers['x-spark-signature']; const hash = crypto.createHmac('sha1', this.options.secret).update(JSON.stringify(payload)).digest('hex'); if (signature !== hash) { console.warn('WARNING: Webhook received message with invalid signature. Potential malicious behavior!'); return false; } } if (payload.resource === 'messages' && payload.event === 'created') { const decrypted_message = yield this._api.messages.get(payload.data); activity = { id: decrypted_message.id, timestamp: new Date(), channelId: 'webex', conversation: { id: decrypted_message.roomId, parentId: decrypted_message.parentId }, from: { id: decrypted_message.personId, name: decrypted_message.personEmail }, recipient: { id: this.identity.id }, text: decrypted_message.text, channelData: decrypted_message, type: botbuilder_1.ActivityTypes.Message, parentId: decrypted_message.parentId }; // add in some fields from the original payload activity.channelData.orgId = payload.orgId; activity.channelData.createdBy = payload.createdBy; activity.channelData.appId = payload.appId; activity.channelData.actorId = payload.actorId; // this is the bot speaking if (activity.from.id === this.identity.id) { activity.channelData.botkitEventType = 'self_message'; activity.type = botbuilder_1.ActivityTypes.Event; } else { // change the event type of messages sent in 1:1s if (activity.channelData.roomType === 'direct') { activity.channelData.botkitEventType = 'direct_message'; } } if (decrypted_message.html) { // strip the mention & HTML from the message let pattern = new RegExp('^(<p>|<div>)?<spark-mention .*?data-object-id="' + this.identity.id + '".*?>.*?</spark-mention>', 'im'); if (!decrypted_message.html.match(pattern)) { const encoded_id = this.identity.id; const decoded = Buffer.from(encoded_id, 'base64').toString('ascii'); // this should look like ciscospark://us/PEOPLE/<id string> let matches; if ((matches = decoded.match(/ciscospark:\/\/.*\/(.*)/im))) { pattern = new RegExp('^(<p>|<div>)?<spark-mention .*?data-object-id="' + matches[1] + '".*?>.*?</spark-mention>', 'im'); } } let action = decrypted_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 activity.text = action; } else { const pattern = new RegExp('^' + this.identity.displayName + '\\s+', 'i'); if (activity.text) { activity.text = activity.text.replace(pattern, ''); } } // create a conversation reference const context = new botbuilder_1.TurnContext(this, activity); this.runMiddleware(context, logic) .catch((err) => { console.error(err.toString()); }); } else if (payload.resource === 'attachmentActions' && payload.event === 'created') { const decrypted_message = yield this._api.attachmentActions.get(payload.data); activity = { id: decrypted_message.id, timestamp: new Date(), channelId: 'webex', conversation: { id: decrypted_message.roomId }, from: { id: decrypted_message.personId, name: decrypted_message.personEmail }, recipient: { id: this.identity.id }, value: decrypted_message.inputs, channelData: decrypted_message, type: botbuilder_1.ActivityTypes.Event }; // add in some fields from the original payload activity.channelData.orgId = payload.orgId; activity.channelData.createdBy = payload.createdBy; activity.channelData.appId = payload.appId; activity.channelData.actorId = payload.actorId; activity.channelData.botkitEventType = 'attachmentActions'; // create a conversation reference const context = new botbuilder_1.TurnContext(this, activity); this.runMiddleware(context, logic) .catch((err) => { console.error(err.toString()); }); } else { // type == payload.resource + '.' + payload.event // memberships.deleted for example // payload.data contains stuff activity = { id: payload.id, timestamp: new Date(), channelId: 'webex', conversation: { id: payload.data.roomId }, from: { id: payload.actorId }, recipient: { id: this.identity.id }, channelData: Object.assign(Object.assign({}, payload), { botkitEventType: payload.resource + '.' + payload.event }), type: botbuilder_1.ActivityTypes.Event }; const context = new botbuilder_1.TurnContext(this, activity); this.runMiddleware(context, logic) .catch((err) => { console.error(err.toString()); }); } }); } } exports.WebexAdapter = WebexAdapter; //# sourceMappingURL=webex_adapter.js.map