diff --git a/samples/javascript_typescript/30.activity-handler/PREREQUISITES.md b/samples/javascript_typescript/30.activity-handler/PREREQUISITES.md new file mode 100644 index 0000000000..f5d17ea06d --- /dev/null +++ b/samples/javascript_typescript/30.activity-handler/PREREQUISITES.md @@ -0,0 +1,24 @@ +# Azure Deployment Prerequisites +This bot has prerequisite requirements in order to deploy the bot to Azure. + +This document will enumerate the required prerequisites and show how to install them. + +## Overview +There are a small set of CLI tools that will automate the process of deploying this bot to Azure. These CLI tools are only required for deployment. If you only plan to run the bot locally, these prerequisites are not required. + +## Prerequisites +- If you don't have an Azure subscription, create a [free account][5]. +- Install the latest version of the [Azure CLI][6] tool. Version 2.0.54 or higher. +- Install latest version of the `MSBot` CLI tool. Version 4.3.2 or higher. + ```bash + # install msbot CLI tool + npm install -g msbot + ``` + +[Return to README.md][3] + + +[3]: ./README.md +[4]: https://nodejs.org +[5]: https://azure.microsoft.com/free/ +[6]: https://docs.microsoft.com/cli/azure/install-azure-cli?view=azure-cli-latest diff --git a/samples/javascript_typescript/30.activity-handler/README.md b/samples/javascript_typescript/30.activity-handler/README.md new file mode 100644 index 0000000000..cd13bdb049 --- /dev/null +++ b/samples/javascript_typescript/30.activity-handler/README.md @@ -0,0 +1,114 @@ +# ActivityHandler Bot +Bot Builder v4 ActivityHandler sample + +This bot has been created using [Microsoft Bot Framework][1], it shows how to create a simple bot that demonstrates the use of the ActivityHandler class for +creating an event-driven bot. + +## Prerequisites +- [Node.js][4] version 8.5 or higher + ```bash + # determine node version + node --version + ``` + +# To try this sample +- Clone the repository + ```bash + git clone https://github.com/microsoft/botbuilder-samples.git + ``` +- In a console, navigate to `samples/javascript_typescript/02.a.echobot` + ```bash + cd samples/javascript_typescript/02.a.echobot + ``` +- Install modules + ```bash + npm install + ``` +- Start the bot + ```bash + npm start + ``` + +# Testing the bot using Bot Framework Emulator **v4** +[Microsoft Bot Framework Emulator][5] is a desktop application that allows bot developers to test and debug their bots on localhost or running remotely through a tunnel. + +- Install the Bot Framework Emulator version 4.1.0 or greater from [here][6] + +## Connect to the bot using Bot Framework Emulator **v4** +- Launch Bot Framework Emulator +- File -> Open Bot Configuration +- Navigate to `echobot` folder +- Select `samplebot.bot` file + +# Deploy the bot to Azure +## Prerequisites +- [Azure Deployment Prerequisites][41] + +## Provision a Bot with Azure Bot Service +After creating the bot and testing it locally, you can deploy it to Azure to make it accessible from anywhere. To deploy your bot to Azure: + +```bash +# login to Azure +az login +``` + +```bash +# set you Azure subscription +az account set --subscription "" +``` + +```bash +# provision Azure Bot Services resources to host your bot +msbot clone services --name "" --code-dir "." --location westus --sdkLanguage "Node" --folder deploymentScripts/msbotClone --verbose + +``` + +### Publishing Changes to Azure Bot Service +As you make changes to your bot running locally, and want to deploy those change to Azure Bot Service, you can _publish_ those change using either `publish.cmd` if you are on Windows or `./publish` if you are on a non-Windows platform. The following is an example of publishing + +```bash +# build the TypeScript bot before you publish +npm run build +``` + +```bash +# run the publish helper (non-Windows) to update Azure Bot Service. Use publish.cmd if running on Windows +./publish +``` + +### Getting Additional Help with Deploying to Azure +To learn more about deploying a bot to Azure, see [Deploy your bot to Azure][40] for a complete list of deployment instructions. + + +# Further reading +- [Bot Framework Documentation][20] +- [Bot Basics][32] +- [Azure Bot Service Introduction][21] +- [Azure Bot Service Documentation][22] +- [Azure CLI][7] +- [msbot CLI][9] +- [Azure Portal][10] +- [Language Understanding using LUIS][11] +- [TypeScript][2] +- [Restify][30] +- [dotenv][31] + +[1]: https://dev.botframework.com +[2]: https://www.typescriptlang.org +[3]: https://www.typescriptlang.org/#download-links +[4]: https://nodejs.org +[5]: https://github.com/microsoft/botframework-emulator +[6]: https://github.com/Microsoft/BotFramework-Emulator/releases +[7]: https://docs.microsoft.com/en-us/cli/azure/?view=azure-cli-latest +[8]: https://docs.microsoft.com/en-us/cli/azure/install-azure-cli?view=azure-cli-latest +[9]: https://github.com/Microsoft/botbuilder-tools/tree/master/packages/MSBot +[10]: https://portal.azure.com +[11]: https://www.luis.ai +[20]: https://docs.botframework.com +[21]: https://docs.microsoft.com/en-us/azure/bot-service/bot-service-overview-introduction?view=azure-bot-service-4.0 +[22]: https://docs.microsoft.com/en-us/azure/bot-service/?view=azure-bot-service-4.0 +[30]: https://www.npmjs.com/package/restify +[31]: https://www.npmjs.com/package/dotenv +[32]: https://docs.microsoft.com/en-us/azure/bot-service/bot-builder-basics?view=azure-bot-service-4.0 +[40]: https://aka.ms/azuredeployment +[41]: ./PREREQUISITES.md diff --git a/samples/javascript_typescript/30.activity-handler/deploymentScripts/msbotClone/bot.recipe b/samples/javascript_typescript/30.activity-handler/deploymentScripts/msbotClone/bot.recipe new file mode 100644 index 0000000000..46967dd84d --- /dev/null +++ b/samples/javascript_typescript/30.activity-handler/deploymentScripts/msbotClone/bot.recipe @@ -0,0 +1,16 @@ +{ + "version": "1.0", + "resources": [ + { + "type": "endpoint", + "id": "1", + "name": "development", + "url": "http://localhost:3978/api/messages" + }, + { + "type": "abs", + "id": "3", + "name": "echobot-abs" + } + ] +} diff --git a/samples/javascript_typescript/30.activity-handler/deploymentScripts/webConfigPrep.js b/samples/javascript_typescript/30.activity-handler/deploymentScripts/webConfigPrep.js new file mode 100644 index 0000000000..3e47be5f04 --- /dev/null +++ b/samples/javascript_typescript/30.activity-handler/deploymentScripts/webConfigPrep.js @@ -0,0 +1,21 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +// DO NOT MODIFY THIS CODE +// This script is run as part of the Post Deploy step when +// deploying the bot to Azure. It ensures the Azure Web App +// is configured correctly to host a TypeScript authored bot. +const fs = require('fs'); +const path = require('path'); +const replace = require('replace'); +const WEB_CONFIG_FILE = './web.config'; + +if (fs.existsSync(path.resolve(WEB_CONFIG_FILE))) { + replace({ + regex: "url=\"index.js\"", + replacement: "url=\"lib/index.js\"", + paths: ['./web.config'], + recursive: false, + silent: true, + }) +} diff --git a/samples/javascript_typescript/30.activity-handler/package.json b/samples/javascript_typescript/30.activity-handler/package.json new file mode 100644 index 0000000000..fc44f16d42 --- /dev/null +++ b/samples/javascript_typescript/30.activity-handler/package.json @@ -0,0 +1,35 @@ +{ + "name": "echobot", + "version": "1.0.0", + "description": "Bot Builder v4 echo bot sample", + "author": "Microsoft", + "license": "MIT", + "main": "./lib/index.js", + "scripts": { + "build": "node_modules/.bin/tsc --build", + "lint": "node_modules/.bin/tslint -c tslint.json 'src/**/*.ts'", + "postinstall": "npm run build && node ./deploymentScripts/webConfigPrep.js", + "start": "node_modules/.bin/tsc --build && node ./lib/index.js", + "test": "echo \"Error: no test specified\" && exit 1", + "watch": "node_modules/.bin/nodemon --watch ./src -e ts --exec \"npm run start\"" + }, + "repository": { + "type": "git", + "url": "https://github.com" + }, + "dependencies": { + "botbuilder": "^4.1.5", + "botbuilder-dialogs": "^4.2.1", + "botframework-config": "^4.1.5", + "dotenv": "^6.1.0", + "replace": "^1.0.0", + "restify": "^7.2.3" + }, + "devDependencies": { + "@types/dotenv": "6.1.0", + "@types/restify": "7.2.6", + "nodemon": "^1.18.6", + "tslint": "^5.11.0", + "typescript": "^3.1.6" + } +} diff --git a/samples/javascript_typescript/30.activity-handler/resources/echo.chat b/samples/javascript_typescript/30.activity-handler/resources/echo.chat new file mode 100644 index 0000000000..dd5a6e6293 --- /dev/null +++ b/samples/javascript_typescript/30.activity-handler/resources/echo.chat @@ -0,0 +1,7 @@ +user=Vishwac +bot=EchoBot + +user: Hi +bot: 1: You said "Hi" +user: Hello +bot: 2: You said "Hello" diff --git a/samples/javascript_typescript/30.activity-handler/sample.bot b/samples/javascript_typescript/30.activity-handler/sample.bot new file mode 100644 index 0000000000..8684cf21ca --- /dev/null +++ b/samples/javascript_typescript/30.activity-handler/sample.bot @@ -0,0 +1,15 @@ +{ + "name": "echobot", + "services": [ + { + "type": "endpoint", + "name": "development", + "endpoint": "http://localhost:3978/api/messages", + "appId": "", + "appPassword": "", + "id": "1" + } + ], + "padlock": "", + "version": "2.0" +} diff --git a/samples/javascript_typescript/30.activity-handler/src/ActivityHandler.ts b/samples/javascript_typescript/30.activity-handler/src/ActivityHandler.ts new file mode 100644 index 0000000000..f909286a08 --- /dev/null +++ b/samples/javascript_typescript/30.activity-handler/src/ActivityHandler.ts @@ -0,0 +1,481 @@ +/** + * @module botbuilder + */ +/** + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. + */ +import { ActivityTypes, TurnContext, Activity } from 'botbuilder'; + +export type BotHandler = (context: TurnContext, next: () => Promise) => Promise; + +/** + * Event-emitting base class bots. + * + * @remarks + * This provides an extensible base class for handling incoming + * activities in an event-driven way. Developers may bind one or + * more handlers for each type of event. + * + * To bind a handler to an event, use the `on()` method, for example: + * + * ```Javascript + * bot.onMessage(async (context, next) => { + * // do something + * // then `await next()` to continue processing + * await next(); + * }); + * ``` + * + * A series of events will be emitted while the activity is being processed. + * Handlers can stop the propagation of the event by omitting a call to `next()`. + * + * * Turn - emitted for every activity + * * Type-specific - an event, based on activity.type + * * Sub-type - any specialized events, based on activity content + * * Dialog - the final event, used for processing Dialog actions + * + * A simple implementation: + * ```Javascript + * const bot = new ActivityHandler(); + * + * server.post('/api/messages', (req, res) => { + * adapter.processActivity(req, res, async (context) => { + * // Route to main dialog. + * await bot.run(context); + * }); + * }); + * + * bot.onMessage(async (context, next) => { + * // do stuff + * await context.sendActivity(`Echo: ${ context.activity.text }`); + * // proceed with further processing + * await next(); + * }); + * ``` + */ +export class ActivityHandler { + private readonly handlers: {[type: string]: BotHandler[]} = {}; + + /** + * Bind a handler to the Turn event that is fired for every incoming activity, regardless of type + * @remarks + * @param handler BotHandler A handler function in the form async(context, next) => { ... } + */ + public onTurn(handler: BotHandler): this { + return this.on('Turn', handler); + } + + /** + * Receives all incoming Message activities + * @remarks + * Message activities represent content intended to be shown within a conversational interface. + * Message activities may contain text, speech, interactive cards, and binary or unknown attachments. + * Note that while most messages do contain text, this field is not always present! + * @param handler BotHandler A handler function in the form async(context, next) => { ... } + */ + public onMessage(handler: BotHandler): this { + return this.on('Message', handler); + } + + /** + * Receives all ContactRelationUpdate activities + * @remarks + * Contact relation update activities signal a change in the relationship between the recipient and a user within the channel. + * @param handler BotHandler A handler function in the form async(context, next) => { ... } + */ + public onContactRelationUpdate(handler: BotHandler): this { + return this.on('ContactRelationUpdate', handler); + } + + /** + * Receives all ConversationUpdate activities, regardless of whether members were added or removed + * @remarks + * Conversation update activities describe a change in a conversation's members, description, existence, or otherwise. + * @param handler BotHandler A handler function in the form async(context, next) => { ... } + */ + public onConversationUpdate(handler: BotHandler): this { + return this.on('ConversationUpdate', handler); + } + + /** + * Receives only ConversationUpdate activities representing members being added. + * @remarks + * context.activity.membersAdded will include at least one entry. + * @param handler BotHandler A handler function in the form async(context, next) => { ... } + */ + public onMembersAdded(handler: BotHandler): this { + return this.on('MembersAdded', handler); + } + + /** + * Receives only ConversationUpdate activities representing members being removed. + * @remarks + * context.activity.membersRemoved will include at least one entry. + * @param handler BotHandler A handler function in the form async(context, next) => { ... } + */ + public onMembersRemoved(handler: BotHandler): this { + return this.on('MembersRemoved', handler); + } + + /** + * Receives all EndOfConversation activities. + * @remarks + * End of conversation activities signal the end of a conversation from the recipient's perspective. + * This may be because the conversation has been completely ended, or because the recipient has been + * removed from the conversation in a way that is indistinguishable from it ending. + * @param handler BotHandler A handler function in the form async(context, next) => { ... } + */ + public onEndOfConversation(handler: BotHandler): this { + return this.on('EndOfConversation', handler); + } + + /** + * Receives all Event activities. + * @remarks + * Event activities communicate programmatic information from a client or channel to a bot. + * The meaning of an event activity is defined by the `name` field. + * @param handler BotHandler A handler function in the form async(context, next) => { ... } + */ + public onEvent(handler: BotHandler): this { + return this.on('Event', handler); + } + + /** + * Receives event activities of type 'tokens/response' + * @remarks + * These events occur during the oauth flow + * @param handler BotHandler A handler function in the form async(context, next) => { ... } + */ + public onTokenResponseEvent(handler: BotHandler): this { + return this.on('TokenResponseEvent', handler); + } + + /** + * Receives all Invoke activities. + * @remarks + * Invoke activities are the synchronous counterpart to event activities. + * @param handler BotHandler A handler function in the form async(context, next) => { ... } + */ + public onInvoke(handler: BotHandler): this { + return this.on('Invoke', handler); + } + + /** + * Receives all MS Teams-specific oauth Invoke activities. + * @remarks + * Invoke activities are the synchronous counterpart to event activities. + * @param handler BotHandler A handler function in the form async(context, next) => { ... } + */ + public onTeamsVerificationInvoke(handler: BotHandler): this { + return this.on('TeamsVerificationInvoke', handler); + } + + + /** + * Receives all InstallationUpdate activities. + * @remarks + * Installation update activities represent an installation or uninstallation of a bot + * within an organizational unit (such as a customer tenant or "team") of a channel. + * @param handler BotHandler A handler function in the form async(context, next) => { ... } + */ + public onInstallationUpdate(handler: BotHandler): this { + return this.on('InstallationUpdate', handler); + } + + /** + * Receives all MessageDelete activities. + * @remarks + * Message delete activities represent a deletion of an existing message activity within a conversation. + * The deleted activity is referred to by the id and conversation fields within the activity. + * @param handler BotHandler A handler function in the form async(context, next) => { ... } + */ + public onMessageDelete(handler: BotHandler): this { + return this.on('MessageDelete', handler); + } + + /** + * Receives all MessageUpdate activities. + * @remarks + * Message update activities represent an update of an existing message activity within a conversation. + * The updated activity is referred to by the id and conversation fields within the activity, and the + * message update activity contains all fields in the revised message activity. + * @param handler BotHandler A handler function in the form async(context, next) => { ... } + */ + public onMessageUpdate(handler: BotHandler): this { + return this.on('MessageUpdate', handler); + } + + /** + * Receives all MessageReaction activities, regardless of the sub-type of event. + * @remarks + * Message reaction activities represent a social interaction on an existing message activity within a conversation. + * The original activity is referred to by the id and conversation fields within the activity. + * The from field represents the source of the reaction (i.e., the user that reacted to the message). + * + * See related events: onMessageReactionAdded and onMessageReactionRemoved + * @param handler BotHandler A handler function in the form async(context, next) => { ... } + */ + public onMessageReaction(handler: BotHandler): this { + return this.on('MessageReaction', handler); + } + + /** + * Receives MessageReaction activities when a reaction is added + * @remarks + * The `context.activity.reactionsAdded` field contains a list of reactions added to this activity. + * @param handler BotHandler A handler function in the form async(context, next) => { ... } + */ + public onMessageReactionAdded(handler: BotHandler): this { + return this.on('MessageReactionAdded', handler); + } + + /** + * Receives MessageReaction activities when a reaction is removed + * @remarks + * The `context.activity.reactionsRemoved` field contains a list of reactions added from this activity. + * @param handler BotHandler A handler function in the form async(context, next) => { ... } + */ + public onMessageReactionRemoved(handler: BotHandler): this { + return this.on('MessageReactionRemoved', handler); + } + + /** + * Receives any Typing activities received + * @remarks + * Typing activities represent ongoing input from a user or a bot. This activity is often sent when keystrokes are being entered by a user. + * @param handler BotHandler A handler function in the form async(context, next) => { ... } + */ + public onTyping(handler: BotHandler): this { + return this.on('Typing', handler); + } + + /** + * Receives Handoff activities when a reaction is removed + * @remarks + * Handoff activities are used to request or signal a change in focus between elements inside a bot. + * @param handler BotHandler A handler function in the form async(context, next) => { ... } + */ + public onHandoff(handler: BotHandler): this { + return this.on('Handoff', handler); + } + + /** + * Receives Event activities that indicate a new conversation has been created. + * @remarks + * activity.context.name will be 'createConversation' + * @param handler BotHandler A handler function in the form async(context, next) => { ... } + */ + public onCreateConversation(handler: BotHandler): this { + return this.on('CreateConversation', handler); + } + + /** + * Receives Event activities that indicate an existing conversation is being continued. + * @remarks + * activity.context.name will be 'continueConversation' + * @param handler BotHandler A handler function in the form async(context, next) => { ... } + */ + public onContinueConversation(handler: BotHandler): this { + return this.on('ContinueConversation', handler); + } + + /** + * UnrecognizedActivityType will fire if an activity is received with a type that has not previously been defined. + * @remarks + * Some channels or custom adapters may create Actitivies with different, "unofficial" types. + * These events will be passed through as UnrecognizedActivityType events. + * Check `context.activity.type` for the type value. + * @param handler BotHandler A handler function in the form async(context, next) => { ... } + */ + public onUnrecognizedActivityType(handler: BotHandler): this { + return this.on('UnrecognizedActivityType', handler); + } + + /** + * onDialog fires at the end of the event emission process, and should be used to handle Dialog activity. + * @remarks + * Sample code: + * ```javascript + * bot.onDialog(async (context, next) => { + * if (context.activity.type === ActivityTypes.Message) { + * const dialogContext = await dialogSet.createContext(context); + * const results = await dialogContext.continueDialog(); + * await conversationState.saveChanges(context); + * } + * + * await next(); + * }); + * ``` + * @param handler BotHandler A handler function in the form async(context, next) => { ... } + */ + public onDialog(handler: BotHandler): this { + return this.on('Dialog', handler); + } + + /** + * `run()` is the main "activity handler" function used to ingest activities into the event emission process. + * @remarks + * Sample code: + * ```javascript + * server.post('/api/messages', (req, res) => { + * adapter.processActivity(req, res, async (context) => { + * // Route to main dialog. + * await bot.run(context); + * }); + * }); + * ``` + * + * @param context TurnContext A TurnContext representing an incoming Activity from an Adapter + */ + public async run(context: TurnContext): Promise { + + // Allow the dialog system to be triggered at the end of the chain + const runDialogs = async (): Promise => { + await this.handle(context, 'Dialog', async () => { + // noop + }); + }; + + // List of all Activity Types: + // https://github.com/Microsoft/botbuilder-js/blob/master/libraries/botframework-schema/src/index.ts#L1627 + await this.handle(context, 'Turn', async () => { + switch (context.activity.type) { + case ActivityTypes.Message: + await this.handle(context, 'Message', runDialogs); + break; + case ActivityTypes.ContactRelationUpdate: + await this.handle(context, 'ContactRelationUpdate', runDialogs); + break; + case ActivityTypes.ConversationUpdate: + await this.handle(context, 'ConversationUpdate', async () => { + if (context.activity.membersAdded && context.activity.membersAdded.length > 0) { + await this.handle(context, 'MembersAdded', runDialogs); + } else if (context.activity.membersRemoved && context.activity.membersRemoved.length > 0) { + await this.handle(context, 'MembersRemoved', runDialogs); + } else { + await runDialogs(); + } + }); + break; + case ActivityTypes.EndOfConversation: + await this.handle(context, 'EndOfConversation', runDialogs); + break; + case ActivityTypes.Event: + await this.handle(context, 'Event', async () => { + if (context.activity.name === 'tokens/response') { + await this.handle(context, 'TokenResponseEvent', runDialogs); + } else if (context.activity.name === 'createConversation') { + await this.handle(context, 'CreateConversation', runDialogs); + } else if (context.activity.name === 'continueConversation') { + await this.handle(context, 'ContinueConversation', runDialogs); + } else { + await runDialogs(); + } + }); + break; + case ActivityTypes.Invoke: + // TODO: Capture invoke response + let response = null; + response = await this.handle(context, 'Invoke', async () => { + if (context.activity.name === 'signin/verifyState') { + // if we have a more specialized handler for this, get our invoke response from that. + if (this.handlers.TeamsVerificationInvoke && this.handlers.TeamsVerificationInvoke.length) { + response = await this.handle(context, 'TeamsVerificationInvoke', runDialogs); + } + } else { + await runDialogs(); + } + }); + // pass invoke response on + if (response !== null) { + // Send an invokeResponse activity. + // This will actually be cached and sent as the http response + // by the BotFrameworkAdapter (if in use) + const invokeResponseActivity = { + type: 'invokeResponse', + value: response, + } as Activity; + await context.sendActivity(invokeResponseActivity); + } + break; + case ActivityTypes.InstallationUpdate: + await this.handle(context, 'InstallationUpdate', runDialogs); + break; + case ActivityTypes.MessageDelete: + await this.handle(context, 'MessageDelete', runDialogs); + break; + case ActivityTypes.MessageUpdate: + await this.handle(context, 'MessageUpdate', runDialogs); + break; + case ActivityTypes.MessageReaction: + await this.handle(context, 'MessageReaction', async() => { + if (context.activity.reactionsAdded && context.activity.reactionsAdded.length) { + await this.handle(context, 'MessageReactionAdded', runDialogs); + } else if (context.activity.reactionsRemoved && context.activity.reactionsRemoved.length) { + await this.handle(context, 'MessageReactionRemoved', runDialogs); + } else { + await runDialogs(); + } + }); + + break; + case ActivityTypes.Typing: + await this.handle(context, 'Typing', runDialogs); + break; + case ActivityTypes.Handoff: + await this.handle(context, 'Handoff', runDialogs); + break; + default: + // handler for unknown or unhandled types + await this.handle(context, 'UnrecognizedActivityType', runDialogs); + break; + } + }); + } + + /** + * Private method used to bind handlers to events by name + * @param type string + * @param handler BotHandler + */ + private on(type: string, handler: BotHandler) { + if (!this.handlers[type]) { + this.handlers[type] = [handler]; + } else { + this.handlers[type].push(handler); + } + return this; + } + + /** + * Private method used to fire events and execute any bound handlers + * @param type string + * @param handler BotHandler + */ + private async handle(context: TurnContext, type: string, onNext: () => Promise): Promise { + let returnValue: any = null; + + async function runHandler(index: number): Promise { + if (index < handlers.length) { + const val = await handlers[index](context, () => runHandler(index + 1)); + // if a value is returned, and we have not yet set the return value, + // capture it. This is used to allow InvokeResponses to be returned. + if (typeof(val) !== 'undefined' && returnValue === null) { + returnValue = val; + } + } else { + const val = await onNext(); + if (typeof(val) !== 'undefined') { + returnValue = val; + } + } + } + + const handlers = this.handlers[type] || []; + await runHandler(0); + + return returnValue; + } + +} diff --git a/samples/javascript_typescript/30.activity-handler/src/index.ts b/samples/javascript_typescript/30.activity-handler/src/index.ts new file mode 100644 index 0000000000..25ca98ee6b --- /dev/null +++ b/samples/javascript_typescript/30.activity-handler/src/index.ts @@ -0,0 +1,173 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +import { config } from 'dotenv'; +import * as path from 'path'; +import * as restify from 'restify'; + +// Import required bot services. +// See https://aka.ms/bot-services to learn more about the different parts of a bot. +import { ActivityTypes, BotFrameworkAdapter, ConversationState, MemoryStorage } from 'botbuilder'; +import { DialogSet, DialogTurnStatus, TextPrompt, WaterfallDialog } from 'botbuilder-dialogs'; + +// Import the ActivityHandler class onto which handlers will be bound. +import { ActivityHandler } from './ActivityHandler'; + +// Import required bot configuration. +import { BotConfiguration, IEndpointService } from 'botframework-config'; + +// Read botFilePath and botFileSecret from .env file. +// Note: Ensure you have a .env file and include botFilePath and botFileSecret. +const ENV_FILE = path.join(__dirname, '..', '.env'); +config({ path: ENV_FILE }); + +// bot endpoint name as defined in .bot file +// See https://aka.ms/about-bot-file to learn more about .bot file its use and bot configuration. +const DEV_ENVIRONMENT = 'development'; + +// bot name as defined in .bot file +// See https://aka.ms/about-bot-file to learn more about .bot file its use and bot configuration. +const BOT_CONFIGURATION = (process.env.NODE_ENV || DEV_ENVIRONMENT); + +// Create HTTP server. +const server = restify.createServer(); +server.listen(process.env.port || process.env.PORT || 3978, () => { + console.log(`\n${server.name} listening to ${server.url}`); + console.log(`\nGet Bot Framework Emulator: https://aka.ms/botframework-emulator`); + console.log(`\nTo talk to your bot, open sample.bot file in the Emulator.`); +}); + +// .bot file path +const BOT_FILE = path.join(__dirname, '..', (process.env.botFilePath || '')); + +// Read bot configuration from .bot file. +let botConfig = {}; +try { + botConfig = BotConfiguration.loadSync(BOT_FILE, process.env.botFileSecret); +} catch (err) { + console.error(`\nError reading bot file. Please ensure you have valid botFilePath and botFileSecret set for your environment.`); + console.error(`\n - The botFileSecret is available under appsettings for your Azure Bot Service bot.`); + console.error(`\n - If you are running this bot locally, consider adding a .env file with botFilePath and botFileSecret.\n\n`); + process.exit(); +} + +// Get bot endpoint configuration by service name +const endpointConfig = botConfig.findServiceByNameOrId(BOT_CONFIGURATION) as IEndpointService; + +// Create adapter. +// See https://aka.ms/about-bot-adapter to learn more about .bot file its use and bot configuration. +const adapter = new BotFrameworkAdapter({ + appId: endpointConfig.appId || process.env.microsoftAppID, + appPassword: endpointConfig.appPassword || process.env.microsoftAppPassword, +}); + +const storage = new MemoryStorage({}); + +const conversationState = new ConversationState(storage); +const dialogState = conversationState.createProperty('dialogState'); +const dialogSet = new DialogSet(dialogState); + +dialogSet.add(new TextPrompt('textPrompt')); + +dialogSet.add(new WaterfallDialog('hello', [ + async (step) => { + await step.context.sendActivity('Hi!'); + return step.next(); + }, + async (step) => { + return step.prompt('textPrompt', 'How are you?'); + }, + async (step) => { + await step.context.sendActivity('ACKNOWLEDGED.'); + return step.next(); + }, +])); + +// Catch-all for errors. +adapter.onTurnError = async (context, error) => { + // This check writes out errors to console log .vs. app insights. + console.error(`\n [onTurnError]: ${ error }`); + // Send a message to the user + await context.sendActivity(`Oops. Something went wrong!`); +}; + +// Create the main dialog. +const myBot = new ActivityHandler(); + +myBot.onTurn(async (context, next) => { + // await context.sendActivity(`Received activity of type ${ context.activity.type }`); + await next(); +}); + +myBot.onMessage(async (context, next) => { + // await context.sendActivity(`Echo: ${ context.activity.text }`); + await next(); +}); + +myBot.onMessage(async (context, next) => { + // await context.sendActivity(`Echo: ${ context.activity.text }`); + + if (context.activity.text.match(/help/i)) { + await context.sendActivity(`YOU NEED HELP! I saw this in my onMessage handler and will stop processing this message any further.`); + } else { + await next(); + } + +}); + +myBot.onConversationUpdate(async (context, next) => { + await context.sendActivity('Something about this conversation has been updated.'); + await next(); +}); + +myBot.onMembersAdded(async (context, next) => { + await context.sendActivity('Members have been added.'); + await next(); +}); + +myBot.onMembersRemoved(async (context, next) => { + await context.sendActivity('Members have been removed.'); + await next(); +}); + +myBot.onUnrecognizedActivityType(async (context, next) => { + await context.sendActivity(`I am not sure to do with an event of type ${ context.activity.type }.`); + await next(); +}); + +myBot.onInvoke(async (context, next) => { + + await next(); + + // return an invoke response which is sent directly via the http response + return { + status: 200, + body: 'invoked', + }; +}); + + +myBot.onDialog(async (context, next) => { + + if (context.activity.type === ActivityTypes.Message) { + const dialogContext = await dialogSet.createContext(context); + const results = await dialogContext.continueDialog(); + + // fallback behavior is to run welcome dialog + if (results.status === DialogTurnStatus.empty) { + await dialogContext.beginDialog('hello'); + } + + await conversationState.saveChanges(context); + } + + await next(); + +}); + +// Listen for incoming requests. +server.post('/api/messages', (req, res) => { + adapter.processActivity(req, res, async (context) => { + // Route to main dialog. + await myBot.run(context); + }); +}); diff --git a/samples/javascript_typescript/30.activity-handler/tsconfig.json b/samples/javascript_typescript/30.activity-handler/tsconfig.json new file mode 100644 index 0000000000..8535f08059 --- /dev/null +++ b/samples/javascript_typescript/30.activity-handler/tsconfig.json @@ -0,0 +1,12 @@ +{ + "compilerOptions": { + "composite": true, + "declaration": true, + "target": "es2016", + "module": "commonjs", + "outDir": "./lib", + "rootDir": "./src", + "sourceMap": true, + "strict": true + } +} diff --git a/samples/javascript_typescript/30.activity-handler/tslint.json b/samples/javascript_typescript/30.activity-handler/tslint.json new file mode 100644 index 0000000000..a7f88eeaf9 --- /dev/null +++ b/samples/javascript_typescript/30.activity-handler/tslint.json @@ -0,0 +1,13 @@ +{ + "defaultSeverity": "error", + "extends": [ + "tslint:recommended" + ], + "jsRules": {}, + "rules": { + "max-line-length": [false], + "no-console": [false, "log", "error"], + "quotemark": [true, "single"] + }, + "rulesDirectory": [] +}