From 18e2c7902c502f56e66886fa91a41ebce78be3ac Mon Sep 17 00:00:00 2001 From: Ben Brown Date: Fri, 21 Aug 2020 14:59:01 -0500 Subject: [PATCH] Fix for #1961: add teams helper features --- packages/botkit/src/adapter.ts | 14 +- packages/botkit/src/index.ts | 1 + packages/botkit/src/teamsHelpers.ts | 97 ++++++++++++++ packages/botkit/tests/Core.tests.js | 4 +- packages/testbot/features/middlewares.js | 4 +- packages/testbot/features/teams_features.js | 135 ++++++++++++++++++++ 6 files changed, 239 insertions(+), 16 deletions(-) create mode 100644 packages/botkit/src/teamsHelpers.ts create mode 100644 packages/testbot/features/teams_features.js diff --git a/packages/botkit/src/adapter.ts b/packages/botkit/src/adapter.ts index 2dd81ec35..f09c1bd3a 100644 --- a/packages/botkit/src/adapter.ts +++ b/packages/botkit/src/adapter.ts @@ -7,6 +7,7 @@ */ import { BotFrameworkAdapter, TurnContext } from 'botbuilder'; import { ConnectorClient, TokenApiClient } from 'botframework-connector'; +import { TeamsBotWorker } from './teamsHelpers'; import * as request from 'request'; import * as os from 'os'; @@ -27,19 +28,8 @@ const USER_AGENT: string = `Microsoft-BotFramework/3.1 Botkit/${ pjson.version } * * Adds middleware for adjusting location of tenant id field (MS Teams) */ export class BotkitBotFrameworkAdapter extends BotFrameworkAdapter { - public constructor(options) { - super(options); - // Fix a (temporary) issue with transitional location of MS Teams tenantId - // this fix should already be present in botbuilder 4.4 - // when/if that happens, this can be removed. - this.use(async (context, next) => { - if (!context.activity.conversation.tenantId && context.activity.channelData && context.activity.channelData.tenant) { - context.activity.conversation.tenantId = context.activity.channelData.tenant.id; - } - await next(); - }); - } + public botkit_worker = TeamsBotWorker; /** * Allows for mocking of the connector client in unit tests. diff --git a/packages/botkit/src/index.ts b/packages/botkit/src/index.ts index c99f5b900..bd9ec77c3 100644 --- a/packages/botkit/src/index.ts +++ b/packages/botkit/src/index.ts @@ -10,4 +10,5 @@ export * from './core'; export * from './conversation'; export * from './botworker'; export * from './dialogWrapper'; +export * from './teamsHelpers'; export * from './testClient'; diff --git a/packages/botkit/src/teamsHelpers.ts b/packages/botkit/src/teamsHelpers.ts new file mode 100644 index 000000000..5a0ecd226 --- /dev/null +++ b/packages/botkit/src/teamsHelpers.ts @@ -0,0 +1,97 @@ +/** + * @module botkit + */ +/** + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. + */ +import { Botkit, BotkitMessage } from './core'; +import { BotWorker } from './botworker'; +import { TeamsInfo, MiddlewareSet, TurnContext, TaskModuleTaskInfo } from 'botbuilder'; + +/** + * An extension of the core BotWorker class that exposes the TeamsInfo helper class for MS Teams. + * This BotWorker is used with the built-in Bot Framework adapter. + */ +export class TeamsBotWorker extends BotWorker { + + public constructor(controller: Botkit, config) { + super(controller, config); + } + + /** + * Grants access to the TeamsInfo helper class + * See: https://docs.microsoft.com/en-us/javascript/api/botbuilder/teamsinfo?view=botbuilder-ts-latest + */ + public teams: TeamsInfo = TeamsInfo; + + /** + * Reply to a Teams task module task/fetch or task/submit with a task module response. + * See https://docs.microsoft.com/en-us/microsoftteams/platform/task-modules-and-cards/task-modules/task-modules-bots + * @param message + * @param taskInfo { type: 'continue|message', value: {} } + */ + public async replyWithTaskInfo(message: BotkitMessage, taskInfo: any) { + if (!taskInfo || taskInfo == {}) { + // send a null response back + taskInfo = { + type: 'message', + value: '', + } + } + return new Promise((resolve, reject) => { + this.controller.middleware.send.run(this, taskInfo, async (err, bot, taskInfo) => { + if (err) { + return reject(err); + } + resolve(await this.getConfig('context').sendActivity({ + type: 'invokeResponse', + value: { + status: 200, + body: { + task: taskInfo + } + } + })); + }); + }); + } +} + +/** + * When used, causes Botkit to emit special events for teams "invokes" + * Based on https://github.com/microsoft/botbuilder-js/blob/master/libraries/botbuilder/src/teamsActivityHandler.ts + * This allows Botkit bots to respond directly to task/fetch or task/submit events, as an example. + */ +export class TeamsInvokeMiddleware extends MiddlewareSet { + /** + * Not for direct use - implements the MiddlewareSet's required onTurn function used to process the event + * @param context + * @param next + */ + public async onTurn(context: TurnContext, next: () => Promise): Promise { + if (context.activity.type === 'invoke') { + if (!context.activity.name && context.activity.channelId === 'msteams') { + context.activity.channelData.botkitEventType = 'cardAction'; + } else { + switch (context.activity.name) { + case 'fileConsent/invoke': + case 'actionableMessage/executeAction': + case 'composeExtension/queryLink': + case 'composeExtension/query': + case 'composeExtension/selectItem': + case 'composeExtension/submitAction': + case 'composeExtension/fetchTask': + case 'composeExtension/querySettingUrl': + case 'composeExtension/setting': + case 'composeExtension/onCardButtonClicked': + case 'task/fetch': + case 'task/submit': + context.activity.channelData.botkitEventType = context.activity.name; + break; + } + } + } + await next(); + } +} diff --git a/packages/botkit/tests/Core.tests.js b/packages/botkit/tests/Core.tests.js index 80e749612..bccef07c0 100644 --- a/packages/botkit/tests/Core.tests.js +++ b/packages/botkit/tests/Core.tests.js @@ -1,5 +1,5 @@ const assert = require('assert'); -const { Botkit, BotWorker } = require('../'); +const { Botkit, TeamsBotWorker } = require('../'); const { TwilioAdapter, TwilioBotWorker } = require('../../botbuilder-adapter-twilio-sms'); describe('Botkit', function() { @@ -26,7 +26,7 @@ describe('Botkit', function() { const anotherAdapter = new TwilioAdapter({enable_incomplete: true}); const bot = await controller.spawn({}); - assert((bot instanceof BotWorker), 'Default Bot worker is wrong type'); + assert((bot instanceof TeamsBotWorker), 'Default Bot worker is wrong type'); const tbot = await controller.spawn({}, anotherAdapter); assert((tbot instanceof TwilioBotWorker), 'Secondary Bot worker is wrong type'); diff --git a/packages/testbot/features/middlewares.js b/packages/testbot/features/middlewares.js index 48d670d4a..056e173e9 100644 --- a/packages/testbot/features/middlewares.js +++ b/packages/testbot/features/middlewares.js @@ -1,12 +1,12 @@ module.exports = function(controller) { controller.middleware.receive.use((bot, message, next) => { - console.log('IN > ', message.text); + console.log('IN > ', message.text, message.type); next(); }); controller.middleware.send.use((bot, message, next) => { - console.log('OUT > ', message.text, message.channelData.quick_replies ? message.channelData.quick_replies : null, message.channelData.attachments ? message.channelData.attachments : null); + console.log('OUT > ', message.text, message.channelData && message.channelData.quick_replies ? message.channelData.quick_replies : null, message.channelData && message.channelData.attachments ? message.channelData.attachments : null); next(); }); diff --git a/packages/testbot/features/teams_features.js b/packages/testbot/features/teams_features.js new file mode 100644 index 000000000..2416f79be --- /dev/null +++ b/packages/testbot/features/teams_features.js @@ -0,0 +1,135 @@ +const request = require('request'); + +module.exports = function(controller) { + + if (!controller.adapter.name) { + + const adaptiveCard = { + "type": "AdaptiveCard", + "body": [ + { + "type": "TextBlock", + "text": "Here is a ninja cat:" + }, + { + "type": "Image", + "url": "http://adaptivecards.io/content/cats/1.png", + "size": "Medium" + }, + { + "type": "ActionSet", + "actions": [ + { + "type": "Action.Submit", + "title": "Message Submit", + "id": "begin", + "data": { + "command": "message" + } + }, + { + "type": "Action.Submit", + "title": "Card Submit", + "id": "begin", + "data": { + "command": "card" + } + }, + { + "type": "Action.Submit", + "title": "Close", + "id": "begin", + "data": { + "command": "close" + } + }, + ], + "horizontalAlignment": "Center" + } + ], + "version": "1.0" + } + + controller.hears('getTeamDetails', 'message', async(bot, message) => { + try { + await bot.reply(message, JSON.stringify(await bot.teams.getTeamDetails(bot.getConfig('context')))); + } catch(err) { + await bot.reply(message, err.message); + } + }); + + + controller.hears('getMember', 'message', async(bot, message) => { + try { + await bot.reply(message, JSON.stringify(await bot.teams.getMember(bot.getConfig('context'), message.user))); + } catch(err) { + await bot.reply(message, err.message); + } + }); + + controller.hears('taskModule', 'message', async(bot, message) => { + await bot.reply(message,{ + attachments: [{ + "contentType": "application/vnd.microsoft.card.hero", + "content": { + "buttons": [ + { + "type": "invoke", + "title": "Task Module", + "value": {type: 'task/fetch'} + } + ], + "text": "Launch a task module by clicking the button.", + "title": "INVOKE A TASK MODULE!" + } + }] + }); + }); + + controller.on('task/fetch', async(bot, message) => { + await bot.replyWithTaskInfo(message,{ + "type": "continue", + "value": { + "title": "Task module title", + "height": 500, + "width": "medium", + card: { + contentType: 'application/vnd.microsoft.card.adaptive', + content: adaptiveCard, + } + } + }) + }); + + + controller.on('task/submit', async(bot, message) => { + + if (message.value.data.command == 'message') { + // reply with a message + await bot.replyWithTaskInfo(message, { + type: 'message', + value: 'Submitted!', + }); + } else if (message.value.data.command == 'card') { + // reply with another card + await bot.replyWithTaskInfo(message, { + "type": "continue", + "value": { + "title": "Task module title", + "height": 500, + "width": "medium", + card: { + contentType: 'application/vnd.microsoft.card.adaptive', + content: adaptiveCard, + } + } + }); + } else { + // just close the task module + await bot.replyWithTaskInfo(message, null); + } + + }); + + } +} \ No newline at end of file