Skip to content

Commit

Permalink
Fix for howdyai#1961: add teams helper features
Browse files Browse the repository at this point in the history
  • Loading branch information
benbrown committed Aug 21, 2020
1 parent 9090887 commit 18e2c79
Show file tree
Hide file tree
Showing 6 changed files with 239 additions and 16 deletions.
14 changes: 2 additions & 12 deletions packages/botkit/src/adapter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';

Expand All @@ -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.
Expand Down
1 change: 1 addition & 0 deletions packages/botkit/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,4 +10,5 @@ export * from './core';
export * from './conversation';
export * from './botworker';
export * from './dialogWrapper';
export * from './teamsHelpers';
export * from './testClient';
97 changes: 97 additions & 0 deletions packages/botkit/src/teamsHelpers.ts
Original file line number Diff line number Diff line change
@@ -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<any>): Promise<void> {
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();
}
}
4 changes: 2 additions & 2 deletions packages/botkit/tests/Core.tests.js
Original file line number Diff line number Diff line change
@@ -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() {
Expand All @@ -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');
Expand Down
4 changes: 2 additions & 2 deletions packages/testbot/features/middlewares.js
Original file line number Diff line number Diff line change
@@ -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();
});

Expand Down
135 changes: 135 additions & 0 deletions packages/testbot/features/teams_features.js
Original file line number Diff line number Diff line change
@@ -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);
}

});

}
}

0 comments on commit 18e2c79

Please sign in to comment.