├── packages ├── botbuilder-toybox-controls │ ├── .npmignore │ ├── .gitignore │ ├── tests │ │ ├── mocha.opts │ │ ├── ensureTermsMiddleware.test.js │ │ ├── termsControl.test.js │ │ └── listControl.test.js │ ├── src │ │ ├── selectors │ │ │ ├── index.ts │ │ │ └── simpleActionSelector.ts │ │ ├── slots │ │ │ ├── index.ts │ │ │ ├── actionSlotSet.ts │ │ │ ├── statePropertySlot.ts │ │ │ └── entitySlot.ts │ │ ├── actions │ │ │ ├── index.ts │ │ │ ├── endDialogAction.ts │ │ │ ├── sendActivityAction.ts │ │ │ ├── replaceDialogAction.ts │ │ │ ├── beginDialogAction.ts │ │ │ ├── actionHandler.ts │ │ │ └── httpPostAction.ts │ │ ├── index.ts │ │ ├── actionContext.ts │ │ ├── addChoices.ts │ │ ├── ensureTermsMiddleware.ts │ │ └── intentDialog.ts │ ├── README.md │ └── package.json ├── botbuilder-toybox-menus │ ├── .npmignore │ ├── .gitignore │ ├── tests │ │ └── mocha.opts │ ├── src │ │ ├── index.ts │ │ └── manageMenusMiddleware.ts │ ├── .nycrc │ ├── README.md │ └── package.json ├── botbuilder-toybox-remoting │ ├── .npmignore │ ├── .gitignore │ ├── tests │ │ └── mocha.opts │ ├── src │ │ ├── index.ts │ │ └── remoteDialog.ts │ ├── .nycrc │ ├── README.md │ └── package.json ├── botbuilder-toybox-declarative │ ├── .npmignore │ ├── .gitignore │ ├── tests │ │ ├── mocha.opts │ │ ├── filterActivityMiddleware.test.js │ │ ├── checkVersionMiddleware.test.js │ │ └── patchFromMiddleware.test.js │ ├── src │ │ ├── factories │ │ │ ├── index.ts │ │ │ ├── botbuilder-ai.ts │ │ │ └── botbuilder.ts │ │ ├── index.ts │ │ ├── configurableComponentDialog.ts │ │ └── typeFactory.ts │ ├── .nycrc │ ├── README.md │ └── package.json └── botbuilder-toybox-extensions │ ├── .npmignore │ ├── .gitignore │ ├── tests │ ├── mocha.opts │ ├── filterActivityMiddleware.test.js │ ├── checkVersionMiddleware.test.js │ └── patchFromMiddleware.test.js │ ├── src │ ├── matchers │ │ ├── index.ts │ │ ├── intentMatcher.ts │ │ ├── regExpMatcher.ts │ │ ├── attachmentMatcher.ts │ │ └── recognizerMatcher.ts │ ├── properties │ │ ├── index.ts │ │ ├── readonlyProperty.ts │ │ ├── listReferenceProperty.ts │ │ ├── childProperty.ts │ │ ├── expiringProperty.ts │ │ ├── listProperty.ts │ │ └── historyProperty.ts │ ├── index.ts │ ├── bindings │ │ └── activityBinding.ts │ ├── patchFromMiddleware.ts │ ├── showTypingMiddleware.ts │ ├── filterActivityMiddleware.ts │ ├── checkVersionMiddleware.ts │ └── activityFactory.ts │ ├── .nycrc │ ├── README.md │ └── package.json ├── samples ├── menus-ts │ ├── src │ │ ├── models.ts │ │ ├── showAlarmsDialog.ts │ │ ├── menus.ts │ │ ├── app.ts │ │ ├── addAlarmDialog.ts │ │ └── deleteAlarmDialog.ts │ └── package.json ├── declarative-ts │ ├── src │ │ ├── models.ts │ │ ├── showAlarmsDialog.ts │ │ ├── menus.ts │ │ ├── app.ts │ │ ├── dialogs │ │ │ ├── mainDialog.ts │ │ │ └── addAlarmDialog.ts │ │ └── deleteAlarmDialog.ts │ └── package.json ├── remote-dialogs-ts │ ├── control │ │ ├── src │ │ │ ├── models.ts │ │ │ ├── changeNameControl.ts │ │ │ ├── changeEmailControl.ts │ │ │ ├── createProfileControl.ts │ │ │ └── app.ts │ │ └── package.json │ ├── .vscode │ │ └── launch.json │ ├── app │ │ └── package.json │ └── README.md ├── list-control-ts │ ├── README.md │ ├── package.json │ └── src │ │ └── app.ts ├── show-typing-ts │ ├── README.md │ ├── package.json │ └── src │ │ └── app.ts └── federation-ts │ ├── child │ ├── package.json │ └── src │ │ └── app.ts │ ├── parent │ ├── package.json │ └── src │ │ └── app.ts │ └── README.md ├── lerna.json ├── docs ├── utilities.md ├── controls.md ├── building.md ├── middleware.md ├── reference │ ├── interfaces │ │ ├── botbuilder_toybox.listcontroloptions.md │ │ ├── botbuilder_toybox.listpagerresult.md │ │ ├── botbuilder_toybox.listcontrolresult.md │ │ ├── botbuilder_toybox.termscontrolsettings.md │ │ └── botbuilder_toybox.readonlyfragment.md │ └── classes │ │ ├── botbuilder_toybox.patchfrom.md │ │ ├── botbuilder_toybox.showtyping.md │ │ ├── botbuilder_toybox.catcherror.md │ │ ├── botbuilder_toybox.filteractivity.md │ │ ├── botbuilder_toybox.termscontrol.md │ │ ├── botbuilder_toybox.ensureterms.md │ │ ├── botbuilder_toybox.checkversion.md │ │ └── botbuilder_toybox.managescopes.md └── README.md ├── package.json ├── README.md ├── LICENSE └── .gitignore /packages/botbuilder-toybox-controls/.npmignore: -------------------------------------------------------------------------------- 1 | /**/node_modules 2 | /**/.vscode 3 | coverage 4 | .nyc_output -------------------------------------------------------------------------------- /packages/botbuilder-toybox-menus/.npmignore: -------------------------------------------------------------------------------- 1 | /**/node_modules 2 | /**/.vscode 3 | coverage 4 | .nyc_output -------------------------------------------------------------------------------- /packages/botbuilder-toybox-remoting/.npmignore: -------------------------------------------------------------------------------- 1 | /**/node_modules 2 | /**/.vscode 3 | coverage 4 | .nyc_output -------------------------------------------------------------------------------- /packages/botbuilder-toybox-declarative/.npmignore: -------------------------------------------------------------------------------- 1 | /**/node_modules 2 | /**/.vscode 3 | coverage 4 | .nyc_output -------------------------------------------------------------------------------- /packages/botbuilder-toybox-extensions/.npmignore: -------------------------------------------------------------------------------- 1 | /**/node_modules 2 | /**/.vscode 3 | coverage 4 | .nyc_output -------------------------------------------------------------------------------- /packages/botbuilder-toybox-menus/.gitignore: -------------------------------------------------------------------------------- 1 | /**/node_modules 2 | /**/.vscode 3 | /**/lib 4 | coverage 5 | .nyc_output -------------------------------------------------------------------------------- /packages/botbuilder-toybox-controls/.gitignore: -------------------------------------------------------------------------------- 1 | /**/node_modules 2 | /**/.vscode 3 | /**/lib 4 | coverage 5 | .nyc_output -------------------------------------------------------------------------------- /packages/botbuilder-toybox-declarative/.gitignore: -------------------------------------------------------------------------------- 1 | /**/node_modules 2 | /**/.vscode 3 | /**/lib 4 | coverage 5 | .nyc_output -------------------------------------------------------------------------------- /packages/botbuilder-toybox-extensions/.gitignore: -------------------------------------------------------------------------------- 1 | /**/node_modules 2 | /**/.vscode 3 | /**/lib 4 | coverage 5 | .nyc_output -------------------------------------------------------------------------------- /packages/botbuilder-toybox-remoting/.gitignore: -------------------------------------------------------------------------------- 1 | /**/node_modules 2 | /**/.vscode 3 | /**/lib 4 | coverage 5 | .nyc_output -------------------------------------------------------------------------------- /samples/menus-ts/src/models.ts: -------------------------------------------------------------------------------- 1 | 2 | export interface Alarm { 3 | title: string; 4 | time: string; 5 | } 6 | 7 | -------------------------------------------------------------------------------- /samples/declarative-ts/src/models.ts: -------------------------------------------------------------------------------- 1 | 2 | export interface Alarm { 3 | title: string; 4 | time: string; 5 | } 6 | 7 | -------------------------------------------------------------------------------- /samples/remote-dialogs-ts/control/src/models.ts: -------------------------------------------------------------------------------- 1 | 2 | export interface Profile { 3 | name: string; 4 | email: string; 5 | } 6 | -------------------------------------------------------------------------------- /packages/botbuilder-toybox-controls/tests/mocha.opts: -------------------------------------------------------------------------------- 1 | --require ts-node/register 2 | --require source-map-support/register 3 | --recursive 4 | **/*.js -------------------------------------------------------------------------------- /packages/botbuilder-toybox-menus/tests/mocha.opts: -------------------------------------------------------------------------------- 1 | --require ts-node/register 2 | --require source-map-support/register 3 | --recursive 4 | **/*.js -------------------------------------------------------------------------------- /packages/botbuilder-toybox-remoting/tests/mocha.opts: -------------------------------------------------------------------------------- 1 | --require ts-node/register 2 | --require source-map-support/register 3 | --recursive 4 | **/*.js -------------------------------------------------------------------------------- /packages/botbuilder-toybox-declarative/tests/mocha.opts: -------------------------------------------------------------------------------- 1 | --require ts-node/register 2 | --require source-map-support/register 3 | --recursive 4 | **/*.js -------------------------------------------------------------------------------- /packages/botbuilder-toybox-extensions/tests/mocha.opts: -------------------------------------------------------------------------------- 1 | --require ts-node/register 2 | --require source-map-support/register 3 | --recursive 4 | **/*.js -------------------------------------------------------------------------------- /samples/list-control-ts/README.md: -------------------------------------------------------------------------------- 1 | This sample demonstrates the use of the ListControl class within a bot. Run sample using the emulator to see an enumerable list of images. 2 | -------------------------------------------------------------------------------- /packages/botbuilder-toybox-declarative/src/factories/index.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @module botbuilder-toybox 3 | */ 4 | /** Licensed under the MIT License. */ 5 | export * from './botbuilder-ai'; -------------------------------------------------------------------------------- /packages/botbuilder-toybox-controls/src/selectors/index.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @module botbuilder-toybox 3 | */ 4 | /** Licensed under the MIT License. */ 5 | export * from './simpleActionSelector'; -------------------------------------------------------------------------------- /packages/botbuilder-toybox-remoting/src/index.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @module botbuilder-toybox 3 | */ 4 | /** Licensed under the MIT License. */ 5 | export * from './httpAdapter'; 6 | export * from './remoteDialog'; 7 | -------------------------------------------------------------------------------- /packages/botbuilder-toybox-menus/src/index.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @module botbuilder-toybox 3 | */ 4 | /** Licensed under the MIT License. */ 5 | export * from './manageMenusMiddleware'; 6 | export * from './menu'; 7 | export * from './menuManager'; 8 | -------------------------------------------------------------------------------- /packages/botbuilder-toybox-controls/src/slots/index.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @module botbuilder-toybox 3 | */ 4 | /** Licensed under the MIT License. */ 5 | export * from './actionSlotSet'; 6 | export * from './entitySlot'; 7 | export * from './statePropertySlot'; -------------------------------------------------------------------------------- /packages/botbuilder-toybox-declarative/src/index.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @module botbuilder-toybox 3 | */ 4 | /** Licensed under the MIT License. */ 5 | export * from './factories/index'; 6 | export * from './configurableComponentDialog'; 7 | export * from './typeFactory'; -------------------------------------------------------------------------------- /packages/botbuilder-toybox-extensions/src/matchers/index.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @module botbuilder-toybox 3 | */ 4 | /** Licensed under the MIT License. */ 5 | export * from './attachmentMatcher'; 6 | export * from './intentMatcher'; 7 | export * from './regExpMatcher'; 8 | -------------------------------------------------------------------------------- /lerna.json: -------------------------------------------------------------------------------- 1 | { 2 | "lerna": "2.8.0", 3 | "packages": [ 4 | "packages/*", 5 | "samples/*", 6 | "samples/federation-ts/*", 7 | "samples/remote-dialogs-ts/*" 8 | ], 9 | "version": "independent", 10 | "commands": { 11 | "bootstrap": { 12 | "hoist": true 13 | } 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /packages/botbuilder-toybox-menus/.nycrc: -------------------------------------------------------------------------------- 1 | { 2 | "extension": [ 3 | ".js" 4 | ], 5 | "include": [ 6 | "lib/**/*.js" 7 | ], 8 | "exclude": [ 9 | "**/node_modules/**", 10 | "**/tests/**", 11 | "**/coverage/**", 12 | "**/*.d.ts" 13 | ], 14 | "reporter": [ 15 | "html" 16 | ], 17 | "all": true, 18 | "cache": true 19 | } -------------------------------------------------------------------------------- /packages/botbuilder-toybox-remoting/.nycrc: -------------------------------------------------------------------------------- 1 | { 2 | "extension": [ 3 | ".js" 4 | ], 5 | "include": [ 6 | "lib/**/*.js" 7 | ], 8 | "exclude": [ 9 | "**/node_modules/**", 10 | "**/tests/**", 11 | "**/coverage/**", 12 | "**/*.d.ts" 13 | ], 14 | "reporter": [ 15 | "html" 16 | ], 17 | "all": true, 18 | "cache": true 19 | } -------------------------------------------------------------------------------- /packages/botbuilder-toybox-declarative/.nycrc: -------------------------------------------------------------------------------- 1 | { 2 | "extension": [ 3 | ".js" 4 | ], 5 | "include": [ 6 | "lib/**/*.js" 7 | ], 8 | "exclude": [ 9 | "**/node_modules/**", 10 | "**/tests/**", 11 | "**/coverage/**", 12 | "**/*.d.ts" 13 | ], 14 | "reporter": [ 15 | "html" 16 | ], 17 | "all": true, 18 | "cache": true 19 | } -------------------------------------------------------------------------------- /packages/botbuilder-toybox-extensions/.nycrc: -------------------------------------------------------------------------------- 1 | { 2 | "extension": [ 3 | ".js" 4 | ], 5 | "include": [ 6 | "lib/**/*.js" 7 | ], 8 | "exclude": [ 9 | "**/node_modules/**", 10 | "**/tests/**", 11 | "**/coverage/**", 12 | "**/*.d.ts" 13 | ], 14 | "reporter": [ 15 | "html" 16 | ], 17 | "all": true, 18 | "cache": true 19 | } -------------------------------------------------------------------------------- /packages/botbuilder-toybox-controls/src/actions/index.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @module botbuilder-toybox 3 | */ 4 | /** Licensed under the MIT License. */ 5 | export * from './actionHandler'; 6 | export * from './beginDialogAction'; 7 | export * from './endDialogAction'; 8 | export * from './httpPostAction'; 9 | export * from './replaceDialogAction'; 10 | export * from './sendActivityAction'; -------------------------------------------------------------------------------- /samples/show-typing-ts/README.md: -------------------------------------------------------------------------------- 1 | This sample demonstrates the use of ShowTyping middleware within a bot. Run sample using the emulator to see typing indicators. 2 | 3 | ## Running 4 | To run this sample folow the common instructions for running all of the samples found [here](../README.md#running). This sample can be built using `npm run build-sample` and started using `npm run start`. -------------------------------------------------------------------------------- /packages/botbuilder-toybox-extensions/src/properties/index.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @module botbuilder-toybox 3 | */ 4 | /** Licensed under the MIT License. */ 5 | export * from './childProperty'; 6 | export * from './expiringProperty'; 7 | export * from './historyProperty'; 8 | export * from './listProperty'; 9 | export * from './listReferenceProperty'; 10 | export * from './readonlyProperty'; -------------------------------------------------------------------------------- /docs/utilities.md: -------------------------------------------------------------------------------- 1 | # Utilities 2 | In addition to [middleware](./middleware.md), the **botbuilder-toybox-extensions** package contains utility classes to assist with building bots. 3 | 4 | | Class | Description | 5 | |------------|-------------| 6 | | [ActivityFactory](./reference/classes/botbuilder_toybox.activityfactory.md) | A set of static helper methods to assist with formatting activities. | 7 | -------------------------------------------------------------------------------- /packages/botbuilder-toybox-extensions/src/index.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @module botbuilder-toybox 3 | */ 4 | /** Licensed under the MIT License. */ 5 | export * from './matchers'; 6 | export * from './properties'; 7 | export * from './activityFactory'; 8 | export * from './checkVersionMiddleware'; 9 | export * from './filterActivityMiddleware'; 10 | export * from './patchFromMiddleware'; 11 | export * from './showTypingMiddleware'; 12 | -------------------------------------------------------------------------------- /packages/botbuilder-toybox-extensions/src/bindings/activityBinding.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @module botbuilder-toybox 3 | */ 4 | /** 5 | * Copyright (c) Microsoft Corporation. All rights reserved. 6 | * Licensed under the MIT License. 7 | */ 8 | import { TurnContext, Activity, ResourceResponse } from 'botbuilder-core'; 9 | 10 | export interface ActivityBinding { 11 | compose(context: TurnContext): Promise>; 12 | send(context: TurnContext): Promise; 13 | } -------------------------------------------------------------------------------- /packages/botbuilder-toybox-extensions/src/matchers/intentMatcher.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @module botbuilder-toybox 3 | */ 4 | /** 5 | * Copyright (c) Microsoft Corporation. All rights reserved. 6 | * Licensed under the MIT License. 7 | */ 8 | import { TurnContext } from 'botbuilder-core'; 9 | 10 | export interface IntentMatcher { 11 | matches(context: TurnContext): Promise; 12 | } 13 | 14 | export interface MatchedIntent { 15 | succeeded: boolean; 16 | score: number; 17 | } 18 | -------------------------------------------------------------------------------- /samples/remote-dialogs-ts/.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | // Use IntelliSense to learn about possible attributes. 3 | // Hover to view descriptions of existing attributes. 4 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 5 | "version": "0.2.0", 6 | "configurations": [ 7 | { 8 | "type": "node", 9 | "request": "launch", 10 | "name": "Launch Program", 11 | "program": "${file}" 12 | } 13 | ] 14 | } -------------------------------------------------------------------------------- /packages/botbuilder-toybox-controls/src/index.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @module botbuilder-toybox 3 | */ 4 | /** Licensed under the MIT License. */ 5 | export * from './actions/index'; 6 | export * from './selectors/index'; 7 | export * from './slots/index'; 8 | export * from './actionContext'; 9 | export * from './actionDialog'; 10 | export * from './addChoices'; 11 | export * from './ensureTermsMiddleware'; 12 | export * from './intentDialog'; 13 | export * from './listControl'; 14 | export * from './suggestedActionsDialog'; 15 | export * from './termsControl'; 16 | -------------------------------------------------------------------------------- /samples/federation-ts/child/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "federation-child-ts", 3 | "version": "1.0.0", 4 | "description": "Child bot for Bot Builder Toybox federation sample.", 5 | "main": "./lib/app.js", 6 | "scripts": { 7 | "build-sample": "tsc", 8 | "start": "tsc && node ./lib/app.js" 9 | }, 10 | "author": "", 11 | "license": "MIT", 12 | "dependencies": { 13 | "@types/node": "^9.3.0", 14 | "@types/restify": "^5.0.7", 15 | "botbuilder": "^4.0.0", 16 | "botbuilder-toybox-extensions": "^0.1.0", 17 | "restify": "6.3.4" 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /samples/show-typing-ts/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "show-typing-ts", 3 | "version": "1.0.0", 4 | "description": "Bot Builder Toybox example to let you test the ShowTyping middleware.", 5 | "main": "./lib/app.js", 6 | "scripts": { 7 | "build-sample": "tsc", 8 | "start": "tsc && node ./lib/app.js" 9 | }, 10 | "author": "", 11 | "license": "MIT", 12 | "dependencies": { 13 | "@types/node": "^9.3.0", 14 | "@types/restify": "^5.0.7", 15 | "botbuilder": "^4.0.0", 16 | "botbuilder-toybox-extensions": "^0.1.0", 17 | "restify": "6.3.4" 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /samples/declarative-ts/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "declarative-ts", 3 | "version": "1.0.0", 4 | "description": "Sample showing use of declarative dialogs.", 5 | "main": "./lib/app.js", 6 | "scripts": { 7 | "build-sample": "tsc", 8 | "start": "tsc && node ./lib/app.js" 9 | }, 10 | "author": "", 11 | "license": "MIT", 12 | "dependencies": { 13 | "@types/node": "^9.3.0", 14 | "@types/restify": "^5.0.7", 15 | "botbuilder": "^4.0.0", 16 | "botbuilder-dialogs": "^4.0.0", 17 | "botbuilder-toybox-declarative": "^0.1.0", 18 | "restify": "6.3.4" 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /samples/list-control-ts/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "list-control-ts", 3 | "version": "1.0.0", 4 | "description": "Bot Builder Toybox example showing usage of the ListControl class.", 5 | "main": "./lib/app.js", 6 | "scripts": { 7 | "build-sample": "tsc", 8 | "start": "tsc && node ./lib/app.js" 9 | }, 10 | "author": "", 11 | "license": "MIT", 12 | "dependencies": { 13 | "@types/node": "^9.3.0", 14 | "@types/restify": "^5.0.7", 15 | "botbuilder": "^4.0.0", 16 | "botbuilder-dialogs": "^4.0.0", 17 | "botbuilder-toybox-controls": "^0.1.0", 18 | "restify": "6.3.4" 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /samples/remote-dialogs-ts/control/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "remote-dialogs-control-ts", 3 | "version": "1.0.0", 4 | "description": "Control side of Bot Builder Toybox remote-dialogs sample.", 5 | "main": "./lib/app.js", 6 | "scripts": { 7 | "build-sample": "tsc", 8 | "start": "tsc && node ./lib/app.js" 9 | }, 10 | "author": "", 11 | "license": "MIT", 12 | "dependencies": { 13 | "@types/node": "^9.3.0", 14 | "@types/restify": "^5.0.7", 15 | "botbuilder": "^4.0.0", 16 | "botbuilder-dialogs": "^4.0.0", 17 | "botbuilder-toybox-extensions": "^0.1.0", 18 | "restify": "6.3.4" 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /samples/menus-ts/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "menus-ts", 3 | "version": "1.0.0", 4 | "description": "Sample showing use Bot Builder Toybox menus.", 5 | "main": "./lib/app.js", 6 | "scripts": { 7 | "build-sample": "tsc", 8 | "start": "tsc && node ./lib/app.js" 9 | }, 10 | "author": "", 11 | "license": "MIT", 12 | "dependencies": { 13 | "@types/node": "^9.3.0", 14 | "@types/restify": "^5.0.7", 15 | "botbuilder": "^4.0.0", 16 | "botbuilder-dialogs": "^4.0.0", 17 | "botbuilder-toybox-extensions": "^0.1.0", 18 | "botbuilder-toybox-controls": "^0.1.0", 19 | "restify": "6.3.4" 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /samples/federation-ts/parent/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "federation-parent-ts", 3 | "version": "1.0.0", 4 | "description": "Bot Builder Toybox example to show simple usage of memory fragments.", 5 | "main": "./lib/app.js", 6 | "scripts": { 7 | "build-sample": "tsc", 8 | "start": "tsc && node ./lib/app.js" 9 | }, 10 | "author": "", 11 | "license": "MIT", 12 | "dependencies": { 13 | "@types/node": "^9.3.0", 14 | "@types/node-fetch": "^1.6.9", 15 | "@types/restify": "^5.0.7", 16 | "botbuilder": "^4.0.0", 17 | "botbuilder-toybox-extensions": "^0.1.0", 18 | "node-fetch": "^2.1.2", 19 | "restify": "6.3.4" 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /packages/botbuilder-toybox-extensions/src/properties/readonlyProperty.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @module botbuilder-toybox 3 | */ 4 | /** Licensed under the MIT License. */ 5 | import { TurnContext, StatePropertyAccessor } from 'botbuilder-core'; 6 | 7 | export class ReadonlyProperty { 8 | constructor (private property: StatePropertyAccessor) { 9 | } 10 | 11 | public async get(context: TurnContext): Promise { 12 | const value = await this.property.get(context); 13 | if (typeof value === 'object' || Array.isArray(value)) { 14 | return JSON.parse(JSON.stringify(value)); 15 | } 16 | return value; 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /packages/botbuilder-toybox-menus/README.md: -------------------------------------------------------------------------------- 1 | # botbuilder-toybox-menus 2 | A configurable menu system for [Bot Builder v4](https://github.com/Microsoft/botbuilder-js). This is part of a broader set of [botbuilder-toybox](https://github.com/Stevenic/botbuilder-toybox) packages designed to enhance the bot building experience. 3 | 4 | - [Installing](#installing) 5 | - [Conceptual Docs](https://github.com/Stevenic/botbuilder-toybox/blob/master/docs/README.md) 6 | - [Reference Docs](https://github.com/Stevenic/botbuilder-toybox/blob/master/docs/reference/README.md) 7 | 8 | ## Installing 9 | To add this package to your bot type: 10 | 11 | ```bash 12 | npm install --save botbuilder-toybox-menus 13 | ``` 14 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "devDependencies": { 3 | "lerna": "^2.8.0", 4 | "typedoc": "^0.9.0", 5 | "typedoc-plugin-external-module-name": "^1.0.10", 6 | "typedoc-plugin-markdown": "^1.0.12" 7 | }, 8 | "scripts": { 9 | "build-docs": "typedoc --theme markdown --entryPoint botbuilder-toybox --excludePrivate --includeDeclarations --ignoreCompilerErrors --module amd --out .\\docs\\reference .\\packages\\botbuilder-toybox-controls\\lib\\index.d.ts .\\packages\\botbuilder-toybox-extensions\\lib\\index.d.ts .\\packages\\botbuilder-toybox-memories\\lib\\index.d.ts --hideGenerator --name \"Bot Builder Toybox\" --readme none" 10 | }, 11 | "dependencies": { 12 | "@types/lodash": "^4.14.116" 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /packages/botbuilder-toybox-controls/README.md: -------------------------------------------------------------------------------- 1 | # botbuilder-toybox-controls 2 | A collection of controls for [Bot Builder v4](https://github.com/Microsoft/botbuilder-js). This is part of a broader set of [botbuilder-toybox](https://github.com/Stevenic/botbuilder-toybox) packages designed to enhance the bot building experience. 3 | 4 | - [Installing](#installing) 5 | - [Conceptual Docs](https://github.com/Stevenic/botbuilder-toybox/blob/master/docs/README.md) 6 | - [Reference Docs](https://github.com/Stevenic/botbuilder-toybox/blob/master/docs/reference/README.md) 7 | 8 | ## Installing 9 | To add this package to your bot type: 10 | 11 | ```bash 12 | npm install --save botbuilder-toybox-controls 13 | ``` 14 | -------------------------------------------------------------------------------- /packages/botbuilder-toybox-remoting/README.md: -------------------------------------------------------------------------------- 1 | # botbuilder-toybox-remoting 2 | A dialog remoting system for [Bot Builder v4](https://github.com/Microsoft/botbuilder-js). This is part of a broader set of [botbuilder-toybox](https://github.com/Stevenic/botbuilder-toybox) packages designed to enhance the bot building experience. 3 | 4 | - [Installing](#installing) 5 | - [Conceptual Docs](https://github.com/Stevenic/botbuilder-toybox/blob/master/docs/README.md) 6 | - [Reference Docs](https://github.com/Stevenic/botbuilder-toybox/blob/master/docs/reference/README.md) 7 | 8 | ## Installing 9 | To add this package to your bot type: 10 | 11 | ```bash 12 | npm install --save botbuilder-toybox-remoting 13 | ``` 14 | -------------------------------------------------------------------------------- /packages/botbuilder-toybox-declarative/README.md: -------------------------------------------------------------------------------- 1 | # botbuilder-toybox-extensions 2 | A useful collection of extensions for [Bot Builder v4](https://github.com/Microsoft/botbuilder-js). This is part of a broader set of [botbuilder-toybox](https://github.com/Stevenic/botbuilder-toybox) packages designed to enhance the bot building experience. 3 | 4 | - [Installing](#installing) 5 | - [Conceptual Docs](https://github.com/Stevenic/botbuilder-toybox/blob/master/docs/README.md) 6 | - [Reference Docs](https://github.com/Stevenic/botbuilder-toybox/blob/master/docs/reference/README.md) 7 | 8 | ## Installing 9 | To add this package to your bot type: 10 | 11 | ```bash 12 | npm install --save botbuilder-toybox-extensions 13 | ``` 14 | -------------------------------------------------------------------------------- /packages/botbuilder-toybox-extensions/README.md: -------------------------------------------------------------------------------- 1 | # botbuilder-toybox-extensions 2 | A useful collection of extensions for [Bot Builder v4](https://github.com/Microsoft/botbuilder-js). This is part of a broader set of [botbuilder-toybox](https://github.com/Stevenic/botbuilder-toybox) packages designed to enhance the bot building experience. 3 | 4 | - [Installing](#installing) 5 | - [Conceptual Docs](https://github.com/Stevenic/botbuilder-toybox/blob/master/docs/README.md) 6 | - [Reference Docs](https://github.com/Stevenic/botbuilder-toybox/blob/master/docs/reference/README.md) 7 | 8 | ## Installing 9 | To add this package to your bot type: 10 | 11 | ```bash 12 | npm install --save botbuilder-toybox-extensions 13 | ``` 14 | -------------------------------------------------------------------------------- /samples/remote-dialogs-ts/app/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "remote-dialogs-app-ts", 3 | "version": "1.0.0", 4 | "description": "App side of Bot Builder Toybox remote-dialogs sample.", 5 | "main": "./lib/app.js", 6 | "scripts": { 7 | "build-sample": "tsc", 8 | "start": "tsc && node ./lib/app.js" 9 | }, 10 | "author": "", 11 | "license": "MIT", 12 | "dependencies": { 13 | "@types/node": "^9.3.0", 14 | "@types/node-fetch": "^1.6.9", 15 | "@types/restify": "^5.0.7", 16 | "botbuilder": "^4.0.0", 17 | "botbuilder-dialogs": "^4.0.0", 18 | "botbuilder-toybox-extensions": "^0.1.0", 19 | "botbuilder-toybox-controls": "^0.1.0", 20 | "node-fetch": "^2.1.2", 21 | "restify": "6.3.4" 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /docs/controls.md: -------------------------------------------------------------------------------- 1 | # Controls 2 | The **botbuilder-toybox-controls** package contains a number of controls to simply building bots. 3 | 4 | | Control | Description | 5 | |---------|-------------| 6 | | [ListControl](./reference/classes/botbuilder_toybox.listcontrol.md) | List control capable of displaying multiple pages of results to a user. | 7 | | [TermsControl](./reference/classes/botbuilder_toybox.termscontrol.md) | Control that prompts a user to agree to a Terms of Usage Statement. | 8 | 9 | The package also contains some related middleware components as well. 10 | 11 | | Middleware | Description | 12 | |------------|-------------| 13 | | [EnsureTerms](./reference/classes/botbuilder_toybox.ensureterms.md) | Middleware prevents a user from using the bot until they've agreed to the bots Terms of Use. | 14 | -------------------------------------------------------------------------------- /packages/botbuilder-toybox-declarative/tests/filterActivityMiddleware.test.js: -------------------------------------------------------------------------------- 1 | const assert = require('assert'); 2 | const { TestAdapter } = require('botbuilder-core'); 3 | const { FilterActivityMiddleware } = require('../lib'); 4 | 5 | describe('FilterActivityMiddleware', function() { 6 | this.timeout(5000); 7 | 8 | it('should filter out activity.', function (done) { 9 | const adapter = new TestAdapter((context) => { 10 | assert(false, `Activity not filtered.`); 11 | }); 12 | 13 | adapter.use(new FilterActivityMiddleware('conversationUpdate', async (context, next) => { 14 | await context.sendActivity(`filtered`); 15 | })); 16 | 17 | adapter.test({ type: "conversationUpdate" }, `filtered`) 18 | .then(() => done()); 19 | }); 20 | }); 21 | -------------------------------------------------------------------------------- /packages/botbuilder-toybox-extensions/tests/filterActivityMiddleware.test.js: -------------------------------------------------------------------------------- 1 | const assert = require('assert'); 2 | const { TestAdapter } = require('botbuilder-core'); 3 | const { FilterActivityMiddleware } = require('../lib'); 4 | 5 | describe('FilterActivityMiddleware', function() { 6 | this.timeout(5000); 7 | 8 | it('should filter out activity.', function (done) { 9 | const adapter = new TestAdapter((context) => { 10 | assert(false, `Activity not filtered.`); 11 | }); 12 | 13 | adapter.use(new FilterActivityMiddleware('conversationUpdate', async (context, next) => { 14 | await context.sendActivity(`filtered`); 15 | })); 16 | 17 | adapter.test({ type: "conversationUpdate" }, `filtered`) 18 | .then(() => done()); 19 | }); 20 | }); 21 | -------------------------------------------------------------------------------- /samples/remote-dialogs-ts/control/src/changeNameControl.ts: -------------------------------------------------------------------------------- 1 | import { DialogContainer, TextPrompt } from 'botbuilder-dialogs'; 2 | import { Profile } from './models'; 3 | 4 | export class ChangeNameControl extends DialogContainer { 5 | constructor() { 6 | super('updateProfile'); 7 | 8 | this.dialogs.add('updateProfile', [ 9 | async function(dc, profile: Profile) { 10 | dc.activeDialog.state.profile = profile || {}; 11 | await dc.prompt('namePrompt', `What is your new name?`); 12 | }, 13 | async function(dc, name: string) { 14 | const profile = dc.activeDialog.state.profile as Profile; 15 | profile.name = name; 16 | await dc.end(profile); 17 | } 18 | ]); 19 | 20 | this.dialogs.add('namePrompt', new TextPrompt()); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /packages/botbuilder-toybox-controls/src/actions/endDialogAction.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @module botbuilder-toybox 3 | */ 4 | /** Licensed under the MIT License. */ 5 | import { DialogTurnResult } from 'botbuilder-dialogs'; 6 | import { ActionHandler } from './actionHandler'; 7 | import { ActionContext } from '../actionContext'; 8 | 9 | export class EndDialogAction extends ActionHandler { 10 | constructor(...slotsReturned: string[]) { 11 | super(); 12 | if (slotsReturned) { 13 | ActionHandler.prototype.requires.apply(this, slotsReturned); 14 | } 15 | } 16 | 17 | public async onRun(action: ActionContext): Promise { 18 | let result: object; 19 | if (this.requiredSlots && this.requiredSlots.length) { 20 | result = this.requiredSlots.map(name => action.slots[name].value); 21 | } 22 | return await action.endDialog(result); 23 | } 24 | } -------------------------------------------------------------------------------- /packages/botbuilder-toybox-controls/src/actions/sendActivityAction.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @module botbuilder-toybox 3 | */ 4 | /** Licensed under the MIT License. */ 5 | import { Activity, MessageFactory } from 'botbuilder-core'; 6 | import { DialogTurnResult, Dialog } from 'botbuilder-dialogs'; 7 | import { ActionHandler } from './actionHandler'; 8 | import { ActionContext } from '../actionContext'; 9 | 10 | 11 | export class SendActivityAction extends ActionHandler { 12 | private readonly prompt: Partial; 13 | 14 | constructor(activityOrText: Partial|string, speak?: string, inputHint?: string) { 15 | super(); 16 | this.prompt = typeof activityOrText === 'string' ? MessageFactory.text(activityOrText, speak, inputHint) : activityOrText; 17 | } 18 | 19 | public async onRun(action: ActionContext): Promise { 20 | await action.context.sendActivity(this.prompt); 21 | return Dialog.EndOfTurn; 22 | } 23 | } -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # botbuilder-toybox 2 | A collection of npm packages that provide useful extensions for the JavaScript version of [Bot Builder v4](https://github.com/Microsoft/botbuilder-js). See the sections below to get started: 3 | 4 | - [Overview](./docs/README.md#overview) 5 | - [Installing](./docs/README.md#installing-packages) 6 | - [Conceptual Docs](./docs/README.md) 7 | - [Reference Docs](./docs/reference/README.md) 8 | - [Building Packages](./docs/building.md) 9 | 10 | ## Contributing 11 | 12 | This project welcomes contributions and suggestions. 13 | 14 | This project has adopted the [Microsoft Open Source Code of Conduct](https://opensource.microsoft.com/codeofconduct/). 15 | For more information see the [Code of Conduct FAQ](https://opensource.microsoft.com/codeofconduct/faq/) or 16 | contact [opencode@microsoft.com](mailto:opencode@microsoft.com) with any additional questions or comments. 17 | 18 | ## License 19 | 20 | Copyright (c) 2018 Steven Ickman 21 | 22 | Licensed under the [MIT](LICENSE.md) License. 23 | 24 | -------------------------------------------------------------------------------- /docs/building.md: -------------------------------------------------------------------------------- 1 | # Building 2 | In order to build the packages, ensure that you have [Git](https://git-scm.com/downloads) and [Node.js](https://nodejs.org/en/) installed. 3 | 4 | Clone a copy of the repo: 5 | 6 | ```bash 7 | git clone https://github.com/Stevenic/botbuilder-toybox.git 8 | ``` 9 | 10 | Change to the toybox directory: 11 | 12 | ```bash 13 | cd botbuilder-toybox 14 | ``` 15 | 16 | Install [Lerna](https://lernajs.io/) and dev dependencies: 17 | 18 | ```bash 19 | npm install --global lerna 20 | npm install --global typescript 21 | npm install --global mocha 22 | npm install 23 | ``` 24 | 25 | Run lerna bootstrap: 26 | 27 | ```bash 28 | lerna bootstrap --hoist 29 | ``` 30 | 31 | Run any of the following scripts to build and test: 32 | 33 | ``` 34 | lerna run build # Build all of the extension packages. 35 | lerna run clean # Delete all built files for extension packages. 36 | lerna run test # Execute all unit tests for extension packages. 37 | lerna run build-docs # Generate all reference documentation for packages. 38 | ``` 39 | -------------------------------------------------------------------------------- /packages/botbuilder-toybox-controls/src/actions/replaceDialogAction.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @module botbuilder-toybox 3 | */ 4 | /** Licensed under the MIT License. */ 5 | import { DialogTurnResult } from 'botbuilder-dialogs'; 6 | import { ActionHandler } from './actionHandler'; 7 | import { ActionContext } from '../actionContext'; 8 | 9 | export class ReplaceDialogAction extends ActionHandler { 10 | private readonly dialogId: string; 11 | 12 | constructor(dialogId: string, ...slotsForwarded: string[]) { 13 | super(); 14 | this.dialogId = dialogId; 15 | if (slotsForwarded) { 16 | ActionHandler.prototype.requires.apply(this, slotsForwarded); 17 | } 18 | } 19 | 20 | public async onRun(action: ActionContext): Promise { 21 | let result: object; 22 | if (this.requiredSlots && this.requiredSlots.length) { 23 | result = this.requiredSlots.map(name => action.slots[name].value); 24 | } 25 | return await action.replaceDialog(this.dialogId, result); 26 | } 27 | } -------------------------------------------------------------------------------- /samples/remote-dialogs-ts/README.md: -------------------------------------------------------------------------------- 1 | This sample shows how to call dialogs hosted on another server using the `RemoteDialog` class. 2 | 3 | The control side of this sample eposes 3 services for managing a users profile conversationally, "createProfile", "changeName", and "changeEmail". The app side contains a "firstrun" dialog which will call the "createProfile" 4 | service to initialize the users profile. Once created the user can say "change name" or "change email" at any 5 | time to update their profile. 6 | 7 | ## Running 8 | To run this sample you'll need two console windows. In the first console window type: 9 | 10 | ```bash 11 | cd control 12 | npm run build-sample 13 | npm run start 14 | ``` 15 | 16 | In the second console window type: 17 | 18 | ```bash 19 | cd ..\app 20 | npm run build-sample 21 | npm run start 22 | ``` 23 | 24 | Both the app and control servers should be started and listening for incoming requests on port 3978 and 4000 respectively. You can now connect to the app server using the emulator and say "hello". The bot will guide you through the rest of the demo. 25 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 Steven Ickman 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | 8 | # Runtime data 9 | pids 10 | *.pid 11 | *.seed 12 | *.pid.lock 13 | 14 | # Directory for instrumented libs generated by jscoverage/JSCover 15 | lib-cov 16 | 17 | # Coverage directory used by tools like istanbul 18 | coverage 19 | 20 | # nyc test coverage 21 | .nyc_output 22 | 23 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 24 | .grunt 25 | 26 | # Bower dependency directory (https://bower.io/) 27 | bower_components 28 | 29 | # node-waf configuration 30 | .lock-wscript 31 | 32 | # Compiled binary addons (http://nodejs.org/api/addons.html) 33 | build/Release 34 | 35 | # Dependency directories 36 | node_modules/ 37 | jspm_packages/ 38 | 39 | # Typescript v1 declaration files 40 | typings/ 41 | 42 | # Optional npm cache directory 43 | .npm 44 | 45 | # Optional eslint cache 46 | .eslintcache 47 | 48 | # Optional REPL history 49 | .node_repl_history 50 | 51 | # Output of 'npm pack' 52 | *.tgz 53 | 54 | # Yarn Integrity file 55 | .yarn-integrity 56 | 57 | # dotenv environment variables file 58 | .env 59 | 60 | -------------------------------------------------------------------------------- /packages/botbuilder-toybox-menus/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "botbuilder-toybox-menus", 3 | "author": "stevenic@microsoft.com", 4 | "description": "A configurable menu system for Bot Builder v4.", 5 | "version": "0.1.0", 6 | "license": "MIT", 7 | "keywords": [ 8 | "botbuilder", 9 | "botbuilder-extensions", 10 | "botbuilder-toybox" 11 | ], 12 | "bugs": { 13 | "url": "https://github.com/Stevenic/botbuilder-toybox/issues" 14 | }, 15 | "repository": { 16 | "type": "git", 17 | "url": "https://github.com/Stevenic/botbuilder-toybox.git" 18 | }, 19 | "main": "./lib/index.js", 20 | "typings": "./lib/index.d.ts", 21 | "dependencies": { 22 | "botbuilder-core": "^4.0.0", 23 | "botbuilder-dialogs": "^4.0.0" 24 | }, 25 | "devDependencies": { 26 | "@types/mocha": "^2.2.47", 27 | "@types/node": "^9.3.0", 28 | "codelyzer": "^4.1.0", 29 | "mocha": "^5.0.0", 30 | "nyc": "^11.4.1", 31 | "source-map-support": "^0.5.3", 32 | "ts-node": "^4.1.0" 33 | }, 34 | "scripts": { 35 | "test": "tsc && nyc mocha tests/", 36 | "build": "tsc", 37 | "clean": "erase /q lib\\*.*" 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /packages/botbuilder-toybox-extensions/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "botbuilder-toybox-extensions", 3 | "author": "stevenic@microsoft.com", 4 | "description": "Useful collection of extensions for Bot Builder v4.", 5 | "version": "0.1.0", 6 | "license": "MIT", 7 | "keywords": [ 8 | "botbuilder", 9 | "botbuilder-extensions", 10 | "botbuilder-toybox" 11 | ], 12 | "bugs": { 13 | "url": "https://github.com/Stevenic/botbuilder-toybox/issues" 14 | }, 15 | "repository": { 16 | "type": "git", 17 | "url": "https://github.com/Stevenic/botbuilder-toybox.git" 18 | }, 19 | "main": "./lib/index.js", 20 | "typings": "./lib/index.d.ts", 21 | "dependencies": { 22 | "botbuilder-core": "^4.0.0" 23 | }, 24 | "devDependencies": { 25 | "@types/mocha": "^2.2.47", 26 | "@types/node": "^9.3.0", 27 | "@types/uuid": "^3.4.4", 28 | "codelyzer": "^4.1.0", 29 | "mocha": "^5.0.0", 30 | "nyc": "^11.4.1", 31 | "source-map-support": "^0.5.3", 32 | "ts-node": "^4.1.0", 33 | "uuid": "^3.3.2" 34 | }, 35 | "scripts": { 36 | "test": "tsc && nyc mocha tests/", 37 | "build": "tsc", 38 | "clean": "erase /q lib\\*.*" 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /packages/botbuilder-toybox-declarative/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "botbuilder-toybox-declarative", 3 | "author": "stevenic@microsoft.com", 4 | "description": "Declarative system for loading Bot Builder v4 dialogs.", 5 | "version": "0.1.0", 6 | "license": "MIT", 7 | "keywords": [ 8 | "botbuilder", 9 | "botbuilder-extensions", 10 | "botbuilder-toybox" 11 | ], 12 | "bugs": { 13 | "url": "https://github.com/Stevenic/botbuilder-toybox/issues" 14 | }, 15 | "repository": { 16 | "type": "git", 17 | "url": "https://github.com/Stevenic/botbuilder-toybox.git" 18 | }, 19 | "main": "./lib/index.js", 20 | "typings": "./lib/index.d.ts", 21 | "dependencies": { 22 | "botbuilder": "^4.0.0", 23 | "botbuilder-dialogs": "^4.0.0", 24 | "botbuilder-ai": "^4.0.0" 25 | }, 26 | "devDependencies": { 27 | "@types/mocha": "^2.2.47", 28 | "@types/node": "^9.3.0", 29 | "codelyzer": "^4.1.0", 30 | "mocha": "^5.0.0", 31 | "nyc": "^11.4.1", 32 | "source-map-support": "^0.5.3", 33 | "ts-node": "^4.1.0" 34 | }, 35 | "scripts": { 36 | "test": "tsc && nyc mocha tests/", 37 | "build": "tsc", 38 | "clean": "erase /q lib\\*.*" 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /docs/middleware.md: -------------------------------------------------------------------------------- 1 | # Middleware 2 | The **botbuilder-toybox-extensions** package contains a number of useful pieces of middleware that can be added to your bots adapter. 3 | 4 | | Middleware | Description | 5 | |------------|-------------| 6 | | [CatchError](./reference/classes/botbuilder_toybox.catcherror.md) | Catches any errors thrown by your bot. You can use it to send the user an error message and cleanup your bots state. | 7 | | [CheckVersion](./reference/classes/botbuilder_toybox.checkversion.md) | Watches for older versions of conversation state. You can use it to cleanup old state anytime you make a major change to your bots conversation flow. | 8 | | [FilterActivity](./reference/classes/botbuilder_toybox.filteractivity.md) | Looks for activities of a particular type to be received. You can use it to handle things like `conversationUpdate` activities before they make it to your bots logic. | 9 | | [PatchFrom](./reference/classes/botbuilder_toybox.patchfrom.md) | Fixes an issue with `conversationUpdate` activities that they offten have an incorrect `from` address. | 10 | | [ShowTyping](./reference/classes/botbuilder_toybox.showtyping.md) | Automatically sends a `typing` activity to the user for longer running requests. | 11 | -------------------------------------------------------------------------------- /packages/botbuilder-toybox-extensions/src/properties/listReferenceProperty.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @module botbuilder-toybox 3 | */ 4 | /** Licensed under the MIT License. */ 5 | import { TurnContext, StatePropertyAccessor } from 'botbuilder-core'; 6 | import { ListProperty } from './listProperty'; 7 | 8 | export class ListReferenceProperty implements StatePropertyAccessor { 9 | constructor (private property: StatePropertyAccessor, public readonly list: ListProperty) { 10 | } 11 | 12 | public delete(context: TurnContext): Promise { 13 | return this.property.delete(context); 14 | } 15 | 16 | public get(context: TurnContext, defaultValue?: string): Promise { 17 | return this.property.get(context, defaultValue); 18 | } 19 | 20 | public set(context: TurnContext, value: string): Promise { 21 | return this.property.set(context, value); 22 | } 23 | 24 | public async getItem(context: TurnContext): Promise { 25 | const id = await this.property.get(context); 26 | if (id) { 27 | return await this.list.getItem(context, id); 28 | } 29 | return undefined; 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /packages/botbuilder-toybox-extensions/src/properties/childProperty.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @module botbuilder-toybox 3 | */ 4 | /** Licensed under the MIT License. */ 5 | import { TurnContext, StatePropertyAccessor } from 'botbuilder-core'; 6 | 7 | export class ChildProperty implements StatePropertyAccessor { 8 | constructor (private parent: StatePropertyAccessor, private name: string) { 9 | } 10 | 11 | public async delete(context: TurnContext): Promise { 12 | const values: any = await this.parent.get(context, {}); 13 | if (values.hasOwnProperty(this.name)) { 14 | delete values[this.name]; 15 | } 16 | } 17 | 18 | public async get(context: TurnContext, defaultValue?: T): Promise { 19 | const values: any = await this.parent.get(context, {}); 20 | if (defaultValue !== undefined && !values.hasOwnProperty(this.name)) { 21 | values[this.name] = defaultValue; 22 | } 23 | return values[this.name]; 24 | } 25 | 26 | public async set(context: TurnContext, value: T): Promise { 27 | const values: any = await this.parent.get(context, {}); 28 | values[this.name] = value; 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /packages/botbuilder-toybox-controls/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "botbuilder-toybox-controls", 3 | "author": "stevenic@microsoft.com", 4 | "description": "A collection of controls for Bot Builder v4.", 5 | "version": "0.1.0", 6 | "license": "MIT", 7 | "keywords": [ 8 | "botbuilder", 9 | "botbuilder-extensions", 10 | "botbuilder-toybox" 11 | ], 12 | "bugs": { 13 | "url": "https://github.com/Stevenic/botbuilder-toybox/issues" 14 | }, 15 | "repository": { 16 | "type": "git", 17 | "url": "https://github.com/Stevenic/botbuilder-toybox.git" 18 | }, 19 | "main": "./lib/index.js", 20 | "typings": "./lib/index.d.ts", 21 | "dependencies": { 22 | "@types/node-fetch": "^1.6.9", 23 | "botbuilder-core": "^4.0.0", 24 | "botbuilder-dialogs": "^4.0.0", 25 | "node-fetch": "^2.1.2" 26 | }, 27 | "devDependencies": { 28 | "@types/jsonwebtoken": "7.2.5", 29 | "@types/mocha": "^2.2.47", 30 | "@types/node": "^9.3.0", 31 | "codelyzer": "^4.1.0", 32 | "mocha": "^5.0.0", 33 | "nyc": "^11.4.1", 34 | "source-map-support": "^0.5.3", 35 | "ts-node": "^4.1.0" 36 | }, 37 | "scripts": { 38 | "test": "tsc && nyc mocha tests/", 39 | "build": "tsc", 40 | "clean": "erase /q lib\\*.*" 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /packages/botbuilder-toybox-remoting/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "botbuilder-toybox-remoting", 3 | "author": "stevenic@microsoft.com", 4 | "description": "A dialog remoting system for Bot Builder v4.", 5 | "version": "0.1.0", 6 | "license": "MIT", 7 | "keywords": [ 8 | "botbuilder", 9 | "botbuilder-extensions", 10 | "botbuilder-toybox" 11 | ], 12 | "bugs": { 13 | "url": "https://github.com/Stevenic/botbuilder-toybox/issues" 14 | }, 15 | "repository": { 16 | "type": "git", 17 | "url": "https://github.com/Stevenic/botbuilder-toybox.git" 18 | }, 19 | "main": "./lib/index.js", 20 | "typings": "./lib/index.d.ts", 21 | "dependencies": { 22 | "@types/node-fetch": "^1.6.9", 23 | "botbuilder-core": "^4.0.0", 24 | "botbuilder-dialogs": "^4.0.0", 25 | "node-fetch": "^2.1.2" 26 | }, 27 | "devDependencies": { 28 | "@types/jsonwebtoken": "7.2.5", 29 | "@types/mocha": "^2.2.47", 30 | "@types/node": "^9.3.0", 31 | "codelyzer": "^4.1.0", 32 | "mocha": "^5.0.0", 33 | "nyc": "^11.4.1", 34 | "source-map-support": "^0.5.3", 35 | "ts-node": "^4.1.0" 36 | }, 37 | "scripts": { 38 | "test": "tsc && nyc mocha tests/", 39 | "build": "tsc", 40 | "clean": "erase /q lib\\*.*" 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /samples/menus-ts/src/showAlarmsDialog.ts: -------------------------------------------------------------------------------- 1 | import { DialogContainer } from 'botbuilder-dialogs'; 2 | import { ReadOnlyFragment } from 'botbuilder-toybox-memories'; 3 | import { MenuContext } from 'botbuilder-toybox-controls'; 4 | import { UserState } from 'botbuilder'; 5 | import * as moment from 'moment'; 6 | 7 | import { Alarm } from './models'; 8 | 9 | export class ShowAlarmsDialog extends DialogContainer { 10 | constructor(alarmsList: ReadOnlyFragment) { 11 | super('showAlarms'); 12 | 13 | this.dialogs.add('showAlarms', [ 14 | async function (dc) { 15 | let msg = `No alarms found.`; 16 | const alarms = await alarmsList.get(dc.context); 17 | if (alarms.length > 0) { 18 | msg = `**Current Alarms**\n\n`; 19 | let connector = ''; 20 | alarms.forEach((alarm) => { 21 | msg += connector + `- ${alarm.title} (${moment(alarm.time).format("ddd, MMM Do, h:mm a")})`; 22 | connector = '\n'; 23 | }); 24 | } 25 | await dc.context.sendActivity(msg); 26 | await dc.end(); 27 | } 28 | ]); 29 | } 30 | } -------------------------------------------------------------------------------- /samples/declarative-ts/src/showAlarmsDialog.ts: -------------------------------------------------------------------------------- 1 | import { DialogContainer } from 'botbuilder-dialogs'; 2 | import { ReadOnlyFragment } from 'botbuilder-toybox-memories'; 3 | import { MenuContext } from 'botbuilder-toybox-controls'; 4 | import { UserState } from 'botbuilder'; 5 | import * as moment from 'moment'; 6 | 7 | import { Alarm } from './models'; 8 | 9 | export class ShowAlarmsDialog extends DialogContainer { 10 | constructor(alarmsList: ReadOnlyFragment) { 11 | super('showAlarms'); 12 | 13 | this.dialogs.add('showAlarms', [ 14 | async function (dc) { 15 | let msg = `No alarms found.`; 16 | const alarms = await alarmsList.get(dc.context); 17 | if (alarms.length > 0) { 18 | msg = `**Current Alarms**\n\n`; 19 | let connector = ''; 20 | alarms.forEach((alarm) => { 21 | msg += connector + `- ${alarm.title} (${moment(alarm.time).format("ddd, MMM Do, h:mm a")})`; 22 | connector = '\n'; 23 | }); 24 | } 25 | await dc.context.sendActivity(msg); 26 | await dc.end(); 27 | } 28 | ]); 29 | } 30 | } -------------------------------------------------------------------------------- /docs/reference/interfaces/botbuilder_toybox.listcontroloptions.md: -------------------------------------------------------------------------------- 1 | [Bot Builder Toybox](../README.md) > [ListControlOptions](../interfaces/botbuilder_toybox.listcontroloptions.md) 2 | 3 | 4 | 5 | # Interface: ListControlOptions 6 | 7 | 8 | :package: **botbuilder-toybox-controls** 9 | 10 | Options passed in by a caller to a ListControl on the call to `begin()`. 11 | 12 | 13 | ## Properties 14 | 15 | 16 | ### «Optional» continueToken 17 | 18 | **● continueToken**: *`any`* 19 | 20 | *Defined in [packages/botbuilder-toybox-controls/lib/listControl.d.ts:53](https://github.com/Stevenic/botbuilder-toybox/blob/dd57c76/packages/botbuilder-toybox-controls/lib/listControl.d.ts#L53)* 21 | 22 | 23 | 24 | (Optional) continuation token used to fetch the first page of results. This is useful for resuming a list that was paused for some reason. 25 | 26 | 27 | 28 | 29 | ___ 30 | 31 | 32 | 33 | ### «Optional» filter 34 | 35 | **● filter**: *`any`* 36 | 37 | *Defined in [packages/botbuilder-toybox-controls/lib/listControl.d.ts:48](https://github.com/Stevenic/botbuilder-toybox/blob/dd57c76/packages/botbuilder-toybox-controls/lib/listControl.d.ts#L48)* 38 | 39 | 40 | 41 | (Optional) filter that will be passed to the controls pager. 42 | 43 | 44 | 45 | 46 | ___ 47 | 48 | 49 | -------------------------------------------------------------------------------- /packages/botbuilder-toybox-declarative/src/factories/botbuilder-ai.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @module botbuilder-toybox 3 | */ 4 | /** Licensed under the MIT License. */ 5 | import * as ai from 'botbuilder-ai'; 6 | import { TypeFactory, TypeConfiguration } from '../typeFactory'; 7 | 8 | //========================================================= 9 | // LUIS Type Factory 10 | //========================================================= 11 | 12 | export interface LuisRecognizerConfiguration extends TypeConfiguration { 13 | application: ai.LuisApplication; 14 | options?: ai.LuisPredictionOptions; 15 | includeApiResults?: boolean; 16 | } 17 | 18 | TypeFactory.register('botbuilder-ai.LuisRecognizer', (config: LuisRecognizerConfiguration) => { 19 | return new ai.LuisRecognizer(config.application, config.options, config.includeApiResults); 20 | }); 21 | 22 | //========================================================= 23 | // QnA Maker Type Factory 24 | //========================================================= 25 | 26 | export interface QnAMakerConfiguration extends TypeConfiguration { 27 | endpoint: ai.QnAMakerEndpoint; 28 | options?: ai.QnAMakerOptions; 29 | } 30 | 31 | TypeFactory.register('botbuilder-ai.QnAMakerRecognizer', (config: QnAMakerConfiguration) => { 32 | return new ai.QnAMaker(config.endpoint, config.options); 33 | }); 34 | -------------------------------------------------------------------------------- /packages/botbuilder-toybox-declarative/tests/checkVersionMiddleware.test.js: -------------------------------------------------------------------------------- 1 | const assert = require('assert'); 2 | const { TestAdapter, MemoryStorage, ConversationState } = require('botbuilder-core'); 3 | const { CheckVersionMiddleware } = require('../lib'); 4 | 5 | describe('ConversationVersionMiddleware', function() { 6 | this.timeout(5000); 7 | 8 | it('should upgrade version.', function (done) { 9 | const convoState = new ConversationState(new MemoryStorage()); 10 | const convoVersion = convoState.createProperty('convoVersion'); 11 | 12 | const adapter = new TestAdapter(async (context) => { 13 | const version = await convoVersion.get(context); 14 | assert(version === 2.0, `not upgraded`); 15 | await context.sendActivity(`upgraded`); 16 | }); 17 | adapter.use(async (context, next) => { 18 | // Set version to 1.0 19 | await convoVersion.set(context, 1.0); 20 | await next(); 21 | }); 22 | adapter.use(new CheckVersionMiddleware(convoVersion, 2.0, async (context, version, next) => { 23 | assert(version === 1.0, `unexpected initial version number.`); 24 | await next(); 25 | })); 26 | 27 | adapter.test('test', `upgraded`) 28 | .then(() => done()); 29 | }); 30 | }); 31 | -------------------------------------------------------------------------------- /packages/botbuilder-toybox-extensions/tests/checkVersionMiddleware.test.js: -------------------------------------------------------------------------------- 1 | const assert = require('assert'); 2 | const { TestAdapter, MemoryStorage, ConversationState } = require('botbuilder-core'); 3 | const { CheckVersionMiddleware } = require('../lib'); 4 | 5 | describe('ConversationVersionMiddleware', function() { 6 | this.timeout(5000); 7 | 8 | it('should upgrade version.', function (done) { 9 | const convoState = new ConversationState(new MemoryStorage()); 10 | const convoVersion = convoState.createProperty('convoVersion'); 11 | 12 | const adapter = new TestAdapter(async (context) => { 13 | const version = await convoVersion.get(context); 14 | assert(version === 2.0, `not upgraded`); 15 | await context.sendActivity(`upgraded`); 16 | }); 17 | adapter.use(async (context, next) => { 18 | // Set version to 1.0 19 | await convoVersion.set(context, 1.0); 20 | await next(); 21 | }); 22 | adapter.use(new CheckVersionMiddleware(convoVersion, 2.0, async (context, version, next) => { 23 | assert(version === 1.0, `unexpected initial version number.`); 24 | await next(); 25 | })); 26 | 27 | adapter.test('test', `upgraded`) 28 | .then(() => done()); 29 | }); 30 | }); 31 | -------------------------------------------------------------------------------- /docs/reference/interfaces/botbuilder_toybox.listpagerresult.md: -------------------------------------------------------------------------------- 1 | [Bot Builder Toybox](../README.md) > [ListPagerResult](../interfaces/botbuilder_toybox.listpagerresult.md) 2 | 3 | 4 | 5 | # Interface: ListPagerResult 6 | 7 | 8 | :package: **botbuilder-toybox-controls** 9 | 10 | Result object returned from a `ListPager` function. 11 | 12 | 13 | ## Properties 14 | 15 | 16 | ### «Optional» continueToken 17 | 18 | **● continueToken**: *`any`* 19 | 20 | *Defined in [packages/botbuilder-toybox-controls/lib/listControl.d.ts:37](https://github.com/Stevenic/botbuilder-toybox/blob/dd57c76/packages/botbuilder-toybox-controls/lib/listControl.d.ts#L37)* 21 | 22 | 23 | 24 | (Optional) continuation token that should be used to retrieve the next page of results. If this is omitted the results will be rendered and then the ListControl will end. 25 | 26 | 27 | 28 | 29 | ___ 30 | 31 | 32 | 33 | ### «Optional» result 34 | 35 | **● result**: *[Partial]()`Activity`* 36 | 37 | *Defined in [packages/botbuilder-toybox-controls/lib/listControl.d.ts:32](https://github.com/Stevenic/botbuilder-toybox/blob/dd57c76/packages/botbuilder-toybox-controls/lib/listControl.d.ts#L32)* 38 | 39 | 40 | 41 | (Optional) result activity to render to the user. If this is omitted the ListControl will end immediately. 42 | 43 | 44 | 45 | 46 | ___ 47 | 48 | 49 | -------------------------------------------------------------------------------- /samples/remote-dialogs-ts/control/src/changeEmailControl.ts: -------------------------------------------------------------------------------- 1 | import { DialogContainer, TextPrompt } from 'botbuilder-dialogs'; 2 | import { Profile } from './models'; 3 | 4 | export class ChangeEmailControl extends DialogContainer { 5 | constructor() { 6 | super('updateProfile'); 7 | 8 | this.dialogs.add('updateProfile', [ 9 | async function(dc, profile: Profile) { 10 | dc.activeDialog.state.profile = profile || {}; 11 | await dc.prompt('emailPrompt', `What is your new email address?`); 12 | }, 13 | async function(dc, email: string) { 14 | const profile = dc.activeDialog.state.profile as Profile; 15 | profile.email = email; 16 | await dc.end(profile); 17 | } 18 | ]); 19 | 20 | 21 | this.dialogs.add('emailPrompt', new TextPrompt(async (context, value) => { 22 | const matched = EMAIL_REGEX.exec(value); 23 | if (matched) { 24 | return matched[1]; 25 | } else { 26 | await context.sendActivity(`Please enter a valid email address like "bob@example.com".`); 27 | return undefined; 28 | } 29 | })); 30 | } 31 | } 32 | 33 | const EMAIL_REGEX = /([a-zA-Z0-9_\-\.]+)@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.)|(([a-zA-Z0-9\-]+\.)+))([a-zA-Z]{2,4}|[0-9]{1,3})/i; -------------------------------------------------------------------------------- /packages/botbuilder-toybox-controls/src/slots/actionSlotSet.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @module botbuilder-toybox 3 | */ 4 | /** Licensed under the MIT License. */ 5 | import { ActionContext } from '../actionContext'; 6 | 7 | export enum ActionSlotRecognizePolicy { 8 | allways = 'always', 9 | untilFilled = 'untilFilled', 10 | ifExpected = 'ifExpected' 11 | } 12 | 13 | export interface ActionSlotFiller { 14 | updateSlots(action: ActionContext): Promise; 15 | } 16 | 17 | export class ActionSlotSet implements ActionSlotFiller { 18 | private readonly slots: ActionSlotFiller[] = []; 19 | private readonly multiPass: boolean; 20 | 21 | constructor(multiPass = true) { 22 | this.multiPass = multiPass; 23 | } 24 | 25 | public addSlot(slot: ActionSlotFiller): this { 26 | this.slots.push(slot); 27 | return this; 28 | } 29 | 30 | public async updateSlots(action: ActionContext): Promise { 31 | let hasChanged = false; 32 | let changedCount: number; 33 | do { 34 | changedCount = 0; 35 | for (let i = 0; i < this.slots.length; i++) { 36 | if (await this.slots[i].updateSlots(action)) { 37 | changedCount++; 38 | hasChanged = true; 39 | } 40 | } 41 | } while(changedCount > 1 && this.multiPass); 42 | return hasChanged; 43 | } 44 | } -------------------------------------------------------------------------------- /packages/botbuilder-toybox-extensions/src/matchers/regExpMatcher.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @module botbuilder-toybox 3 | */ 4 | /** 5 | * Copyright (c) Microsoft Corporation. All rights reserved. 6 | * Licensed under the MIT License. 7 | */ 8 | import { TurnContext, ActivityTypes } from 'botbuilder-core'; 9 | import { IntentMatcher, MatchedIntent } from './intentMatcher'; 10 | 11 | export interface MatchedRegExp extends MatchedIntent { 12 | matched: string[]; 13 | } 14 | 15 | export class RegExpMatcher implements IntentMatcher { 16 | private exp: RegExp; 17 | 18 | constructor(expOrPattern: RegExp|string, flags?: string) { 19 | this.exp = typeof expOrPattern === 'string' ? new RegExp(expOrPattern, flags) : expOrPattern; 20 | } 21 | 22 | public async matches(context: TurnContext): Promise { 23 | if (context.activity.type === ActivityTypes.Message) { 24 | const utterance = context.activity.text; 25 | const matched = this.exp.exec(utterance); 26 | if (matched) { 27 | // Return match 28 | return { 29 | succeeded: true, 30 | score: (matched[0].length / utterance.length), 31 | matched: matched 32 | }; 33 | } 34 | } 35 | 36 | // Not matched 37 | return { succeeded: false, score: 0.0, matched: [] }; 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /packages/botbuilder-toybox-controls/src/actions/beginDialogAction.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @module botbuilder-toybox 3 | */ 4 | /** Licensed under the MIT License. */ 5 | import { DialogTurnResult } from 'botbuilder-dialogs'; 6 | import { ActionHandler } from './actionHandler'; 7 | import { ActionContext } from '../actionContext'; 8 | 9 | 10 | export class BeginDialogAction extends ActionHandler { 11 | private readonly dialogId: string; 12 | private _resultSlot?: string; 13 | 14 | constructor(dialogId: string, ...slotsForwarded: string[]) { 15 | super(); 16 | this.dialogId = dialogId; 17 | if (slotsForwarded) { 18 | ActionHandler.prototype.requires.apply(this, slotsForwarded); 19 | } 20 | } 21 | 22 | public resultSlot(name: string): this { 23 | this._resultSlot = name; 24 | return this; 25 | } 26 | 27 | public async onRun(action: ActionContext): Promise { 28 | let result: object; 29 | if (this.requiredSlots && this.requiredSlots.length) { 30 | result = this.requiredSlots.map(name => action.slots[name].value); 31 | } 32 | return await action.replaceDialog(this.dialogId, result); 33 | } 34 | 35 | public async onResume(action: ActionContext): Promise { 36 | if (this._resultSlot) { 37 | action.slots[this._resultSlot] = action.result; 38 | return true; 39 | } 40 | return false; 41 | } 42 | } -------------------------------------------------------------------------------- /packages/botbuilder-toybox-extensions/src/matchers/attachmentMatcher.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @module botbuilder-toybox 3 | */ 4 | /** 5 | * Copyright (c) Microsoft Corporation. All rights reserved. 6 | * Licensed under the MIT License. 7 | */ 8 | import { TurnContext, ActivityTypes, Attachment } from 'botbuilder-core'; 9 | import { IntentMatcher, MatchedIntent } from './intentMatcher'; 10 | 11 | export interface MatchedAttachments extends MatchedIntent { 12 | attachments: Attachment[]; 13 | } 14 | 15 | export class AttachmentMatcher implements IntentMatcher { 16 | constructor(private filter?: (value: Attachment, index: number, array: Attachment[]) => Attachment|undefined) { } 17 | 18 | public async matches(context: TurnContext): Promise { 19 | const activity = context.activity; 20 | if (activity.type === ActivityTypes.Message && activity.attachments && activity.attachments.length > 0) { 21 | // Optionally filter the received attachments 22 | const filtered = this.filter ? activity.attachments.filter(this.filter) : activity.attachments; 23 | if (filtered.length > 0) { 24 | // Return match 25 | return { 26 | succeeded: true, 27 | score: 1.0, 28 | attachments: filtered 29 | }; 30 | } 31 | } 32 | 33 | // Not matched 34 | return { succeeded: false, score: 0.0, attachments: [] }; 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /samples/federation-ts/parent/src/app.ts: -------------------------------------------------------------------------------- 1 | import { BotFrameworkAdapter, Activity } from 'botbuilder'; 2 | import * as restify from 'restify'; 3 | import fetch from 'node-fetch'; 4 | 5 | // Create server 6 | let server = restify.createServer(); 7 | server.listen(process.env.port || process.env.PORT || 3978, function () { 8 | console.log(`${server.name} listening to ${server.url}`); 9 | }); 10 | 11 | // Create adapter 12 | const adapter = new BotFrameworkAdapter({ 13 | appId: process.env.MICROSOFT_APP_ID, 14 | appPassword: process.env.MICROSOFT_APP_PASSWORD 15 | }); 16 | 17 | // Listen for incoming requests 18 | server.post('/api/messages', (req, res) => { 19 | // Route received request to adapter for processing 20 | adapter.processActivity(req, res, async (context) => { 21 | // Copy activity and remove 'serviceUrl' 22 | const activity = Object.assign({}, context.activity); 23 | delete activity.serviceUrl; 24 | 25 | // Forward activity to child bot 26 | const childUrl = process.env.CHILD_URL || 'http://localhost:4000/activities'; 27 | const res = await fetch(childUrl, { 28 | method: 'POST', 29 | body: JSON.stringify(activity), 30 | headers: { 'Content-Type': 'application/json' } 31 | }); 32 | const activities = await res.json() as Activity[]; 33 | 34 | // Respond with activities returned from child bot 35 | await context.sendActivities(activities); 36 | }); 37 | }); 38 | -------------------------------------------------------------------------------- /samples/menus-ts/src/menus.ts: -------------------------------------------------------------------------------- 1 | import { TurnContext } from 'botbuilder'; 2 | import { Menu, MergeStyle, MenuManager, DialogSet, MenuContext } from 'botbuilder-toybox-controls'; 3 | 4 | export function createAlarmMenu(name: string, dialogs: DialogSet): Menu { 5 | const menu = new Menu(name, { 6 | isDefaultMenu: true, 7 | showAsButton: true, 8 | mergeStyle: MergeStyle.left, 9 | buttonTitleOrChoice: { title: '🗃️', value: 'menu' } 10 | }); 11 | menu.addChoice('add alarm', (context) => beginDialog(context, dialogs, 'addAlarm')) 12 | .addChoice('delete alarm', (context) => beginDialog(context, dialogs, 'deleteAlarm')) 13 | .addChoice('show alarms', (context) => beginDialog(context, dialogs, 'showAlarms')); 14 | return menu; 15 | } 16 | 17 | export function createCancelMenu(name: string, dialogs: DialogSet): Menu { 18 | const menu = new Menu(name, { 19 | mergeStyle: MergeStyle.right, 20 | hideAfterClick: true 21 | }); 22 | menu.addChoice('cancel', async (context) => { 23 | const dc = await dialogs.createContext(context); 24 | await dc.context.sendActivity(`Ok... Cancelled.`); 25 | await dc.endAll(); 26 | }); 27 | return menu; 28 | } 29 | 30 | async function beginDialog(context: MenuContext, dialogs: DialogSet, dialogId: string, dialogArgs?: any): Promise { 31 | await context.menus.hideMenu(); 32 | const dc = await dialogs.createContext(context); 33 | await dc.endAll().begin(dialogId, dialogArgs); 34 | } 35 | -------------------------------------------------------------------------------- /samples/declarative-ts/src/menus.ts: -------------------------------------------------------------------------------- 1 | import { TurnContext } from 'botbuilder'; 2 | import { Menu, MergeStyle, MenuManager, DialogSet, MenuContext } from 'botbuilder-toybox-controls'; 3 | 4 | export function createAlarmMenu(name: string, dialogs: DialogSet): Menu { 5 | const menu = new Menu(name, { 6 | isDefaultMenu: true, 7 | showAsButton: true, 8 | mergeStyle: MergeStyle.left, 9 | buttonTitleOrChoice: { title: '🗃️', value: 'menu' } 10 | }); 11 | menu.addChoice('add alarm', (context) => beginDialog(context, dialogs, 'addAlarm')) 12 | .addChoice('delete alarm', (context) => beginDialog(context, dialogs, 'deleteAlarm')) 13 | .addChoice('show alarms', (context) => beginDialog(context, dialogs, 'showAlarms')); 14 | return menu; 15 | } 16 | 17 | export function createCancelMenu(name: string, dialogs: DialogSet): Menu { 18 | const menu = new Menu(name, { 19 | mergeStyle: MergeStyle.right, 20 | hideAfterClick: true 21 | }); 22 | menu.addChoice('cancel', async (context) => { 23 | const dc = await dialogs.createContext(context); 24 | await dc.context.sendActivity(`Ok... Cancelled.`); 25 | await dc.endAll(); 26 | }); 27 | return menu; 28 | } 29 | 30 | async function beginDialog(context: MenuContext, dialogs: DialogSet, dialogId: string, dialogArgs?: any): Promise { 31 | await context.menus.hideMenu(); 32 | const dc = await dialogs.createContext(context); 33 | await dc.endAll().begin(dialogId, dialogArgs); 34 | } 35 | -------------------------------------------------------------------------------- /samples/federation-ts/README.md: -------------------------------------------------------------------------------- 1 | This sample shows a simple federation scenario of a parent bot dispatching received activities to a child bot. 2 | 3 | In this example the child bot is a simple echo bot which has been updated to use the `HttpAdapter`. The child listens for incoming activities at `http://localhost:4000/activities`. The parent bot uses the standard `BotFrameworkAdapter` and contains logic to forward any activities it receives to its lone child bot. 4 | 5 | In a more realistic example, the parent would want to analyze the incoming request and dispatch it to the appropriate child bot based upon the intent of the request. The parent would likely also want to use conversation state to track which child it last spoke to and it would then continue forwarding activities to this child until either an interruption occurs or the child sends and "endOfConversation" activity. 6 | 7 | ## Running 8 | To run this sample you'll need two console windows. In the first console window type: 9 | 10 | ```bash 11 | cd child 12 | npm run build-sample 13 | npm run start 14 | ``` 15 | 16 | In the second console window type: 17 | 18 | ```bash 19 | cd ..\parent 20 | npm run build-sample 21 | npm run start 22 | ``` 23 | 24 | Both the parent and its child servers should be started and listening for incoming requests on port 3978 and 4000 respectively. You can now connect to the parent server using the emulator and say "hello". The parent will forward this message to the child server and you should see the response from the child displayed in the emulators chat window. 25 | -------------------------------------------------------------------------------- /docs/reference/classes/botbuilder_toybox.patchfrom.md: -------------------------------------------------------------------------------- 1 | [Bot Builder Toybox](../README.md) > [PatchFrom](../classes/botbuilder_toybox.patchfrom.md) 2 | 3 | 4 | 5 | # Class: PatchFrom 6 | 7 | 8 | :package: **botbuilder-toybox-extensions** 9 | 10 | This middleware patches an issue where for some channels, including the emulator, the initial `from` field for a `conversationUpdate` activity is either missing or not correct. The issue is this ends up causing state management plugins to load/save state for the wrong (or no) user from the bots perspective and can cause you to pull your hair out when debugging. Unfortunately, this issue apparently isn't an easy issue for the channels to fix as they don't often know who the correct user is. Especially, in group conversations. 11 | 12 | This plugin does it's best to patch the issue by ensuring that the `from` account for non-message activities is never the bot or some system account. In 1-on-1 conversations this should result in a solid fix and in group conversations it sort of depends whether all the conversation members get added at once or not. If members are added individually things will be fine but if multiple members get added to the conversation at the same time we leave the `from` field alone unless its missing. Then we just assign the first member from the group as the sender. 13 | 14 | **Usage Example** 15 | 16 | const { PatchFrom } = require('botbuilder-toybox-extensions'); 17 | 18 | adapter.use(new PatchFrom()); 19 | 20 | ## Implements 21 | 22 | * `any` 23 | 24 | ## Index 25 | 26 | 27 | --- 28 | 29 | -------------------------------------------------------------------------------- /packages/botbuilder-toybox-controls/src/actions/actionHandler.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @module botbuilder-toybox 3 | */ 4 | /** Licensed under the MIT License. */ 5 | import { DialogTurnResult } from 'botbuilder-dialogs'; 6 | import { ActionContext } from '../actionContext'; 7 | 8 | export abstract class ActionHandler { 9 | private _expectedSlots: string[] = []; 10 | private _changedSlots: string[] = []; 11 | private _requiredSlots: string[] = []; 12 | private _onlyOnce: boolean = false; 13 | 14 | public abstract onRun(action: ActionContext): Promise; 15 | 16 | public onResume(action: ActionContext): Promise { 17 | return Promise.resolve(false); 18 | } 19 | 20 | public get expectedSlots(): string[] { 21 | return this._expectedSlots; 22 | } 23 | 24 | public get changedSlots(): string[] { 25 | return this._changedSlots; 26 | } 27 | 28 | public get requiredSlots(): string[] { 29 | return this._requiredSlots; 30 | } 31 | 32 | public get onlyOnce(): boolean { 33 | return this._onlyOnce; 34 | } 35 | 36 | public expects(...slots: string[]): this { 37 | this._expectedSlots = slots; 38 | return this; 39 | } 40 | 41 | public whenChanged(...slots: string[]): this { 42 | this._changedSlots = slots; 43 | return this; 44 | } 45 | 46 | public requires(...slots: string[]): this { 47 | this._requiredSlots = slots; 48 | return this; 49 | } 50 | 51 | public runOnce(flag = true) { 52 | this._onlyOnce = flag; 53 | return this; 54 | } 55 | } 56 | 57 | -------------------------------------------------------------------------------- /samples/federation-ts/child/src/app.ts: -------------------------------------------------------------------------------- 1 | import { MemoryStorage, TurnContext } from 'botbuilder'; 2 | import { ConversationScope, ManageScopes, ScopeAccessor } from 'botbuilder-toybox-memories'; 3 | import { HttpAdapter } from 'botbuilder-toybox-extensions'; 4 | import * as restify from 'restify'; 5 | 6 | // Create server (on port 4000) 7 | let server = restify.createServer(); 8 | server.listen(process.env.port || process.env.PORT || 4000, function () { 9 | console.log(`${server.name} listening to ${server.url}`); 10 | }); 11 | 12 | // Create adapter 13 | const adapter = new HttpAdapter(); 14 | 15 | // Define scopes and add to adapter 16 | const storage = new MemoryStorage(); 17 | const convoScope = new ConversationScope(storage); 18 | adapter.use(new ManageScopes(convoScope)); 19 | 20 | // Define memory fragments 21 | convoScope.fragment('count', 0).forgetAfter(10); 22 | 23 | // Extend TurnContext interface 24 | interface MyContext extends TurnContext { 25 | conversation: ScopeAccessor; 26 | } 27 | 28 | // Listen for incoming requests 29 | server.post('/activities', (req, res) => { 30 | // Route received request to adapter for processing 31 | adapter.processActivity(req, res, async (context: MyContext) => { 32 | if (context.activity.type === 'message') { 33 | let count = await context.conversation.get('count') as number; 34 | await context.sendActivity(`${++count}: You said "${context.activity.text}"`); 35 | await context.conversation.set('count', count); 36 | } else { 37 | await context.sendActivity(`[${context.activity.type} event detected]`); 38 | } 39 | }); 40 | }); 41 | -------------------------------------------------------------------------------- /docs/reference/interfaces/botbuilder_toybox.listcontrolresult.md: -------------------------------------------------------------------------------- 1 | [Bot Builder Toybox](../README.md) > [ListControlResult](../interfaces/botbuilder_toybox.listcontrolresult.md) 2 | 3 | 4 | 5 | # Interface: ListControlResult 6 | 7 | 8 | :package: **botbuilder-toybox-controls** 9 | 10 | Result resulted by a ListControl when it ends. 11 | 12 | 13 | ## Properties 14 | 15 | 16 | ### «Optional» action 17 | 18 | **● action**: *`string`* 19 | 20 | *Defined in [packages/botbuilder-toybox-controls/lib/listControl.d.ts:68](https://github.com/Stevenic/botbuilder-toybox/blob/dd57c76/packages/botbuilder-toybox-controls/lib/listControl.d.ts#L68)* 21 | 22 | 23 | 24 | (Optional) `value` of custom action that was triggered. 25 | 26 | 27 | 28 | 29 | ___ 30 | 31 | 32 | 33 | ### «Optional» continueToken 34 | 35 | **● continueToken**: *`any`* 36 | 37 | *Defined in [packages/botbuilder-toybox-controls/lib/listControl.d.ts:73](https://github.com/Stevenic/botbuilder-toybox/blob/dd57c76/packages/botbuilder-toybox-controls/lib/listControl.d.ts#L73)* 38 | 39 | 40 | 41 | (Optional) continuation token for the next page of results. If this is missing then the end of the result set was reached. 42 | 43 | 44 | 45 | 46 | ___ 47 | 48 | 49 | 50 | ### noResults 51 | 52 | **● noResults**: *`boolean`* 53 | 54 | *Defined in [packages/botbuilder-toybox-controls/lib/listControl.d.ts:64](https://github.com/Stevenic/botbuilder-toybox/blob/dd57c76/packages/botbuilder-toybox-controls/lib/listControl.d.ts#L64)* 55 | 56 | 57 | 58 | If `true` then no results were found. 59 | 60 | 61 | 62 | 63 | ___ 64 | 65 | 66 | -------------------------------------------------------------------------------- /samples/remote-dialogs-ts/control/src/createProfileControl.ts: -------------------------------------------------------------------------------- 1 | import { DialogContainer, TextPrompt } from 'botbuilder-dialogs'; 2 | import { Profile } from './models'; 3 | 4 | export class CreateProfileControl extends DialogContainer { 5 | constructor() { 6 | super('fillProfile'); 7 | 8 | this.dialogs.add('fillProfile', [ 9 | async function(dc) { 10 | dc.activeDialog.state.profile = {}; 11 | await dc.prompt('namePrompt', `What is your name?`); 12 | }, 13 | async function(dc, name: string) { 14 | const profile = dc.activeDialog.state.profile as Profile; 15 | profile.name = name; 16 | await dc.prompt('emailPrompt', `What is your email address?`); 17 | }, 18 | async function(dc, email: string) { 19 | const profile = dc.activeDialog.state.profile as Profile; 20 | profile.email = email; 21 | await dc.end(profile); 22 | } 23 | ]); 24 | 25 | this.dialogs.add('namePrompt', new TextPrompt()); 26 | 27 | this.dialogs.add('emailPrompt', new TextPrompt(async (context, value) => { 28 | const matched = EMAIL_REGEX.exec(value); 29 | if (matched) { 30 | return matched[1]; 31 | } else { 32 | await context.sendActivity(`Please enter a valid email address like "bob@example.com".`); 33 | return undefined; 34 | } 35 | })); 36 | } 37 | } 38 | 39 | const EMAIL_REGEX = /([a-zA-Z0-9_\-\.]+)@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.)|(([a-zA-Z0-9\-]+\.)+))([a-zA-Z]{2,4}|[0-9]{1,3})/i; -------------------------------------------------------------------------------- /packages/botbuilder-toybox-extensions/src/properties/expiringProperty.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @module botbuilder-toybox 3 | */ 4 | /** Licensed under the MIT License. */ 5 | import { TurnContext, StatePropertyAccessor } from 'botbuilder-core'; 6 | 7 | export interface TimestampedPropertyValue { 8 | timestamp: number; 9 | value: T; 10 | } 11 | 12 | export class ExpiringProperty implements StatePropertyAccessor { 13 | constructor (private property: StatePropertyAccessor>, public readonly ttl: number) { 14 | } 15 | 16 | public delete(context: TurnContext): Promise { 17 | return this.property.delete(context); 18 | } 19 | 20 | public async get(context: TurnContext, defaultValue?: T): Promise { 21 | const now = new Date().getTime(); 22 | const val = await this.property.get(context); 23 | if (val && now < (val.timestamp + this.ttl)) { 24 | val.timestamp = now; 25 | return val.value; 26 | } else if (defaultValue !== undefined) { 27 | await this.set(context, defaultValue); 28 | return defaultValue; 29 | } 30 | return undefined; 31 | } 32 | 33 | public set(context: TurnContext, value: T): Promise { 34 | const val: TimestampedPropertyValue = { timestamp: new Date().getTime(), value: value }; 35 | return this.property.set(context, val); 36 | } 37 | 38 | public async getTimestamp(context: TurnContext): Promise { 39 | const val = await this.property.get(context); 40 | if (val) { 41 | return new Date(val.timestamp); 42 | } 43 | return undefined; 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /packages/botbuilder-toybox-controls/src/actions/httpPostAction.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @module botbuilder-toybox 3 | */ 4 | /** Licensed under the MIT License. */ 5 | import { DialogTurnResult } from 'botbuilder-dialogs'; 6 | import { ActionHandler } from './actionHandler'; 7 | import { ActionContext } from '../actionContext'; 8 | import fetch from 'node-fetch'; 9 | 10 | export class HttpPostAction extends ActionHandler { 11 | private readonly url: string; 12 | private _headers: { [name: string]: string; } = {}; 13 | 14 | constructor(url: string, ...slotsForwarded: string[]) { 15 | super(); 16 | this.url = url; 17 | if (slotsForwarded) { 18 | ActionHandler.prototype.requires.apply(this, slotsForwarded); 19 | } 20 | } 21 | 22 | public headers(headers: { [name: string]: string; }): this { 23 | this._headers = headers; 24 | return this; 25 | } 26 | 27 | public async onRun(action: ActionContext): Promise { 28 | // Get required slots 29 | let result: object; 30 | if (this.requiredSlots && this.requiredSlots.length) { 31 | result = this.requiredSlots.map(name => action.slots[name].value); 32 | } 33 | // Prepare outgoing request 34 | const body = JSON.stringify(result || {}); 35 | const headers = Object.assign({}, this.headers) as any; 36 | if (!headers.hasOwnProperty('Content-Type')) { headers['Content-Type'] = 'application/json' } 37 | 38 | // Post activity to other servers webhook 39 | const res = await fetch(this.url, { 40 | method: 'POST', 41 | body: body, 42 | headers: headers 43 | }); 44 | if (!res.ok) { throw new Error(`HttpAdapter.sendActivity(): outgoing request failed with a status of "${res.status} ${res.statusText}".`) } 45 | 46 | return await action.endDialog(); 47 | } 48 | } -------------------------------------------------------------------------------- /docs/reference/classes/botbuilder_toybox.showtyping.md: -------------------------------------------------------------------------------- 1 | [Bot Builder Toybox](../README.md) > [ShowTyping](../classes/botbuilder_toybox.showtyping.md) 2 | 3 | 4 | 5 | # Class: ShowTyping 6 | 7 | 8 | :package: **botbuilder-toybox-extensions** 9 | 10 | This middleware lets you automatically send a 'typing' activity if your bot is taking too long to reply to a message. Most channels require you periodically send an additional 'typing' activity in order to keep the typing indicator lite so the middleware plugin will automatically send additional messages at a given rate until it sees the bot send a reply. 11 | 12 | **Usage Example** 13 | 14 | const { ShowTyping } = require('botbuilder-toybox-extensions'); 15 | 16 | adapter.use(new ShowTyping()); 17 | 18 | > It should be noted that the plugin sends 'typing' activities directly through the bots adapter so these additional activities will not go through middleware or be logged. 19 | 20 | ## Implements 21 | 22 | * `any` 23 | 24 | ## Index 25 | 26 | ### Constructors 27 | 28 | * [constructor](botbuilder_toybox.showtyping.md#constructor) 29 | 30 | 31 | 32 | --- 33 | ## Constructors 34 | 35 | 36 | 37 | ### ⊕ **new ShowTyping**(delay?: *`number`*, frequency?: *`number`*): [ShowTyping](botbuilder_toybox.showtyping.md) 38 | 39 | 40 | *Defined in [packages/botbuilder-toybox-extensions/lib/showTyping.d.ts:27](https://github.com/Stevenic/botbuilder-toybox/blob/dd57c76/packages/botbuilder-toybox-extensions/lib/showTyping.d.ts#L27)* 41 | 42 | 43 | 44 | Creates a new instance of `ShowTyping` middleware. 45 | 46 | 47 | **Parameters:** 48 | 49 | | Param | Type | Description | 50 | | ------ | ------ | ------ | 51 | | delay | `number` | (Optional) initial delay before sending first typing indicator. Defaults to 500ms. | 52 | | frequency | `number` | (Optional) rate at which additional typing indicators will be sent. Defaults to every 2000ms. | 53 | 54 | 55 | 56 | 57 | 58 | **Returns:** [ShowTyping](botbuilder_toybox.showtyping.md) 59 | 60 | --- 61 | 62 | 63 | 64 | -------------------------------------------------------------------------------- /samples/show-typing-ts/src/app.ts: -------------------------------------------------------------------------------- 1 | import { BotFrameworkAdapter, MemoryStorage, TurnContext } from 'botbuilder'; 2 | import { ConversationScope, ManageScopes, ScopeAccessor } from 'botbuilder-toybox-memories'; 3 | import { ShowTyping } from 'botbuilder-toybox-extensions'; 4 | import * as restify from 'restify'; 5 | 6 | // Create server 7 | let server = restify.createServer(); 8 | server.listen(process.env.port || process.env.PORT || 3978, function () { 9 | console.log(`${server.name} listening to ${server.url}`); 10 | }); 11 | 12 | // Create adapter 13 | const adapter = new BotFrameworkAdapter({ 14 | appId: process.env.MICROSOFT_APP_ID, 15 | appPassword: process.env.MICROSOFT_APP_PASSWORD 16 | }); 17 | 18 | 19 | // Define scopes and add to adapter 20 | const storage = new MemoryStorage(); 21 | const convoScope = new ConversationScope(storage); 22 | adapter.use(new ManageScopes(convoScope)); 23 | 24 | // Define memory fragments 25 | convoScope.fragment('count', 0).forgetAfter(10); 26 | 27 | // Extend TurnContext interface 28 | interface MyContext extends TurnContext { 29 | conversation: ScopeAccessor; 30 | } 31 | 32 | // Add ShowTyping middleware 33 | adapter.use(new ShowTyping()); 34 | 35 | // Listen for incoming requests 36 | server.post('/api/messages', (req, res) => { 37 | // Route received request to adapter for processing 38 | adapter.processActivity(req, res, async (context: MyContext) => { 39 | if (context.activity.type === 'message') { 40 | await longRequest(3000); 41 | let count = await context.conversation.get('count') as number; 42 | await context.sendActivity(`${++count}: You said "${context.activity.text}"`); 43 | await context.conversation.set('count', count); 44 | } else { 45 | await context.sendActivity(`[${context.activity.type} event detected]`); 46 | } 47 | }); 48 | }); 49 | 50 | function longRequest(delay: number): Promise { 51 | return new Promise((resolve, reject) => { 52 | setTimeout(() => resolve(), delay); 53 | }); 54 | } 55 | -------------------------------------------------------------------------------- /packages/botbuilder-toybox-extensions/tests/patchFromMiddleware.test.js: -------------------------------------------------------------------------------- 1 | const assert = require('assert'); 2 | const { TurnContext, TestAdapter } = require('botbuilder-core'); 3 | const { PatchFromMiddleware } = require('../lib'); 4 | 5 | describe('PatchFromMiddleware', function() { 6 | this.timeout(5000); 7 | 8 | it('should patch activity.', function (done) { 9 | const adapter = new TestAdapter(async (context) => { 10 | assert(context.activity.from, `From field missing`); 11 | assert(context.activity.from.id === 'default-user', `Invalid from.id.`); 12 | 13 | const ref = TurnContext.getConversationReference(context.activity); 14 | assert(ref.user, `User field missing`); 15 | assert(ref.user.id === 'default-user', `Invalid user.id.`); 16 | 17 | await context.sendActivity(`valid`); 18 | }); 19 | 20 | adapter 21 | .use(async (context, next) => { 22 | // Patch a bug in test adapter :( 23 | delete context.activity.from; 24 | context.activity.recipient = { "id": "default-bot", "name": "Bot" } 25 | await next(); 26 | }) 27 | .use(new PatchFromMiddleware()); 28 | 29 | adapter 30 | .test({ 31 | "type": "conversationUpdate", 32 | "membersAdded": [ 33 | { 34 | "id": "default-user", 35 | "name": "User" 36 | } 37 | ], 38 | "id": "j777lh59719", 39 | "channelId": "emulator", 40 | "timestamp": "2018-02-13T10:11:00.603Z", 41 | "localTimestamp": "2018-02-13T02:11:00-08:00", 42 | "recipient": { 43 | "id": "default-bot", 44 | "name": "Bot" 45 | }, 46 | "conversation": { 47 | "id": "1micliaf37k8" 48 | }, 49 | "serviceUrl": "http://localhost:63982" 50 | }, `valid`) 51 | .then(() => done()); 52 | }); 53 | }); 54 | -------------------------------------------------------------------------------- /packages/botbuilder-toybox-declarative/tests/patchFromMiddleware.test.js: -------------------------------------------------------------------------------- 1 | const assert = require('assert'); 2 | const { TurnContext, TestAdapter } = require('botbuilder-core'); 3 | const { PatchFromMiddleware } = require('../lib'); 4 | 5 | describe('PatchFromMiddleware', function() { 6 | this.timeout(5000); 7 | 8 | it('should patch activity.', function (done) { 9 | const adapter = new TestAdapter(async (context) => { 10 | assert(context.activity.from, `From field missing`); 11 | assert(context.activity.from.id === 'default-user', `Invalid from.id.`); 12 | 13 | const ref = TurnContext.getConversationReference(context.activity); 14 | assert(ref.user, `User field missing`); 15 | assert(ref.user.id === 'default-user', `Invalid user.id.`); 16 | 17 | await context.sendActivity(`valid`); 18 | }); 19 | 20 | adapter 21 | .use(async (context, next) => { 22 | // Patch a bug in test adapter :( 23 | delete context.activity.from; 24 | context.activity.recipient = { "id": "default-bot", "name": "Bot" } 25 | await next(); 26 | }) 27 | .use(new PatchFromMiddleware()); 28 | 29 | adapter 30 | .test({ 31 | "type": "conversationUpdate", 32 | "membersAdded": [ 33 | { 34 | "id": "default-user", 35 | "name": "User" 36 | } 37 | ], 38 | "id": "j777lh59719", 39 | "channelId": "emulator", 40 | "timestamp": "2018-02-13T10:11:00.603Z", 41 | "localTimestamp": "2018-02-13T02:11:00-08:00", 42 | "recipient": { 43 | "id": "default-bot", 44 | "name": "Bot" 45 | }, 46 | "conversation": { 47 | "id": "1micliaf37k8" 48 | }, 49 | "serviceUrl": "http://localhost:63982" 50 | }, `valid`) 51 | .then(() => done()); 52 | }); 53 | }); 54 | -------------------------------------------------------------------------------- /docs/reference/classes/botbuilder_toybox.catcherror.md: -------------------------------------------------------------------------------- 1 | [Bot Builder Toybox](../README.md) > [CatchError](../classes/botbuilder_toybox.catcherror.md) 2 | 3 | 4 | 5 | # Class: CatchError 6 | 7 | 8 | :package: **botbuilder-toybox-extensions** 9 | 10 | This middleware gives you a centralized place to catch errors that either the bot or another piece of middleware throws. The middleware will only invoke your handler once per conversation so while you may want to use other middleware to log errors that occur this provides a perfect place to notify the user that an error occurred: 11 | 12 | **Usage Example** 13 | 14 | const { CatchError } from 'botbuilder-toybox-extensions'; 15 | 16 | const conversationState = new ConversationState(new MemoryStorage()); 17 | 18 | adapter.use(new CatchError(async (context, error) => { 19 | conversationState.clear(context); 20 | await context.sendActivity(`I'm sorry... Something went wrong.`); 21 | })); 22 | 23 | The example catches the error and reports it to the user then clears the conversation state to prevent the user from getting trapped within a conversational loop. This protects against cases where your bot is throwing errors because of some bad state its in. 24 | 25 | ## Implements 26 | 27 | * `any` 28 | 29 | ## Index 30 | 31 | ### Constructors 32 | 33 | * [constructor](botbuilder_toybox.catcherror.md#constructor) 34 | 35 | 36 | 37 | --- 38 | ## Constructors 39 | 40 | 41 | 42 | ### ⊕ **new CatchError**(handler: *[CatchErrorHandler](../#catcherrorhandler)*): [CatchError](botbuilder_toybox.catcherror.md) 43 | 44 | 45 | *Defined in [packages/botbuilder-toybox-extensions/lib/catchError.d.ts:41](https://github.com/Stevenic/botbuilder-toybox/blob/dd57c76/packages/botbuilder-toybox-extensions/lib/catchError.d.ts#L41)* 46 | 47 | 48 | 49 | Creates a new CatchError instance. 50 | 51 | 52 | **Parameters:** 53 | 54 | | Param | Type | Description | 55 | | ------ | ------ | ------ | 56 | | handler | [CatchErrorHandler](../#catcherrorhandler) | Function called should an error be raised by the bot or another piece of middleware. | 57 | 58 | 59 | 60 | 61 | 62 | **Returns:** [CatchError](botbuilder_toybox.catcherror.md) 63 | 64 | --- 65 | 66 | 67 | 68 | -------------------------------------------------------------------------------- /packages/botbuilder-toybox-controls/tests/ensureTermsMiddleware.test.js: -------------------------------------------------------------------------------- 1 | const assert = require('assert'); 2 | const { TestAdapter, MemoryStorage, ConversationState, UserState, AutoSaveStateMiddleware } = require('botbuilder-core'); 3 | const { EnsureTermsMiddleware } = require('../lib'); 4 | 5 | describe('EnsureTerms', function() { 6 | this.timeout(5000); 7 | 8 | it('should prompt user to agree to terms.', function (done) { 9 | const userState = new UserState(new MemoryStorage()); 10 | const convoState = new ConversationState(new MemoryStorage()); 11 | const userVersion = userState.createProperty('userVersion'); 12 | const convoDialogs = convoState.createProperty('dialogs'); 13 | 14 | const adapter = new TestAdapter(async (context) => { 15 | assert(false, `Shouldn't run bots logic.`); 16 | }); 17 | adapter.use(new AutoSaveStateMiddleware(convoState, userState)); 18 | adapter.use(new EnsureTermsMiddleware(convoDialogs, userVersion, { 19 | currentVersion: 1, 20 | termsStatement: 'please agree' 21 | })); 22 | 23 | adapter.test('test', `please agree (1) I Agree`) 24 | .then(() => done()); 25 | }); 26 | 27 | it('should support agreeing to terms.', function (done) { 28 | const userState = new UserState(new MemoryStorage()); 29 | const convoState = new ConversationState(new MemoryStorage()); 30 | const userVersion = userState.createProperty('userVersion'); 31 | const convoDialogs = convoState.createProperty('dialogs'); 32 | 33 | const adapter = new TestAdapter(async (context) => { 34 | const version = await userVersion.get(context); 35 | assert(version === 1); 36 | await context.sendActivity('agreed'); 37 | }); 38 | adapter.use(new AutoSaveStateMiddleware(convoState, userState)); 39 | adapter.use(new EnsureTermsMiddleware(convoDialogs, userVersion, { 40 | currentVersion: 1, 41 | termsStatement: 'please agree' 42 | })) 43 | 44 | adapter.test('test', `please agree (1) I Agree`) 45 | .test('I Agree', 'agreed') 46 | .then(() => done()); 47 | }); 48 | }); 49 | -------------------------------------------------------------------------------- /packages/botbuilder-toybox-controls/tests/termsControl.test.js: -------------------------------------------------------------------------------- 1 | const assert = require('assert'); 2 | const { TestAdapter, MemoryStorage } = require('botbuilder-core-extensions'); 3 | const { UserScope } = require('botbuilder-toybox-memories'); 4 | const { TermsControl } = require('../lib'); 5 | 6 | describe('TermsControl', function() { 7 | this.timeout(5000); 8 | 9 | it('should prompt user to agree to terms.', function (done) { 10 | const userScope = new UserScope(new MemoryStorage()); 11 | const userVersion = userScope.fragment('userVersion'); 12 | const control = new TermsControl(userVersion, { 13 | currentVersion: 1, 14 | termsStatement: 'please agree' 15 | }); 16 | 17 | const state = {}; 18 | const adapter = new TestAdapter(async (context) => { 19 | const completed = await control.begin(context, state); 20 | assert(completed && !completed.isCompleted, `Completed and shouldn't be.`); 21 | }); 22 | 23 | adapter.test('test', `please agree (1) I Agree`) 24 | .then(() => done()); 25 | }); 26 | 27 | it('should support agreeing to terms.', function (done) { 28 | const userScope = new UserScope(new MemoryStorage()); 29 | const userVersion = userScope.fragment('userVersion'); 30 | const control = new TermsControl(userVersion, { 31 | currentVersion: 1, 32 | termsStatement: 'please agree' 33 | }); 34 | 35 | let started = false; 36 | const state = {}; 37 | const adapter = new TestAdapter(async (context) => { 38 | if (!started) { 39 | started = true; 40 | await control.begin(context, state); 41 | } else { 42 | const completed = await control.continue(context, state); 43 | assert(completed && completed.isCompleted); 44 | const version = await userVersion.get(context); 45 | assert(version === 1); 46 | await context.sendActivity(`agreed`); 47 | } 48 | }); 49 | 50 | adapter.test('test', `please agree (1) I Agree`) 51 | .test('I Agree', 'agreed') 52 | .then(() => done()); 53 | }); 54 | }); 55 | -------------------------------------------------------------------------------- /packages/botbuilder-toybox-declarative/src/configurableComponentDialog.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @module botbuilder-toybox 3 | */ 4 | /** Licensed under the MIT License. */ 5 | import { ComponentDialog, Dialog } from 'botbuilder-dialogs'; 6 | import { TypeFactory, TypeConfiguration } from './typeFactory'; 7 | 8 | export interface DialogConfiguration extends TypeConfiguration { 9 | /** 10 | * Unique ID of the dialog. 11 | */ 12 | id: string; 13 | } 14 | 15 | export interface ComponentDialogConfiguration extends DialogConfiguration { 16 | /** 17 | * Components collection of child dialogs. 18 | */ 19 | dialogs?: DialogConfiguration[]; 20 | 21 | /** 22 | * (Optional) initial dialog id. 23 | */ 24 | initialDialogId?: string; 25 | } 26 | 27 | export class ConfigurableComponentDialog extends ComponentDialog { 28 | 29 | public configure(config: ComponentDialogConfiguration, factory: TypeFactory): this { 30 | this.onConfigure(config, factory); 31 | return this; 32 | } 33 | 34 | protected onConfigure(config: ComponentDialogConfiguration, factory: TypeFactory): void { 35 | if (config.initialDialogId) { 36 | this.initialDialogId = config.initialDialogId; 37 | } 38 | } 39 | 40 | protected configureDialogs(config: ComponentDialogConfiguration, factory: TypeFactory): void { 41 | if (config.dialogs) { 42 | config.dialogs.forEach((child) => this.onConfigureDialog(child, factory)); 43 | } 44 | } 45 | 46 | protected onConfigureDialog(config: DialogConfiguration, factory: TypeFactory): void { 47 | // Find existing dialog 48 | let dialog = this.findDialog(config.id); 49 | if (dialog) { 50 | // Try to configure existing dialog 51 | if (typeof (dialog as any).configure === 'function') { 52 | (dialog as any).configure(config); 53 | } 54 | } else if (config.type) { 55 | // Create and add new dialog 56 | dialog = factory.create(config) as Dialog; 57 | this.addDialog(dialog); 58 | } else { 59 | throw new Error(`ConfigurableComponentDialog.onConfigureDialog(): can't find dialog with an id of '${config.id}'.`); 60 | } 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /packages/botbuilder-toybox-controls/src/slots/statePropertySlot.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @module botbuilder-toybox 3 | */ 4 | /** Licensed under the MIT License. */ 5 | import { StatePropertyAccessor } from 'botbuilder-core'; 6 | import { ActionSlotFiller } from './actionSlotSet'; 7 | import { ActionContext } from '../actionContext'; 8 | import { ActionSlotRecognizePolicy } from './actionSlotSet'; 9 | 10 | export class StatePropertySlot implements ActionSlotFiller { 11 | private readonly stateProperty: StatePropertyAccessor; 12 | private readonly slot: string; 13 | private _recognizePolicy = ActionSlotRecognizePolicy.untilFilled; 14 | 15 | constructor(stateProperty: StatePropertyAccessor, slot: string) { 16 | this.stateProperty = stateProperty; 17 | this.slot = slot; 18 | } 19 | 20 | public recognizePolicy(policy: ActionSlotRecognizePolicy): this { 21 | this._recognizePolicy = policy; 22 | return this; 23 | } 24 | 25 | public async updateSlots(action: ActionContext): Promise { 26 | let changed = false; 27 | const slot = action.slots[this.slot]; 28 | const filled = slot != undefined && slot.value !== undefined; 29 | switch (this._recognizePolicy) { 30 | case ActionSlotRecognizePolicy.allways: 31 | changed = await this.recognize(action); 32 | break; 33 | case ActionSlotRecognizePolicy.untilFilled: 34 | if (!filled) { 35 | changed = await this.recognize(action); 36 | } 37 | break; 38 | case ActionSlotRecognizePolicy.ifExpected: 39 | if (action.isSlotExpected(this.slot)) { 40 | changed = await this.recognize(action); 41 | } 42 | break; 43 | } 44 | return changed; 45 | } 46 | 47 | private async recognize(action: ActionContext): Promise { 48 | const current = action.slots[this.slot].value; 49 | const value = await this.stateProperty.get(action.context); 50 | if (value !== undefined && JSON.stringify(current) !== JSON.stringify(value)) { 51 | action.slots[this.slot] = { 52 | value: value, 53 | turnChanged: action.turnCount 54 | }; 55 | return true; 56 | } 57 | return false; 58 | } 59 | } -------------------------------------------------------------------------------- /docs/reference/classes/botbuilder_toybox.filteractivity.md: -------------------------------------------------------------------------------- 1 | [Bot Builder Toybox](../README.md) > [FilterActivity](../classes/botbuilder_toybox.filteractivity.md) 2 | 3 | 4 | 5 | # Class: FilterActivity 6 | 7 | 8 | :package: **botbuilder-toybox-extensions** 9 | 10 | This middleware lets you easily filter out activity types your bot doesn't care about. For example here's how to filter out 'contactRelationUpdate' and 'conversationUpdate' activities: 11 | 12 | **Usage Example** 13 | 14 | adapter.use(new FilterActivity('contactRelationUpdate', (context, next) => { }) 15 | .use(new FilterActivity('conversationUpdate', (context, next) => { })); 16 | 17 | You can also use an activity filter to greet a user as they join a conversation: 18 | 19 | adapter.use(new FilterActivity('conversationUpdate', async (context, next) => { 20 | const added = context.activity.membersAdded || []; 21 | for (let i = 0; i < added.length; i++) { 22 | if (added[i].id !== context.activity.recipient.id) { 23 | await context.sendActivity(`Welcome to my bot!`); 24 | break; 25 | } 26 | } 27 | })); 28 | 29 | ## Implements 30 | 31 | * `any` 32 | 33 | ## Index 34 | 35 | ### Constructors 36 | 37 | * [constructor](botbuilder_toybox.filteractivity.md#constructor) 38 | 39 | 40 | 41 | --- 42 | ## Constructors 43 | 44 | 45 | 46 | ### ⊕ **new FilterActivity**(type: *`string`*, handler: *[FilterActivityHandler](../#filteractivityhandler)*): [FilterActivity](botbuilder_toybox.filteractivity.md) 47 | 48 | 49 | *Defined in [packages/botbuilder-toybox-extensions/lib/filterActivity.d.ts:44](https://github.com/Stevenic/botbuilder-toybox/blob/dd57c76/packages/botbuilder-toybox-extensions/lib/filterActivity.d.ts#L44)* 50 | 51 | 52 | 53 | Creates a new instance of an `FilterActivity` middleware. 54 | 55 | 56 | **Parameters:** 57 | 58 | | Param | Type | Description | 59 | | ------ | ------ | ------ | 60 | | type | `string` | Type of activity to trigger on. | 61 | | handler | [FilterActivityHandler](../#filteractivityhandler) | Function that will be called anytime an activity of the specified type is received. Simply avoid calling `next()` to prevent the activity from being further routed. | 62 | 63 | 64 | 65 | 66 | 67 | **Returns:** [FilterActivity](botbuilder_toybox.filteractivity.md) 68 | 69 | --- 70 | 71 | 72 | 73 | -------------------------------------------------------------------------------- /packages/botbuilder-toybox-declarative/src/typeFactory.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @module botbuilder-toybox 3 | */ 4 | /** Licensed under the MIT License. */ 5 | 6 | export interface TypeConfiguration { 7 | /** 8 | * The types name. 9 | */ 10 | type: string; 11 | 12 | /** 13 | * (Optional) ID of the created instance. 14 | * 15 | * @remarks 16 | * If specified the instance will be cached on a per `TypeFactory` basis and can be later retrieved. 17 | */ 18 | id?: string; 19 | 20 | /** 21 | * Allow other properties. 22 | */ 23 | [otherProps: string]: any; 24 | } 25 | 26 | const types: { [name: string]: (config: TypeConfiguration, factory: TypeFactory) => any; } = {}; 27 | 28 | export class TypeFactory { 29 | private instances: { [id: string]: any; } = {}; 30 | 31 | public create(config: T): any { 32 | if (!config.type) { throw new Error(`TypeFactory.create(): not 'type' specified.`) } 33 | const factory = types[config.type]; 34 | if (!factory) { throw new Error(`TypeFactory.create(): could not find a type named '${config.type}'.`) } 35 | const instance = factory(config, this); 36 | if (config.id) { 37 | this.set(config.id, instance); 38 | } 39 | return instance; 40 | } 41 | 42 | public get(id: string): any { 43 | const instance = this.instances[id]; 44 | if (!instance) { throw new Error(`TypeFactory.get(): could not find an instance with an ID of '${id}'.`) } 45 | return instance; 46 | } 47 | 48 | public has(id: string): boolean { 49 | return this.instances.hasOwnProperty(id); 50 | } 51 | 52 | public set(id: string, instance: any): void { 53 | if (this.instances.hasOwnProperty(id)) { throw new Error(`TypeFactory.set(): an instance with an ID of '${id}' has already been set.`) } 54 | this.instances[id] = instance; 55 | } 56 | 57 | static hasType(typeName: string): boolean { 58 | return types.hasOwnProperty(typeName); 59 | } 60 | 61 | static register(typeName: string, factory: ((config: T, factory: TypeFactory) => any)): void { 62 | if (types.hasOwnProperty(typeName)) { throw new Error(`TypeFactory.register(): a type named '${typeName}' has already been registered.`) } 63 | types[typeName] = factory as any; 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /docs/reference/interfaces/botbuilder_toybox.termscontrolsettings.md: -------------------------------------------------------------------------------- 1 | [Bot Builder Toybox](../README.md) > [TermsControlSettings](../interfaces/botbuilder_toybox.termscontrolsettings.md) 2 | 3 | 4 | 5 | # Interface: TermsControlSettings 6 | 7 | 8 | :package: **botbuilder-toybox-controls** 9 | 10 | Settings used to configure a `TermsControl` instance. 11 | 12 | 13 | ## Properties 14 | 15 | 16 | ### «Optional» agreeButtonTitle 17 | 18 | **● agreeButtonTitle**: *`string`* 19 | 20 | *Defined in packages/botbuilder-toybox-controls/lib/termsControl.d.ts:38* 21 | 22 | 23 | 24 | (Optional) title of the agree button resented to users. Defaults to "I Agree". 25 | 26 | 27 | 28 | 29 | ___ 30 | 31 | 32 | 33 | ### currentVersion 34 | 35 | **● currentVersion**: *`number`* 36 | 37 | *Defined in packages/botbuilder-toybox-controls/lib/termsControl.d.ts:20* 38 | 39 | 40 | 41 | Current version number for the terms statement. 42 | 43 | Incrementing this number in future versions of the bot will cause existing users to have to re-confirm the new terms statement. 44 | 45 | 46 | 47 | 48 | ___ 49 | 50 | 51 | 52 | ### «Optional» retryPrompt 53 | 54 | **● retryPrompt**: *`string`⎮[Partial]()`Activity`* 55 | 56 | *Defined in packages/botbuilder-toybox-controls/lib/termsControl.d.ts:34* 57 | 58 | 59 | 60 | (Optional) retry prompt to present to users when they fail to agree to the terms statement. Defaults to just re-presenting the statement to the user. 61 | 62 | 63 | 64 | 65 | ___ 66 | 67 | 68 | 69 | ### termsStatement 70 | 71 | **● termsStatement**: *`string`⎮[Partial]()`Activity`* 72 | 73 | *Defined in packages/botbuilder-toybox-controls/lib/termsControl.d.ts:24* 74 | 75 | 76 | 77 | Terms statement to present to user. 78 | 79 | 80 | 81 | 82 | ___ 83 | 84 | 85 | 86 | ### «Optional» upgradedTermsStatement 87 | 88 | **● upgradedTermsStatement**: *`string`⎮[Partial]()`Activity`* 89 | 90 | *Defined in packages/botbuilder-toybox-controls/lib/termsControl.d.ts:29* 91 | 92 | 93 | 94 | (Optional) terms statement to present to users being upgraded. If this is omitted existing users will be asked to confirm the primary [termsStatement](#termsstatement). 95 | 96 | 97 | 98 | 99 | ___ 100 | 101 | 102 | -------------------------------------------------------------------------------- /packages/botbuilder-toybox-menus/src/manageMenusMiddleware.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @module botbuilder-toybox 3 | */ 4 | /** Licensed under the MIT License. */ 5 | import { TurnContext, Middleware, ActivityTypes, Activity, StatePropertyAccessor } from 'botbuilder-core'; 6 | import { Menu } from './menu'; 7 | import { MenuManager, MenuMap, MenuContext } from './menuManager'; 8 | 9 | export class ManageMenusMiddleware implements Middleware { 10 | private readonly menus: MenuMap = {}; 11 | 12 | constructor(private menuState: StatePropertyAccessor, ...menus: Menu[]) { 13 | // Ensure all menu names unique and only one default menu exists 14 | let defaultMenu = ''; 15 | menus.forEach((m) => { 16 | if (m.settings.isDefaultMenu) { 17 | if (defaultMenu.length > 0) { throw new Error(`ManageMenus: can't add default menu named '${m.name}' because a default menu name '${defaultMenu}' has already been added.`) } 18 | defaultMenu = m.name; 19 | } 20 | if (this.menus.hasOwnProperty(m.name)) { throw new Error(`ManageMenus: duplicate menu named '${m.name}' detected.`) } 21 | this.menus[m.name] = m; 22 | }); 23 | } 24 | 25 | /** @private */ 26 | public async onTurn(context: TurnContext, next: () => Promise): Promise { 27 | // Extend context object with menu manager 28 | const manager = new MenuManager(context as MenuContext, this.menuState, this.menus); 29 | Object.defineProperty(context, 'menus', { 30 | get() { return manager; } 31 | }); 32 | 33 | // Listen for outgoing messages to augment with suggested actions 34 | context.onSendActivities(async (context, activities, next) => { 35 | // Find last message being sent 36 | let lastMsg: Partial|undefined; 37 | activities.forEach((a) => { 38 | if (a.type === ActivityTypes.Message) { lastMsg = a } 39 | }); 40 | 41 | // Append actions to last message 42 | if (lastMsg) { 43 | await manager.appendSuggestedActions(lastMsg); 44 | } 45 | 46 | // Deliver activities 47 | return await next(); 48 | }); 49 | 50 | // Recognize any invoked menu commands 51 | if (context.activity.type === ActivityTypes.Message) { 52 | await manager.recognizeUtterance(next); 53 | } else { 54 | await next(); 55 | } 56 | } 57 | } -------------------------------------------------------------------------------- /packages/botbuilder-toybox-extensions/src/matchers/recognizerMatcher.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @module botbuilder-toybox 3 | */ 4 | /** 5 | * Copyright (c) Microsoft Corporation. All rights reserved. 6 | * Licensed under the MIT License. 7 | */ 8 | import { TurnContext, ActivityTypes, RecognizerResult } from 'botbuilder-core'; 9 | import { IntentMatcher, MatchedIntent } from './intentMatcher'; 10 | 11 | export interface MatchedRecognizerResult extends MatchedIntent { 12 | recognized: RecognizerResult; 13 | } 14 | 15 | export class RecognizerMatcher implements IntentMatcher { 16 | constructor( 17 | private recognizer: { recognize(context: TurnContext): Promise }, 18 | private intent: string, 19 | private topIntentOnly = true, 20 | private minScore = 0) { 21 | 22 | } 23 | 24 | public async matches(context: TurnContext): Promise { 25 | const activity = context.activity; 26 | if (activity.type === ActivityTypes.Message) { 27 | const recognized = await this.recognizer.recognize(context); 28 | if (recognized && recognized.intents) { 29 | // Find top intent and matched score 30 | let topIntent = ''; 31 | let topScore = -1; 32 | let matchedScore = -1; 33 | for (const name in recognized.intents) { 34 | const score = recognized.intents[name].score; 35 | if (score > topScore) { 36 | topIntent = name; 37 | topScore = score; 38 | } 39 | if (name === this.intent && score > matchedScore) { 40 | matchedScore = score; 41 | } 42 | } 43 | 44 | // Ensure matches top intent 45 | if (!this.topIntentOnly || topIntent === this.intent) { 46 | // Ensure greater then min score 47 | if (matchedScore >= this.minScore) { 48 | // Return match 49 | return { 50 | succeeded: true, 51 | score: matchedScore, 52 | recognized: recognized 53 | }; 54 | } 55 | } 56 | } 57 | } 58 | 59 | // Not matched 60 | return { succeeded: false, score: 0.0, recognized: {} as RecognizerResult }; 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /packages/botbuilder-toybox-controls/src/actionContext.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @module botbuilder-toybox 3 | */ 4 | /** Licensed under the MIT License. */ 5 | import { DialogContext } from 'botbuilder-dialogs'; 6 | 7 | /** 8 | * Values passed to the `ActionStepContext` constructor. 9 | */ 10 | export interface ActionStepInfo { 11 | turnCount: number; 12 | 13 | /** 14 | * Results returned by a dialog or prompt that was called by an action. 15 | */ 16 | result?: any; 17 | 18 | /** 19 | * A dictionary of slot values which will be persisted across all actions. 20 | */ 21 | slots: { [name: string]: SlotValue; }; 22 | 23 | /** 24 | * The slots expected by the last action executed. 25 | */ 26 | expectedSlots: string[]; 27 | } 28 | 29 | export interface SlotValue { 30 | value: any; 31 | turnChanged: number; 32 | } 33 | 34 | /** 35 | * Context object passed in to a `ActionStep`. 36 | * @param O (Optional) type of options passed to the steps action dialog in the call to `DialogContext.beginDialog()`. 37 | */ 38 | export class ActionContext extends DialogContext { 39 | private _info: ActionStepInfo; 40 | 41 | /** 42 | * Creates a new ActionStepContext instance. 43 | * @param dc The dialog context for the current turn of conversation. 44 | * @param info Values to initialize the step context with. 45 | */ 46 | constructor(dc: DialogContext, info: ActionStepInfo) { 47 | super(dc.dialogs, dc.context, { dialogStack: dc.stack }); 48 | this._info = info; 49 | } 50 | 51 | public get turnCount(): number { 52 | return this._info.turnCount; 53 | } 54 | 55 | /** 56 | * Results returned by a dialog or prompt that was called in the previous action step. 57 | */ 58 | public get result(): any { 59 | return this._info.result; 60 | } 61 | 62 | /** 63 | * A dictionary of slots which will be persisted across all action steps. 64 | */ 65 | public get slots(): { [name: string]: SlotValue; } { 66 | return this._info.slots; 67 | } 68 | 69 | public get expectedSlots(): string[] { 70 | return this._info.expectedSlots; 71 | } 72 | 73 | public isSlotExpected(slot: string): boolean { 74 | for (let i = 0; i < this._info.expectedSlots.length; i++) { 75 | if (this._info.expectedSlots[i] === slot) { 76 | return true; 77 | } 78 | } 79 | return false; 80 | } 81 | 82 | } -------------------------------------------------------------------------------- /docs/README.md: -------------------------------------------------------------------------------- 1 | # botbuilder-toybox 2 | A collection of npm packages that provide useful extensions for the JavaScript version of [Bot Builder v4](https://github.com/Microsoft/botbuilder-js). 3 | 4 | - [Overview](#overview) 5 | - [Installing](#installing-packages) 6 | - Conceptual Docs 7 | - [Memories](./memories.md) 8 | - [Controls](./controls.md) 9 | - [Middleware](./middleware.md) 10 | - [Utilities](./utilities.md) 11 | - [Reference Docs](./reference/README.md) 12 | - [Building Packages](./building.md) 13 | 14 | ## Overview 15 | The packages in toybox represent various bot related ideas that I'm currently exploring. In some cases I'm just working out how something should work before being included in the main Bot Builder SDK and in other cases they're too opinionated to be in the core SDK. While you could consider them experimental the plan is to follow standard semantic versioning rules so once published I'd personally consider them safe for production use and of course your feedback, ideas, and bug fixes are always welcome. 16 | 17 | I've already had to deprecate two of the packages (**botbuilder-toybox-prompts** and **botbuilder-toybox-dialogs**) because we ended up promoting them to being official packages in the SDK. If any of the other packages in this collection end up becoming overly popular I'd expect them to likely suffer a similar fate, in which case the toybox package will be deprecated and instructions for migrating to the official package will be provided. 18 | 19 | ## Installing Packages 20 | While the v4 SDK is in "preview" status the packages here will also remain in preview. Breaking changes should be expected but I'll try to make them as gentle as possible. You can install the preview versions of the packages from NPM using an @preview tag: 21 | 22 | ```bash 23 | npm install --save botbuilder-toybox-memories@preview 24 | npm install --save botbuilder-toybox-extensions@preview 25 | npm install --save botbuilder-toybox-controls@preview 26 | ``` 27 | 28 | While these package is in preview it's possible for updates to include build breaks. To avoid having any updates break your bot it's recommended that you update the dependency table of your bots `package.json` file to lock down the specific version of the package you're using: 29 | 30 | ```JSON 31 | { 32 | "dependencies": { 33 | "botbuilder": "4.0.0-preview1.1", 34 | "botbuilder-toybox-memories": "0.1.0-preview1.2", 35 | "botbuilder-toybox-extensions": "0.1.0-preview1.2", 36 | "botbuilder-toybox-controls": "0.1.0-preview1.2.1" 37 | } 38 | } 39 | ``` 40 | -------------------------------------------------------------------------------- /packages/botbuilder-toybox-extensions/src/patchFromMiddleware.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @module botbuilder-toybox 3 | */ 4 | /** Licensed under the MIT License. */ 5 | import { Middleware, TurnContext } from 'botbuilder-core'; 6 | 7 | /** 8 | * :package: **botbuilder-toybox-extensions** 9 | * 10 | * Middleware to patch the `from` field for incoming `conversationUpdate` activities. 11 | * 12 | * ## Remarks 13 | * This middleware patches an issue where for some channels, including the emulator, the initial 14 | * `from` field for a `conversationUpdate` activity is either missing or not correct. The issue is 15 | * this ends up causing state management plugins to load/save state for the wrong (or no) user from 16 | * the bots perspective and can cause you to pull your hair out when debugging. Unfortunately, this 17 | * issue apparently isn't an easy issue for the channels to fix as they don't often know who the 18 | * correct user is. Especially, in group conversations. 19 | * 20 | * This plugin does it's best to patch the issue by ensuring that the `from` account for non-message 21 | * activities is never the bot or some system account. In 1-on-1 conversations this should result in 22 | * a solid fix and in group conversations it sort of depends whether all the conversation members get 23 | * added at once or not. If members are added individually things will be fine but if multiple members 24 | * get added to the conversation at the same time we leave the `from` field alone unless its missing. 25 | * Then we just assign the first member from the group as the sender. 26 | * 27 | * **Usage Example** 28 | * 29 | * ```JavaScript 30 | * const { PatchFromMiddleware } = require('botbuilder-toybox-extensions'); 31 | * 32 | * adapter.use(new PatchFromMiddleware()); 33 | * ``` 34 | */ 35 | export class PatchFromMiddleware implements Middleware { 36 | 37 | /** @private */ 38 | public async onTurn(context: TurnContext, next: () => Promise): Promise { 39 | if (context.activity && context.activity.type !== 'message') { 40 | const members = context.activity.membersAdded ? context.activity.membersAdded : context.activity.membersRemoved; 41 | const accounts = members && context.activity.recipient ? members.filter((m) => m.id !== (context.activity as any).recipient) : []; 42 | const l = accounts.length 43 | if (l > 0 && (l === 1 || !context.activity.from)) { 44 | context.activity.from = accounts[0]; 45 | } 46 | } 47 | await next(); 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /packages/botbuilder-toybox-controls/src/slots/entitySlot.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @module botbuilder-toybox 3 | */ 4 | /** Licensed under the MIT License. */ 5 | import { RecognizerResult, TurnContext } from 'botbuilder-core'; 6 | import { ActionSlotFiller } from './actionSlotSet'; 7 | import { ActionContext } from '../actionContext'; 8 | import { ActionSlotRecognizePolicy } from './actionSlotSet'; 9 | import { Recognizer } from '../intentDialog'; 10 | 11 | export class EntitySlot implements ActionSlotFiller { 12 | private readonly recognizer: Recognizer; 13 | private readonly entity: string; 14 | private readonly slot: string; 15 | private _recognizePolicy = ActionSlotRecognizePolicy.untilFilled; 16 | 17 | constructor(recognizer: Recognizer, entity: string, slot?: string) { 18 | this.recognizer = recognizer; 19 | this.entity = entity; 20 | this.slot = slot || entity; 21 | } 22 | 23 | public recognizePolicy(policy: ActionSlotRecognizePolicy): this { 24 | this._recognizePolicy = policy; 25 | return this; 26 | } 27 | 28 | public async updateSlots(action: ActionContext): Promise { 29 | let changed = false; 30 | const slot = action.slots[this.slot]; 31 | const filled = slot != undefined && slot.value !== undefined; 32 | switch (this._recognizePolicy) { 33 | case ActionSlotRecognizePolicy.allways: 34 | changed = await this.recognize(action); 35 | break; 36 | case ActionSlotRecognizePolicy.untilFilled: 37 | if (!filled) { 38 | changed = await this.recognize(action); 39 | } 40 | break; 41 | case ActionSlotRecognizePolicy.ifExpected: 42 | if (action.isSlotExpected(this.slot)) { 43 | changed = await this.recognize(action); 44 | } 45 | break; 46 | } 47 | return changed; 48 | } 49 | 50 | private async recognize(action: ActionContext): Promise { 51 | const results = await this.recognizer.recognize(action.context); 52 | if (results.entities && results.entities.hasOwnProperty(this.entity)) { 53 | const value = results.entities[this.entity]; 54 | if (JSON.stringify(value) !== JSON.stringify(action.slots[this.slot].value)) { 55 | action.slots[this.slot] = { 56 | value: value, 57 | turnChanged: action.turnCount 58 | }; 59 | return true; 60 | } 61 | } 62 | return false; 63 | } 64 | } -------------------------------------------------------------------------------- /docs/reference/interfaces/botbuilder_toybox.readonlyfragment.md: -------------------------------------------------------------------------------- 1 | [Bot Builder Toybox](../README.md) > [ReadOnlyFragment](../interfaces/botbuilder_toybox.readonlyfragment.md) 2 | 3 | 4 | 5 | # Interface: ReadOnlyFragment 6 | 7 | 8 | :package: **botbuilder-toybox-memories** 9 | 10 | Component binding to a `MemoryFragment` that can only be read from. The binding will typically clone the value returned by `get()` as to avoid any tampering. 11 | 12 | ## Type parameters 13 | #### T 14 | 15 | (Optional) fragments data type. Defaults to a value of `any`. 16 | 17 | 18 | ## Methods 19 | 20 | 21 | ### get 22 | 23 | ► **get**(context: *`TurnContext`*): `Promise`.<`T`⎮`undefined`> 24 | 25 | 26 | 27 | *Defined in [packages/botbuilder-toybox-memories/lib/memoryFragment.d.ts:47](https://github.com/Stevenic/botbuilder-toybox/blob/dd57c76/packages/botbuilder-toybox-memories/lib/memoryFragment.d.ts#L47)* 28 | 29 | 30 | 31 | Returns the fragments current/default value and will typically clone the value as to avoid any tampering with the underlying value. 32 | 33 | A value of `undefined` will be returned if the fragment has never been `set()` and no "default value" has been configured. 34 | 35 | The fragments value should be read in on first access and cached such that future calls to `get()` are fast and relatively inexpensive. 36 | 37 | **Usage Example** 38 | 39 | const value = await fragment.get(context); 40 | 41 | 42 | **Parameters:** 43 | 44 | | Param | Type | Description | 45 | | ------ | ------ | ------ | 46 | | context | `TurnContext` | Context for the current turn of conversation. | 47 | 48 | 49 | 50 | 51 | 52 | **Returns:** `Promise`.<`T`⎮`undefined`> 53 | 54 | 55 | 56 | 57 | 58 | ___ 59 | 60 | 61 | 62 | ### has 63 | 64 | ► **has**(context: *`TurnContext`*): `Promise`.<`boolean`> 65 | 66 | 67 | 68 | *Defined in [packages/botbuilder-toybox-memories/lib/memoryFragment.d.ts:63](https://github.com/Stevenic/botbuilder-toybox/blob/dd57c76/packages/botbuilder-toybox-memories/lib/memoryFragment.d.ts#L63)* 69 | 70 | 71 | 72 | Returns `true` if the fragment currently has a value. 73 | 74 | Be aware that this will always return `true` if the fragment has a "default value" configured. 75 | 76 | **Usage Example** 77 | 78 | if (fragment.has(context)) { 79 | await fragment.forget(context); 80 | } 81 | 82 | 83 | **Parameters:** 84 | 85 | | Param | Type | Description | 86 | | ------ | ------ | ------ | 87 | | context | `TurnContext` | Context for the current turn of conversation. | 88 | 89 | 90 | 91 | 92 | 93 | **Returns:** `Promise`.<`boolean`> 94 | 95 | 96 | 97 | 98 | 99 | ___ 100 | 101 | 102 | -------------------------------------------------------------------------------- /packages/botbuilder-toybox-extensions/src/properties/listProperty.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @module botbuilder-toybox 3 | */ 4 | /** Licensed under the MIT License. */ 5 | import { TurnContext, StatePropertyAccessor } from 'botbuilder-core'; 6 | import * as uuidv4 from 'uuid/v4'; 7 | 8 | export interface ListItem { 9 | id: string; 10 | item: T; 11 | } 12 | 13 | export class ListProperty implements StatePropertyAccessor[]> { 14 | constructor (private property: StatePropertyAccessor[]>) { 15 | } 16 | 17 | public delete(context: TurnContext): Promise { 18 | return this.property.delete(context); 19 | } 20 | 21 | public get(context: TurnContext, defaultValue?: ListItem[]): Promise[] | undefined> { 22 | return this.property.get(context, defaultValue); 23 | } 24 | 25 | public set(context: TurnContext, value: ListItem[]): Promise { 26 | return this.property.set(context, value); 27 | } 28 | 29 | public async addItem(context: TurnContext, item: T, id?: string): Promise> { 30 | const items = await this.property.get(context, []) as ListItem[]; 31 | if (!id) { id = uuidv4() } 32 | if (this.findId(items, id) >= 0) { throw new Error(`ListProperty.addItem(): an item with an id of '${id}' already exists.`) } 33 | const entry: ListItem = { id: id, item: item }; 34 | items.push(entry); 35 | return entry; 36 | } 37 | 38 | public async deleteItem(context: TurnContext, id: string): Promise { 39 | const items = await this.property.get(context); 40 | const pos = this.findId(items, id); 41 | if (items && pos >= 0) { 42 | items.splice(pos, 1); 43 | } 44 | } 45 | 46 | public async getCount(context: TurnContext): Promise { 47 | const items = await this.property.get(context); 48 | return items ? items.length : 0; 49 | } 50 | 51 | public async getItem(context: TurnContext, id: string): Promise { 52 | const items = await this.property.get(context); 53 | const pos = this.findId(items, id); 54 | if (items && pos >= 0) { 55 | return items[pos].item; 56 | } 57 | } 58 | 59 | public async hasItem(context: TurnContext, id: string): Promise { 60 | const items = await this.property.get(context); 61 | return this.findId(items, id) >= 0; 62 | } 63 | 64 | private findId(items: ListItem[] | undefined, id: string): number { 65 | if (items) { 66 | for (let i = 0; i < items.length; i++) { 67 | if (items[i].id === id) { 68 | return i; 69 | } 70 | } 71 | } 72 | return -1; 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /docs/reference/classes/botbuilder_toybox.termscontrol.md: -------------------------------------------------------------------------------- 1 | [Bot Builder Toybox](../README.md) > [TermsControl](../classes/botbuilder_toybox.termscontrol.md) 2 | 3 | 4 | 5 | # Class: TermsControl 6 | 7 | 8 | :package: **botbuilder-toybox-controls** 9 | 10 | Control that prompts a user to agree to a Terms of Usage Statement. 11 | 12 | **Usage Example** 13 | 14 | const { TermsControl } = require('botbuilder-toybox-controls'); 15 | 16 | // Define memory fragments 17 | const userTermsVersion = userScope.fragment('termsVersion'); 18 | 19 | // Add control to dialogs 20 | dialogs.add('confirmTOU', new TermsControl(userTermsVersion, { 21 | currentVersion: 2, 22 | termsStatement: `You must agree to our Terms of Use before continuing: http://example.com/tou`, 23 | upgradedTermsStatement: `Out Terms of Use have changed. Please agree before continuing: http://example.com/tou`, 24 | retryPrompt: `Please agree to our Terms of Use before continuing: http://example.com/tou` 25 | })); 26 | 27 | // Confirm TOU as part of first run 28 | dialogs.add('firstRun', [ 29 | async function (dc) { 30 | await dc.begin('fillProfile'); 31 | }, 32 | async function (dc) { 33 | await dc.begin('confirmTOU'); 34 | }, 35 | async function (dc) { 36 | await dc.end(); 37 | } 38 | ]); 39 | 40 | ## Hierarchy 41 | 42 | 43 | ↳ [TermsControl](botbuilder_toybox.termscontrol.md) 44 | 45 | **↳ TermsControl** 46 | 47 | ↳ [TermsControl](botbuilder_toybox.termscontrol.md) 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | ## Index 59 | 60 | ### Constructors 61 | 62 | * [constructor](botbuilder_toybox.termscontrol.md#constructor) 63 | 64 | 65 | 66 | --- 67 | ## Constructors 68 | 69 | 70 | 71 | ### ⊕ **new TermsControl**(usersVersion: *[ReadWriteFragment](../interfaces/botbuilder_toybox.readwritefragment.md)`number`*, settings: *[TermsControlSettings](../interfaces/botbuilder_toybox.termscontrolsettings.md)*): [TermsControl](botbuilder_toybox.termscontrol.md) 72 | 73 | 74 | *Defined in packages/botbuilder-toybox-controls/lib/termsControl.d.ts:77* 75 | 76 | 77 | 78 | Creates a new TermsControl instance. 79 | 80 | 81 | **Parameters:** 82 | 83 | | Param | Type | Description | 84 | | ------ | ------ | ------ | 85 | | usersVersion | [ReadWriteFragment](../interfaces/botbuilder_toybox.readwritefragment.md)`number` | Memory fragment used to read & write the agreed to version number for the user (if any.) | 86 | | settings | [TermsControlSettings](../interfaces/botbuilder_toybox.termscontrolsettings.md) | Settings used to configure the control. | 87 | 88 | 89 | 90 | 91 | 92 | **Returns:** [TermsControl](botbuilder_toybox.termscontrol.md) 93 | 94 | --- 95 | 96 | 97 | -------------------------------------------------------------------------------- /docs/reference/classes/botbuilder_toybox.ensureterms.md: -------------------------------------------------------------------------------- 1 | [Bot Builder Toybox](../README.md) > [EnsureTerms](../classes/botbuilder_toybox.ensureterms.md) 2 | 3 | 4 | 5 | # Class: EnsureTerms 6 | 7 | 8 | :package: **botbuilder-toybox-controls** 9 | 10 | Middleware prevents a user from using the bot until they've agreed to the bots Terms of Use. 11 | 12 | Activities like 'conversationUpdate' will still be allowed through to the bots logic but no 'message' activities will be allowed through until the user agrees to the bots terms. 13 | 14 | **Usage Example** 15 | 16 | const { EnsureTerms } = require('botbuilder-toybox-controls'); 17 | 18 | // Define memory fragments 19 | const convoTermsDialog = convoScope.fragment('termsDialog'); 20 | const userTermsVersion = userScope.fragment('termsVersion'); 21 | 22 | // Add middleware to bots adapter 23 | adapter.use(new EnsureTerms(convoTermsDialog, userTermsVersion, { 24 | currentVersion: 2, 25 | termsStatement: `You must agree to our Terms of Use before continuing: http://example.com/tou`, 26 | upgradedTermsStatement: `Out Terms of Use have changed. Please agree before continuing: http://example.com/tou`, 27 | retryPrompt: `Please agree to our Terms of Use before continuing: http://example.com/tou` 28 | })); 29 | 30 | ## Implements 31 | 32 | * `any` 33 | 34 | ## Index 35 | 36 | ### Constructors 37 | 38 | * [constructor](botbuilder_toybox.ensureterms.md#constructor) 39 | 40 | 41 | 42 | --- 43 | ## Constructors 44 | 45 | 46 | 47 | ### ⊕ **new EnsureTerms**(dialogState: *[ReadWriteFragment](../interfaces/botbuilder_toybox.readwritefragment.md)`any`*, usersVersion: *[ReadWriteFragment](../interfaces/botbuilder_toybox.readwritefragment.md)`number`*, settings: *[TermsControlSettings](../interfaces/botbuilder_toybox.termscontrolsettings.md)*): [EnsureTerms](botbuilder_toybox.ensureterms.md) 48 | 49 | 50 | *Defined in packages/botbuilder-toybox-controls/lib/ensureTerms.d.ts:38* 51 | 52 | 53 | 54 | Creates a new EnsureTerms instance. 55 | 56 | 57 | **Parameters:** 58 | 59 | | Param | Type | Description | 60 | | ------ | ------ | ------ | 61 | | dialogState | [ReadWriteFragment](../interfaces/botbuilder_toybox.readwritefragment.md)`any` | Memory fragment used to read & write the dialog prompting the user to agree to the bots terms. | 62 | | usersVersion | [ReadWriteFragment](../interfaces/botbuilder_toybox.readwritefragment.md)`number` | Memory fragment used to read & write the agreed to version number for the user (if any.) | 63 | | settings | [TermsControlSettings](../interfaces/botbuilder_toybox.termscontrolsettings.md) | Settings used to configure the created TermsControl instance. | 64 | 65 | 66 | 67 | 68 | 69 | **Returns:** [EnsureTerms](botbuilder_toybox.ensureterms.md) 70 | 71 | --- 72 | 73 | 74 | 75 | -------------------------------------------------------------------------------- /packages/botbuilder-toybox-extensions/src/showTypingMiddleware.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @module botbuilder-toybox 3 | */ 4 | /** Licensed under the MIT License. */ 5 | import { Middleware, TurnContext } from 'botbuilder-core'; 6 | 7 | /** 8 | * :package: **botbuilder-toybox-extensions** 9 | * 10 | * Middleware for automatically sending a `typing` activity. 11 | * 12 | * ## Remarks 13 | * This middleware lets you automatically send a 'typing' activity if your bot is taking 14 | * too long to reply to a message. Most channels require you periodically send an additional 15 | * 'typing' activity in order to keep the typing indicator lite so the middleware plugin will 16 | * automatically send additional messages at a given rate until it sees the bot send a reply. 17 | * 18 | * **Usage Example** 19 | * 20 | * ```JavaScript 21 | * const { ShowTypingMiddleware } = require('botbuilder-toybox-extensions'); 22 | * 23 | * adapter.use(new ShowTypingMiddleware()); 24 | * ``` 25 | * 26 | * > It should be noted that the plugin sends 'typing' activities directly through the bots 27 | * > adapter so these additional activities will not go through middleware or be logged. 28 | */ 29 | export class ShowTypingMiddleware implements Middleware { 30 | /** 31 | * Creates a new ShowTypingMiddleware instance. 32 | * @param delay (Optional) initial delay before sending first typing indicator. Defaults to 500ms. 33 | * @param frequency (Optional) rate at which additional typing indicators will be sent. Defaults to every 2000ms. 34 | */ 35 | constructor(private delay = 500, private frequency = 2000) { } 36 | 37 | /** @private */ 38 | public onTurn(context: TurnContext, next: () => Promise): Promise { 39 | let finished = false; 40 | let hTimeout: any = undefined; 41 | function sendTyping() { 42 | hTimeout = undefined; 43 | context.adapter.sendActivities(context, [activity]).then(() => { 44 | if (!finished) { 45 | hTimeout = setTimeout(sendTyping, frequency); 46 | } 47 | }, (err) => { 48 | console.error(`showTyping: error sending typing indicator: ${err.toString()}`); 49 | }); 50 | } 51 | 52 | // Initialize activity 53 | const ref = TurnContext.getConversationReference(context.activity); 54 | const activity = TurnContext.applyConversationReference({ type: 'typing' }, ref); 55 | 56 | // Start delay timer and call next() 57 | const { delay, frequency } = this; 58 | hTimeout = setTimeout(sendTyping, delay); 59 | return next().then(() => { 60 | // Stop timer 61 | finished = true; 62 | if (hTimeout) { clearTimeout(hTimeout) } 63 | }, () => { 64 | // Stop timer 65 | finished = true; 66 | if (hTimeout) { clearTimeout(hTimeout) } 67 | }); 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /packages/botbuilder-toybox-extensions/src/filterActivityMiddleware.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @module botbuilder-toybox 3 | */ 4 | /** Licensed under the MIT License. */ 5 | import { Middleware, TurnContext } from 'botbuilder-core'; 6 | 7 | /** 8 | * :package: **botbuilder-toybox-extensions** 9 | * 10 | * Function that will be called anytime an activity of the specified type is received. 11 | * 12 | * ## Remarks 13 | * Simply avoid calling `next()` to prevent the activity from being further routed. 14 | * @param FilterActivityHandler.context Context object for the current turn of conversation. 15 | * @param FilterActivityHandler.next Function that should be called to continue execution to the next piece of middleware. Omitting this call will effectively filter out the activity. 16 | */ 17 | export type FilterActivityHandler = (context: TurnContext, next: () => Promise) => Promise; 18 | 19 | 20 | /** 21 | * :package: **botbuilder-toybox-extensions** 22 | * 23 | * Middleware for filtering incoming activities. 24 | * 25 | * ## Remarks 26 | * This middleware lets you easily filter out activity types your bot doesn't care about. For 27 | * example here's how to filter out 'contactRelationUpdate' and 'conversationUpdate' activities: 28 | * 29 | * ```JavaScript 30 | * const { FilterActivityMiddleware } = require('botbuilder-toybox-extensions'); 31 | * 32 | * adapter.use(new FilterActivityMiddleware('contactRelationUpdate', (context, next) => { }) 33 | * .use(new FilterActivityMiddleware('conversationUpdate', (context, next) => { })); 34 | * ``` 35 | * 36 | * You can also use an activity filter to greet a user as they join a conversation: 37 | * 38 | * ```JavaScript 39 | * adapter.use(new FilterActivityMiddleware('conversationUpdate', async (context, next) => { 40 | * const added = context.activity.membersAdded || []; 41 | * for (let i = 0; i < added.length; i++) { 42 | * if (added[i].id !== context.activity.recipient.id) { 43 | * await context.sendActivity(`Welcome to my bot!`); 44 | * break; 45 | * } 46 | * } 47 | * })); 48 | * ``` 49 | */ 50 | export class FilterActivityMiddleware implements Middleware { 51 | /** 52 | * Creates a new FilterActivityMiddleware instance. 53 | * @param type Type of activity to trigger on. 54 | * @param handler Function that will be called anytime an activity of the specified type is received. Simply avoid calling `next()` to prevent the activity from being further routed. 55 | */ 56 | constructor(private type: string, private handler: FilterActivityHandler) { } 57 | 58 | /** @private */ 59 | public async onTurn(context: TurnContext, next: () => Promise): Promise { 60 | // Call handler if filter matched 61 | if (context.activity && context.activity.type === this.type) { 62 | await Promise.resolve(this.handler(context, next)); 63 | } else { 64 | await next(); 65 | } 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /samples/menus-ts/src/app.ts: -------------------------------------------------------------------------------- 1 | import { BotFrameworkAdapter, MemoryStorage, TurnContext } from 'botbuilder'; 2 | import { ConversationScope, UserScope, ManageScopes, ScopeAccessor } from 'botbuilder-toybox-memories'; 3 | import { ManageMenus, DialogSet, MenuContext } from 'botbuilder-toybox-controls'; 4 | import * as restify from 'restify'; 5 | 6 | import { Alarm } from './models'; 7 | import * as menus from './menus'; 8 | 9 | // Create server 10 | let server = restify.createServer(); 11 | server.listen(process.env.port || process.env.PORT || 3978, function () { 12 | console.log(`${server.name} listening to ${server.url}`); 13 | }); 14 | 15 | // Create adapter 16 | const adapter = new BotFrameworkAdapter({ 17 | appId: process.env.MICROSOFT_APP_ID, 18 | appPassword: process.env.MICROSOFT_APP_PASSWORD 19 | }); 20 | 21 | // Define scopes and add to adapter 22 | const storage = new MemoryStorage(); 23 | const convoScope = new ConversationScope(storage); 24 | const userScope = new UserScope(storage); 25 | adapter.use(new ManageScopes(convoScope, userScope)); 26 | 27 | // Define memory fragments 28 | const alarmsList = userScope.fragment('alarms', []); 29 | const dialogState = convoScope.fragment('dialogState'); 30 | const menuState = convoScope.fragment('menuState'); 31 | 32 | // Add menus 33 | const dialogs = new DialogSet(dialogState); 34 | const alarmMenu = menus.createAlarmMenu('alarm', dialogs); 35 | const cancelMenu = menus.createCancelMenu('cancel', dialogs); 36 | adapter.use(new ManageMenus(menuState, alarmMenu, cancelMenu)); 37 | 38 | // Extend interface for context object 39 | interface BotContext extends MenuContext { 40 | conversation: ScopeAccessor; 41 | user: ScopeAccessor; 42 | } 43 | 44 | // Listen for incoming activities 45 | server.post('/api/messages', (req, res) => { 46 | // Route received activities to adapter for processing 47 | adapter.processActivity(req, res, async (context: BotContext) => { 48 | // Initialize dialog context 49 | const state = await context.conversation.get('dialogState'); 50 | const dc = await dialogs.createContext(context); 51 | 52 | // Continue dialog execution 53 | if (!context.responded) { 54 | await dc.continue(); 55 | } 56 | 57 | // Run fallback logic 58 | const isMessage = context.activity.type === 'message'; 59 | if (!context.responded && isMessage) { 60 | await dc.context.sendActivity(`Hi! I'm a simple alarm bot. Say "add alarm", "delete alarm", or "show alarms".`) 61 | } 62 | }); 63 | }); 64 | 65 | // Setup Dialogs 66 | 67 | import { AddAlarmDialog } from './addAlarmDialog'; 68 | import { DeleteAlarmDialog } from './deleteAlarmDialog'; 69 | import { ShowAlarmsDialog } from './showAlarmsDialog'; 70 | 71 | dialogs.add('addAlarm', new AddAlarmDialog(alarmsList)); 72 | dialogs.add('deleteAlarm', new DeleteAlarmDialog(alarmsList)); 73 | dialogs.add('showAlarms', new ShowAlarmsDialog(alarmsList.asReadOnly())); 74 | -------------------------------------------------------------------------------- /samples/declarative-ts/src/app.ts: -------------------------------------------------------------------------------- 1 | import { BotFrameworkAdapter, MemoryStorage, TurnContext } from 'botbuilder'; 2 | import { ConversationScope, UserScope, ManageScopes, ScopeAccessor } from 'botbuilder-toybox-memories'; 3 | import { ManageMenus, DialogSet, MenuContext } from 'botbuilder-toybox-controls'; 4 | import * as restify from 'restify'; 5 | 6 | import { Alarm } from './models'; 7 | import * as menus from './menus'; 8 | 9 | // Create server 10 | let server = restify.createServer(); 11 | server.listen(process.env.port || process.env.PORT || 3978, function () { 12 | console.log(`${server.name} listening to ${server.url}`); 13 | }); 14 | 15 | // Create adapter 16 | const adapter = new BotFrameworkAdapter({ 17 | appId: process.env.MICROSOFT_APP_ID, 18 | appPassword: process.env.MICROSOFT_APP_PASSWORD 19 | }); 20 | 21 | // Define scopes and add to adapter 22 | const storage = new MemoryStorage(); 23 | const convoScope = new ConversationScope(storage); 24 | const userScope = new UserScope(storage); 25 | adapter.use(new ManageScopes(convoScope, userScope)); 26 | 27 | // Define memory fragments 28 | const alarmsList = userScope.fragment('alarms', []); 29 | const dialogState = convoScope.fragment('dialogState'); 30 | const menuState = convoScope.fragment('menuState'); 31 | 32 | // Add menus 33 | const dialogs = new DialogSet(dialogState); 34 | const alarmMenu = menus.createAlarmMenu('alarm', dialogs); 35 | const cancelMenu = menus.createCancelMenu('cancel', dialogs); 36 | adapter.use(new ManageMenus(menuState, alarmMenu, cancelMenu)); 37 | 38 | // Extend interface for context object 39 | interface BotContext extends MenuContext { 40 | conversation: ScopeAccessor; 41 | user: ScopeAccessor; 42 | } 43 | 44 | // Listen for incoming activities 45 | server.post('/api/messages', (req, res) => { 46 | // Route received activities to adapter for processing 47 | adapter.processActivity(req, res, async (context: BotContext) => { 48 | // Initialize dialog context 49 | const state = await context.conversation.get('dialogState'); 50 | const dc = await dialogs.createContext(context); 51 | 52 | // Continue dialog execution 53 | if (!context.responded) { 54 | await dc.continue(); 55 | } 56 | 57 | // Run fallback logic 58 | const isMessage = context.activity.type === 'message'; 59 | if (!context.responded && isMessage) { 60 | await dc.context.sendActivity(`Hi! I'm a simple alarm bot. Say "add alarm", "delete alarm", or "show alarms".`) 61 | } 62 | }); 63 | }); 64 | 65 | // Setup Dialogs 66 | 67 | import { AddAlarmDialog } from './addAlarmDialog'; 68 | import { DeleteAlarmDialog } from './deleteAlarmDialog'; 69 | import { ShowAlarmsDialog } from './showAlarmsDialog'; 70 | 71 | dialogs.add('addAlarm', new AddAlarmDialog(alarmsList)); 72 | dialogs.add('deleteAlarm', new DeleteAlarmDialog(alarmsList)); 73 | dialogs.add('showAlarms', new ShowAlarmsDialog(alarmsList.asReadOnly())); 74 | -------------------------------------------------------------------------------- /samples/remote-dialogs-ts/control/src/app.ts: -------------------------------------------------------------------------------- 1 | import { MemoryStorage, TurnContext } from 'botbuilder'; 2 | import { ConversationScope, ManageScopes, ScopeAccessor } from 'botbuilder-toybox-memories'; 3 | import { HttpAdapter } from 'botbuilder-toybox-extensions'; 4 | import { DialogContainer, DialogCompletion } from 'botbuilder-dialogs'; 5 | import { CreateProfileControl } from './createProfileControl'; 6 | import { ChangeNameControl } from './changeNameControl'; 7 | import { ChangeEmailControl } from './changeEmailControl'; 8 | import * as restify from 'restify'; 9 | 10 | // Create server (on port 4000) 11 | let server = restify.createServer(); 12 | server.listen(process.env.port || process.env.PORT || 4000, function () { 13 | console.log(`${server.name} listening to ${server.url}`); 14 | }); 15 | 16 | // Create adapter 17 | const adapter = new HttpAdapter(); 18 | 19 | // Define scopes and add to adapter 20 | const storage = new MemoryStorage(); 21 | const convoScope = new ConversationScope(storage); 22 | adapter.use(new ManageScopes(convoScope)); 23 | 24 | // Define memory fragments 25 | convoScope.fragment('controlState'); 26 | 27 | // Extend interface for context object 28 | interface ControlContext extends TurnContext { 29 | conversation: ScopeAccessor; 30 | } 31 | 32 | // CreateProfile Endpoint 33 | const createProfile = new CreateProfileControl(); 34 | server.post('/controls/createProfile', (req, res) => dispatchActivity(createProfile, req, res)); 35 | 36 | // ChangeName Endpoint 37 | const changeName = new ChangeNameControl(); 38 | server.post('/controls/changeName', (req, res) => dispatchActivity(changeName, req, res)); 39 | 40 | 41 | // ChangeEmail Endpoint 42 | const changeEmail = new ChangeEmailControl(); 43 | server.post('/controls/changeEmail', (req, res) => dispatchActivity(changeEmail, req, res)); 44 | 45 | 46 | function dispatchActivity(control: DialogContainer, req: restify.Request, res: restify.Response) { 47 | // Route received activity to control 48 | adapter.processActivity(req, res, async (context: ControlContext) => { 49 | // Check for start of control 50 | let completion: DialogCompletion; 51 | if (context.activity.type === 'event' && context.activity.name === 'dialogBegin') { 52 | // Initialize controls state 53 | const state = {}; 54 | await context.conversation.set('controlState', state); 55 | 56 | // Dispatch to control 57 | const args = context.activity.value; 58 | completion = await control.begin(context, state, args); 59 | } else { 60 | // Get state and continue execution 61 | const state = await context.conversation.get('controlState'); 62 | completion = await control.continue(context, state); 63 | } 64 | 65 | // Check for completion of control 66 | if (completion.isCompleted) { 67 | await context.sendActivity({ type: 'endOfConversation', value: completion.result }); 68 | } 69 | }); 70 | } 71 | -------------------------------------------------------------------------------- /docs/reference/classes/botbuilder_toybox.checkversion.md: -------------------------------------------------------------------------------- 1 | [Bot Builder Toybox](../README.md) > [CheckVersion](../classes/botbuilder_toybox.checkversion.md) 2 | 3 | 4 | 5 | # Class: CheckVersion 6 | 7 | 8 | :package: **botbuilder-toybox-extensions** 9 | 10 | Deploying new versions of your bot more often then not should have little to no impact on the current conversations you're having with a user. Sometimes, however, a change to your bots conversation logic can result in the user getting into a stuck state that can only be fixed by their conversation state being deleted. 11 | 12 | This middleware lets you track a version number for the conversations your bot is having so that you can automatically delete the conversation state anytime a major version number difference is detected. Example: 13 | 14 | **Usage Example** 15 | 16 | const { CheckVersion } = require('botbuilder-toybox-extensions'); 17 | const { ConversationScope } = require('botbuilder-toybox-memories'); 18 | 19 | // Initialize memory fragment to hold our version number. 20 | const convoScope = new ConversationScope(new MemoryStorage()); 21 | const convoVersion = convoScope.fragment('convoVersion'); 22 | 23 | // Add middleware to check the version and clear the scope on change. 24 | adapter.use(new CheckVersion(convoVersion, 2.0, async (context, version, next) => { 25 | await convoScope.forgetAll(context); 26 | await context.sendActivity(`I'm sorry. My service has been upgraded and we need to start over.`); 27 | await next(); 28 | })); 29 | 30 | ## Implements 31 | 32 | * `any` 33 | 34 | ## Index 35 | 36 | ### Constructors 37 | 38 | * [constructor](botbuilder_toybox.checkversion.md#constructor) 39 | 40 | 41 | 42 | --- 43 | ## Constructors 44 | 45 | 46 | 47 | ### ⊕ **new CheckVersion**(versionFragment: *[ReadWriteFragment](../interfaces/botbuilder_toybox.readwritefragment.md)`number`*, version: *`number`*, handler: *[VersionChangedHandler](../#versionchangedhandler)*): [CheckVersion](botbuilder_toybox.checkversion.md) 48 | 49 | 50 | *Defined in [packages/botbuilder-toybox-extensions/lib/checkVersion.d.ts:50](https://github.com/Stevenic/botbuilder-toybox/blob/dd57c76/packages/botbuilder-toybox-extensions/lib/checkVersion.d.ts#L50)* 51 | 52 | 53 | 54 | Creates a new CheckVersion instance. 55 | 56 | 57 | **Parameters:** 58 | 59 | | Param | Type | Description | 60 | | ------ | ------ | ------ | 61 | | versionFragment | [ReadWriteFragment](../interfaces/botbuilder_toybox.readwritefragment.md)`number` | The memory fragment to persist the current version number to. | 62 | | version | `number` | Latest version number in major.minor form. | 63 | | handler | [VersionChangedHandler](../#versionchangedhandler) | Handler that will be invoked anytime an existing conversations version number doesn't match. New conversations will just be initialized to the new version number. | 64 | 65 | 66 | 67 | 68 | 69 | **Returns:** [CheckVersion](botbuilder_toybox.checkversion.md) 70 | 71 | --- 72 | 73 | 74 | 75 | -------------------------------------------------------------------------------- /packages/botbuilder-toybox-extensions/src/properties/historyProperty.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @module botbuilder-toybox 3 | */ 4 | /** Licensed under the MIT License. */ 5 | import { TurnContext, StatePropertyAccessor } from 'botbuilder-core'; 6 | import { TimestampedPropertyValue } from './expiringProperty'; 7 | 8 | export interface HistoryPropertyValue { 9 | current?: TimestampedPropertyValue; 10 | history?: TimestampedPropertyValue[]; 11 | } 12 | 13 | export class HistoryProperty implements StatePropertyAccessor { 14 | constructor (private property: StatePropertyAccessor>, public readonly maxCount?: number, public readonly ttl?: number) { 15 | } 16 | 17 | public async delete(context: TurnContext): Promise { 18 | await this.pushValue(context); 19 | } 20 | 21 | public async get(context: TurnContext, defaultValue?: T): Promise { 22 | const val = await this.property.get(context); 23 | if (val && val.current && val.current.value !== undefined) { 24 | return val.current.value; 25 | } else if (defaultValue !== undefined) { 26 | await this.set(context, defaultValue); 27 | return defaultValue; 28 | } 29 | return undefined; 30 | } 31 | 32 | public async set(context: TurnContext, value: T): Promise { 33 | await this.pushValue(context); 34 | const val = await this.property.get(context, {}) as HistoryPropertyValue; 35 | val.current = { timestamp: new Date().getTime(), value: value } 36 | } 37 | 38 | public async getHistory(context: TurnContext): Promise[]> { 39 | const val = await this.property.get(context); 40 | return val && val.history ? val.history : []; 41 | } 42 | 43 | public async getTimestamp(context: TurnContext): Promise { 44 | const val = await this.property.get(context); 45 | if (val && val.current) { 46 | return new Date(val.current.timestamp); 47 | } 48 | return undefined; 49 | } 50 | 51 | private async pushValue(context: TurnContext): Promise { 52 | const val = await this.property.get(context); 53 | if (val && val.current) { 54 | if (!val.history) { val.history = [] } 55 | val.current.timestamp = new Date().getTime(); 56 | val.history.push(val.current); 57 | val.current = undefined; 58 | await this.purgeHistory(context); 59 | } 60 | } 61 | 62 | private async purgeHistory(context: TurnContext): Promise { 63 | const val = await this.property.get(context); 64 | if (val && val.history) { 65 | const now = new Date().getTime(); 66 | for (let i = val.history.length - 1; i >= 0; i--) { 67 | if (this.maxCount !== undefined && this.maxCount > val.history.length) { 68 | val.history.slice(i, 1); 69 | } else if (this.ttl !== undefined && now >= (val.history[i].timestamp + this.ttl)) { 70 | val.history.slice(i, 1); 71 | } 72 | } 73 | } 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /samples/menus-ts/src/addAlarmDialog.ts: -------------------------------------------------------------------------------- 1 | import { DialogContainer, TextPrompt, DatetimePrompt } from 'botbuilder-dialogs'; 2 | import { ReadWriteFragment } from 'botbuilder-toybox-memories'; 3 | import { MenuContext } from 'botbuilder-toybox-controls'; 4 | import { UserState } from 'botbuilder'; 5 | import * as moment from 'moment'; 6 | 7 | import { Alarm } from './models'; 8 | 9 | export class AddAlarmDialog extends DialogContainer { 10 | constructor(alarmsList: ReadWriteFragment) { 11 | super('addAlarm'); 12 | 13 | this.dialogs.add('addAlarm', [ 14 | async function (dc) { 15 | // Show cancel menu 16 | await dc.context.menus.showMenu('cancel'); 17 | 18 | // Initialize temp alarm and prompt for title 19 | dc.activeDialog.state = {} as Alarm; 20 | await dc.prompt('titlePrompt', `What would you like to call your alarm?`); 21 | }, 22 | async function (dc, title: string) { 23 | // Save alarm title and prompt for time 24 | const alarm = dc.activeDialog.state as Alarm; 25 | alarm.title = title; 26 | await dc.prompt('timePrompt', `What time would you like to set the "${alarm.title}" alarm for?`); 27 | }, 28 | async function (dc, time: Date) { 29 | // Hide menu 30 | await dc.context.menus.hideMenu(); 31 | 32 | // Save alarm time 33 | const alarm = dc.activeDialog.state as Alarm; 34 | alarm.time = time.toISOString(); 35 | 36 | // Alarm completed so set alarm. 37 | const alarms = await alarmsList.get(dc.context); 38 | alarms.push(alarm); 39 | 40 | // Confirm to user 41 | await dc.context.sendActivity(`Your alarm named "${alarm.title}" is set for "${moment(alarm.time).format("ddd, MMM Do, h:mm a")}".`); 42 | await dc.end(); 43 | } 44 | ]); 45 | 46 | this.dialogs.add('titlePrompt', new TextPrompt(async (context, value) => { 47 | if (!value || value.length < 3) { 48 | await context.sendActivity(`Title should be at least 3 characters long.`); 49 | return undefined; 50 | } else { 51 | return value.trim(); 52 | } 53 | })); 54 | 55 | this.dialogs.add('timePrompt', new DatetimePrompt(async (context, values) => { 56 | try { 57 | if (!Array.isArray(values) || values.length < 0) { throw new Error('missing time') } 58 | if (values[0].type !== 'datetime') { throw new Error('unsupported type') } 59 | const value = new Date(values[0].value); 60 | if (value.getTime() < new Date().getTime()) { throw new Error('in the past') } 61 | return value; 62 | } catch (err) { 63 | await context.sendActivity(`Please enter a valid time in the future like "tomorrow at 9am" or say "cancel".`); 64 | return undefined; 65 | } 66 | })); 67 | } 68 | } -------------------------------------------------------------------------------- /samples/declarative-ts/src/dialogs/mainDialog.ts: -------------------------------------------------------------------------------- 1 | import { ActivityTypes, Activity } from 'botbuilder'; 2 | import { DialogContext, DialogTurnResult, DialogTurnStatus } from 'botbuilder-dialogs'; 3 | import { ConfigurableComponentDialog, TypeFactory, ComponentDialogConfiguration } from 'botbuilder-toybox-declarative'; 4 | 5 | export interface MainDialogConfiguration extends ComponentDialogConfiguration { 6 | interruptions: MainDialogInterruption[]; 7 | fallbackMessage?: string|Partial; 8 | } 9 | 10 | export interface MainDialogInterruption { 11 | pattern: string; 12 | flags?: string; 13 | dialogId: string; 14 | options?: object; 15 | } 16 | 17 | export class MainDialog extends ConfigurableComponentDialog { 18 | private readonly interuptions: CachedMainDialogInterruption[] = []; 19 | private fallbackMessage?: string|Partial; 20 | 21 | public addInterruption(interruption: MainDialogInterruption): this { 22 | const cached = Object.assign({}, interruption) as CachedMainDialogInterruption; 23 | cached.expression = new RegExp(cached.pattern, cached.flags || 'i'); 24 | this.interuptions.push(cached); 25 | return this; 26 | } 27 | 28 | protected onConfigure(config: MainDialogConfiguration) { 29 | super.onConfigure(config); 30 | 31 | // Add child dialogs 32 | this.configureDialogs(config); 33 | 34 | // Add interruptions 35 | config.interruptions.forEach(interruption => this.addInterruption(interruption)); 36 | 37 | // Set fallback message 38 | this.fallbackMessage = config.fallbackMessage; 39 | } 40 | 41 | protected onBeginDialog(dc: DialogContext, options: any): Promise { 42 | return this.onRunTurn(dc); 43 | } 44 | 45 | protected onContinueDialog(dc: DialogContext): Promise { 46 | return this.onRunTurn(dc); 47 | } 48 | 49 | protected async onRunTurn(dc: DialogContext): Promise { 50 | // Perform interruption 51 | const isMessage = dc.context.activity.type === ActivityTypes.Message; 52 | if (isMessage) { 53 | const utterance = dc.context.activity.text; 54 | for(let i = 0; i < this.interuptions.length; i++) { 55 | const cached = this.interuptions[i]; 56 | const matched = cached.expression.exec(utterance); 57 | if (matched) { 58 | await dc.cancelAllDialogs(); 59 | return await dc.beginDialog(cached.dialogId, cached.options); 60 | } 61 | } 62 | } 63 | 64 | // Continue execution of current dialog 65 | let result = await dc.continueDialog(); 66 | 67 | // Send fallback message as needed 68 | if (result.status === DialogTurnStatus.empty && isMessage && this.fallbackMessage) { 69 | await dc.context.sendActivity(this.fallbackMessage); 70 | } 71 | return result; 72 | } 73 | } 74 | TypeFactory.register('MainDialog', (config: ComponentDialogConfiguration) => { 75 | const dialog = new MainDialog(config.id); 76 | return dialog.configure(config); 77 | }); 78 | 79 | interface CachedMainDialogInterruption extends MainDialogInterruption { 80 | expression: RegExp; 81 | } 82 | -------------------------------------------------------------------------------- /packages/botbuilder-toybox-controls/src/addChoices.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @module botbuilder-toybox 3 | */ 4 | /** Licensed under the MIT License. */ 5 | import { ActivityTypes, SuggestedActions } from 'botbuilder-core'; 6 | import { 7 | ComponentDialog, Dialog, DialogContext, DialogTurnResult, DialogTurnStatus, 8 | findChoices, FindChoicesOptions, ChoiceFactory, Choice, FoundChoice 9 | } from 'botbuilder-dialogs'; 10 | 11 | export type ChoiceHandler = (dc: DialogContext, choice: FoundChoice) => Promise; 12 | 13 | export class AddChoices extends ComponentDialog { 14 | private readonly choices: (Choice|string)[]; 15 | private readonly onChoice: ChoiceHandler; 16 | 17 | constructor(dialog: Dialog, choices: (Choice|string)[], onChoice: ChoiceHandler) { 18 | // Use passed in dialogs ID for wrappers ID 19 | super(dialog.id); 20 | 21 | // Add wrapped dialog as a child dialog 22 | this.addDialog(dialog); 23 | 24 | // Save other params 25 | this.choices = choices; 26 | this.onChoice = onChoice; 27 | } 28 | 29 | public renderChoices = true; 30 | public recognizerOptions: FindChoicesOptions|undefined; 31 | 32 | protected onBeginDialog(dc: DialogContext, options?: any): Promise { 33 | return this.onRunTurn(dc, options); 34 | } 35 | 36 | protected onContinueDialog(dc: DialogContext): Promise { 37 | return this.onRunTurn(dc); 38 | } 39 | 40 | protected async onRunTurn(dc: DialogContext, options?: any): Promise { 41 | // Add rendering logic 42 | const isMessage = dc.context.activity.type == ActivityTypes.Message; 43 | if (isMessage && this.renderChoices) { 44 | dc.context.onSendActivities(async (ctx, activities, next) => { 45 | // Append choices 46 | for (let i = activities.length - 1; i >= 0; i--) { 47 | if (activities[i].type === ActivityTypes.Message) { 48 | // Render choices as suggested actions to a temp activity 49 | const temp = ChoiceFactory.forChannel(ctx, this.choices); 50 | if (temp.suggestedActions && temp.suggestedActions.actions) { 51 | // Merge with any existing actions 52 | if (!activities[i].suggestedActions) { activities[i].suggestedActions = {} as SuggestedActions } 53 | if (!activities[i].suggestedActions.actions) { activities[i].suggestedActions.actions = [] } 54 | temp.suggestedActions.actions.forEach(a => activities[i].suggestedActions.actions.push(a)); 55 | } 56 | break; 57 | } 58 | } 59 | return await next(); 60 | }); 61 | } 62 | 63 | // Check for matched choice 64 | if (isMessage) { 65 | const matched = findChoices(dc.context.activity.text, this.choices, this.recognizerOptions); 66 | if (matched.length > 0) { 67 | // Route to onChoice() handler 68 | return this.onChoice(dc, matched[0].resolution); 69 | } 70 | } 71 | 72 | // Perform default routing logic 73 | let result = await dc.continueDialog(); 74 | if (result.status === DialogTurnStatus.empty) { 75 | result = await dc.beginDialog(this.initialDialogId, options); 76 | } 77 | return result; 78 | } 79 | } -------------------------------------------------------------------------------- /packages/botbuilder-toybox-controls/tests/listControl.test.js: -------------------------------------------------------------------------------- 1 | const assert = require('assert'); 2 | const { TestAdapter, MemoryStorage, ConversationState } = require('botbuilder-core'); 3 | const { DialogSet, DialogTurnStatus } = require('botbuilder-dialogs'); 4 | const { ListControl } = require('../lib'); 5 | 6 | describe('ListControl', function() { 7 | this.timeout(5000); 8 | 9 | it('should render a single page of results.', function (done) { 10 | const convoState = new ConversationState(new MemoryStorage()); 11 | const dialogState = convoState.createProperty('dialogState'); 12 | const dialogs = new DialogSet(dialogState); 13 | dialogs.add(new ListControl('control', async (list) => { 14 | assert(list); 15 | assert(list.context); 16 | return { 17 | result: { text: 'results' } 18 | }; 19 | })); 20 | 21 | const adapter = new TestAdapter(async (context) => { 22 | const dc = await dialogs.createContext(context); 23 | const result = await dc.beginDialog('control'); 24 | assert(result.status === DialogTurnStatus.complete, `Not completed and should be.`); 25 | }); 26 | 27 | adapter.test('test', `results`) 28 | .then(() => done()); 29 | }); 30 | 31 | it('should more button for multiple pages of results.', function (done) { 32 | const convoState = new ConversationState(new MemoryStorage()); 33 | const dialogState = convoState.createProperty('dialogState'); 34 | const dialogs = new DialogSet(dialogState); 35 | dialogs.add(new ListControl('control', async (list) => { 36 | return { 37 | result: { text: 'results' }, 38 | continueToken: 1 39 | }; 40 | })); 41 | 42 | const adapter = new TestAdapter(async (context) => { 43 | const dc = await dialogs.createContext(context); 44 | const result = await dc.beginDialog('control'); 45 | assert(result.status === DialogTurnStatus.waiting, `Completed and shouldn't be.`); 46 | }); 47 | 48 | adapter.test('test', (activity) => { 49 | assert(activity && activity.text === 'results'); 50 | assert(activity.suggestedActions && activity.suggestedActions.actions); 51 | assert(activity.suggestedActions.actions[0].value === 'more'); 52 | }).then(() => done()); 53 | }); 54 | 55 | it('should render multiple pages of results.', function (done) { 56 | const convoState = new ConversationState(new MemoryStorage()); 57 | const dialogState = convoState.createProperty('dialogState'); 58 | const dialogs = new DialogSet(dialogState); 59 | dialogs.add(new ListControl('control', async (list) => { 60 | const page = typeof list.continueToken === 'number' ? list.continueToken : 0; 61 | return { 62 | result: { text: 'page' + page }, 63 | continueToken: page + 1 64 | }; 65 | })); 66 | 67 | const adapter = new TestAdapter(async (context) => { 68 | const dc = await dialogs.createContext(context); 69 | const result = await dc.continueDialog(); 70 | if (result.status === DialogTurnStatus.empty) { 71 | await dc.beginDialog('control'); 72 | } 73 | await convoState.saveChanges(context); 74 | }); 75 | 76 | adapter.test('test', `page0`) 77 | .test('more', 'page1') 78 | .then(() => done()); 79 | }); 80 | }); 81 | -------------------------------------------------------------------------------- /samples/menus-ts/src/deleteAlarmDialog.ts: -------------------------------------------------------------------------------- 1 | import { DialogContainer, ChoicePrompt, ConfirmPrompt, FoundChoice } from 'botbuilder-dialogs'; 2 | import { ReadWriteFragment } from 'botbuilder-toybox-memories'; 3 | import { MenuContext } from 'botbuilder-toybox-controls'; 4 | import { UserState } from 'botbuilder'; 5 | 6 | import { Alarm } from './models'; 7 | 8 | export class DeleteAlarmDialog extends DialogContainer { 9 | constructor(alarmsList: ReadWriteFragment) { 10 | super('deleteAlarm'); 11 | 12 | this.dialogs.add('deleteAlarm', [ 13 | async function (dc) { 14 | // Divert to appropriate dialog 15 | const alarms = await alarmsList.get(dc.context); 16 | if (alarms.length > 1) { 17 | await dc.begin('deleteAlarmMulti'); 18 | } else if (alarms.length === 1) { 19 | await dc.begin('deleteAlarmSingle'); 20 | } else { 21 | await dc.context.sendActivity(`No alarms set to delete.`); 22 | await dc.end(); 23 | } 24 | } 25 | ]); 26 | 27 | this.dialogs.add('deleteAlarmMulti', [ 28 | async function (dc) { 29 | // Show cancel menu 30 | await dc.context.menus.showMenu('cancel'); 31 | 32 | // Compute list of choices based on alarm titles 33 | const alarms = await alarmsList.get(dc.context); 34 | const choices = alarms.map((value) => value.title); 35 | 36 | // Prompt user for choice (force use of "list" style) 37 | const prompt = `Which alarm would you like to delete? Say "cancel" to quit.`; 38 | await dc.prompt('choicePrompt', prompt, choices); 39 | }, 40 | async function (dc, choice: FoundChoice) { 41 | // Hide menu 42 | await dc.context.menus.hideMenu(); 43 | 44 | // Delete alarm by position 45 | const alarms = await alarmsList.get(dc.context); 46 | if (choice.index < alarms.length) { alarms.splice(choice.index, 1) } 47 | 48 | // Notify user of delete 49 | await dc.context.sendActivity(`Deleted "${choice.value}" alarm.`); 50 | await dc.end(); 51 | } 52 | ]); 53 | 54 | this.dialogs.add('deleteAlarmSingle', [ 55 | async function (dc) { 56 | // Show cancel menu 57 | await dc.context.menus.showMenu('cancel'); 58 | 59 | // Confirm delete 60 | const alarms = await alarmsList.get(dc.context); 61 | const alarm = alarms[0]; 62 | await dc.prompt('confirmPrompt', `Are you sure you want to delete the "${alarm.title}" alarm?`); 63 | }, 64 | async function (dc, confirm: boolean) { 65 | // Hide menu 66 | await dc.context.menus.hideMenu(); 67 | 68 | // Delete alarm 69 | if (confirm) { 70 | alarmsList.forget(dc.context); 71 | await dc.context.sendActivity(`alarm deleted...`); 72 | } else { 73 | await dc.context.sendActivity(`ok...`); 74 | } 75 | await dc.end(); 76 | } 77 | ]); 78 | 79 | this.dialogs.add('choicePrompt', new ChoicePrompt()); 80 | this.dialogs.add('confirmPrompt', new ConfirmPrompt()); 81 | } 82 | } -------------------------------------------------------------------------------- /samples/declarative-ts/src/deleteAlarmDialog.ts: -------------------------------------------------------------------------------- 1 | import { DialogContainer, ChoicePrompt, ConfirmPrompt, FoundChoice } from 'botbuilder-dialogs'; 2 | import { ReadWriteFragment } from 'botbuilder-toybox-memories'; 3 | import { MenuContext } from 'botbuilder-toybox-controls'; 4 | import { UserState } from 'botbuilder'; 5 | 6 | import { Alarm } from './models'; 7 | 8 | export class DeleteAlarmDialog extends DialogContainer { 9 | constructor(alarmsList: ReadWriteFragment) { 10 | super('deleteAlarm'); 11 | 12 | this.dialogs.add('deleteAlarm', [ 13 | async function (dc) { 14 | // Divert to appropriate dialog 15 | const alarms = await alarmsList.get(dc.context); 16 | if (alarms.length > 1) { 17 | await dc.begin('deleteAlarmMulti'); 18 | } else if (alarms.length === 1) { 19 | await dc.begin('deleteAlarmSingle'); 20 | } else { 21 | await dc.context.sendActivity(`No alarms set to delete.`); 22 | await dc.end(); 23 | } 24 | } 25 | ]); 26 | 27 | this.dialogs.add('deleteAlarmMulti', [ 28 | async function (dc) { 29 | // Show cancel menu 30 | await dc.context.menus.showMenu('cancel'); 31 | 32 | // Compute list of choices based on alarm titles 33 | const alarms = await alarmsList.get(dc.context); 34 | const choices = alarms.map((value) => value.title); 35 | 36 | // Prompt user for choice (force use of "list" style) 37 | const prompt = `Which alarm would you like to delete? Say "cancel" to quit.`; 38 | await dc.prompt('choicePrompt', prompt, choices); 39 | }, 40 | async function (dc, choice: FoundChoice) { 41 | // Hide menu 42 | await dc.context.menus.hideMenu(); 43 | 44 | // Delete alarm by position 45 | const alarms = await alarmsList.get(dc.context); 46 | if (choice.index < alarms.length) { alarms.splice(choice.index, 1) } 47 | 48 | // Notify user of delete 49 | await dc.context.sendActivity(`Deleted "${choice.value}" alarm.`); 50 | await dc.end(); 51 | } 52 | ]); 53 | 54 | this.dialogs.add('deleteAlarmSingle', [ 55 | async function (dc) { 56 | // Show cancel menu 57 | await dc.context.menus.showMenu('cancel'); 58 | 59 | // Confirm delete 60 | const alarms = await alarmsList.get(dc.context); 61 | const alarm = alarms[0]; 62 | await dc.prompt('confirmPrompt', `Are you sure you want to delete the "${alarm.title}" alarm?`); 63 | }, 64 | async function (dc, confirm: boolean) { 65 | // Hide menu 66 | await dc.context.menus.hideMenu(); 67 | 68 | // Delete alarm 69 | if (confirm) { 70 | alarmsList.forget(dc.context); 71 | await dc.context.sendActivity(`alarm deleted...`); 72 | } else { 73 | await dc.context.sendActivity(`ok...`); 74 | } 75 | await dc.end(); 76 | } 77 | ]); 78 | 79 | this.dialogs.add('choicePrompt', new ChoicePrompt()); 80 | this.dialogs.add('confirmPrompt', new ConfirmPrompt()); 81 | } 82 | } -------------------------------------------------------------------------------- /samples/list-control-ts/src/app.ts: -------------------------------------------------------------------------------- 1 | import { BotFrameworkAdapter, MemoryStorage, TurnContext, Attachment, MessageFactory, CardFactory } from 'botbuilder'; 2 | import { UserScope, ConversationScope, ManageScopes, ForgetAfter, ScopeAccessor } from 'botbuilder-toybox-memories'; 3 | import { DialogSet } from 'botbuilder-dialogs'; 4 | import { ListControl } from 'botbuilder-toybox-controls'; 5 | import * as restify from 'restify'; 6 | 7 | // Create server 8 | let server = restify.createServer(); 9 | server.listen(process.env.port || process.env.PORT || 3978, function () { 10 | console.log(`${server.name} listening to ${server.url}`); 11 | }); 12 | 13 | // Create adapter 14 | const adapter = new BotFrameworkAdapter({ 15 | appId: process.env.MICROSOFT_APP_ID, 16 | appPassword: process.env.MICROSOFT_APP_PASSWORD 17 | }); 18 | 19 | // Define scopes and add to adapter 20 | const storage = new MemoryStorage(); 21 | const userScope = new UserScope(storage); 22 | const convoScope = new ConversationScope(storage); 23 | adapter.use(new ManageScopes(userScope, convoScope)); 24 | 25 | // Define memory fragments 26 | userScope.fragment('profile', {}); 27 | convoScope.fragment('state', {}).forgetAfter(1 * ForgetAfter.days); 28 | 29 | // Extend TurnContext interface 30 | interface MyContext extends TurnContext { 31 | user: ScopeAccessor; 32 | conversation: ScopeAccessor; 33 | } 34 | 35 | // Listen for incoming requests 36 | server.post('/api/messages', (req, res) => { 37 | // Route received request to adapter for processing 38 | adapter.processActivity(req, res, async (context: MyContext) => { 39 | const state = await context.conversation.get('state'); 40 | const dc = dialogs.createContext(context, state); 41 | 42 | // Check for interruptions 43 | const isMessage = context.activity.type === 'message'; 44 | if (isMessage) { 45 | const utterance = context.activity.text.toLowerCase(); 46 | if (utterance.includes("images")) { 47 | // Show images 48 | await dc.endAll().begin('showImages'); 49 | } 50 | } 51 | 52 | // Continue current dialog 53 | if (!context.responded) { 54 | await dc.continue(); 55 | if (!context.responded && isMessage) { 56 | await context.sendActivity(`To show a list send a reply with "images".`) 57 | } 58 | } 59 | }); 60 | }); 61 | 62 | const dialogs = new DialogSet(); 63 | 64 | dialogs.add('imageList', new ListControl(async (context, filter, continueToken) => { 65 | // Render a page of images to hero cards 66 | const start = filter && typeof filter.start === 'number' ? filter.start : 0; 67 | const page = typeof continueToken === 'number' ? continueToken : 0; 68 | const cards: Attachment[] = []; 69 | for (let i = 0; i < 10; i++) { 70 | const imageNum = i + (page * 10) + 1; 71 | const card = CardFactory.heroCard( 72 | `Image ${imageNum}`, 73 | [`https://picsum.photos/100/100/?image=${start + imageNum}`] 74 | ); 75 | cards.push(card); 76 | } 77 | 78 | // Render cards to user as a carousel 79 | const activity = MessageFactory.carousel(cards); 80 | 81 | // Return page of results 82 | return { result: activity, continueToken: page < 4 ? page + 1 : undefined }; 83 | })); 84 | 85 | dialogs.add('showImages', [ 86 | async function (dc) { 87 | const startImage = Math.floor(Math.random() * 100); 88 | await dc.begin('imageList', { 89 | filter: { start: startImage } 90 | }); 91 | }, 92 | async function (dc, result) { 93 | await dc.end(); 94 | } 95 | ]); -------------------------------------------------------------------------------- /docs/reference/classes/botbuilder_toybox.managescopes.md: -------------------------------------------------------------------------------- 1 | [Bot Builder Toybox](../README.md) > [ManageScopes](../classes/botbuilder_toybox.managescopes.md) 2 | 3 | 4 | 5 | # Class: ManageScopes 6 | 7 | 8 | :package: **botbuilder-toybox-memories** 9 | 10 | Middleware that manages the automatic loading and saving of one or more scopes to storage. 11 | 12 | The middleware quickly learns which scopes a bot accesses for a given activity type and will pre-load in parallel the most likely needed scopes for the activity type received. 13 | 14 | For each turn, the context object is extended to include a `ScopeAccessor` for each of the scopes being managed by the middleware. These accessors are added as properties that are named to match the `namespace` of each scope. 15 | 16 | **Usage Example** 17 | 18 | const { UserScope, ConversationScope, ManageScopes, ForgetAfter } = require('botbuilder-toybox-memories'); 19 | 20 | // Define memory scopes and add to adapter 21 | const storage = new MemoryStorage(); 22 | const userScope = new UserScope(storage); 23 | const convoScope = new ConversationScope(storage); 24 | adapter.use(new ManageScopes(userScope, convoScope)); 25 | 26 | // Define the bots memory fragments 27 | userScope.fragment('profile', {}); 28 | convoScope.fragment('state', {}).forgetAfter(1 * ForgetAfter.days); 29 | 30 | // Listen for incoming requests 31 | server.post('/api/messages', (req, res) => { 32 | adapter.processActivity(req, res, async (context) => { 33 | // Get profile and conversation state 34 | const profile = await context.user.get('profile'); 35 | const state = await context.conversation.get('state'); 36 | 37 | // Process received activity 38 | }); 39 | }); 40 | 41 | If you're using TypeScript you'll need to extend the `TurnContext` interface to avoid compile errors around the new "user" and "conversation" properties added to the context object. 42 | 43 | const { ScopeAccessor } = require('botbuilder-toybox-memories'); 44 | 45 | // Define context extensions 46 | interface MyContext extends TurnContext { 47 | user: ScopeAccessor; 48 | conversation: ScopeAccessor; 49 | } 50 | 51 | // Listen for incoming requests 52 | server.post('/api/messages', (req, res) => { 53 | adapter.processActivity(req, res, async (context: MyContext) => { 54 | // Get profile and conversation state 55 | const profile = await context.user.get('profile'); 56 | const state = await context.conversation.get('state'); 57 | 58 | // Process received activity 59 | }); 60 | }); 61 | 62 | ## Implements 63 | 64 | * `any` 65 | 66 | ## Index 67 | 68 | ### Constructors 69 | 70 | * [constructor](botbuilder_toybox.managescopes.md#constructor) 71 | 72 | 73 | 74 | --- 75 | ## Constructors 76 | 77 | 78 | 79 | ### ⊕ **new ManageScopes**(...scopes: *[MemoryScope](botbuilder_toybox.memoryscope.md)[]*): [ManageScopes](botbuilder_toybox.managescopes.md) 80 | 81 | 82 | *Defined in [packages/botbuilder-toybox-memories/lib/manageScopes.d.ts:71](https://github.com/Stevenic/botbuilder-toybox/blob/dd57c76/packages/botbuilder-toybox-memories/lib/manageScopes.d.ts#L71)* 83 | 84 | 85 | 86 | Creates a new ManageScopes instance. 87 | 88 | 89 | **Parameters:** 90 | 91 | | Param | Type | Description | 92 | | ------ | ------ | ------ | 93 | | scopes | [MemoryScope](botbuilder_toybox.memoryscope.md)[] | One or more scopes to manage. | 94 | 95 | 96 | 97 | 98 | 99 | **Returns:** [ManageScopes](botbuilder_toybox.managescopes.md) 100 | 101 | --- 102 | 103 | 104 | 105 | -------------------------------------------------------------------------------- /packages/botbuilder-toybox-controls/src/ensureTermsMiddleware.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @module botbuilder-toybox 3 | */ 4 | /** Licensed under the MIT License. */ 5 | import { TurnContext, Middleware, ActivityTypes, StatePropertyAccessor } from 'botbuilder-core' 6 | import { DialogSet, DialogState, DialogTurnStatus } from 'botbuilder-dialogs'; 7 | import { TermsControl, TermsControlSettings } from './termsControl' 8 | 9 | /** 10 | * :package: **botbuilder-toybox-controls** 11 | * 12 | * Middleware that prevents a user from using the bot until they've agreed to the bots Terms of 13 | * Use. 14 | * 15 | * ## Remarks 16 | * Activities like 'conversationUpdate' will still be allowed through to the bots logic but no 17 | * 'message' activities will be allowed through until the user agrees to the bots terms. 18 | * 19 | * ```JavaScript 20 | * const { MemoryStorage, ConversationState, UserState } = require('botbuilder'); 21 | * const { EnsureTermsMiddleware } = require('botbuilder-toybox-controls'); 22 | * 23 | * // Define state stores 24 | * const storage = new MemoryStorage(); 25 | * const convoState = new ConversationState(storage); 26 | * const userState = new UserState(storage); 27 | * 28 | * // Define state properties 29 | * const termsDialogState = convoState.createProperty('termsDialogState'); 30 | * const termsVersion = userState.createProperty('termsVersion'); 31 | * 32 | * // Add middleware to bots adapter 33 | * adapter.use(new EnsureTermsMiddleware(termsDialogState, termsVersion, { 34 | * currentVersion: 2, 35 | * termsStatement: `You must agree to our Terms of Use before continuing: http://example.com/tou`, 36 | * upgradedTermsStatement: `Out Terms of Use have changed. Please agree before continuing: http://example.com/tou`, 37 | * retryPrompt: `Please agree to our Terms of Use before continuing: http://example.com/tou` 38 | * })); 39 | * ``` 40 | */ 41 | export class EnsureTermsMiddleware implements Middleware { 42 | private readonly dialogs: DialogSet; 43 | 44 | /** 45 | * Creates a new EnsureTermsMiddleware instance. 46 | * @param dialogState State property used to read & write the dialog prompting the user to agree to the bots terms. 47 | * @param usersVersion State property used to read & write the agreed to version number for the user (if any.) 48 | * @param settings Settings used to configure the created TermsControl instance. 49 | */ 50 | constructor(private dialogState: StatePropertyAccessor, private usersVersion: StatePropertyAccessor, private settings: TermsControlSettings) { 51 | this.dialogs = new DialogSet(dialogState); 52 | this.dialogs.add(new TermsControl('prompt', usersVersion, settings)) 53 | } 54 | 55 | /** @private */ 56 | public async onTurn(context: TurnContext, next: () => Promise): Promise { 57 | if (context.activity.type === ActivityTypes.Message) { 58 | // Check version 59 | const version = await this.usersVersion.get(context); 60 | if (version !== this.settings.currentVersion) { 61 | // Continue existing prompt (if started) 62 | const dc = await this.dialogs.createContext(context); 63 | let result = await dc.continueDialog(); 64 | 65 | // Start prompt if turn 0 66 | if (result.status === DialogTurnStatus.empty) { 67 | result = await dc.beginDialog('prompt'); 68 | } 69 | 70 | // Prevent further routing if still active 71 | if (result.status === DialogTurnStatus.waiting) { 72 | return; 73 | } 74 | } 75 | } 76 | 77 | await next() 78 | } 79 | } 80 | 81 | -------------------------------------------------------------------------------- /packages/botbuilder-toybox-extensions/src/checkVersionMiddleware.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @module botbuilder-toybox 3 | */ 4 | /** Licensed under the MIT License. */ 5 | import { Middleware, TurnContext, StatePropertyAccessor } from 'botbuilder-core'; 6 | 7 | /** 8 | * :package: **botbuilder-toybox-extensions** 9 | * 10 | * Handler that will be called anytime the version number being checked doesn't match the latest 11 | * version. 12 | * @param VersionChangedHandler.context Context object for the current turn of conversation. 13 | * @param VersionChangedHandler.version Current version number. 14 | * @param VersionChangedHandler.next Function that should be called to continue execution to the next piece of middleware. Calling `next()` will first update the version number to match the latest version and then call the next piece of middleware. 15 | */ 16 | export type VersionChangedHandler = (context: TurnContext, version: number, next: () => Promise) => Promise; 17 | 18 | /** 19 | * :package: **botbuilder-toybox-extensions** 20 | * 21 | * Middleware for clearing or migrating conversation state anytime your bots dialogs are changed. 22 | * 23 | * ## Remarks 24 | * Deploying new versions of your bot more often then not should have little to no impact on the 25 | * current conversations you're having with a user. Sometimes, however, a change to your bots 26 | * conversation logic can result in the user getting into a stuck state that can only be fixed by 27 | * their conversation state being deleted. 28 | * 29 | * This middleware lets you track a version number for the conversations your bot is having so that 30 | * you can automatically delete the conversation state anytime a major version number difference is 31 | * detected. Example: 32 | * 33 | * ```JavaScript 34 | * const { ConversationState, MemoryStorage } = require('botbuilder'); 35 | * const { CheckVersionMiddleware } = require('botbuilder-toybox-extensions'); 36 | * 37 | * // Initialize state property to hold our version number. 38 | * const convoState = new ConversationState(new MemoryStorage()); 39 | * const convoVersion = convoState.createProperty('convoVersion'); 40 | * 41 | * // Add middleware to check the version and clear the conversation state on change. 42 | * adapter.use(new CheckVersionMiddleware(convoVersion, 2.0, async (context, version, next) => { 43 | * // Clear conversation state 44 | * await convoState.load(context); 45 | * await convoState.clear(context); 46 | * await convoState.saveChanges(context); 47 | * 48 | * // Notify user 49 | * await context.sendActivity(`I'm sorry. My service has been upgraded and we need to start over.`); 50 | * 51 | * // Continue execution 52 | * await next(); 53 | * })); 54 | * ``` 55 | */ 56 | export class CheckVersionMiddleware implements Middleware { 57 | /** 58 | * Creates a new CheckVersionMiddleware instance. 59 | * @param versionProperty The memory fragment to persist the current version number to. 60 | * @param version Latest version number in major.minor form. 61 | * @param handler Handler that will be invoked anytime an existing conversations version number doesn't match. New conversations will just be initialized to the new version number. 62 | */ 63 | constructor(private versionProperty: StatePropertyAccessor, private version: number, private handler: VersionChangedHandler) { } 64 | 65 | /** @private */ 66 | public async onTurn(context: TurnContext, next: () => Promise): Promise { 67 | // Check for version change 68 | let version = await this.versionProperty.get(context, this.version) as number; 69 | if (version !== this.version) { 70 | await Promise.resolve(this.handler(context, version, async () => { 71 | await this.versionProperty.set(context, this.version); 72 | await next(); 73 | })); 74 | } else { 75 | await next(); 76 | } 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /samples/declarative-ts/src/dialogs/addAlarmDialog.ts: -------------------------------------------------------------------------------- 1 | import { StatePropertyAccessor, MessageFactory } from 'botbuilder'; 2 | import { WaterfallDialog, WaterfallStepContext, PromptValidatorContext, DateTimeResolution } from 'botbuilder-dialogs'; 3 | import { ConfigurableComponentDialog, ConfigurableTextPrompt, ConfigurableDateTimePrompt, TypeFactory, ComponentDialogConfiguration } from 'botbuilder-toybox-declarative'; 4 | import * as moment from 'moment'; 5 | import { Alarm } from '../models'; 6 | 7 | export interface AddAlarmDialogConfiguration extends ComponentDialogConfiguration { 8 | titlePrompt?: string, 9 | timePrompt?: string; 10 | timeRetryPrompt?: string; 11 | } 12 | 13 | export class AddAlarmDialog extends ConfigurableComponentDialog { 14 | private titlePrompt = new ConfigurableTextPrompt('titlePrompt', { 15 | prompt: MessageFactory.text(`Enter alarm title`) 16 | }); 17 | 18 | private timePrompt = new ConfigurableDateTimePrompt('timePrompt', { 19 | prompt: MessageFactory.text(`Enter alarm time`), 20 | retryPrompt: MessageFactory.text(`Please enter a valid time in the future like "tomorrow at 9am".`) 21 | }, this.timePromptValidation.bind(this)); 22 | 23 | constructor(dialogId: string, private alarmsList: StatePropertyAccessor) { 24 | super(dialogId); 25 | 26 | // Add control flow 27 | this.addDialog(new WaterfallDialog('start', [ 28 | this.timePromptStep.bind(this), 29 | this.timePromptStep.bind(this), 30 | this.setAlarmStep.bind(this) 31 | ])); 32 | 33 | // Add prompts 34 | this.addDialog(this.titlePrompt); 35 | this.addDialog(this.timePrompt); 36 | } 37 | 38 | protected onConfigure(config: AddAlarmDialogConfiguration) { 39 | super.onConfigure(config); 40 | if (config.titlePrompt) { 41 | this.titlePrompt.configure({ prompt: MessageFactory.text(config.titlePrompt) }); 42 | } 43 | if (config.timePrompt) { 44 | this.timePrompt.configure({ prompt: MessageFactory.text(config.timePrompt) }); 45 | } 46 | if (config.timeRetryPrompt) { 47 | this.timePrompt.configure({ prompt: MessageFactory.text(config.timeRetryPrompt) }); 48 | } 49 | } 50 | 51 | private async titlePromptStep(step: WaterfallStepContext) { 52 | // Prompt for title 53 | return await step.beginDialog('titlePrompt'); 54 | } 55 | 56 | private async timePromptStep(step: WaterfallStepContext) { 57 | // Save title 58 | step.values['title'] = step.result; 59 | 60 | // Prompt for time 61 | return await step.beginDialog('timePrompt'); 62 | } 63 | 64 | private async setAlarmStep(step: WaterfallStepContext) { 65 | // Save time 66 | step.values['time'] = new Date(step.result[0].value).toISOString(); 67 | 68 | // Set alarm 69 | const alarms = await this.alarmsList.get(step.context, []); 70 | alarms.push(step.values as Alarm); 71 | 72 | // Confirm to user 73 | await step.context.sendActivity(`Your alarm named "${step.values['title']}" is set for "${moment(step.values['time']).format("ddd, MMM Do, h:mm a")}".`); 74 | return await step.endDialog(); 75 | } 76 | 77 | private timePromptValidation(prompt: PromptValidatorContext): boolean { 78 | if (prompt.recognized.succeeded) { 79 | const values = prompt.recognized.value; 80 | if (values[0].type === 'datetime') { 81 | const value = new Date(values[0].value); 82 | if (value.getTime() > new Date().getTime()) { 83 | return true; 84 | } 85 | } 86 | } 87 | return false; 88 | } 89 | } 90 | TypeFactory.register('AddAlarmDialog', (config: ComponentDialogConfiguration) => { 91 | // Get alarms list 92 | const alarmsList = TypeFactory.create({ type: 'AlarmsListProperty' }) as StatePropertyAccessor; 93 | 94 | // Create and configure dialog 95 | const dialog = new AddAlarmDialog(config.id, alarmsList); 96 | return dialog.configure(config); 97 | }); 98 | -------------------------------------------------------------------------------- /packages/botbuilder-toybox-declarative/src/factories/botbuilder.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @module botbuilder-toybox 3 | */ 4 | /** Licensed under the MIT License. */ 5 | import * as botbuilder from 'botbuilder'; 6 | import { TypeFactory, TypeConfiguration } from '../typeFactory'; 7 | 8 | //========================================================= 9 | // BotFrameworkAdapter Type Factory 10 | //========================================================= 11 | 12 | export interface BotFrameworkAdapterConfiguration extends TypeConfiguration { 13 | settings: Partial; 14 | middleware?: TypeConfiguration[]; 15 | } 16 | 17 | TypeFactory.register('botbuilder.BotFrameworkAdapter', (config: BotFrameworkAdapterConfiguration, factory: TypeFactory) => { 18 | const adapter = new botbuilder.BotFrameworkAdapter(config.settings); 19 | if (config.middleware) { 20 | config.middleware.forEach((mwConfig) => { 21 | const middleware = factory.create(mwConfig); 22 | adapter.use(middleware); 23 | }) 24 | } 25 | return adapter; 26 | }); 27 | 28 | //========================================================= 29 | // MemoryStorage Type Factory 30 | //========================================================= 31 | 32 | export interface MemoryStorageConfiguration extends TypeConfiguration { 33 | } 34 | 35 | TypeFactory.register('botbuilder.MemoryStorage', (config: MemoryStorageConfiguration) => { 36 | const service = new botbuilder.MemoryStorage(); 37 | return service; 38 | }); 39 | 40 | //========================================================= 41 | // BotState Type Factories 42 | //========================================================= 43 | 44 | export interface BotStateConfiguration extends TypeConfiguration { 45 | storageId: string; 46 | namespace?: string; 47 | properties?: BotStatePropertyConfiguration[]; 48 | } 49 | 50 | export interface BotStatePropertyConfiguration { 51 | name: string; 52 | id?: string; 53 | } 54 | 55 | function registerBotStateProperties(factory: TypeFactory, service: botbuilder.BotState, properties?: BotStatePropertyConfiguration[]) { 56 | if (properties) { 57 | properties.forEach((config) => { 58 | const prop = service.createProperty(config.name); 59 | if (config.id) { 60 | factory.set(config.id, prop); 61 | } 62 | }); 63 | } 64 | } 65 | 66 | TypeFactory.register('botbuilder.ConversationState', (config: BotStateConfiguration, factory: TypeFactory) => { 67 | const storage = factory.get(config.storageId); 68 | const service = new botbuilder.ConversationState(storage, config.namespace); 69 | registerBotStateProperties(factory, service, config.properties); 70 | return service; 71 | }); 72 | 73 | 74 | TypeFactory.register('botbuilder.PrivateConversationState', (config: BotStateConfiguration, factory: TypeFactory) => { 75 | const storage = factory.get(config.storageId); 76 | const service = new botbuilder.PrivateConversationState(storage, config.namespace); 77 | registerBotStateProperties(factory, service, config.properties); 78 | return service; 79 | }); 80 | 81 | TypeFactory.register('botbuilder.UserState', (config: BotStateConfiguration, factory: TypeFactory) => { 82 | const storage = factory.get(config.storageId); 83 | const service = new botbuilder.UserState(storage, config.namespace); 84 | registerBotStateProperties(factory, service, config.properties); 85 | return service; 86 | }); 87 | 88 | //========================================================= 89 | // AutoSaveStateMiddleware Type Factory 90 | //========================================================= 91 | 92 | export interface AutoSaveStateMiddlewareConfiguration extends TypeConfiguration { 93 | stateIds: string[]; 94 | } 95 | 96 | TypeFactory.register('botbuilder.AutoSaveStateMiddleware', (config: AutoSaveStateMiddlewareConfiguration, factory: TypeFactory) => { 97 | const middleware = new botbuilder.AutoSaveStateMiddleware(); 98 | config.stateIds.forEach((id) => { 99 | const state = factory.get(id); 100 | middleware.add(state); 101 | }) 102 | return middleware; 103 | }); 104 | 105 | -------------------------------------------------------------------------------- /packages/botbuilder-toybox-controls/src/intentDialog.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @module botbuilder-toybox 3 | */ 4 | /** Licensed under the MIT License. */ 5 | import { TurnContext, RecognizerResult, ActivityTypes } from 'botbuilder-core'; 6 | import { ComponentDialog, Dialog, DialogContext, DialogTurnResult } from 'botbuilder-dialogs'; 7 | 8 | export type Recognizer = { recognize(context: TurnContext): Promise; }; 9 | 10 | export enum InterruptionMode { 11 | none = 'none', 12 | replace = 'replace', 13 | append = 'append' 14 | } 15 | 16 | export class IntentDialog extends ComponentDialog { 17 | public readonly intents: { [name: string]: IntentMapping; } = {}; 18 | public readonly recognizer: Recognizer; 19 | public noneIntent: string; 20 | public minScore: number = 0.0; 21 | 22 | constructor(dialogId: string, recognizer: Recognizer, noneIntent = 'None') { 23 | super(dialogId); 24 | this.recognizer = recognizer; 25 | this.noneIntent = noneIntent; 26 | } 27 | 28 | public addIntent(name: string, dialog: Dialog, interruption: InterruptionMode = InterruptionMode.none): this { 29 | if (this.intents.hasOwnProperty(name)) { throw new Error(`IntentDialog.addIntent(): an intent named '${name}' already registered.`) } 30 | this.addDialog(dialog); 31 | this.intents[name] = { name: name, dialogId: dialog.id, interruption: interruption }; 32 | return this; 33 | } 34 | 35 | protected onBeginDialog(dc: DialogContext, options?: any): Promise { 36 | return this.onRunTurn(dc, options); 37 | } 38 | 39 | protected onContinueDialog(dc: DialogContext): Promise { 40 | return this.onRunTurn(dc); 41 | } 42 | 43 | protected async onRunTurn(dc: DialogContext, options?: any): Promise { 44 | // Check for interruptions 45 | const isMessage = dc.context.activity.type == ActivityTypes.Message; 46 | const isRunning = dc.stack.length > 0; 47 | if (isMessage && !isRunning || this.interruptionEnabled()) { 48 | const recognized = await this.recognizer.recognize(dc.context); 49 | const intentName = this.findIntent(recognized); 50 | const intentMapping = this.intents[intentName]; 51 | if (intentMapping) { 52 | if (!isRunning || (intentName !== this.noneIntent && intentMapping.interruption !== InterruptionMode.none)) { 53 | return await this.onBeginInterruption(dc, intentMapping.dialogId, recognized, intentMapping.interruption); 54 | } 55 | } 56 | } 57 | 58 | // Perform default routing logic 59 | return await dc.continueDialog(); 60 | } 61 | 62 | protected async onBeginInterruption(dc: DialogContext, dialogId: string, recognized: RecognizerResult, interruption: InterruptionMode): Promise { 63 | if (interruption === InterruptionMode.replace) { 64 | await dc.cancelAllDialogs(); 65 | } 66 | return await dc.beginDialog(dialogId, recognized); 67 | } 68 | 69 | private interruptionEnabled() { 70 | for (const name in this.intents) { 71 | if (this.intents[name].interruption) { 72 | return true; 73 | } 74 | } 75 | return false; 76 | } 77 | 78 | private findIntent(recognized?: RecognizerResult): string { 79 | if (recognized && recognized.intents) { 80 | // Find top scoring intent 81 | let topName = this.noneIntent; 82 | let topScore = 0; 83 | for (const name in recognized.intents) { 84 | const score = recognized.intents[name].score; 85 | if (score > this.minScore && score > topScore) { 86 | topName = name; 87 | topScore = score; 88 | } 89 | } 90 | 91 | // Filter to intents with dialog mappings 92 | if (this.intents.hasOwnProperty(topName)) { 93 | return topName; 94 | } 95 | } 96 | return this.noneIntent; 97 | } 98 | } 99 | 100 | interface IntentMapping { 101 | name: string; 102 | dialogId: string; 103 | interruption: InterruptionMode; 104 | } 105 | -------------------------------------------------------------------------------- /packages/botbuilder-toybox-extensions/src/activityFactory.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @module botbuilder-toybox 3 | */ 4 | /** 5 | * Copyright (c) Microsoft Corporation. All rights reserved. 6 | * Licensed under the MIT License. 7 | */ 8 | import { Activity, ActivityTypes, EndOfConversationCodes } from 'botbuilder-core'; 9 | 10 | /** 11 | * :package: **botbuilder-toybox-extensions** 12 | * 13 | * A set of static helper methods to assist with formatting various activity types the bot can send 14 | * the user. 15 | */ 16 | export class ActivityFactory { 17 | /** 18 | * Returns a `delay` activity which can be used to pause after sending a typing indicator or 19 | * after sending a card with image(s). 20 | * 21 | * ## Remarks 22 | * Most chat clients download any images sent by the bot to a CDN which can delay the showing 23 | * of the message to the user. If a bot sends a message with only text immediately after 24 | * sending a message with images, the messages could end up being shown to the user out of 25 | * order. To help prevent this you can insert a delay of 2 seconds or so in between replies. 26 | * 27 | * ```JavaScript 28 | * const activity = ActivityFilter.delay(1000); 29 | * ``` 30 | * @param ms Number of milliseconds to pause before delivering the next activity in the batch. 31 | */ 32 | static delay(ms: number): Partial { 33 | return { type: 'delay', value: ms }; 34 | } 35 | 36 | /** 37 | * Returns an `endOfConversation` activity indicating that the bot has completed it's current task 38 | * or skill. 39 | * 40 | * ## Remarks 41 | * For channels like Cortana this is used to tell Cortana that the skill has completed and the skills 42 | * window should close. 43 | * 44 | * ```JavaScript 45 | * const activity = ActivityFilter.endOfConversation(); 46 | * ``` 47 | * @param code (Optional) code to indicate why the bot/skill is ending. Defaults to 48 | * `EndOfConversationCodes.CompletedSuccessfully`. 49 | */ 50 | static endOfConversation(code?: EndOfConversationCodes|string): Partial { 51 | if (code === undefined) { code = EndOfConversationCodes.CompletedSuccessfully } 52 | return { type: ActivityTypes.EndOfConversation, code: code }; 53 | } 54 | 55 | /** 56 | * Returns an `event` activity. 57 | * 58 | * ## Remarks 59 | * This is most useful for DirectLine and WebChat channels as a way of sending a custom named 60 | * event to the client from the bot. 61 | * 62 | * ```JavaScript 63 | * const activity = ActivityFilter.event('refreshUi', updatedState); 64 | * ``` 65 | * @param name Name of the event being sent. 66 | * @param value (Optional) value to include with the event. 67 | */ 68 | static event(name: string, value?: any): Partial { 69 | return { type: ActivityTypes.Event, name: name, value: value }; 70 | } 71 | 72 | /** 73 | * Returns an `invokeResponse` activity containing a status code and optional body to send in 74 | * response to an `invoke` activity that was received. 75 | * 76 | * ## Remarks 77 | * 78 | * ```JavaScript 79 | * const activity = ActivityFilter.invokeResponse(200); 80 | * ``` 81 | * @param status Status code to return for response. 82 | * @param body (Optional) body to return for response. 83 | */ 84 | static invokeResponse(status: number, body?: any): Partial { 85 | return { type: 'invokeResponse', value: { status: status, body: body } }; 86 | } 87 | 88 | /** 89 | * Returns a `typing` activity which causes some channels to show a visual indicator that the 90 | * bot is typing a reply. 91 | * 92 | * ## Remarks 93 | * This indicator typically will be presented to the user for either a few seconds or until 94 | * another message is received. That means that for longer running operations it may be necessary 95 | * to send additional typing indicators every few seconds. 96 | * 97 | * ```JavaScript 98 | * const activity = ActivityFilter.typing(); 99 | * ``` 100 | */ 101 | static typing(): Partial { 102 | return { type: ActivityTypes.Typing }; 103 | } 104 | } 105 | -------------------------------------------------------------------------------- /packages/botbuilder-toybox-controls/src/selectors/simpleActionSelector.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @module botbuilder-toybox 3 | */ 4 | /** Licensed under the MIT License. */ 5 | import { ActionContext } from '../actionContext'; 6 | import { ActionHandler } from '../actions/actionHandler'; 7 | 8 | export interface ActionTrigger { 9 | condition?: string; 10 | action: ActionHandler; 11 | } 12 | 13 | export interface ActionSelector { 14 | nextAction(action: ActionContext, actions: ActionTrigger[], selectorState: object): Promise; 15 | } 16 | 17 | export class SimpleActionSelector implements ActionSelector { 18 | public async nextAction(action: ActionContext, actions: ActionTrigger[], selectorState: object): Promise { 19 | // Initialize tracking state 20 | const state = selectorState as SelectorState; 21 | if (state.turnRun === undefined || state.turnRun.length !== actions.length) { 22 | state.turnRun = []; 23 | actions.forEach(a => state.turnRun.push(-1)); 24 | } 25 | 26 | // Find most specific action to run 27 | let topScore = -1; 28 | let topIndex = -1; 29 | for (let i = 0; i < actions.length; i++) { 30 | const handler = actions[i].action; 31 | let score = 0; 32 | 33 | // Eliminate run once actions 34 | const hasRun = state.turnRun[i] >= 0; 35 | if (handler.onlyOnce && hasRun) { 36 | continue; 37 | } 38 | 39 | // Eliminate actions with all filled expected slots 40 | if (handler.expectedSlots && handler.expectedSlots.length > 0) { 41 | const filled = this.countFilledSlots(action, handler.expectedSlots); 42 | if (filled == handler.expectedSlots.length) { 43 | continue; 44 | } 45 | score += handler.expectedSlots.length - filled; 46 | } 47 | 48 | // Eliminate actions with missing required slots 49 | if (handler.requiredSlots && handler.requiredSlots.length > 0) { 50 | const filled = this.countFilledSlots(action, handler.requiredSlots); 51 | if (filled != handler.requiredSlots.length) { 52 | continue; 53 | } 54 | score += filled; 55 | } 56 | 57 | // Eliminate actions with missing changed slots or that have run since last change 58 | if (handler.changedSlots && handler.changedSlots.length > 0) { 59 | const filled = this.countFilledSlots(action, handler.changedSlots); 60 | if (filled != handler.changedSlots.length) { 61 | continue; 62 | } 63 | if (hasRun) { 64 | const changed = this.countChangedSlots(action, handler.changedSlots, state.turnRun[i]); 65 | if (changed == 0) { 66 | continue; 67 | } 68 | score += changed; 69 | } else { 70 | score += filled; 71 | } 72 | } 73 | 74 | // Update top action 75 | if (score > topScore) { 76 | topScore = score; 77 | topIndex = i; 78 | } 79 | } 80 | 81 | // Return top action 82 | if (topIndex >= 0) { 83 | state.turnRun[topIndex] = action.turnCount; 84 | } 85 | return topIndex; 86 | } 87 | 88 | private countFilledSlots(action: ActionContext, slots: string[]): number { 89 | let count = 0; 90 | slots.forEach((name) => { 91 | if (action.slots.hasOwnProperty(name) && action.slots[name].value !== undefined) { 92 | count++; 93 | } 94 | }); 95 | return count; 96 | } 97 | 98 | private countChangedSlots(action: ActionContext, slots: string[], sinceTurn: number): number { 99 | let count = 0; 100 | slots.forEach((name) => { 101 | if (action.slots.hasOwnProperty(name) && action.slots[name].value !== undefined && action.slots[name].turnChanged > sinceTurn) { 102 | count++; 103 | } 104 | }); 105 | return count; 106 | } 107 | } 108 | 109 | interface SelectorState { 110 | turnRun: number[]; 111 | } -------------------------------------------------------------------------------- /packages/botbuilder-toybox-remoting/src/remoteDialog.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @module botbuilder-toybox 3 | */ 4 | /** Licensed under the MIT License. */ 5 | import { TurnContext, Activity, ActivityTypes } from 'botbuilder-core'; 6 | import { DialogContext, Dialog, DialogTurnResult } from 'botbuilder-dialogs'; 7 | import fetch from 'node-fetch'; 8 | 9 | /** 10 | * :package: **botbuilder-toybox-controls** 11 | * 12 | * Settings used to configure a `RemoteDialog` instance. 13 | */ 14 | export interface RemoteDialogSettings { 15 | /** 16 | * (Optional) additional headers to assign for the outgoing request sent to the remote server. 17 | * 18 | * ## Remarks 19 | * This can be used to pass things like service access tokens if needed. 20 | */ 21 | outgoingHeaders?: object; 22 | } 23 | 24 | /** 25 | * 26 | */ 27 | export type UrlFactory = (context: TurnContext) => string; 28 | 29 | /** 30 | * :package: **botbuilder-toybox-controls** 31 | */ 32 | export class RemoteDialog extends Dialog { 33 | protected readonly settings: RemoteDialogSettings; 34 | 35 | /** 36 | * Creates a new RemoteDialog instance. 37 | */ 38 | constructor(dialogId: string, protected remoteUrl: string|UrlFactory, settings?: RemoteDialogSettings) { 39 | super(dialogId); 40 | this.settings = Object.assign({}, settings); 41 | } 42 | 43 | /** @private */ 44 | public async beginDialog(dc: DialogContext, options?: any): Promise { 45 | // Calculate and remember remote url 46 | const remoteUrl = typeof this.remoteUrl === 'function' ? this.remoteUrl(dc.context) : this.remoteUrl; 47 | dc.activeDialog.state = { remoteUrl: remoteUrl }; 48 | 49 | // Send dialogBegin event to remote 50 | const ref = TurnContext.getConversationReference(dc.context.activity); 51 | const event = { type: ActivityTypes.Event, name: 'dialogBegin', value: options }; 52 | TurnContext.applyConversationReference(event, ref, true); 53 | return await this.forwardActivity(dc, event, remoteUrl); 54 | } 55 | 56 | /** @private */ 57 | public async continueDialog(dc: DialogContext): Promise { 58 | // Forward received activity to remote 59 | const state = dc.activeDialog.state as RemoteDialogState; 60 | return await this.forwardActivity(dc, dc.context.activity, state.remoteUrl); 61 | } 62 | 63 | protected async onForwardActivity(context: TurnContext, activity: Partial, remoteUrl: string): Promise[]> { 64 | // Prepare outgoing request 65 | const body = JSON.stringify(activity); 66 | const headers = Object.assign({}, this.settings.outgoingHeaders) as any; 67 | if (!headers.hasOwnProperty('Content-Type')) { headers['Content-Type'] = 'application/json' } 68 | 69 | // Forward activity to remote dialogs server 70 | const res = await fetch(remoteUrl, { 71 | method: 'POST', 72 | body: body, 73 | headers: headers 74 | }); 75 | if (!res.ok) { throw new Error(`RemoteDialog.onForwardActivity(): outgoing request failed with a status of "${res.status} ${res.statusText}".`) } 76 | 77 | // Return parsed response body 78 | return await res.json(); 79 | } 80 | 81 | private async forwardActivity(dc: DialogContext, activity: Partial, remoteUrl: string): Promise { 82 | // Copy activity and remove 'serviceUrl' 83 | const cpy = Object.assign({}, activity); 84 | if (cpy.serviceUrl) { delete cpy.serviceUrl } 85 | 86 | // Forward to remote 87 | const responses = await this.onForwardActivity(dc.context, cpy, remoteUrl); 88 | 89 | // Check for 'endOfConversation' 90 | let eoc: Partial; 91 | const filtered: Partial[] = []; 92 | responses.forEach((a) => { 93 | if (a.type === ActivityTypes.EndOfConversation) { 94 | eoc = a; 95 | } else { 96 | filtered.push(a); 97 | } 98 | }); 99 | 100 | // Deliver any response activities 101 | if (filtered.length > 0) { 102 | await dc.context.sendActivities(filtered); 103 | } 104 | 105 | // End dialog if remote ended 106 | if (eoc) { 107 | const result = eoc.value; 108 | return await dc.endDialog(result); 109 | } else { 110 | return Dialog.EndOfTurn; 111 | } 112 | } 113 | } 114 | 115 | 116 | /** 117 | * @private 118 | */ 119 | interface RemoteDialogState { 120 | remoteUrl: string; 121 | } --------------------------------------------------------------------------------