├── .eslintrc.json ├── .github ├── FUNDING.yaml └── workflows │ ├── build.yaml │ └── publish.yaml ├── .gitignore ├── .npmignore ├── CHANGELOG.md ├── LICENSE ├── README.md ├── jest.config.js ├── package-lock.json ├── package.json ├── src ├── index.spec.ts └── index.ts └── tsconfig.json /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "node": true, 4 | "jest/globals": true 5 | }, 6 | "extends": [ 7 | "standard" 8 | ], 9 | "parser": "@typescript-eslint/parser", 10 | "parserOptions": { 11 | "ecmaVersion": 12, 12 | "sourceType": "module" 13 | }, 14 | "plugins": [ 15 | "@typescript-eslint", 16 | "jest" 17 | ], 18 | "rules": { 19 | "object-shorthand": 2, 20 | "quotes": [ 21 | "error", 22 | "double" 23 | ], 24 | "semi": [ 25 | "error", 26 | "always" 27 | ], 28 | "indent": [ 29 | "error", 30 | 4, 31 | { 32 | "SwitchCase": 1 33 | } 34 | ], 35 | "space-before-function-paren": "off", 36 | "space-in-parens": "off", 37 | "padded-blocks": "off", 38 | "no-unused-vars": "off" 39 | } 40 | } -------------------------------------------------------------------------------- /.github/FUNDING.yaml: -------------------------------------------------------------------------------- 1 | github: [wictorwilen] -------------------------------------------------------------------------------- /.github/workflows/build.yaml: -------------------------------------------------------------------------------- 1 | name: botbuilder-teams-messagingextensions CI 2 | 3 | on: [push, pull_request] 4 | 5 | jobs: 6 | build: 7 | 8 | runs-on: ubuntu-latest 9 | 10 | strategy: 11 | matrix: 12 | node-version: [10.x, 12.x, 14.x, 16.x] 13 | 14 | steps: 15 | - uses: actions/checkout@v2 16 | - name: Use Node.js ${{ matrix.node-version }} 17 | uses: actions/setup-node@v1 18 | with: 19 | node-version: ${{ matrix.node-version }} 20 | - run: npm install 21 | - run: npm run lint 22 | - run: npm run test 23 | - uses: codecov/codecov-action@v2 24 | with: 25 | directory: ./coverage 26 | flags: unittests 27 | fail_ci_if_error: true 28 | - run: npm run build 29 | env: 30 | CI: true -------------------------------------------------------------------------------- /.github/workflows/publish.yaml: -------------------------------------------------------------------------------- 1 | name: botbuilder-teams-messagingextensions Publish 2 | on: 3 | release: 4 | types: [created] 5 | jobs: 6 | build: 7 | runs-on: ubuntu-latest 8 | steps: 9 | - uses: actions/checkout@v2 10 | - uses: actions/setup-node@v1 11 | with: 12 | node-version: '14.x' 13 | registry-url: 'https://registry.npmjs.org' 14 | - run: npm install 15 | - run: npm run build 16 | - name: Publish release 17 | if: "!contains(github.ref, 'preview')" 18 | run: npm publish 19 | env: 20 | NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} 21 | - name: Publish preview 22 | if: contains(github.ref, 'preview') 23 | run: npm publish --tag preview 24 | env: 25 | NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | logs 2 | *.log 3 | npm-debug.log* 4 | yarn-debug.log* 5 | yarn-error.log* 6 | node_modules/ 7 | .npm 8 | *.tgz 9 | .yarn-integrity 10 | .env 11 | dist 12 | lib 13 | .vscode 14 | junit.xml 15 | coverage -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | .vscode 2 | node_modules 3 | src 4 | .gitignore 5 | npm-shrinkwrap.json 6 | tsconfig.json 7 | *.map 8 | *.spec.js 9 | .travis.yml 10 | tslint.json 11 | .github 12 | .eslintrc.json 13 | junit.xml 14 | jest.config.js 15 | coverage 16 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Change Log 2 | 3 | All notable changes to this project will be documented in this file. 4 | 5 | The format is based on [Keep a Changelog](http://keepachangelog.com/) 6 | and this project adheres to [Semantic Versioning](http://semver.org/). 7 | 8 | 9 | 10 | ## [*1.8.1*] - <*2022-06-23*> 11 | 12 | ### Fixes 13 | 14 | * Updated dependencies 15 | 16 | ## [*1.8.0*] - <*2021-10-22*> 17 | 18 | ### Added 19 | 20 | * Added support for Adaptive Cards 1.4 universal actions `adaptiveCard/action` (#12) 21 | * Added unit tests - 100% code coverage 22 | 23 | ### Changes 24 | 25 | * Moved from Travis CI to Github Actions 26 | * Migrated from TSLint to ESLint 27 | * Migrated to botbuilder 4.14.1 28 | 29 | ### Fixes 30 | 31 | * Fixed issues where submitAction could return an error if overrides was not defined 32 | * Fixed an issue where selectItem returned a success even when an error occurred 33 | 34 | ## [*1.7.0*] - <*2020-10-27*> 35 | 36 | ### Changes 37 | 38 | * Using bot framework `^4.9.0` 39 | 40 | ### Added 41 | 42 | * Support for `send` and `edit` for `submitActions` (#10) 43 | 44 | ## [*1.6.0*] - <*2020-05-17*> 45 | 46 | ### Fixed 47 | 48 | * Fixed an issue where `context.activity.value` is undefined (#6, #7) 49 | 50 | ### Changed 51 | 52 | * Moved `botbuilder-core` to `devDependencies` 53 | 54 | ### Removed 55 | 56 | * Removed `ms-rest-js` package (#1) 57 | 58 | ## [*1.5.0*] - <*2020-03-05*> 59 | 60 | ### Added 61 | 62 | * Added logging (`msteams` namespace) 63 | 64 | ### Fixed 65 | 66 | * Fixed the `type` of the response to `invokeResponse` 67 | 68 | ### Changed 69 | 70 | * Migrated to `botbuilder-core@4.7.1` 71 | * Breaking changes in the `IMessagingExtensionMiddlewareProcessor` where 72 | types from `botbuilder-core` is used instead of custom defintions 73 | * Updated Travis build settings 74 | 75 | ### Removed 76 | 77 | * Removed `botbuilder-teams` 78 | * Removed all custom interface declarations 79 | 80 | ## [*1.4.0*]- <*2019-06-02*> 81 | 82 | ### Changed 83 | 84 | * `onQueryLink` is no longer filtering on `commandId` (as that is not sent in the `composeExtension/onQueryLink`) 85 | * Changed signature for `onQueryLink` to use `IAppBasedLinkQuery` as value, to match official swagger 86 | * Updated devDependencies 87 | 88 | ## [*1.3.0*] - <*2019-05-22*> 89 | 90 | ### Changed 91 | 92 | * Changed signature for `onSubmitAction` and `onFetchTask` to use new `IMessagingExtensionActionRequest` interface 93 | * Changed signature for `onFetchTask` to return `MessagingExtensionResult` (for `auth` and `config`) or `ITaskModuleResult` (when using `continue` or `message`) 94 | 95 | ## [*1.2.1*] - <*2019-05-07*> 96 | 97 | ### Changed 98 | * Fixed versions for dependencies 99 | 100 | ## [*1.2.0*] - <*2019-05-06*> 101 | 102 | ### Added 103 | * Added support for action command responses (`onFetchTask` - `composeExtension/fetchTask`) 104 | * Added support for `Action.Submit` from adaptive cards (`onCardButtonClicked` - `composeExtension/onCardButtonClicked`) 105 | * Added support for select item in Message Extensions (`onSelectItem` - `composeExtension/selectItem`) 106 | 107 | ## [*1.1.0*] - <*2019-04-29*> 108 | 109 | ### Added 110 | * Added support for Link unfurling (`onQueryLink` - `composeExtension/queryLink`) 111 | * Added support for Message Actions (`onSubmitAction` - `composeExtension/submitAction`) 112 | 113 | ### Changes 114 | * Made all methods of `IMessagingExtensionMiddlewareProcessor` optional 115 | 116 | ## [*1.0.0*] - <*2019-03-29*> 117 | 118 | ### Added 119 | * Initial release -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) Wictor Wilén. All rights reserved. 2 | 3 | MIT License 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining 6 | a copy of this software and associated documentation files (the 7 | "Software"), to deal in the Software without restriction, including 8 | without limitation the rights to use, copy, modify, merge, publish, 9 | distribute, sublicense, and/or sell copies of the Software, and to 10 | permit persons to whom the Software is furnished to do so, subject to 11 | the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be 14 | included in all copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 17 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 18 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 19 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 20 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 21 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 22 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Microsoft Teams Messaging Extension Middleware for Microsoft Bot Builder 2 | 3 | [![npm version](https://badge.fury.io/js/botbuilder-teams-messagingextensions.svg)](https://badge.fury.io/js/botbuilder-teams-messagingextensions) 4 | 5 | This middleware for [Bot Builder Framework](https://www.npmjs.com/package/botbuilder) is targeted for [Microsoft Teams](https://docs.microsoft.com/en-us/microsoftteams/platform/) based bots. 6 | 7 | | @master | @preview | 8 | :--------:|:---------: 9 | [![Build Status](https://travis-ci.org/wictorwilen/botbuilder-teams-messagingextensions.svg?branch=master)](https://travis-ci.org/wictorwilen/botbuilder-teams-messagingextensions)|[![Build Status](https://travis-ci.org/wictorwilen/botbuilder-teams-messagingextensions.svg?branch=preview)](https://travis-ci.org/wictorwilen/botbuilder-teams-messagingextensions) 10 | 11 | ## About 12 | 13 | The Microsoft Teams [Messaging Extension](https://docs.microsoft.com/en-us/microsoftteams/platform/concepts/messaging-extensions/messaging-extensions-overview?view=msteams-client-js-latest) Middleware for Microsoft Bot Builder makes building bots for Microsoft Teams easier. By separating out the logic for Message Extensions from the implementation of the bot, you will make your code more readable and easier to debug and troubleshoot. 14 | 15 | The middleware supports the following Message Extension features 16 | 17 | * [Message extension queries](https://docs.microsoft.com/en-us/microsoftteams/platform/concepts/messaging-extensions/search-extensions): `composeExtension/query` 18 | * [Message extension settings url](https://docs.microsoft.com/en-us/microsoftteams/platform/concepts/messaging-extensions/search-extensions#add-event-handlers): `composeExtension/querySettingUrl` 19 | * [Message extension settings](https://docs.microsoft.com/en-us/microsoftteams/platform/concepts/messaging-extensions/search-extensions#add-event-handlers): `composeExtension/setting` 20 | * [Message extension link unfurling](https://developer.microsoft.com/en-us/office/blogs/add-rich-previews-to-messages-using-link-unfurling/): `composeExtension/queryLink` 21 | * [Message extension message actions](https://docs.microsoft.com/en-us/microsoftteams/platform/concepts/messaging-extensions/create-extensions): `composeExtension/submitAction` 22 | * [Fetch task operations for message actions](https://docs.microsoft.com/en-us/microsoftteams/platform/concepts/messaging-extensions/create-extensions): `composeExtension/fetchTask` 23 | * Adaptive Card `Action.Submit` actions: `composeExtension/onCardButtonClicked` 24 | * [Message extension select](https://docs.microsoft.com/en-us/microsoftteams/platform/concepts/messaging-extensions/search-extensions): `composeExtension/selectItem` 25 | 26 | ## Usage 27 | 28 | To implement a Messaging Extension handler create a class like this: 29 | 30 | > NOTE: When combining this with the `botbuilder-teams` you should avoid using the `invokeActivityHandler.onInvoke`, as it might 31 | > invalidate your messaging extension results. 32 | 33 | ``` TypeScript 34 | import { TurnContext, CardFactory, MessagingExtensionQuery, MessagingExtensionResult } from "botbuilder"; 35 | import { IMessagingExtensionMiddlewareProcessor } from "botbuilder-teams-messagingextensions"; 36 | 37 | export default class MyMessageExtension implements IMessagingExtensionMiddlewareProcessor { 38 | 39 | public async onQuery(query: MessagingExtensionQuery): Promise { 40 | const card = CardFactory.heroCard("Test", "Test", ["https://picsum.photos/200/200"]); 41 | 42 | if (query.parameters && query.parameters[0] && query.parameters[0].name === "initialRun") { 43 | return Promise.resolve({ 44 | type: "result", 45 | attachmentLayout: "grid", 46 | attachments: [ 47 | card 48 | ] 49 | }); 50 | } else { 51 | return Promise.resolve({ 52 | type: "result", 53 | attachmentLayout: "list", 54 | attachments: [ 55 | card 56 | ] 57 | }); 58 | } 59 | } 60 | 61 | public async onQuerySettingsUrl(): Promise<{ title: string, value: string }> { 62 | return Promise.resolve({ 63 | title: "Configuration", 64 | value: "https://my-service-com/config.html" 65 | }); 66 | } 67 | 68 | public async onSettingsUpdate(context: TurnContext): Promise { 69 | const setting = context.activity.value.state; 70 | // Save the setting 71 | return Promise.resolve(); 72 | } 73 | } 74 | ``` 75 | 76 | To add the processor to the pipeline use code similar to this: 77 | 78 | ``` TypeScript 79 | import { MessagingExtensionMiddleware } from "botbuilder-teams-messagingextensions"; 80 | 81 | const adapter = new BotFrameworkAdapter(botSettings); 82 | adapter.user(new MessagingExtensionMiddleware("myCommandId", new MyMessageExtension())); 83 | ``` 84 | 85 | Where you should match the command id with the one in the Teams manifest file: 86 | 87 | ``` JSON 88 | "composeExtensions": [{ 89 | "botId": "12341234-1234-1234-123412341234", 90 | "canUpdateConfiguration": true, 91 | "commands": [{ 92 | "id": "myCommandId", 93 | "title": "My Command", 94 | "description": "...", 95 | "initialRun": true, 96 | "parameters": [...] 97 | }] 98 | }], 99 | ``` 100 | 101 | ### Use message actions and task modules 102 | 103 | To create an message action that shows a task module for your input define your message extension as follows in the manifest. The `fetchTask` property set to `true` indicates that we want to use a task module. 104 | 105 | ``` JSON 106 | { 107 | "id": "createToDoMessageExtension", 108 | "title": "Create To-Do", 109 | "description": "Create a To-Do item", 110 | "context": ["message", "commandBox", "compose"], 111 | "fetchTask": true, 112 | "type": "action" 113 | } 114 | ``` 115 | 116 | In the processor you need to implement the `onFetchTask` and `onSubmitAction` methods. You can either return a card using the `card` property or 117 | use the `url` parameter to point to a web page. 118 | 119 | ``` TypeScript 120 | public async onFetchTask(context: TurnContext, value: MessagingExtensionAction): Promise { 121 | return Promise.resolve({ 122 | type: "continue", 123 | value: { 124 | title: "Task Module", 125 | card: CardFactory.adaptiveCard({ 126 | $schema: "http://adaptivecards.io/schemas/adaptive-card.json", 127 | type: "AdaptiveCard", 128 | version: "1.0", 129 | body: [ 130 | { 131 | type: "TextBlock", 132 | text: "Please enter your e-mail" 133 | }, 134 | { 135 | type: "Input.Text", 136 | id: "myEmail", 137 | placeholder: "youremail@example.com", 138 | style: "email" 139 | }, 140 | ], 141 | actions: [ 142 | { 143 | type: "Action.Submit", 144 | title: "OK", 145 | data: { id: "unique-id" } 146 | } 147 | ] 148 | }) 149 | } 150 | }); 151 | } 152 | 153 | // handle response in here 154 | public async onSubmitAction(context: TurnContext, value: MessagingExtensionAction): Promise { 155 | const email = value.data.myEmail; 156 | const id = value.data.id; 157 | ... 158 | } 159 | ``` 160 | 161 | ## Contributors 162 | 163 | * [Wictor Wilén](https://github.com/wictorwilen) - Original author and coordinator 164 | * [Thomas White](https://github.com/tdwhite0) 165 | * [Bill Bliss](https://github.com/billbliss) 166 | * [greyseer256](https://github.com/greyseer256) 167 | * [Kavin Singh](https://github.com/kavins14) 168 | 169 | ## License 170 | 171 | Copyright (c) Wictor Wilén. All rights reserved. 172 | 173 | Licensed under the MIT license. 174 | -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | name: "package", 3 | displayName: "package", 4 | rootDir: "./", 5 | globals: { 6 | "ts-jest": { 7 | tsconfig: "/tsconfig.json", 8 | diagnostics: { 9 | ignoreCodes: [151001] 10 | } 11 | } 12 | }, 13 | preset: "ts-jest/presets/js-with-ts", 14 | testMatch: [ 15 | "/src/**/*.spec.(ts|tsx|js)" 16 | ], 17 | collectCoverageFrom: [ 18 | "/src/**/*.{js,jsx,ts,tsx}", 19 | "!/node_modules/" 20 | ], 21 | coverageReporters: [ 22 | "text", "html", "lcov" 23 | ] 24 | }; 25 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "botbuilder-teams-messagingextensions", 3 | "version": "1.8.1", 4 | "description": "Microsoft Bot Builder Framework v4 middleware for Microsoft Teams Messaging Extensions", 5 | "main": "lib/index.js", 6 | "scripts": { 7 | "build": "tsc -p .", 8 | "clean": "rm -rf lib", 9 | "lint": "eslint ./src/**/*.{js,ts,tsx}", 10 | "test": "jest --ci --reporters=jest-junit --reporters=default --coverage --coverageDirectory='coverage' --collectCoverageFrom='src/**/*.{ts,tsx,js}'" 11 | }, 12 | "keywords": [ 13 | "microsoft-teams", 14 | "msteams", 15 | "botbuilder", 16 | "botbuilder-extension", 17 | "botbuilder-middleware" 18 | ], 19 | "author": "Wictor Wilén", 20 | "maintainers": [ 21 | { 22 | "name": "Wictor Wilén", 23 | "email": "wictor@wictorwilen.se", 24 | "url": "http://www.wictorwilen.se" 25 | } 26 | ], 27 | "contributors": [ 28 | "White, Thomas ", 29 | "Bliss, Bill ", 30 | "", 31 | "" 32 | ], 33 | "repository": { 34 | "type": "git", 35 | "url": "https://github.com/wictorwilen/botbuilder-teams-messagingextensions.git" 36 | }, 37 | "bugs": { 38 | "url": "https://github.com/wictorwilen/botbuilder-teams-messagingextensions/issues" 39 | }, 40 | "homepage": "https://github.com/wictorwilen/botbuilder-teams-messagingextensions", 41 | "license": "MIT", 42 | "devDependencies": { 43 | "@types/debug": "^4.1.7", 44 | "@types/jest": "^27.0.2", 45 | "@typescript-eslint/eslint-plugin": "^4.13.0", 46 | "@typescript-eslint/parser": "^4.13.0", 47 | "botbuilder-core": "^4.14.1", 48 | "eslint": "^7.18.0", 49 | "eslint-config-standard": "^16.0.2", 50 | "eslint-plugin-import": "^2.22.1", 51 | "eslint-plugin-jest": "^24.3.6", 52 | "eslint-plugin-node": "^11.1.0", 53 | "eslint-plugin-promise": "^5.1.0", 54 | "jest": "^27.0.4", 55 | "jest-auto-stub": "^1.0.8", 56 | "jest-junit": "^13.0.0", 57 | "ts-jest": "^27.0.3", 58 | "typescript": "^4.4.3" 59 | }, 60 | "dependencies": { 61 | "debug": "^4.3.2" 62 | }, 63 | "funding": { 64 | "type": "individual", 65 | "url": "https://github.com/sponsors/wictorwilen/" 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /src/index.spec.ts: -------------------------------------------------------------------------------- 1 | import { TurnContext } from "botbuilder-core"; 2 | import { stub } from "jest-auto-stub"; 3 | 4 | import * as exp from "./index"; 5 | 6 | const next = jest.fn().mockResolvedValue(undefined); 7 | const sendActivity = jest.fn().mockResolvedValue(undefined); 8 | 9 | describe("index", () => { 10 | let processor: exp.IMessagingExtensionMiddlewareProcessor; 11 | beforeEach(() => { 12 | processor = stub(); 13 | jest.resetAllMocks(); 14 | }); 15 | 16 | it("Should export MessagingExtensionMiddleware", () => { 17 | expect(exp.MessagingExtensionMiddleware).toBeDefined(); 18 | }); 19 | 20 | it("Should successfully create the MessagingExtensionMiddleware", () => { 21 | const mw = new exp.MessagingExtensionMiddleware("command", processor); 22 | expect(mw).toBeDefined(); 23 | }); 24 | 25 | it("Should successfully call OnTurn and pass through", async () => { 26 | const mw = new exp.MessagingExtensionMiddleware("command", processor); 27 | const result = await mw.onTurn(stub(), next); 28 | expect(result).toBe(undefined); 29 | expect(next).toBeCalled(); 30 | }); 31 | 32 | it("Should successfully call OnTurn and pass through, without activity", async () => { 33 | const mw = new exp.MessagingExtensionMiddleware("command", processor); 34 | const result = await mw.onTurn(stub({ activity: undefined }), next); 35 | expect(result).toBe(undefined); 36 | expect(next).toBeCalled(); 37 | }); 38 | 39 | it("Should successfully call OnTurn and pass through, without activity name", async () => { 40 | const mw = new exp.MessagingExtensionMiddleware("command", processor); 41 | const result = await mw.onTurn(stub({ activity: { name: undefined } }), next); 42 | expect(result).toBe(undefined); 43 | expect(next).toBeCalled(); 44 | }); 45 | 46 | describe("onActionExecute", () => { 47 | it("Should not call onActionExecute", async () => { 48 | processor.onActionExecute = jest.fn().mockResolvedValue({}); 49 | const mw = new exp.MessagingExtensionMiddleware("command", processor); 50 | 51 | const result = await mw.onTurn(stub({ sendActivity }), next); 52 | expect(result).toBe(undefined); 53 | expect(processor.onActionExecute).toBeCalledTimes(0); 54 | expect(sendActivity).not.toBeCalled(); 55 | expect(next).toBeCalled(); 56 | }); 57 | 58 | it("Should call onActionExecute", async () => { 59 | processor.onActionExecute = jest.fn().mockResolvedValue({}); 60 | const mw = new exp.MessagingExtensionMiddleware("command", processor); 61 | 62 | const result = await mw.onTurn(stub({ 63 | activity: { name: "adaptiveCard/action" }, 64 | sendActivity 65 | }), next); 66 | expect(result).toBe(undefined); 67 | expect(sendActivity).toBeCalledTimes(1); 68 | expect(processor.onActionExecute).toBeCalledTimes(1); 69 | expect(next).not.toBeCalled(); 70 | }); 71 | 72 | it("Should handle onActionExecute error", async () => { 73 | processor.onActionExecute = jest.fn().mockRejectedValue("error"); 74 | const mw = new exp.MessagingExtensionMiddleware("command", processor); 75 | 76 | const result = await mw.onTurn(stub({ 77 | activity: { name: "adaptiveCard/action" }, 78 | sendActivity 79 | }), next); 80 | expect(result).toBe(undefined); 81 | expect(processor.onActionExecute).toBeCalledTimes(1); 82 | expect(sendActivity).toBeCalledTimes(1); 83 | expect(sendActivity).toBeCalledWith({ type: "invokeResponse", value: { body: "error", status: 500 } }); 84 | expect(next).not.toBeCalled(); 85 | }); 86 | 87 | it("Should call next if missing onActionExecute", async () => { 88 | processor.onActionExecute = undefined; 89 | const mw = new exp.MessagingExtensionMiddleware("command", processor); 90 | 91 | const result = await mw.onTurn(stub({ 92 | activity: { name: "adaptiveCard/action" }, 93 | sendActivity 94 | }), next); 95 | expect(result).toBe(undefined); 96 | expect(sendActivity).not.toBeCalled(); 97 | expect(next).toBeCalled(); 98 | }); 99 | }); 100 | 101 | describe("onQuery", () => { 102 | it("Should call onQuery", async () => { 103 | processor.onQuery = jest.fn().mockResolvedValue({}); 104 | const mw = new exp.MessagingExtensionMiddleware("command", processor); 105 | 106 | const result = await mw.onTurn(stub({ 107 | activity: { 108 | name: "composeExtension/query", 109 | value: { 110 | commandId: "command" 111 | } 112 | }, 113 | sendActivity 114 | }), next); 115 | expect(result).toBe(undefined); 116 | expect(processor.onQuery).toBeCalledTimes(1); 117 | expect(sendActivity).toBeCalledTimes(1); 118 | expect(next).not.toBeCalled(); 119 | }); 120 | 121 | it("Should handle onQuery error", async () => { 122 | processor.onQuery = jest.fn().mockRejectedValue("error"); 123 | const mw = new exp.MessagingExtensionMiddleware("command", processor); 124 | 125 | const result = await mw.onTurn(stub({ 126 | activity: { 127 | name: "composeExtension/query", 128 | value: { 129 | commandId: "command" 130 | } 131 | }, 132 | sendActivity 133 | }), next); 134 | expect(result).toBe(undefined); 135 | expect(processor.onQuery).toBeCalledTimes(1); 136 | expect(sendActivity).toBeCalledTimes(1); 137 | expect(sendActivity).toBeCalledWith({ type: "invokeResponse", value: { body: "error", status: 500 } }); 138 | expect(next).not.toBeCalled(); 139 | }); 140 | 141 | it("Should not call onQuery - invalid command id", async () => { 142 | processor.onQuery = jest.fn().mockResolvedValue({}); 143 | const mw = new exp.MessagingExtensionMiddleware("command", processor); 144 | 145 | const result = await mw.onTurn(stub({ 146 | activity: { 147 | name: "composeExtension/query", 148 | value: { 149 | commandId: "wrong" 150 | } 151 | } 152 | }), next); 153 | expect(result).toBe(undefined); 154 | expect(processor.onQuery).toBeCalledTimes(0); 155 | expect(next).toBeCalled(); 156 | }); 157 | 158 | it("Should not call onQuery - missing onQuery method", async () => { 159 | processor.onQuery = undefined; 160 | const mw = new exp.MessagingExtensionMiddleware("command", processor); 161 | 162 | const result = await mw.onTurn(stub({ 163 | activity: { 164 | name: "composeExtension/query", 165 | value: { 166 | commandId: "command" 167 | } 168 | } 169 | }), next); 170 | expect(result).toBe(undefined); 171 | expect(next).toBeCalled(); 172 | }); 173 | 174 | it("Should not call onQuery - not correct activity name", async () => { 175 | processor.onQuery = jest.fn().mockResolvedValue({}); 176 | const mw = new exp.MessagingExtensionMiddleware("command", processor); 177 | 178 | const result = await mw.onTurn(stub({}), next); 179 | expect(result).toBe(undefined); 180 | expect(processor.onQuery).toBeCalledTimes(0); 181 | expect(next).toBeCalled(); 182 | }); 183 | }); 184 | 185 | describe("onQuerySettingsUrl", () => { 186 | it("Should call onQuerySettingsUrl", async () => { 187 | processor.onQuerySettingsUrl = jest.fn().mockResolvedValue({}); 188 | const mw = new exp.MessagingExtensionMiddleware("command", processor); 189 | 190 | const result = await mw.onTurn(stub({ 191 | activity: { 192 | name: "composeExtension/querySettingUrl", 193 | value: { 194 | commandId: "command" 195 | } 196 | }, 197 | sendActivity 198 | }), next); 199 | expect(result).toBe(undefined); 200 | expect(processor.onQuerySettingsUrl).toBeCalledTimes(1); 201 | expect(sendActivity).toBeCalledTimes(1); 202 | expect(next).not.toBeCalled(); 203 | }); 204 | 205 | it("Should handle onQuerySettingsUrl error", async () => { 206 | processor.onQuerySettingsUrl = jest.fn().mockRejectedValue("error"); 207 | const mw = new exp.MessagingExtensionMiddleware("command", processor); 208 | 209 | const result = await mw.onTurn(stub({ 210 | activity: { 211 | name: "composeExtension/querySettingUrl", 212 | value: { 213 | commandId: "command" 214 | } 215 | }, 216 | sendActivity 217 | }), next); 218 | expect(result).toBe(undefined); 219 | expect(processor.onQuerySettingsUrl).toBeCalledTimes(1); 220 | expect(sendActivity).toBeCalledTimes(1); 221 | expect(sendActivity).toBeCalledWith({ type: "invokeResponse", value: { body: "error", status: 500 } }); 222 | expect(next).not.toBeCalled(); 223 | }); 224 | 225 | it("Should not call onQuerySettingsUrl - invalid command id", async () => { 226 | processor.onQuerySettingsUrl = jest.fn().mockResolvedValue({}); 227 | const mw = new exp.MessagingExtensionMiddleware("command", processor); 228 | 229 | const result = await mw.onTurn(stub({ 230 | activity: { 231 | name: "composeExtension/querySettingUrl", 232 | value: { 233 | commandId: "wrong" 234 | } 235 | } 236 | }), next); 237 | expect(result).toBe(undefined); 238 | expect(processor.onQuerySettingsUrl).toBeCalledTimes(0); 239 | expect(next).toBeCalled(); 240 | }); 241 | 242 | it("Should not call onQuerySettingsUrl - missing onQuery method", async () => { 243 | processor.onQuerySettingsUrl = undefined; 244 | const mw = new exp.MessagingExtensionMiddleware("command", processor); 245 | 246 | const result = await mw.onTurn(stub({ 247 | activity: { 248 | name: "composeExtension/querySettingUrl", 249 | value: { 250 | commandId: "command" 251 | } 252 | } 253 | }), next); 254 | expect(result).toBe(undefined); 255 | expect(next).toBeCalled(); 256 | }); 257 | 258 | it("Should not call onQuerySettingsUrl - not correct activity name", async () => { 259 | processor.onQuerySettingsUrl = jest.fn().mockResolvedValue({}); 260 | const mw = new exp.MessagingExtensionMiddleware("command", processor); 261 | 262 | const result = await mw.onTurn(stub({}), next); 263 | expect(result).toBe(undefined); 264 | expect(processor.onQuerySettingsUrl).toBeCalledTimes(0); 265 | expect(next).toBeCalled(); 266 | }); 267 | }); 268 | 269 | describe("onSettings", () => { 270 | it("Should call onSettings", async () => { 271 | processor.onSettings = jest.fn().mockResolvedValue({}); 272 | const mw = new exp.MessagingExtensionMiddleware("command", processor); 273 | 274 | const result = await mw.onTurn(stub({ 275 | activity: { 276 | name: "composeExtension/setting", 277 | value: { 278 | commandId: "command" 279 | } 280 | }, 281 | sendActivity 282 | }), next); 283 | expect(result).toBe(undefined); 284 | expect(processor.onSettings).toBeCalledTimes(1); 285 | expect(sendActivity).toBeCalledTimes(1); 286 | expect(next).not.toBeCalled(); 287 | }); 288 | 289 | it("Should handle onSettings error", async () => { 290 | processor.onSettings = jest.fn().mockRejectedValue("error"); 291 | const mw = new exp.MessagingExtensionMiddleware("command", processor); 292 | 293 | const result = await mw.onTurn(stub({ 294 | activity: { 295 | name: "composeExtension/setting", 296 | value: { 297 | commandId: "command" 298 | } 299 | }, 300 | sendActivity 301 | }), next); 302 | expect(result).toBe(undefined); 303 | expect(processor.onSettings).toBeCalledTimes(1); 304 | expect(sendActivity).toBeCalledTimes(1); 305 | expect(sendActivity).toBeCalledWith({ type: "invokeResponse", value: { body: "error", status: 500 } }); 306 | expect(next).not.toBeCalled(); 307 | }); 308 | 309 | it("Should not call onSettings - invalid command id", async () => { 310 | processor.onSettings = jest.fn().mockResolvedValue({}); 311 | const mw = new exp.MessagingExtensionMiddleware("command", processor); 312 | 313 | const result = await mw.onTurn(stub({ 314 | activity: { 315 | name: "composeExtension/setting", 316 | value: { 317 | commandId: "wrong" 318 | } 319 | } 320 | }), next); 321 | expect(result).toBe(undefined); 322 | expect(processor.onSettings).toBeCalledTimes(0); 323 | expect(next).toBeCalled(); 324 | }); 325 | 326 | it("Should not call onSettings - missing onQuery method", async () => { 327 | processor.onSettings = undefined; 328 | const mw = new exp.MessagingExtensionMiddleware("command", processor); 329 | 330 | const result = await mw.onTurn(stub({ 331 | activity: { 332 | name: "composeExtension/setting", 333 | value: { 334 | commandId: "command" 335 | } 336 | } 337 | }), next); 338 | expect(result).toBe(undefined); 339 | expect(next).toBeCalled(); 340 | }); 341 | 342 | it("Should not call onSettings - not correct activity name", async () => { 343 | processor.onSettings = jest.fn().mockResolvedValue({}); 344 | const mw = new exp.MessagingExtensionMiddleware("command", processor); 345 | 346 | const result = await mw.onTurn(stub({}), next); 347 | expect(result).toBe(undefined); 348 | expect(processor.onSettings).toBeCalledTimes(0); 349 | expect(next).toBeCalled(); 350 | }); 351 | }); 352 | 353 | describe("onQueryLink", () => { 354 | it("Should call onQueryLink", async () => { 355 | processor.onQueryLink = jest.fn().mockResolvedValue({}); 356 | const mw = new exp.MessagingExtensionMiddleware("command", processor); 357 | 358 | const result = await mw.onTurn(stub({ 359 | activity: { 360 | name: "composeExtension/queryLink", 361 | value: { 362 | commandId: "command" 363 | } 364 | }, 365 | sendActivity 366 | }), next); 367 | expect(result).toBe(undefined); 368 | expect(processor.onQueryLink).toBeCalledTimes(1); 369 | expect(sendActivity).toBeCalledTimes(1); 370 | expect(next).not.toBeCalled(); 371 | }); 372 | 373 | it("Should handle onQueryLink error", async () => { 374 | processor.onQueryLink = jest.fn().mockRejectedValue("error"); 375 | const mw = new exp.MessagingExtensionMiddleware("command", processor); 376 | 377 | const result = await mw.onTurn(stub({ 378 | activity: { 379 | name: "composeExtension/queryLink", 380 | value: { 381 | commandId: "command" 382 | } 383 | }, 384 | sendActivity 385 | }), next); 386 | expect(result).toBe(undefined); 387 | expect(processor.onQueryLink).toBeCalledTimes(1); 388 | expect(sendActivity).toBeCalledTimes(1); 389 | expect(sendActivity).toBeCalledWith({ type: "invokeResponse", value: { body: "error", status: 500 } }); 390 | expect(next).not.toBeCalled(); 391 | }); 392 | 393 | it("Should call onQueryLink - with invalid command id", async () => { 394 | processor.onQueryLink = jest.fn().mockResolvedValue({}); 395 | const mw = new exp.MessagingExtensionMiddleware("command", processor); 396 | 397 | const result = await mw.onTurn(stub({ 398 | activity: { 399 | name: "composeExtension/queryLink", 400 | value: { 401 | commandId: "command" 402 | } 403 | }, 404 | sendActivity 405 | }), next); 406 | expect(result).toBe(undefined); 407 | expect(processor.onQueryLink).toBeCalledTimes(1); 408 | expect(sendActivity).toBeCalledTimes(1); 409 | expect(next).not.toBeCalled(); 410 | }); 411 | 412 | it("Should not call onQueryLink - missing onQuery method", async () => { 413 | processor.onQueryLink = undefined; 414 | const mw = new exp.MessagingExtensionMiddleware("command", processor); 415 | 416 | const result = await mw.onTurn(stub({ 417 | activity: { 418 | name: "composeExtension/queryLink", 419 | value: { 420 | commandId: "command" 421 | } 422 | } 423 | }), next); 424 | expect(result).toBe(undefined); 425 | expect(next).toBeCalled(); 426 | }); 427 | 428 | it("Should not call onQueryLink - not correct activity name", async () => { 429 | processor.onQueryLink = jest.fn().mockResolvedValue({}); 430 | const mw = new exp.MessagingExtensionMiddleware("command", processor); 431 | 432 | const result = await mw.onTurn(stub({}), next); 433 | expect(result).toBe(undefined); 434 | expect(processor.onQueryLink).toBeCalledTimes(0); 435 | expect(next).toBeCalled(); 436 | }); 437 | }); 438 | 439 | describe("onSelectItem", () => { 440 | it("Should call onSelectItem", async () => { 441 | processor.onSelectItem = jest.fn().mockResolvedValue({}); 442 | const mw = new exp.MessagingExtensionMiddleware("command", processor); 443 | 444 | const result = await mw.onTurn(stub({ 445 | activity: { 446 | name: "composeExtension/selectItem", 447 | value: { 448 | commandId: "command" 449 | } 450 | }, 451 | sendActivity 452 | }), next); 453 | expect(result).toBe(undefined); 454 | expect(processor.onSelectItem).toBeCalledTimes(1); 455 | expect(sendActivity).toBeCalledTimes(1); 456 | expect(next).not.toBeCalled(); 457 | }); 458 | 459 | it("Should handle onSelectItem error", async () => { 460 | processor.onSelectItem = jest.fn().mockRejectedValue("error"); 461 | const mw = new exp.MessagingExtensionMiddleware("command", processor); 462 | 463 | const result = await mw.onTurn(stub({ 464 | activity: { 465 | name: "composeExtension/selectItem", 466 | value: { 467 | commandId: "command" 468 | } 469 | }, 470 | sendActivity 471 | }), next); 472 | expect(result).toBe(undefined); 473 | expect(processor.onSelectItem).toBeCalledTimes(1); 474 | expect(sendActivity).toBeCalledTimes(1); 475 | expect(sendActivity).toBeCalledWith({ type: "invokeResponse", value: { body: "error", status: 500 } }); 476 | expect(next).not.toBeCalled(); 477 | }); 478 | 479 | it("Should call onSelectItem - with invalid command id", async () => { 480 | processor.onSelectItem = jest.fn().mockResolvedValue({}); 481 | const mw = new exp.MessagingExtensionMiddleware("command", processor); 482 | 483 | const result = await mw.onTurn(stub({ 484 | activity: { 485 | name: "composeExtension/selectItem", 486 | value: { 487 | commandId: "command" 488 | } 489 | }, 490 | sendActivity 491 | }), next); 492 | expect(result).toBe(undefined); 493 | expect(processor.onSelectItem).toBeCalledTimes(1); 494 | expect(sendActivity).toBeCalledTimes(1); 495 | expect(next).not.toBeCalled(); 496 | }); 497 | 498 | it("Should not call onSelectItem - missing onQuery method", async () => { 499 | processor.onSelectItem = undefined; 500 | const mw = new exp.MessagingExtensionMiddleware("command", processor); 501 | 502 | const result = await mw.onTurn(stub({ 503 | activity: { 504 | name: "composeExtension/selectItem", 505 | value: { 506 | commandId: "command" 507 | } 508 | } 509 | }), next); 510 | expect(result).toBe(undefined); 511 | expect(next).toBeCalled(); 512 | }); 513 | 514 | it("Should not call onSelectItem - not correct activity name", async () => { 515 | processor.onSelectItem = jest.fn().mockResolvedValue({}); 516 | const mw = new exp.MessagingExtensionMiddleware("command", processor); 517 | 518 | const result = await mw.onTurn(stub({}), next); 519 | expect(result).toBe(undefined); 520 | expect(processor.onSelectItem).toBeCalledTimes(0); 521 | expect(next).toBeCalled(); 522 | }); 523 | }); 524 | 525 | describe("onCardButtonClicked", () => { 526 | it("Should call onCardButtonClicked", async () => { 527 | processor.onCardButtonClicked = jest.fn().mockResolvedValue({}); 528 | const mw = new exp.MessagingExtensionMiddleware("command", processor); 529 | 530 | const result = await mw.onTurn(stub({ 531 | activity: { 532 | name: "composeExtension/onCardButtonClicked", 533 | value: { 534 | commandId: "command" 535 | } 536 | }, 537 | sendActivity 538 | }), next); 539 | expect(result).toBe(undefined); 540 | expect(processor.onCardButtonClicked).toBeCalledTimes(1); 541 | expect(sendActivity).toBeCalledTimes(1); 542 | expect(next).toBeCalled(); 543 | }); 544 | 545 | it("Should handle onCardButtonClicked error", async () => { 546 | processor.onCardButtonClicked = jest.fn().mockRejectedValue("error"); 547 | const mw = new exp.MessagingExtensionMiddleware("command", processor); 548 | 549 | const result = await mw.onTurn(stub({ 550 | activity: { 551 | name: "composeExtension/onCardButtonClicked", 552 | value: { 553 | commandId: "command" 554 | } 555 | }, 556 | sendActivity 557 | }), next); 558 | expect(result).toBe(undefined); 559 | expect(processor.onCardButtonClicked).toBeCalledTimes(1); 560 | expect(sendActivity).toBeCalledTimes(1); 561 | expect(sendActivity).toBeCalledWith({ type: "invokeResponse", value: { body: "error", status: 500 } }); 562 | expect(next).toBeCalled(); 563 | }); 564 | 565 | it("Should call onCardButtonClicked - with invalid command id", async () => { 566 | processor.onCardButtonClicked = jest.fn().mockResolvedValue({}); 567 | const mw = new exp.MessagingExtensionMiddleware("command", processor); 568 | 569 | const result = await mw.onTurn(stub({ 570 | activity: { 571 | name: "composeExtension/onCardButtonClicked", 572 | value: { 573 | commandId: "command" 574 | } 575 | }, 576 | sendActivity 577 | }), next); 578 | expect(result).toBe(undefined); 579 | expect(processor.onCardButtonClicked).toBeCalledTimes(1); 580 | expect(sendActivity).toBeCalledTimes(1); 581 | expect(next).toBeCalled(); 582 | }); 583 | 584 | it("Should not call onCardButtonClicked - missing onQuery method", async () => { 585 | processor.onCardButtonClicked = undefined; 586 | const mw = new exp.MessagingExtensionMiddleware("command", processor); 587 | 588 | const result = await mw.onTurn(stub({ 589 | activity: { 590 | name: "composeExtension/onCardButtonClicked", 591 | value: { 592 | commandId: "command" 593 | } 594 | } 595 | }), next); 596 | expect(result).toBe(undefined); 597 | expect(next).toBeCalled(); 598 | }); 599 | 600 | it("Should not call onCardButtonClicked - not correct activity name", async () => { 601 | processor.onCardButtonClicked = jest.fn().mockResolvedValue({}); 602 | const mw = new exp.MessagingExtensionMiddleware("command", processor); 603 | 604 | const result = await mw.onTurn(stub({}), next); 605 | expect(result).toBe(undefined); 606 | expect(processor.onCardButtonClicked).toBeCalledTimes(0); 607 | expect(next).toBeCalled(); 608 | }); 609 | }); 610 | 611 | describe("onFetchTask/fetchTask", () => { 612 | it("Should call onFetchTask", async () => { 613 | processor.onFetchTask = jest.fn().mockResolvedValue({}); 614 | const mw = new exp.MessagingExtensionMiddleware("command", processor); 615 | 616 | const result = await mw.onTurn(stub({ 617 | activity: { 618 | name: "composeExtension/fetchTask", 619 | value: { 620 | commandId: "command" 621 | } 622 | }, 623 | sendActivity 624 | }), next); 625 | expect(result).toBe(undefined); 626 | expect(processor.onFetchTask).toBeCalledTimes(1); 627 | expect(sendActivity).toBeCalledTimes(1); 628 | expect(next).not.toBeCalled(); 629 | }); 630 | 631 | it("Should handle onFetchTask error", async () => { 632 | processor.onFetchTask = jest.fn().mockRejectedValue("error"); 633 | const mw = new exp.MessagingExtensionMiddleware("command", processor); 634 | 635 | const result = await mw.onTurn(stub({ 636 | activity: { 637 | name: "composeExtension/fetchTask", 638 | value: { 639 | commandId: "command" 640 | } 641 | }, 642 | sendActivity 643 | }), next); 644 | expect(result).toBe(undefined); 645 | expect(processor.onFetchTask).toBeCalledTimes(1); 646 | expect(sendActivity).toBeCalledTimes(1); 647 | expect(sendActivity).toBeCalledWith({ type: "invokeResponse", value: { body: "error", status: 500 } }); 648 | expect(next).not.toBeCalled(); 649 | }); 650 | 651 | it("Should call onFetchTask - with invalid command id", async () => { 652 | processor.onFetchTask = jest.fn().mockResolvedValue({}); 653 | const mw = new exp.MessagingExtensionMiddleware("command", processor); 654 | 655 | const result = await mw.onTurn(stub({ 656 | activity: { 657 | name: "composeExtension/fetchTask", 658 | value: { 659 | commandId: "command" 660 | } 661 | }, 662 | sendActivity 663 | }), next); 664 | expect(result).toBe(undefined); 665 | expect(processor.onFetchTask).toBeCalledTimes(1); 666 | expect(sendActivity).toBeCalledTimes(1); 667 | expect(next).not.toBeCalled(); 668 | }); 669 | 670 | it("Should not call onFetchTask - missing onQuery method", async () => { 671 | processor.onFetchTask = undefined; 672 | const mw = new exp.MessagingExtensionMiddleware("command", processor); 673 | 674 | const result = await mw.onTurn(stub({ 675 | activity: { 676 | name: "composeExtension/fetchTask", 677 | value: { 678 | commandId: "command" 679 | } 680 | } 681 | }), next); 682 | expect(result).toBe(undefined); 683 | expect(next).toBeCalled(); 684 | }); 685 | 686 | it("Should not call onFetchTask - not correct activity name", async () => { 687 | processor.onFetchTask = jest.fn().mockResolvedValue({}); 688 | const mw = new exp.MessagingExtensionMiddleware("command", processor); 689 | 690 | const result = await mw.onTurn(stub({}), next); 691 | expect(result).toBe(undefined); 692 | expect(processor.onFetchTask).toBeCalledTimes(0); 693 | expect(next).toBeCalled(); 694 | }); 695 | }); 696 | 697 | describe("onFetchTask/fetch", () => { 698 | it("Should call onFetchTask", async () => { 699 | processor.onFetchTask = jest.fn().mockResolvedValue({}); 700 | const mw = new exp.MessagingExtensionMiddleware("command", processor); 701 | 702 | const result = await mw.onTurn(stub({ 703 | activity: { 704 | name: "task/fetch", 705 | value: { 706 | commandId: "command" 707 | } 708 | }, 709 | sendActivity 710 | }), next); 711 | expect(result).toBe(undefined); 712 | expect(processor.onFetchTask).toBeCalledTimes(1); 713 | expect(sendActivity).toBeCalledTimes(1); 714 | expect(next).not.toBeCalled(); 715 | }); 716 | 717 | it("Should call onFetchTask, without a commandId", async () => { 718 | processor.onFetchTask = jest.fn().mockResolvedValue({}); 719 | const mw = new exp.MessagingExtensionMiddleware(undefined, processor); 720 | 721 | const result = await mw.onTurn(stub({ 722 | activity: { 723 | name: "task/fetch", 724 | value: { 725 | commandId: "command" 726 | } 727 | }, 728 | sendActivity 729 | }), next); 730 | expect(result).toBe(undefined); 731 | expect(processor.onFetchTask).toBeCalledTimes(1); 732 | expect(sendActivity).toBeCalledTimes(1); 733 | expect(sendActivity).toBeCalledWith({ 734 | type: "invokeResponse", 735 | value: { body: { composeExtension: {} }, status: 200 } 736 | }); 737 | expect(next).not.toBeCalled(); 738 | }); 739 | 740 | it("Should call onFetchTask, returning a continue message", async () => { 741 | processor.onFetchTask = jest.fn().mockResolvedValue({ type: "continue" }); 742 | const mw = new exp.MessagingExtensionMiddleware("command", processor); 743 | 744 | const result = await mw.onTurn(stub({ 745 | activity: { 746 | name: "task/fetch", 747 | value: { 748 | commandId: "command" 749 | } 750 | }, 751 | sendActivity 752 | }), next); 753 | expect(result).toBe(undefined); 754 | expect(processor.onFetchTask).toBeCalledTimes(1); 755 | expect(sendActivity).toBeCalledTimes(1); 756 | expect(sendActivity).toBeCalledWith({ 757 | type: "invokeResponse", 758 | value: { body: { task: { type: "continue" } }, status: 200 } 759 | }); 760 | 761 | expect(next).not.toBeCalled(); 762 | }); 763 | 764 | it("Should call onFetchTask, returning a message message", async () => { 765 | processor.onFetchTask = jest.fn().mockResolvedValue({ type: "message" }); 766 | const mw = new exp.MessagingExtensionMiddleware("command", processor); 767 | 768 | const result = await mw.onTurn(stub({ 769 | activity: { 770 | name: "task/fetch", 771 | value: { 772 | commandId: "command" 773 | } 774 | }, 775 | sendActivity 776 | }), next); 777 | expect(result).toBe(undefined); 778 | expect(processor.onFetchTask).toBeCalledTimes(1); 779 | expect(sendActivity).toBeCalledTimes(1); 780 | expect(sendActivity).toBeCalledWith({ 781 | type: "invokeResponse", 782 | value: { body: { task: { type: "message" } }, status: 200 } 783 | }); 784 | 785 | expect(next).not.toBeCalled(); 786 | }); 787 | 788 | it("Should handle onFetchTask error", async () => { 789 | processor.onFetchTask = jest.fn().mockRejectedValue("error"); 790 | const mw = new exp.MessagingExtensionMiddleware("command", processor); 791 | 792 | const result = await mw.onTurn(stub({ 793 | activity: { 794 | name: "task/fetch", 795 | value: { 796 | commandId: "command" 797 | } 798 | }, 799 | sendActivity 800 | }), next); 801 | expect(result).toBe(undefined); 802 | expect(processor.onFetchTask).toBeCalledTimes(1); 803 | expect(sendActivity).toBeCalledTimes(1); 804 | expect(sendActivity).toBeCalledWith({ type: "invokeResponse", value: { body: "error", status: 500 } }); 805 | expect(next).not.toBeCalled(); 806 | }); 807 | 808 | it("Should call onFetchTask - with invalid command id", async () => { 809 | processor.onFetchTask = jest.fn().mockResolvedValue({}); 810 | const mw = new exp.MessagingExtensionMiddleware("command", processor); 811 | 812 | const result = await mw.onTurn(stub({ 813 | activity: { 814 | name: "task/fetch", 815 | value: { 816 | commandId: "command" 817 | } 818 | }, 819 | sendActivity 820 | }), next); 821 | expect(result).toBe(undefined); 822 | expect(processor.onFetchTask).toBeCalledTimes(1); 823 | expect(sendActivity).toBeCalledTimes(1); 824 | expect(next).not.toBeCalled(); 825 | }); 826 | 827 | it("Should not call onFetchTask - missing onQuery method", async () => { 828 | processor.onFetchTask = undefined; 829 | const mw = new exp.MessagingExtensionMiddleware("command", processor); 830 | 831 | const result = await mw.onTurn(stub({ 832 | activity: { 833 | name: "task/fetch", 834 | value: { 835 | commandId: "command" 836 | } 837 | } 838 | }), next); 839 | expect(result).toBe(undefined); 840 | expect(next).toBeCalled(); 841 | }); 842 | 843 | it("Should not call onFetchTask - not correct activity name", async () => { 844 | processor.onFetchTask = jest.fn().mockResolvedValue({}); 845 | const mw = new exp.MessagingExtensionMiddleware("command", processor); 846 | 847 | const result = await mw.onTurn(stub({}), next); 848 | expect(result).toBe(undefined); 849 | expect(processor.onFetchTask).toBeCalledTimes(0); 850 | expect(next).toBeCalled(); 851 | }); 852 | }); 853 | 854 | describe("onSubmitAction", () => { 855 | it("Should call onSubmitAction", async () => { 856 | processor.onSubmitAction = jest.fn().mockResolvedValue({}); 857 | const mw = new exp.MessagingExtensionMiddleware("command", processor); 858 | 859 | const result = await mw.onTurn(stub({ 860 | activity: { 861 | name: "composeExtension/submitAction", 862 | value: { 863 | commandId: "command" 864 | } 865 | }, 866 | sendActivity 867 | }), next); 868 | expect(result).toBe(undefined); 869 | expect(processor.onSubmitAction).toBeCalledTimes(1); 870 | expect(sendActivity).toBeCalledTimes(1); 871 | expect(next).not.toBeCalled(); 872 | }); 873 | 874 | it("Should handle onSubmitAction error", async () => { 875 | processor.onSubmitAction = jest.fn().mockRejectedValue("error"); 876 | const mw = new exp.MessagingExtensionMiddleware("command", processor); 877 | 878 | const result = await mw.onTurn(stub({ 879 | activity: { 880 | name: "composeExtension/submitAction", 881 | value: { 882 | commandId: "command" 883 | } 884 | }, 885 | sendActivity 886 | }), next); 887 | expect(result).toBe(undefined); 888 | expect(processor.onSubmitAction).toBeCalledTimes(1); 889 | expect(sendActivity).toBeCalledTimes(1); 890 | expect(sendActivity).toBeCalledWith({ type: "invokeResponse", value: { body: "error", status: 500 } }); 891 | expect(next).not.toBeCalled(); 892 | }); 893 | 894 | it("Should not call onSubmitAction - invalid command id", async () => { 895 | processor.onSubmitAction = jest.fn().mockResolvedValue({}); 896 | const mw = new exp.MessagingExtensionMiddleware("command", processor); 897 | 898 | const result = await mw.onTurn(stub({ 899 | activity: { 900 | name: "composeExtension/submitAction", 901 | value: { 902 | commandId: "wrong" 903 | } 904 | } 905 | }), next); 906 | expect(result).toBe(undefined); 907 | expect(processor.onSubmitAction).toBeCalledTimes(0); 908 | expect(next).toBeCalled(); 909 | }); 910 | 911 | it("Should not call onSubmitAction - missing onSubmitAction method", async () => { 912 | processor.onSubmitAction = undefined; 913 | const mw = new exp.MessagingExtensionMiddleware("command", processor); 914 | 915 | const result = await mw.onTurn(stub({ 916 | activity: { 917 | name: "composeExtension/submitAction", 918 | value: { 919 | commandId: "command" 920 | } 921 | }, 922 | sendActivity 923 | }), next); 924 | expect(result).toBe(undefined); 925 | expect(sendActivity).not.toBeCalled(); 926 | expect(next).toBeCalled(); 927 | }); 928 | 929 | it("Should not call onSubmitAction - not correct activity name", async () => { 930 | processor.onSubmitAction = jest.fn().mockResolvedValue({}); 931 | const mw = new exp.MessagingExtensionMiddleware("command", processor); 932 | 933 | const result = await mw.onTurn(stub({}), next); 934 | expect(result).toBe(undefined); 935 | expect(processor.onSubmitAction).toBeCalledTimes(0); 936 | expect(next).toBeCalled(); 937 | }); 938 | }); 939 | 940 | describe("onBotMessagePreviewEdit ", () => { 941 | it("Should call onBotMessagePreviewEdit ", async () => { 942 | processor.onBotMessagePreviewEdit = jest.fn().mockResolvedValue({}); 943 | const mw = new exp.MessagingExtensionMiddleware("command", processor); 944 | 945 | const result = await mw.onTurn(stub({ 946 | activity: { 947 | name: "composeExtension/submitAction", 948 | value: { 949 | commandId: "command", 950 | botMessagePreviewAction: "edit" 951 | } 952 | }, 953 | sendActivity 954 | }), next); 955 | expect(result).toBe(undefined); 956 | expect(processor.onBotMessagePreviewEdit).toBeCalledTimes(1); 957 | expect(sendActivity).toBeCalledTimes(1); 958 | expect(next).not.toBeCalled(); 959 | }); 960 | 961 | it("Should handle onBotMessagePreviewEdit error", async () => { 962 | processor.onBotMessagePreviewEdit = jest.fn().mockRejectedValue("error"); 963 | const mw = new exp.MessagingExtensionMiddleware("command", processor); 964 | 965 | const result = await mw.onTurn(stub({ 966 | activity: { 967 | name: "composeExtension/submitAction", 968 | value: { 969 | commandId: "command", 970 | botMessagePreviewAction: "edit" 971 | } 972 | }, 973 | sendActivity 974 | }), next); 975 | expect(result).toBe(undefined); 976 | expect(processor.onBotMessagePreviewEdit).toBeCalledTimes(1); 977 | expect(sendActivity).toBeCalledTimes(1); 978 | expect(sendActivity).toBeCalledWith({ type: "invokeResponse", value: { body: "error", status: 500 } }); 979 | expect(next).not.toBeCalled(); 980 | }); 981 | 982 | it("Should not call onBotMessagePreviewEdit - invalid command id", async () => { 983 | processor.onBotMessagePreviewEdit = jest.fn().mockResolvedValue({}); 984 | const mw = new exp.MessagingExtensionMiddleware("command", processor); 985 | 986 | const result = await mw.onTurn(stub({ 987 | activity: { 988 | name: "composeExtension/submitAction", 989 | value: { 990 | commandId: "wrong", 991 | botMessagePreviewAction: "edit" 992 | } 993 | } 994 | }), next); 995 | expect(result).toBe(undefined); 996 | expect(processor.onBotMessagePreviewEdit).toBeCalledTimes(0); 997 | expect(next).toBeCalled(); 998 | }); 999 | 1000 | it("Should not call onBotMessagePreviewEdit - missing onBotMessagePreviewEdit method", async () => { 1001 | processor.onBotMessagePreviewEdit = undefined; 1002 | const mw = new exp.MessagingExtensionMiddleware("command", processor); 1003 | 1004 | const result = await mw.onTurn(stub({ 1005 | activity: { 1006 | name: "composeExtension/submitAction", 1007 | value: { 1008 | commandId: "command", 1009 | botMessagePreviewAction: "edit" 1010 | } 1011 | }, 1012 | sendActivity 1013 | }), next); 1014 | expect(result).toBe(undefined); 1015 | expect(sendActivity).not.toBeCalled(); 1016 | expect(next).toBeCalled(); 1017 | }); 1018 | 1019 | it("Should not call onBotMessagePreviewEdit - not correct activity name", async () => { 1020 | processor.onBotMessagePreviewEdit = jest.fn().mockResolvedValue({}); 1021 | const mw = new exp.MessagingExtensionMiddleware("command", processor); 1022 | 1023 | const result = await mw.onTurn(stub({}), next); 1024 | expect(result).toBe(undefined); 1025 | expect(processor.onBotMessagePreviewEdit).toBeCalledTimes(0); 1026 | expect(next).toBeCalled(); 1027 | }); 1028 | }); 1029 | 1030 | describe("onBotMessagePreviewSend ", () => { 1031 | it("Should call onBotMessagePreviewSend ", async () => { 1032 | processor.onBotMessagePreviewSend = jest.fn().mockResolvedValue({}); 1033 | const mw = new exp.MessagingExtensionMiddleware("command", processor); 1034 | 1035 | const result = await mw.onTurn(stub({ 1036 | activity: { 1037 | name: "composeExtension/submitAction", 1038 | value: { 1039 | commandId: "command", 1040 | botMessagePreviewAction: "send" 1041 | } 1042 | }, 1043 | sendActivity 1044 | }), next); 1045 | expect(result).toBe(undefined); 1046 | expect(processor.onBotMessagePreviewSend).toBeCalledTimes(1); 1047 | expect(sendActivity).toBeCalledTimes(1); 1048 | expect(next).not.toBeCalled(); 1049 | }); 1050 | 1051 | it("Should handle onBotMessagePreviewSend error", async () => { 1052 | processor.onBotMessagePreviewSend = jest.fn().mockRejectedValue("error"); 1053 | const mw = new exp.MessagingExtensionMiddleware("command", processor); 1054 | 1055 | const result = await mw.onTurn(stub({ 1056 | activity: { 1057 | name: "composeExtension/submitAction", 1058 | value: { 1059 | commandId: "command", 1060 | botMessagePreviewAction: "send" 1061 | } 1062 | }, 1063 | sendActivity 1064 | }), next); 1065 | expect(result).toBe(undefined); 1066 | expect(processor.onBotMessagePreviewSend).toBeCalledTimes(1); 1067 | expect(sendActivity).toBeCalledTimes(1); 1068 | expect(sendActivity).toBeCalledWith({ type: "invokeResponse", value: { body: "error", status: 500 } }); 1069 | expect(next).not.toBeCalled(); 1070 | }); 1071 | 1072 | it("Should not call onBotMessagePreviewSend - invalid command id", async () => { 1073 | processor.onBotMessagePreviewSend = jest.fn().mockResolvedValue({}); 1074 | const mw = new exp.MessagingExtensionMiddleware("command", processor); 1075 | 1076 | const result = await mw.onTurn(stub({ 1077 | activity: { 1078 | name: "composeExtension/submitAction", 1079 | value: { 1080 | commandId: "wrong", 1081 | botMessagePreviewAction: "send" 1082 | } 1083 | } 1084 | }), next); 1085 | expect(result).toBe(undefined); 1086 | expect(processor.onBotMessagePreviewSend).toBeCalledTimes(0); 1087 | expect(next).toBeCalled(); 1088 | }); 1089 | 1090 | it("Should not call onBotMessagePreviewSend - missing onBotMessagePreviewSend method", async () => { 1091 | processor.onBotMessagePreviewSend = undefined; 1092 | const mw = new exp.MessagingExtensionMiddleware("command", processor); 1093 | 1094 | const result = await mw.onTurn(stub({ 1095 | activity: { 1096 | name: "composeExtension/submitAction", 1097 | value: { 1098 | commandId: "command", 1099 | botMessagePreviewAction: "send" 1100 | } 1101 | }, 1102 | sendActivity 1103 | }), next); 1104 | expect(result).toBe(undefined); 1105 | expect(sendActivity).not.toBeCalled(); 1106 | expect(next).toBeCalled(); 1107 | }); 1108 | 1109 | it("Should not call onBotMessagePreviewSend - not correct activity name", async () => { 1110 | processor.onBotMessagePreviewEdit = jest.fn().mockResolvedValue({}); 1111 | const mw = new exp.MessagingExtensionMiddleware("command", processor); 1112 | 1113 | const result = await mw.onTurn(stub({}), next); 1114 | expect(result).toBe(undefined); 1115 | expect(processor.onBotMessagePreviewSend).toBeCalledTimes(0); 1116 | expect(next).toBeCalled(); 1117 | }); 1118 | }); 1119 | }); 1120 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | // Copyright (c) Wictor Wilén. All rights reserved. 2 | // Licensed under the MIT license. 3 | 4 | import { 5 | Middleware, TaskModuleMessageResponse, TurnContext, 6 | AppBasedLinkQuery, 7 | MessagingExtensionAction, 8 | MessagingExtensionQuery, 9 | MessagingExtensionResult, 10 | TaskModuleContinueResponse, 11 | AdaptiveCardInvokeAction 12 | } from "botbuilder-core"; 13 | 14 | import { debug } from "debug"; 15 | 16 | // Initialize debug logging module 17 | const log = debug("msteams"); 18 | 19 | export interface AdaptiveCardRequestValue { 20 | action: AdaptiveCardInvokeAction; 21 | trigger: "automatic" | "manual"; 22 | } 23 | 24 | export interface AdaptiveCardResponseAdaptiveCardBody { 25 | statusCode: 200; 26 | type: "application/vnd.microsoft.card.adaptive", 27 | value: Record; 28 | } 29 | export interface AdaptiveCardResponseMessageBody { 30 | statusCode: 200; 31 | type: "application/vnd.microsoft.activity.message", 32 | value: string; 33 | } 34 | 35 | export interface AdaptiveCardResponseInvalidRequestBody { 36 | statusCode: 400; 37 | type: "application/vnd.microsoft.error", 38 | value: Error; 39 | } 40 | 41 | export declare type AdaptiveCardResponseBody = AdaptiveCardResponseAdaptiveCardBody | AdaptiveCardResponseMessageBody | AdaptiveCardResponseInvalidRequestBody; 42 | 43 | /** 44 | * Defines the processor for the Messaging Extension Middleware 45 | */ 46 | export interface IMessagingExtensionMiddlewareProcessor { 47 | /** 48 | * Processes incoming queries (composeExtension/query) 49 | * @param context the turn context 50 | * @param value the value of the query 51 | * @returns {Promise} 52 | */ 53 | onQuery?(context: TurnContext, value: MessagingExtensionQuery): Promise; 54 | /** 55 | * Process incoming requests for Messaging Extension settings (composeExtension/querySettingUrl) 56 | * @param context the turn context 57 | * @returns {Promise<{ title: string, value: string }} 58 | */ 59 | onQuerySettingsUrl?(context: TurnContext): Promise<{ title: string, value: string }>; 60 | /** 61 | * Processes incoming setting updates (composeExtension/setting) 62 | * @param context the turn context 63 | * @returns {Promise} 64 | */ 65 | onSettings?(context: TurnContext): Promise; 66 | /** 67 | * Processes incoming link queries (composeExtension/queryLink) 68 | * @param context the turn context 69 | * @param value the value of the query 70 | * @returns {Promise} 71 | */ 72 | onQueryLink?(context: TurnContext, value: AppBasedLinkQuery): Promise; 73 | /** 74 | * Processes incoming link actions (composeExtension/submitAction) 75 | * @param context the turn context 76 | * @param value the value of the query 77 | * @returns {Promise} 78 | */ 79 | onSubmitAction?(context: TurnContext, value: MessagingExtensionAction): Promise; 80 | /** 81 | * Processes incoming link actions (composeExtension/submitAction) where the `botMessagePreviewAction` is set to `send` 82 | * @param context the turn context 83 | * @param value the value of the query 84 | * @returns {Promise} 85 | */ 86 | onBotMessagePreviewSend?(context: TurnContext, value: MessagingExtensionAction): Promise; 87 | /** 88 | * Processes incoming link actions (composeExtension/submitAction) where the `botMessagePreviewAction` is set to `edit` 89 | * @param context the turn context 90 | * @param value the value of the query 91 | * @returns {Promise} 92 | */ 93 | onBotMessagePreviewEdit?(context: TurnContext, value: MessagingExtensionAction): Promise; 94 | /** 95 | * Processes incoming fetch task actions (`composeExtension/fetchTask`) 96 | * @param context the turn context 97 | * @param value commandContext 98 | * @returns {Promise} Promise object is either a `MessagingExtensionResult` for `conf` or `auth` or a `TaskModuleContinueResponse` for `message` or `continue` 99 | */ 100 | onFetchTask?(context: TurnContext, value: MessagingExtensionAction): Promise; 101 | /** 102 | * Handles Action.Submit from adaptive cards 103 | * 104 | * Note: this is experimental and it does not filter on the commandId which means that if there are 105 | * multiple registered message extension processors all will receive this command. You should ensure to 106 | * add a specific identifier to your adaptivecard. 107 | * @param context the turn context 108 | * @param value the card data 109 | * @returns {Promise} 110 | */ 111 | onCardButtonClicked?(context: TurnContext, value: any): Promise; 112 | 113 | /** 114 | * Handles when an item is selected from the result list 115 | * 116 | * Note: this is experimental and it does not filter on the commandId which means that if there are 117 | * multiple registered message extension processors all will receive this command. You should ensure to 118 | * add a specific identifier to your invoke action. 119 | * @param context the turn context 120 | * @param value object passed in to invoke action 121 | * @returns {Promise} 122 | */ 123 | onSelectItem?(context: TurnContext, value: any): Promise; 124 | 125 | /** 126 | * Handles Universal Bot Actions 127 | * Examples: 128 | * public async onActionExecute(context: TurnContext, value: AdaptiveCardRequestValue): Promise { 129 | * return { 130 | * statusCode: 200, 131 | * type: "application/vnd.microsoft.activity.message", 132 | * value: "A message" 133 | * }; 134 | * } 135 | * or 136 | * public async onActionExecute(context: TurnContext, value: AdaptiveCardRequestValue): Promise { 137 | * return { 138 | * statusCode: 200, 139 | * type: "application/vnd.microsoft.card.adaptive", 140 | * value: adaptiveCardObject 141 | * }; 142 | * } 143 | * See https://docs.microsoft.com/en-us/microsoftteams/platform/task-modules-and-cards/cards/universal-actions-for-adaptive-cards/work-with-universal-actions-for-adaptive-cards#adaptivecardaction-invoke-activity 144 | * @param context 145 | * @param value 146 | * @returns 147 | */ 148 | onActionExecute?(context: TurnContext, value: AdaptiveCardRequestValue): 149 | Promise; 150 | } 151 | 152 | const INVOKERESPONSE = "invokeResponse"; 153 | 154 | /** 155 | * A Messaging Extension Middleware for Microsoft Teams 156 | */ 157 | export class MessagingExtensionMiddleware implements Middleware { 158 | 159 | /** 160 | * Default constructor 161 | * @param commandId The commandId of the Messaging Extension to process, 162 | * or `undefined` to process all incoming requests 163 | * @param processor The processor 164 | */ 165 | // eslint-disable-next-line no-useless-constructor 166 | public constructor( 167 | private commandId: string | undefined, 168 | private processor: IMessagingExtensionMiddlewareProcessor) { 169 | // nop 170 | } 171 | 172 | /** 173 | * Bot Framework `onTurn` method 174 | * @param context the turn context 175 | * @param next the next function 176 | */ 177 | public async onTurn(context: TurnContext, next: () => Promise): Promise { 178 | if (context.activity !== undefined && context.activity.name !== undefined) { 179 | log(`Activity received - activity.name: ${context.activity.name}`); 180 | if (this.commandId !== undefined && context.activity.value !== undefined) { 181 | log(` commandId: ${context.activity.value.commandId}`); 182 | log(` parameters: ${JSON.stringify(context.activity.value.parameters)}`); 183 | } else { 184 | log(` activity.value: ${JSON.stringify(context.activity.value)}`); 185 | } 186 | 187 | switch (context.activity.name) { 188 | case "composeExtension/query": 189 | if ((this.commandId === context.activity.value.commandId || this.commandId === undefined) && 190 | this.processor.onQuery) { 191 | try { 192 | const result = await this.processor.onQuery(context, context.activity.value); 193 | context.sendActivity({ 194 | type: INVOKERESPONSE, 195 | value: { 196 | body: { 197 | composeExtension: result 198 | }, 199 | status: 200 200 | } 201 | }); 202 | } catch (err) { 203 | context.sendActivity({ 204 | type: INVOKERESPONSE, 205 | value: { 206 | body: err, 207 | status: 500 208 | } 209 | }); 210 | } 211 | return; 212 | } 213 | break; 214 | case "composeExtension/querySettingUrl": 215 | if ((this.commandId === context.activity.value.commandId || this.commandId === undefined) && 216 | this.processor.onQuerySettingsUrl) { 217 | try { 218 | const result = await this.processor.onQuerySettingsUrl(context); 219 | context.sendActivity({ 220 | type: INVOKERESPONSE, 221 | value: { 222 | body: { 223 | composeExtension: { 224 | suggestedActions: { 225 | actions: [{ 226 | type: "openApp", 227 | ...result 228 | }] 229 | }, 230 | type: "config" 231 | } 232 | }, 233 | status: 200 234 | } 235 | }); 236 | } catch (err) { 237 | context.sendActivity({ 238 | type: INVOKERESPONSE, 239 | value: { 240 | body: err, 241 | status: 500 242 | } 243 | }); 244 | } 245 | return; 246 | } 247 | break; 248 | case "composeExtension/setting": 249 | if ((this.commandId === context.activity.value.commandId || this.commandId === undefined) && 250 | this.processor.onSettings) { 251 | try { 252 | await this.processor.onSettings(context); 253 | context.sendActivity({ 254 | type: INVOKERESPONSE, 255 | value: { 256 | status: 200 257 | } 258 | }); 259 | } catch (err) { 260 | context.sendActivity({ 261 | type: INVOKERESPONSE, 262 | value: { 263 | body: err, 264 | status: 500 265 | } 266 | }); 267 | } 268 | return; 269 | } 270 | break; 271 | case "composeExtension/queryLink": 272 | if (this.processor.onQueryLink) { 273 | try { 274 | const result = await this.processor.onQueryLink(context, context.activity.value); 275 | context.sendActivity({ 276 | type: INVOKERESPONSE, 277 | value: { 278 | body: { 279 | composeExtension: result 280 | }, 281 | status: 200 282 | } 283 | }); 284 | } catch (err) { 285 | context.sendActivity({ 286 | type: INVOKERESPONSE, 287 | value: { 288 | body: err, 289 | status: 500 290 | } 291 | }); 292 | } 293 | return; 294 | // we're doing a return here and not next() so we're not colliding with 295 | // any botbuilder-teams invoke things. This however will also invalidate the use 296 | // of multiple message extensions using queryLink - only the first one will be triggered 297 | } 298 | break; 299 | case "composeExtension/submitAction": 300 | if (this.commandId === context.activity.value.commandId || this.commandId === undefined) { 301 | try { 302 | let result; 303 | let body; 304 | switch (context.activity.value.botMessagePreviewAction) { 305 | case "send": 306 | if (this.processor.onBotMessagePreviewSend) { 307 | result = await this.processor.onBotMessagePreviewSend(context, context.activity.value); 308 | body = result; 309 | } 310 | break; 311 | case "edit": 312 | if (this.processor.onBotMessagePreviewEdit) { 313 | result = await this.processor.onBotMessagePreviewEdit(context, context.activity.value); 314 | body = { 315 | task: result 316 | }; 317 | } 318 | break; 319 | default: 320 | if (this.processor.onSubmitAction) { 321 | result = await this.processor.onSubmitAction(context, context.activity.value); 322 | body = { 323 | composeExtension: result 324 | }; 325 | } 326 | break; 327 | } 328 | if (result) { 329 | context.sendActivity({ 330 | type: INVOKERESPONSE, 331 | value: { 332 | body, 333 | status: 200 334 | } 335 | }); 336 | return; 337 | } 338 | } catch (err) { 339 | context.sendActivity({ 340 | type: INVOKERESPONSE, 341 | value: { 342 | body: err, 343 | status: 500 344 | } 345 | }); 346 | return; 347 | } 348 | 349 | } 350 | break; 351 | case "composeExtension/fetchTask": 352 | case "task/fetch": // for some reason Teams sends this instead of the composeExtension/fetchTask after a config/auth flow 353 | if ((this.commandId === context.activity.value.commandId || this.commandId === undefined) && 354 | this.processor.onFetchTask) { 355 | try { 356 | const result = await this.processor.onFetchTask(context, context.activity.value); 357 | 358 | const body = result.type === "continue" || result.type === "message" 359 | ? { task: result } 360 | : { composeExtension: result }; 361 | context.sendActivity({ 362 | type: INVOKERESPONSE, 363 | value: { 364 | body, 365 | status: 200 366 | } 367 | }); 368 | } catch (err) { 369 | context.sendActivity({ 370 | type: INVOKERESPONSE, 371 | value: { 372 | body: err, 373 | status: 500 374 | } 375 | }); 376 | } 377 | return; 378 | } 379 | break; 380 | case "composeExtension/onCardButtonClicked": 381 | if (this.processor.onCardButtonClicked) { 382 | try { 383 | await this.processor.onCardButtonClicked(context, context.activity.value); 384 | context.sendActivity({ 385 | type: INVOKERESPONSE, 386 | value: { 387 | status: 200 388 | } 389 | }); 390 | } catch (err) { 391 | context.sendActivity({ 392 | type: INVOKERESPONSE, 393 | value: { 394 | body: err, 395 | status: 500 396 | } 397 | }); 398 | } 399 | } 400 | break; 401 | case "composeExtension/selectItem": 402 | if (this.processor.onSelectItem) { 403 | try { 404 | const result = await this.processor.onSelectItem(context, context.activity.value); 405 | context.sendActivity({ 406 | type: INVOKERESPONSE, 407 | value: { 408 | body: { 409 | composeExtension: result 410 | }, 411 | status: 200 412 | } 413 | }); 414 | } catch (err) { 415 | context.sendActivity({ 416 | type: INVOKERESPONSE, 417 | value: { 418 | body: err, 419 | status: 500 420 | } 421 | }); 422 | } 423 | return; 424 | // we're doing a return here and not next() so we're not colliding with 425 | // any botbuilder-teams invoke things. This however will also invalidate the use 426 | // of multiple message extensions using selectItem - only the first one will be triggered 427 | 428 | } 429 | break; 430 | case "adaptiveCard/action": 431 | if (this.processor.onActionExecute) { 432 | try { 433 | const result = await this.processor.onActionExecute(context, context.activity.value); 434 | 435 | context.sendActivity({ 436 | type: INVOKERESPONSE, 437 | value: { 438 | body: result, 439 | status: 200 440 | } 441 | }); 442 | } catch (err) { 443 | context.sendActivity({ 444 | type: INVOKERESPONSE, 445 | value: { 446 | body: err, 447 | status: 500 448 | } 449 | }); 450 | } 451 | return; 452 | } 453 | break; 454 | default: 455 | // nop 456 | break; 457 | } 458 | } 459 | return next(); 460 | } 461 | } 462 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | /* Basic Options */ 4 | "target": "es2015", /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017','ES2018' or 'ESNEXT'. */ 5 | "module": "commonjs", /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', or 'ESNext'. */ 6 | // "lib": [], /* Specify library files to be included in the compilation. */ 7 | // "allowJs": true, /* Allow javascript files to be compiled. */ 8 | // "checkJs": true, /* Report errors in .js files. */ 9 | // "jsx": "preserve", /* Specify JSX code generation: 'preserve', 'react-native', or 'react'. */ 10 | "declaration": true, /* Generates corresponding '.d.ts' file. */ 11 | // "declarationMap": true, /* Generates a sourcemap for each corresponding '.d.ts' file. */ 12 | "sourceMap": true, /* Generates corresponding '.map' file. */ 13 | // "outFile": "./", /* Concatenate and emit output to single file. */ 14 | "outDir": "./lib", /* Redirect output structure to the directory. */ 15 | "rootDir": "./src", /* Specify the root directory of input files. Use to control the output directory structure with --outDir. */ 16 | // "composite": true, /* Enable project compilation */ 17 | // "removeComments": true, /* Do not emit comments to output. */ 18 | // "noEmit": true, /* Do not emit outputs. */ 19 | // "importHelpers": true, /* Import emit helpers from 'tslib'. */ 20 | // "downlevelIteration": true, /* Provide full support for iterables in 'for-of', spread, and destructuring when targeting 'ES5' or 'ES3'. */ 21 | // "isolatedModules": true, /* Transpile each file as a separate module (similar to 'ts.transpileModule'). */ 22 | /* Strict Type-Checking Options */ 23 | "strict": false, /* Enable all strict type-checking options. */ 24 | // "noImplicitAny": true, /* Raise error on expressions and declarations with an implied 'any' type. */ 25 | // "strictNullChecks": true, /* Enable strict null checks. */ 26 | // "strictFunctionTypes": true, /* Enable strict checking of function types. */ 27 | // "strictBindCallApply": true, /* Enable strict 'bind', 'call', and 'apply' methods on functions. */ 28 | // "strictPropertyInitialization": true, /* Enable strict checking of property initialization in classes. */ 29 | // "noImplicitThis": true, /* Raise error on 'this' expressions with an implied 'any' type. */ 30 | // "alwaysStrict": true, /* Parse in strict mode and emit "use strict" for each source file. */ 31 | /* Additional Checks */ 32 | // "noUnusedLocals": true, /* Report errors on unused locals. */ 33 | // "noUnusedParameters": true, /* Report errors on unused parameters. */ 34 | // "noImplicitReturns": true, /* Report error when not all code paths in function return a value. */ 35 | // "noFallthroughCasesInSwitch": true, /* Report errors for fallthrough cases in switch statement. */ 36 | /* Module Resolution Options */ 37 | // "moduleResolution": "node", /* Specify module resolution strategy: 'node' (Node.js) or 'classic' (TypeScript pre-1.6). */ 38 | // "baseUrl": "./", /* Base directory to resolve non-absolute module names. */ 39 | // "paths": {}, /* A series of entries which re-map imports to lookup locations relative to the 'baseUrl'. */ 40 | // "rootDirs": [], /* List of root folders whose combined content represents the structure of the project at runtime. */ 41 | // "typeRoots": [], /* List of folders to include type definitions from. */ 42 | // "types": [], /* Type declaration files to be included in compilation. */ 43 | // "allowSyntheticDefaultImports": true, /* Allow default imports from modules with no default export. This does not affect code emit, just typechecking. */ 44 | "esModuleInterop": true /* Enables emit interoperability between CommonJS and ES Modules via creation of namespace objects for all imports. Implies 'allowSyntheticDefaultImports'. */ 45 | // "preserveSymlinks": true, /* Do not resolve the real path of symlinks. */ 46 | /* Source Map Options */ 47 | // "sourceRoot": "", /* Specify the location where debugger should locate TypeScript files instead of source locations. */ 48 | // "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */ 49 | // "inlineSourceMap": true, /* Emit a single file with source maps instead of having a separate file. */ 50 | // "inlineSources": true, /* Emit the source alongside the sourcemaps within a single file; requires '--inlineSourceMap' or '--sourceMap' to be set. */ 51 | /* Experimental Options */ 52 | // "experimentalDecorators": true, /* Enables experimental support for ES7 decorators. */ 53 | // "emitDecoratorMetadata": true, /* Enables experimental support for emitting type metadata for decorators. */ 54 | }, 55 | "exclude": [ 56 | "lib", 57 | "node_modules", 58 | "*.spec.ts" 59 | ], 60 | "include": [ 61 | "src/index.ts" 62 | ] 63 | } --------------------------------------------------------------------------------