├── .editorconfig ├── .eslintrc.json ├── .gitignore ├── .travis.yml ├── LICENSE ├── README.md ├── examples ├── facebook_bot.js └── slack_bot.js ├── images ├── bot_welcome.png ├── default_intent.png ├── message_diff.png └── save_json.png ├── index.js ├── package.json ├── src ├── api.js ├── botkit-middleware-dialogflow.js ├── options.js ├── structjson.js └── util.js └── test ├── credentials.json ├── dialogflow-mock-en.js ├── dialogflow-mock-en2.js ├── dialogflow-mock-fr.js ├── test.action.js ├── test.api.js ├── test.grpc.js ├── test.hears.js ├── test.options.js ├── test.receive.js ├── test.receive_language.js ├── test.receive_other.js ├── test.util.js └── v1 ├── config.json ├── test.action.js ├── test.hears.js ├── test.receive.js └── test.receive_language.js /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | charset = utf-8 5 | end_of_line = lf 6 | insert_final_newline = true 7 | indent_style = space 8 | indent_size = 4 9 | 10 | [*.json] 11 | indent_size = 2 -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "parserOptions": { 3 | "ecmaVersion": 2017, 4 | "sourceType": "module" 5 | }, 6 | "env": { 7 | "es6": true, 8 | "node": true, 9 | "mocha": true 10 | }, 11 | "extends": "eslint:recommended", 12 | "rules": { 13 | "indent": ["error", 2], 14 | "linebreak-style": ["error", "unix"], 15 | "quotes": ["error", "single"], 16 | "semi": ["error", "always"], 17 | "no-var": ["error"], 18 | "prefer-const": ["error"], 19 | "no-console": 0 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | .vscode 3 | .env 4 | .log 5 | package-lock.json 6 | temp/ -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | sudo: false 3 | node_js: 4 | - "node" 5 | - "lts/* " 6 | - "8" 7 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2017 Jeff Schnurr 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 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | [![Build Status](https://travis-ci.org/jschnurr/botkit-middleware-dialogflow.svg?branch=master)](https://travis-ci.org/jschnurr/botkit-middleware-dialogflow) 5 | 6 | # Botkit Middleware Dialogflow 7 | 8 | A middleware plugin for [Botkit](http://howdy.ai/botkit) that allows developers to integrate with [Google Dialogflow](https://dialogflow.com/), leveraging the power of both to build chatbot applications on Node for social platforms like Slack, Facebook and Twilio. 9 | 10 | Dialogflow's Natural Language Processing (NLP) platform transforms real-world user input into structured 11 | **intents** and **entities**, and can optionally trigger **actions** and **fulfillment** (webhooks). Configuration 12 | and training are done in the convenient and powerful [Dialogflow Console](https://console.dialogflow.com/), with 13 | the results being immediately available to your bot. 14 | 15 | - [Requirements](#requirements) 16 | - [Installation](#installation) 17 | - [Migrating from earlier versions](#migrating-from-earlier-versions) 18 | - [Quick Start](#quick-start) 19 | - [1. Setup an Agent in Dialogflow](#1-setup-an-agent-in-dialogflow) 20 | - [2. Create a service account](#2-create-a-service-account) 21 | - [3. Add the middleware to your Bot](#3-add-the-middleware-to-your-bot) 22 | - [4. Try it out!](#4-try-it-out) 23 | - [Middleware functions](#middleware-functions) 24 | - [receive()](#receive) 25 | - [hears()](#hears) 26 | - [action()](#action) 27 | - [Options](#options) 28 | - [Language Support](#language-support) 29 | - [Debugging](#debugging) 30 | - [Legacy V1 API](#legacy-v1-api) 31 | - [Change Log](#change-log) 32 | - [Contributing](#contributing) 33 | - [Credit](#credit) 34 | - [License](#license) 35 | 36 | ## Requirements 37 | - Botkit v0.7.x 38 | - Node 8+ 39 | 40 | ## Installation 41 | 42 | ```bash 43 | npm install botkit-middleware-dialogflow 44 | ``` 45 | 46 | ## Migrating from earlier versions 47 | Dialogflow has two versions of their API. V2 is the standard, and should be the default for new agents. 48 | 49 | However, if you need to [migrate](https://dialogflow.com/docs/reference/v1-v2-migration-guide) from Dialogflow API V1, or are upgrading from earlier versions of `botkit-middleware-dialogflow`, consider the following factors: 50 | - some Botkit `message` properties have changed. 51 | - `fulfillment.speech` -> `fulfillment.text` 52 | - `action` property is new 53 | - `nlpResponse` object structure has changed significantly. 54 | - V2 users must provide a JSON keyfile instead of an API key for DialogFlow authentication 55 | - options parameter `minimum_confidence` has been renamed `minimumConfidence` to match the predominant style. 56 | 57 | `botkit-middleware-dialogflow` continues to support both versions of the API. Instructions for legacy V1 are [below]((#legacy-v1-api)). 58 | 59 | ## Quick Start 60 | 61 | ### 1. Setup an Agent in Dialogflow 62 | 63 | 64 | Google describes `Agents` as *NLU (Natural Language Understanding) modules*. They transform natural user requests into structured, actionable data. 65 | 66 | 1. In the [Dialogflow Console](https://console.dialogflow.com/), create an [agent](https://dialogflow.com/docs/agents) 67 | 2. Choose or create a [Google Cloud Platform (GCP) Project](https://cloud.google.com/docs/overview/#projects). 68 | 3. Dialogflow will automatically setup a `Default Welcome Intent`, which you can try from the test console. 69 | 70 | ### 2. Create a service account 71 | 72 | 73 | In order for your Bot to access your Dialogflow Agent, you will need to create a `service account`. A [Service account](https://cloud.google.com/compute/docs/access/service-accounts) is an identity that allows your bot to access the Dialogflow services on your behalf. Once configured, you can download the private key for your service account as a JSON file. 74 | 75 | 1. Open the [GCP Cloud Console](https://console.cloud.google.com), and select the project which contains your agent. 76 | 2. From the `nav` menu, choose `IAM & admin`, `Service accounts`. 77 | 3. Select `Dialogflow Integrations` (created by default by Dialogflow), or create your own. 78 | 4. Under `actions`, select `create key`, select `JSON` and download the file. 79 | 80 | ### 3. Add the middleware to your Bot 81 | Using Slack (as an example), wire up your Bot to listen for the `Default Welcome Intent`, and then pass along the reply that Dialogflow recommends in `fulfillment.text`. 82 | 83 | ``` javascript 84 | const Botkit = require('botkit'); 85 | const dialogflowMiddleware = require('botkit-middleware-dialogflow')({ 86 | keyFilename: './mybot-service-key.json', // service account private key file from Google Cloud Console 87 | }); 88 | 89 | const slackController = Botkit.slackbot(); 90 | const slackBot = slackController.spawn({ 91 | token: 'xoxb-082028214871-xEEQbIkyAHH3poFMpUG3dkGW', // Slack API Token 92 | }); 93 | 94 | slackController.middleware.receive.use(dialogflowMiddleware.receive); 95 | slackBot.startRTM(); 96 | 97 | slackController.hears(['Default Welcome Intent'], 'direct_message', dialogflowMiddleware.hears, function( 98 | bot, 99 | message 100 | ) { 101 | replyText = message.fulfillment.text; // message object has new fields added by Dialogflow 102 | bot.reply(message, replyText); 103 | }); 104 | ``` 105 | 106 | ### 4. Try it out! 107 | 108 | 109 | ## Middleware functions 110 | `Botkit` supports middleware integration into core bot processes in a few useful places, described [here](https://botkit.ai/docs/middleware.html). 111 | 112 | ### receive() 113 | Each time the chat platform (eg. Slack, Facebook etc) emits a message to Botkit, `botkit-middleware-dialogflow` uses `receive` middleware to process that message and optionally modify it, before passing it back to Botkit and on down the chain. 114 | 115 | #### Setup 116 | First, create an instance of the middleware: 117 | 118 | ```javascript 119 | const dialogflowMiddleware = require('botkit-middleware-dialogflow')(options); 120 | ``` 121 | 122 | Typically `keyFilename` is the only property of `options` that needs to be set. See [options](#options) section for full list. 123 | 124 | Next, tell the `controller` that you want to use the middleware: 125 | 126 | ```javascript 127 | slackController.middleware.receive.use(dialogflowMiddleware.receive); 128 | ``` 129 | 130 | #### ignoreType 131 | Not every message should be sent to DialogFlow, such as `user_typing` indicators. To avoid these uneccessary calls, `botkit-middleware-dialogflow` allows you to specify which message types to ignore, using the `ignoreType` option. 132 | 133 | Since the middleware is part of the [message pipeline](https://botkit.ai/docs/readme-pipeline.html), Botkit has already ingested, normalized and categorized the message by the time we apply this filter. Keep this in mind when choosing which types to ignore. 134 | 135 | #### API Call 136 | Assuming the message has passed the `ignoreType` filter, it's sent off to the Dialogflow API for processing. The response is parsed and applied to the `message` object itself. 137 | 138 | Specifically, here are the new `message` properties available after processing: 139 | 140 | * `message.intent` [intents](https://dialogflow.com/docs/intents) recognized by Dialogflow (eg. saying 'hi' might trigger the `hello-intent`) 141 | * `message.entities` [entities](https://dialogflow.com/docs/entities) found as defined in Dialogflow (eg. dates, places, etc) 142 | * `message.action` [actions and parameters](https://dialogflow.com/docs/actions-and-parameters) triggered by the intent 143 | * `message.fulfillment` [fulfillment](https://dialogflow.com/docs/fulfillment) triggered by the intent, such as webhooks or text responses. 144 | * `message.confidence` intent detection confidence. Values range from 0.0 (completely uncertain) to 1.0 (completely certain). 145 | * `message.nlpResponse` the raw Dialogflow API response. 146 | 147 | Here is a diff of a message object, before and after middleware processing. 148 | 149 | 150 | 151 | 152 | ### hears() 153 | To make your bot listen for the intent name configured in Dialogflow, we need to change the way Botkit "hears" triggers, by passing our middleware into the `hears()` event handler. 154 | 155 | For example, using our `dialogflowMiddleware` object defined above: 156 | 157 | ``` javascript 158 | controller.hears('hello-intent', 'direct_message', dialogflowMiddleware.hears, function(bot, message) { 159 | // do something 160 | }); 161 | ``` 162 | 163 | Notice we are listening for `hello-intent` - that's the name we gave the intent in the [Dialogflow Console](https://console.dialogflow.com/). 164 | 165 | Patterns used to match the intent name can be provided as comma seperated strings, regex, or an array of strings and regex. 166 | 167 | - `'hello-intent'` // matches hello-intent, HELLO-INTENT case insensitive 168 | - `['hello-intent', /^HELLO.*/i]` // matches hello-intent, hello, or HELLotherejimmy 169 | - `'hello-intent,greeting-intent'` // matches hello-intent or greeting-intent' 170 | 171 | Patterns are compared with the `message.intent` property after the `receives()` function has processed it. 172 | 173 | ### action() 174 | When an intent is triggered, a Dialogflow agent can be configured to take an [action](https://dialogflow.com/docs/actions-and-parameters). The name of the action is captured in the `message.action` property, after procesing by the middleware. 175 | 176 | You can setup a `hears()` event handler to trigger on `message.action` as well. 177 | 178 | ``` javascript 179 | controller.hears('hello-intent', 'direct_message', dialogflowMiddleware.action, function(bot, message) { 180 | // do something 181 | }); 182 | ``` 183 | 184 | The patterns format is the same as `hears()`. 185 | 186 | 187 | ## Options 188 | 189 | When creating the middleware object, pass an options object with the following parameters. 190 | 191 | | Property | Required | Default | Description | 192 | | ----------------- | :------------ | :------------------------------------: | :-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | 193 | | ignoreType | No | 'self_message' | Skip Dialogflow processing if the `type` matches the pattern. Useful to avoid unneccessary API calls. Patterns can be provided as a string, regex, or array of either. | 194 | | minimumConfidence | No | 0.0 | Dialogflow returns a confidence (in the range 0.0 to 1.0) for each matching intent. This value is the cutoff - the `hears` and `action` middleware will only return a match for confidence values equal or greather than this value. | 195 | | sessionIdProps | No | ['user', 'channel'] | Session ID's help Dialogflow preserve context across multiple calls. By default, this session ID is an MD5 hash of the `user` and `channel` properties on the `message` object. If you'd like to use different properties, provide them as a string or array of strings. If none of the desired properties are available on a `message`, the middleware will use a random session ID instead. | 196 | | lang | No | 'en' | if the `message` object does not have a `lang` property, this language will be used as the default. | 197 | | version | No | v2 | Version of the dialogflow API to use. Your agent needs to use the same setting for your [agent](https://dialogflow.com/docs/agents) in the DialogFlow console. | 198 | | token | Yes (v1 only) | | Client access token, from the Dialogflow Console. Only required with version v1. | 199 | | keyFilename | Yes (v2 only) | | Path to the a .json key downloaded from the Google Developers Console. Can be relative to where the process is being run from. Alternatively, set DIALOGFLOW_CLIENT_EMAIL and DIALOGFLOW_PRIVATE_KEY in the environment using values found in the keyFile. | 200 | | projectId | No | value of `project_id` in `keyFilename` | The Google project ID your Dialogflow V2 agent belongs to. You can find it in the agent settings. If using a keyFile, the middleware will find it automatically. May also be set using DIALOGFLOW_PROJECT_ID in the environment. | 201 | > v2 users can optionally provide a path to a .pem or .p12 `keyFilename`, in which case you must specify an `email` and `projectId` parameter as well. 202 | 203 | ## Language Support 204 | 205 | Dialogflow supports [multi-language agents](https://dialogflow.com/docs/multi-language). If the `message` object has a `lang` value set, 206 | the middleware will send it to Dialogflow and the response will be in that language, if the agent supports it. 207 | 208 | By default, Botkit `message` objects do not have a langauge specified, so Dialogflow defaults to `en`. 209 | 210 | For example, to invoke the Dialogflow agent in French, set your `message` as such: 211 | 212 | ```javascript 213 | message.lang = 'fr'; 214 | ``` 215 | 216 | ## Debugging 217 | 218 | To enable debug logging, specify `dialogflow-middleware` in the `DEBUG` environment variable, like this: 219 | 220 | ```bash 221 | DEBUG=dialogflow-middleware node your_awesome_bot.js 222 | ``` 223 | 224 | By default, objects are only logged to a depth of 2. To recurse indefinitely, set `DEBUG_DEPTH` to `null`, like this: 225 | 226 | ```bash 227 | DEBUG=dialogflow-middleware DEBUG_DEPTH=null node your_awesome_bot.js 228 | ``` 229 | 230 | ## Legacy V1 API 231 | To use the legacy V1 version of the Dialogflow API: 232 | 233 | - In the Dialogflow console: 234 | - In the agent settings, select `V1 API`. 235 | - Note the `Client access token`. 236 | - Set options for the middleware: 237 | - `token` is the `Client access token` from the Dialogflow console. 238 | - `version` should be set to `v1`, telling `botkit-middleware-dialogflow` to use the legacy API. 239 | 240 | 241 | ## Change Log 242 | 243 | * 6-May-2019 v2.1.0 244 | * minimumConfidence now defaults to 0.0 245 | * Dialogflow credentials can be set using environment variables 246 | * drop support for Node 7 247 | 248 | * 8-Sept-2018 v2.0.4 249 | * Fix projectId not detected when passed in config 250 | 251 | * 16-July-2018 v2.0.2 252 | * refactor to support Dialogflow API V2 253 | * readme updates 254 | * defaults and examples now use Dialogflow API V2 255 | 256 | * 12-June-2018 v1.4.1 257 | * sessionId sent to DF based on user and channel properties of message 258 | * allow customization of sessionId to use different properties as desired 259 | 260 | * 24-May-2018 v1.4.0 261 | 262 | * support for sending queries to Dialogflow in different languages, specified by lang prop on message 263 | * add TOC to README 264 | 265 | * 7-May-2018 v1.3.0 266 | 267 | * fix #9 add support for ignoreType to avoid unneccessary API calls to DF 268 | * more debugging tips in README 269 | * restore images in readme 270 | 271 | * 31-Mar-2018 v1.2.0 272 | 273 | * fix #5 add full support for regex and strings for intents and actions 274 | * change slack example env variable to improve clarity 275 | * add tests for existing functionality 276 | 277 | * 9-Dec-2017 v1.1.0 278 | 279 | * update criteria for skipping middleware automatically 280 | * remove skip_bot option 281 | * travis and changelog added 282 | * readme updates 283 | * updated examples 284 | * filter out self_message type from slack 285 | * ignore editor files 286 | * migrate to eslint and apply formatter to comply with .eslintrc rules 287 | * add debug logging 288 | 289 | * 3-Dec-2017 v1.0.1 290 | 291 | * rebrand as dialogflow 292 | 293 | * pre-fork as botkit-middleware-apiai 294 | * initial release 295 | 296 | ## Contributing 297 | If you would like to help make `botkit-middleware-dialogflow` better, please open an issue in Github, or send me a pull request. 298 | 299 | Feedback, suggestions and PRs are welcome. 300 | 301 | ## Credit 302 | 303 | Forked from [botkit-middleware-apiai](https://github.com/abeai/botkit-middleware-apiai). Thanks to 304 | [@abeai](https://github.com/abeai) for the original work. 305 | 306 | Also thanks to [@ehrhart](https://github.com/ehrhart) for patches supporting V2. 307 | 308 | ## License 309 | 310 | This library is licensed under the MIT license. Full text is available in LICENSE. 311 | -------------------------------------------------------------------------------- /examples/facebook_bot.js: -------------------------------------------------------------------------------- 1 | /* ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 2 | ______ ______ ______ __ __ __ ______ 3 | /\ == \ /\ __ \ /\__ _\ /\ \/ / /\ \ /\__ _\ 4 | \ \ __< \ \ \/\ \ \/_/\ \/ \ \ _"-. \ \ \ \/_/\ \/ 5 | \ \_____\ \ \_____\ \ \_\ \ \_\ \_\ \ \_\ \ \_\ 6 | \/_____/ \/_____/ \/_/ \/_/\/_/ \/_/ \/_/ 7 | 8 | 9 | This is a sample Facebook bot built with Botkit, using the Dialogflow middleware. 10 | 11 | This bot demonstrates many of the core features of Botkit: 12 | 13 | * Connect to Facebook's Messenger APIs 14 | * Receive messages based on "spoken" patterns 15 | * Reply to messages 16 | 17 | # RUN THE BOT: 18 | 19 | Follow the instructions here to set up your Facebook app and page: 20 | 21 | -> https://developers.facebook.com/docs/messenger-platform/implementation 22 | 23 | Get a JSON file with your service account key from the Google Cloud Console 24 | 25 | -> https://console.cloud.google.com 26 | 27 | Run your bot from the command line: 28 | 29 | app_secret= \ 30 | page_token= \ 31 | verify_token= \ 32 | dialogflow= \ 33 | node facebook_bot.js [--lt [--ltsubdomain LOCALTUNNEL_SUBDOMAIN]] 34 | 35 | Use the --lt option to make your bot available on the web through localtunnel.me. 36 | 37 | # USE THE BOT: 38 | 39 | Train an intent titled "hello-intent" inside Dialogflow. Give it a bunch of examples 40 | of how someone might say "Hello" to your bot. 41 | 42 | Find your bot inside Facebook to send it a direct message. 43 | 44 | Say: "Hello" 45 | 46 | The bot should reply "Hello!" If it didn't, your intent hasn't been 47 | properly trained - check out the dialogflow console! 48 | 49 | Make sure to invite your bot into other channels using /invite @! 50 | 51 | # EXTEND THE BOT: 52 | 53 | Botkit is has many features for building cool and useful bots! 54 | 55 | Read all about it here: 56 | 57 | -> http://howdy.ai/botkit 58 | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~*/ 59 | 60 | if (!process.env.page_token) { 61 | console.log('Error: Specify page_token in environment'); 62 | process.exit(1); 63 | } 64 | 65 | if (!process.env.verify_token) { 66 | console.log('Error: Specify verify_token in environment'); 67 | process.exit(1); 68 | } 69 | 70 | if (!process.env.app_secret) { 71 | console.log('Error: Specify app_secret in environment'); 72 | process.exit(1); 73 | } 74 | 75 | if (!process.env.dialogflow) { 76 | console.log('Error: Specify dialogflow in environment'); 77 | process.exit(1); 78 | } 79 | 80 | const Botkit = require('botkit'); 81 | const commandLineArgs = require('command-line-args'); 82 | const localtunnel = require('localtunnel'); 83 | 84 | const ops = commandLineArgs([ 85 | { 86 | name: 'lt', 87 | alias: 'l', 88 | args: 1, 89 | description: 'Use localtunnel.me to make your bot available on the web.', 90 | type: Boolean, 91 | defaultValue: false, 92 | }, 93 | { 94 | name: 'ltsubdomain', 95 | alias: 's', 96 | args: 1, 97 | description: 98 | 'Custom subdomain for the localtunnel.me URL. This option can only be used together with --lt.', 99 | type: String, 100 | defaultValue: null, 101 | }, 102 | ]); 103 | 104 | if (ops.lt === false && ops.ltsubdomain !== null) { 105 | console.log('error: --ltsubdomain can only be used together with --lt.'); 106 | process.exit(); 107 | } 108 | 109 | const controller = Botkit.facebookbot({ 110 | debug: true, 111 | log: true, 112 | access_token: process.env.page_token, 113 | verify_token: process.env.verify_token, 114 | app_secret: process.env.app_secret, 115 | validate_requests: true, // Refuse any requests that don't provide the app_secret specified 116 | }); 117 | 118 | const dialogflow = require('../')({ 119 | keyFilename: process.env.dialogflow, 120 | }); 121 | 122 | controller.middleware.receive.use(dialogflow.receive); 123 | 124 | const bot = controller.spawn({}); 125 | 126 | controller.setupWebserver(process.env.port || 3000, function(err, webserver) { 127 | controller.createWebhookEndpoints(webserver, bot, function() { 128 | console.log('ONLINE!'); 129 | if (ops.lt) { 130 | const tunnel = localtunnel(process.env.port || 3000, { subdomain: ops.ltsubdomain }, function( 131 | err, 132 | tunnel 133 | ) { 134 | if (err) { 135 | console.log(err); 136 | process.exit(); 137 | } 138 | console.log( 139 | 'Your bot is available on the web at the following URL: ' + 140 | tunnel.url + 141 | '/facebook/receive' 142 | ); 143 | }); 144 | 145 | tunnel.on('close', function() { 146 | console.log('Your bot is no longer available on the web at the localtunnnel.me URL.'); 147 | process.exit(); 148 | }); 149 | } 150 | }); 151 | }); 152 | 153 | controller.api.messenger_profile.greeting('Hello! I\'m a Botkit bot!'); 154 | controller.api.messenger_profile.menu([ 155 | { 156 | locale: 'default', 157 | composer_input_disabled: false, 158 | }, 159 | ]); 160 | 161 | /* note this uses example middleware defined above */ 162 | controller.hears(['hello-intent'], 'message_received,facebook_postback', dialogflow.hears, function( 163 | bot, 164 | message 165 | ) { 166 | bot.reply(message, 'Hello!'); 167 | }); 168 | -------------------------------------------------------------------------------- /examples/slack_bot.js: -------------------------------------------------------------------------------- 1 | /* ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 2 | ______ ______ ______ __ __ __ ______ 3 | /\ == \ /\ __ \ /\__ _\ /\ \/ / /\ \ /\__ _\ 4 | \ \ __< \ \ \/\ \ \/_/\ \/ \ \ _"-. \ \ \ \/_/\ \/ 5 | \ \_____\ \ \_____\ \ \_\ \ \_\ \_\ \ \_\ \ \_\ 6 | \/_____/ \/_____/ \/_/ \/_/\/_/ \/_/ \/_/ 7 | 8 | 9 | This is a sample Slack bot built with Botkit, using the Dialogflow middleware. 10 | 11 | This bot demonstrates many of the core features of Botkit: 12 | 13 | * Connect to Slack using the real time API 14 | * Receive messages based on "spoken" patterns 15 | * Reply to messages 16 | 17 | # RUN THE BOT: 18 | 19 | Get a Bot API token from Slack: 20 | 21 | -> http://my.slack.com/services/new/bot 22 | 23 | Get a JSON file with your service account key from the Google Cloud Console 24 | 25 | -> https://console.cloud.google.com 26 | 27 | Run your bot from the command line: 28 | 29 | slack= dialogflow= node example_bot.js 30 | 31 | # USE THE BOT: 32 | 33 | Train an intent titled "hello-intent" inside Dialogflow. Give it a bunch of examples 34 | of how someone might say "Hello" to your bot. 35 | 36 | Find your bot inside Slack to send it a direct message. 37 | 38 | Say: "Hello" 39 | 40 | The bot should reply "Hello!" If it didn't, your intent hasn't been 41 | properly trained - check out the dialogflow console! 42 | 43 | Make sure to invite your bot into other channels using /invite @! 44 | 45 | # EXTEND THE BOT: 46 | 47 | Botkit is has many features for building cool and useful bots! 48 | 49 | Read all about it here: 50 | 51 | -> http://howdy.ai/botkit 52 | 53 | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~*/ 54 | 55 | if (!process.env.slack) { 56 | console.log('Error: Specify slack API token in environment'); 57 | process.exit(1); 58 | } 59 | 60 | if (!process.env.dialogflow) { 61 | console.log('Error: Specify dialogflow in environment'); 62 | process.exit(1); 63 | } 64 | 65 | const Botkit = require('botkit'); 66 | 67 | const slackController = Botkit.slackbot({ 68 | debug: true, 69 | }); 70 | 71 | const slackBot = slackController.spawn({ 72 | token: process.env.slack, 73 | }); 74 | 75 | const dialogflowMiddleware = require('../')({ 76 | keyFilename: process.env.dialogflow, 77 | }); 78 | 79 | slackController.middleware.receive.use(dialogflowMiddleware.receive); 80 | slackBot.startRTM(); 81 | 82 | /* note this uses example middlewares defined above */ 83 | slackController.hears(['hello-intent'], 'direct_message', dialogflowMiddleware.hears, function( 84 | bot, 85 | message 86 | ) { 87 | bot.reply(message, 'Hello!'); 88 | }); 89 | -------------------------------------------------------------------------------- /images/bot_welcome.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jschnurr/botkit-middleware-dialogflow/6c20134822d9e30348c83c1db741d737f616201a/images/bot_welcome.png -------------------------------------------------------------------------------- /images/default_intent.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jschnurr/botkit-middleware-dialogflow/6c20134822d9e30348c83c1db741d737f616201a/images/default_intent.png -------------------------------------------------------------------------------- /images/message_diff.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jschnurr/botkit-middleware-dialogflow/6c20134822d9e30348c83c1db741d737f616201a/images/message_diff.png -------------------------------------------------------------------------------- /images/save_json.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jschnurr/botkit-middleware-dialogflow/6c20134822d9e30348c83c1db741d737f616201a/images/save_json.png -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | module.exports = require('./src/botkit-middleware-dialogflow'); 2 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "botkit-middleware-dialogflow", 3 | "version": "2.1.0", 4 | "description": "Middleware for using Dialogflow (formerly Api.ai) with Botkit-powered bots", 5 | "main": "src/botkit-middleware-dialogflow.js", 6 | "scripts": { 7 | "test": "npm run lint && mocha 'test/**/test*' --exit", 8 | "lint": "eslint src/*.js", 9 | "prepublishOnly": "npm run test" 10 | }, 11 | "repository": { 12 | "type": "git", 13 | "url": "https://github.com/jschnurr/botkit-middleware-dialogflow.git" 14 | }, 15 | "keywords": [ 16 | "botkit", 17 | "apiai", 18 | "dialogflow", 19 | "bots", 20 | "slack" 21 | ], 22 | "author": { 23 | "name": "Jeff Schnurr", 24 | "email": "jschnurr@gmail.com" 25 | }, 26 | "license": "MIT", 27 | "bugs": { 28 | "url": "https://github.com/jschnurr/botkit-middleware-dialogflow/issues" 29 | }, 30 | "homepage": "https://github.com/jschnurr/botkit-middleware-dialogflow#readme", 31 | "dependencies": { 32 | "apiai": "^4.0.3", 33 | "debug": "^3.1.0", 34 | "dialogflow": "^0.8.2", 35 | "hasha": "^3.0.0", 36 | "lodash": "^4.17.10", 37 | "uuid": "^3.3.2" 38 | }, 39 | "devDependencies": { 40 | "botkit": "^0.7.4", 41 | "chai": "^4.1.2", 42 | "clone": "^2.1.2", 43 | "eslint": "^5.5.0", 44 | "mocha": "^5.2.0", 45 | "mockery": "^2.1.0", 46 | "nock": "^9.6.1", 47 | "sinon": "^6.2.0" 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /src/api.js: -------------------------------------------------------------------------------- 1 | const debug = require('debug')('dialogflow-middleware'); 2 | const apiai = require('apiai'); 3 | const dialogflow = require('dialogflow'); 4 | const structProtoToJson = require('./structjson').structProtoToJson; 5 | const _ = require('lodash'); 6 | 7 | module.exports = function(config) { 8 | if (config.version.toUpperCase() === 'V1') { 9 | return new DialogFlowAPI_V1(config); 10 | } else { 11 | return new DialogFlowAPI_V2(config); 12 | } 13 | }; 14 | 15 | class DialogFlowAPI_V1 { 16 | constructor(config) { 17 | this.config = config; 18 | this.app = apiai(config.token); 19 | } 20 | 21 | query(sessionId, languageCode, text) { 22 | this.app.language = languageCode; 23 | 24 | const request = this.app.textRequest(text, { 25 | sessionId: sessionId, 26 | }); 27 | 28 | return new Promise((resolve, reject) => { 29 | request.on('response', function(response) { 30 | try { 31 | const data = DialogFlowAPI_V1._normalize(response); 32 | debug('dialogflow api response: ', response); 33 | resolve(data); 34 | } catch (err) { 35 | debug('dialogflow api error: ', err); 36 | reject(err); 37 | } 38 | }); 39 | 40 | request.on('error', function(error) { 41 | debug('dialogflow api error: ', error); 42 | reject(error); 43 | }); 44 | 45 | request.end(); 46 | }); 47 | } 48 | 49 | // return standardized format 50 | static _normalize(response) { 51 | return { 52 | intent: _.get(response, 'result.metadata.intentName', null), 53 | entities: _.get(response, 'result.parameters', null), 54 | action: _.get(response, 'result.action', null), 55 | fulfillment: _.get(response, 'result.fulfillment', null), 56 | confidence: _.get(response, 'result.score', null), 57 | nlpResponse: response, 58 | }; 59 | } 60 | } 61 | 62 | class DialogFlowAPI_V2 { 63 | constructor(config) { 64 | this.config = config; 65 | const opts = _.pick(config, [ 66 | 'credentials', 67 | 'keyFilename', 68 | 'projectId', 69 | 'email', 70 | 'port', 71 | 'promise', 72 | 'servicePath', 73 | ]); 74 | 75 | this.projectId = opts.projectId; 76 | 77 | this.app = new dialogflow.SessionsClient(opts); 78 | } 79 | 80 | query(sessionId, languageCode, text) { 81 | const request = { 82 | session: this.app.sessionPath(this.projectId, sessionId), 83 | queryInput: { 84 | text: { 85 | text: text, 86 | languageCode: languageCode, 87 | }, 88 | }, 89 | }; 90 | 91 | return new Promise((resolve, reject) => { 92 | this.app.detectIntent(request, function(error, response) { 93 | if (error) { 94 | debug('dialogflow api error: ', error); 95 | reject(error); 96 | } else { 97 | debug('dialogflow api response: ', response); 98 | try { 99 | const data = DialogFlowAPI_V2._normalize(response); 100 | resolve(data); 101 | } catch (err) { 102 | reject(err); 103 | } 104 | } 105 | }); 106 | }); 107 | } 108 | 109 | // return standardized format 110 | static _normalize(response) { 111 | return { 112 | intent: _.get(response, 'queryResult.intent.displayName', null), 113 | entities: structProtoToJson(response.queryResult.parameters), 114 | action: _.get(response, 'queryResult.action', null), 115 | fulfillment: { 116 | text: _.get(response, 'queryResult.fulfillmentText', null), 117 | messages: _.get(response, 'queryResult.fulfillmentMessages', null), 118 | }, 119 | confidence: _.get(response, 'queryResult.intentDetectionConfidence', null), 120 | nlpResponse: response, 121 | }; 122 | } 123 | } 124 | -------------------------------------------------------------------------------- /src/botkit-middleware-dialogflow.js: -------------------------------------------------------------------------------- 1 | const debug = require('debug')('dialogflow-middleware'); 2 | const util = require('./util'); 3 | const api = require('./api'); 4 | const options = require('./options'); 5 | 6 | module.exports = function(config) { 7 | config = options.checkOptions(config); 8 | 9 | const ignoreTypePatterns = util.makeArrayOfRegex(config.ignoreType || []); 10 | const middleware = {}; 11 | 12 | const app = (middleware.api = api(config)); 13 | 14 | middleware.receive = async function(bot, message, next) { 15 | if (!message.text || message.is_echo || message.type === 'self_message') { 16 | next(); 17 | return; 18 | } 19 | 20 | for (const pattern of ignoreTypePatterns) { 21 | if (pattern.test(message.type)) { 22 | debug('skipping call to Dialogflow since type matched ', pattern); 23 | next(); 24 | return; 25 | } 26 | } 27 | 28 | const sessionId = util.generateSessionId(config, message); 29 | const lang = message.lang || config.lang; 30 | 31 | debug( 32 | 'Sending message to dialogflow. sessionId=%s, language=%s, text=%s', 33 | sessionId, 34 | lang, 35 | message.text 36 | ); 37 | 38 | try { 39 | const response = await app.query(sessionId, lang, message.text); 40 | Object.assign(message, response); 41 | 42 | debug('dialogflow annotated message: %O', message); 43 | next(); 44 | } catch (error) { 45 | debug('dialogflow returned error', error); 46 | next(error); 47 | } 48 | }; 49 | 50 | middleware.hears = function(patterns, message) { 51 | const regexPatterns = util.makeArrayOfRegex(patterns); 52 | 53 | for (const pattern of regexPatterns) { 54 | if (pattern.test(message.intent) && message.confidence >= config.minimumConfidence) { 55 | debug('dialogflow intent matched hear pattern', message.intent, pattern); 56 | return true; 57 | } 58 | } 59 | return false; 60 | }; 61 | 62 | middleware.action = function(patterns, message) { 63 | const regexPatterns = util.makeArrayOfRegex(patterns); 64 | 65 | for (const pattern of regexPatterns) { 66 | if (pattern.test(message.action) && message.confidence >= config.minimumConfidence) { 67 | debug('dialogflow action matched hear pattern', message.intent, pattern); 68 | return true; 69 | } 70 | } 71 | return false; 72 | }; 73 | 74 | return middleware; 75 | }; 76 | 77 | -------------------------------------------------------------------------------- /src/options.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const debug = require('debug')('dialogflow-middleware'); 3 | 4 | /** 5 | * Validate config and set defaults as required 6 | * 7 | * @param {object} config - the configuration provided by the user 8 | * @return {object} validated configuration with defaults applied 9 | */ 10 | 11 | exports.checkOptions = function checkOptions(config = {}) { 12 | // start with defaults 13 | const defaults = { 14 | version: 'v2', 15 | minimumConfidence: 0.0, 16 | sessionIdProps: ['user', 'channel'], 17 | ignoreType: 'self_message', 18 | lang: 'en', 19 | }; 20 | 21 | // overlay any explicit configuration 22 | config = Object.assign({}, defaults, config); 23 | 24 | // overlay keyfile data and environment variables 25 | if (config.version.toUpperCase() === 'V2') { 26 | if (config.keyFilename) { 27 | // overlay project and credentials from keyfile 28 | config = Object.assign({}, config, getKeyData(config.keyFilename)); 29 | } 30 | 31 | if (process.env.DIALOGFLOW_PROJECT_ID) { 32 | config.projectId = process.env.DIALOGFLOW_PROJECT_ID; 33 | } 34 | 35 | if (process.env.DIALOGFLOW_CLIENT_EMAIL && process.env.DIALOGFLOW_PRIVATE_KEY) { 36 | config.credentials = { 37 | private_key: process.env.DIALOGFLOW_PRIVATE_KEY.replace(/\\n/g, '\n'), 38 | client_email: process.env.DIALOGFLOW_CLIENT_EMAIL, 39 | }; 40 | } 41 | } 42 | 43 | // clean and validate options 44 | config.version = config.version.toUpperCase(); 45 | if (config.version === 'V1') { 46 | // V1 47 | if (!config.token) { 48 | throw new Error('Dialogflow token must be provided for v1.'); 49 | } 50 | } else { 51 | // V2 52 | if (!config.projectId || !config.credentials) { 53 | throw new Error( 54 | 'projectId and credentials required, either via keyFile or environment variables.' 55 | ); 56 | } 57 | 58 | if ( 59 | config.keyFilename && 60 | (process.env.DIALOGFLOW_CLIENT_EMAIL || process.env.DIALOGFLOW_PRIVATE_KEY) 61 | ) { 62 | throw new Error( 63 | 'Invalid configuration - cannot provide both keyfile and explicit credentials.' 64 | ); 65 | } 66 | } 67 | 68 | debug(`settings are ${JSON.stringify(config)}`); 69 | return config; 70 | }; 71 | 72 | function getKeyData(keyFilename) { 73 | if (!path.isAbsolute(keyFilename)) { 74 | keyFilename = path.join(process.cwd(), keyFilename); 75 | } 76 | const keyFile = require(keyFilename); 77 | 78 | return { 79 | keyFilename: keyFilename, 80 | credentials: { 81 | private_key: keyFile.private_key, 82 | client_email: keyFile.client_email, 83 | }, 84 | projectId: keyFile.project_id, 85 | }; 86 | } 87 | -------------------------------------------------------------------------------- /src/structjson.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2017, Google, Inc. 3 | * Licensed under the Apache License, Version 2.0 (the "License"); 4 | * you may not use this file except in compliance with the License. 5 | * You may obtain a copy of the License at 6 | * 7 | * http://www.apache.org/licenses/LICENSE-2.0 8 | * 9 | * Unless required by applicable law or agreed to in writing, software 10 | * distributed under the License is distributed on an "AS IS" BASIS, 11 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | * See the License for the specific language governing permissions and 13 | * limitations under the License. 14 | */ 15 | 16 | /** 17 | * @fileoverview Utilities for converting between JSON and goog.protobuf.Struct 18 | * proto. 19 | */ 20 | 21 | 'use strict'; 22 | 23 | function jsonToStructProto(json) { 24 | const fields = {}; 25 | for (const k in json) { 26 | fields[k] = jsonValueToProto(json[k]); 27 | } 28 | 29 | return { fields }; 30 | } 31 | 32 | const JSON_SIMPLE_TYPE_TO_PROTO_KIND_MAP = { 33 | [typeof 0]: 'numberValue', 34 | [typeof '']: 'stringValue', 35 | [typeof false]: 'boolValue', 36 | }; 37 | 38 | const JSON_SIMPLE_VALUE_KINDS = new Set(['numberValue', 'stringValue', 'boolValue']); 39 | 40 | function jsonValueToProto(value) { 41 | const valueProto = {}; 42 | 43 | if (value === null) { 44 | valueProto.kind = 'nullValue'; 45 | valueProto.nullValue = 'NULL_VALUE'; 46 | } else if (value instanceof Array) { 47 | valueProto.kind = 'listValue'; 48 | valueProto.listValue = { values: value.map(jsonValueToProto) }; 49 | } else if (typeof value === 'object') { 50 | valueProto.kind = 'structValue'; 51 | valueProto.structValue = jsonToStructProto(value); 52 | } else if (typeof value in JSON_SIMPLE_TYPE_TO_PROTO_KIND_MAP) { 53 | const kind = JSON_SIMPLE_TYPE_TO_PROTO_KIND_MAP[typeof value]; 54 | valueProto.kind = kind; 55 | valueProto[kind] = value; 56 | } else { 57 | console.warn('Unsupported value type ', typeof value); 58 | } 59 | return valueProto; 60 | } 61 | 62 | function structProtoToJson(proto) { 63 | if (!proto || !proto.fields) { 64 | return {}; 65 | } 66 | const json = {}; 67 | for (const k in proto.fields) { 68 | json[k] = valueProtoToJson(proto.fields[k]); 69 | } 70 | return json; 71 | } 72 | 73 | function valueProtoToJson(proto) { 74 | if (!proto || !proto.kind) { 75 | return null; 76 | } 77 | 78 | if (JSON_SIMPLE_VALUE_KINDS.has(proto.kind)) { 79 | return proto[proto.kind]; 80 | } else if (proto.kind === 'nullValue') { 81 | return null; 82 | } else if (proto.kind === 'listValue') { 83 | if (!proto.listValue || !proto.listValue.values) { 84 | console.warn('Invalid JSON list value proto: ', JSON.stringify(proto)); 85 | } 86 | return proto.listValue.values.map(valueProtoToJson); 87 | } else if (proto.kind === 'structValue') { 88 | return structProtoToJson(proto.structValue); 89 | } else { 90 | console.warn('Unsupported JSON value proto kind: ', proto.kind); 91 | return null; 92 | } 93 | } 94 | 95 | module.exports = { 96 | jsonToStructProto, 97 | structProtoToJson, 98 | }; 99 | 100 | /* eslint require-jsdoc: off, guard-for-in: off */ 101 | -------------------------------------------------------------------------------- /src/util.js: -------------------------------------------------------------------------------- 1 | const hasha = require('hasha'); 2 | const uuidv1 = require('uuid/v1'); 3 | const debug = require('debug')('dialogflow-middleware'); 4 | 5 | /* 6 | Botkit allows patterns to be an array or a comma separated string containing a list of regular expressions. 7 | This function converts regex, string, or array of either into an array of RexExp. 8 | */ 9 | exports.makeArrayOfRegex = function(data) { 10 | const patterns = []; 11 | 12 | if (typeof data === 'string') { 13 | data = data.split(','); 14 | } 15 | 16 | if (data instanceof RegExp) { 17 | return [data]; 18 | } 19 | 20 | for (const item of data) { 21 | if (item instanceof RegExp) { 22 | patterns.push(item); 23 | } else { 24 | patterns.push(new RegExp('^' + item + '$', 'i')); 25 | } 26 | } 27 | return patterns; 28 | }; 29 | 30 | /** 31 | * Create a session ID using a hash of fields on the message. 32 | * 33 | * The Sessionid is an md5 hash of select message object properties, concatenated together. 34 | * In the event the message object doesn't have those object properties, use random uuid. 35 | * 36 | * @param {object} config - the configuration set on the middleware 37 | * @param {object} message - a message object 38 | * @return {string} session identifier 39 | */ 40 | exports.generateSessionId = function(config, message) { 41 | let props; 42 | 43 | if (typeof config.sessionIdProps === 'string') { 44 | props = [config.sessionIdProps]; 45 | } else { 46 | props = config.sessionIdProps; 47 | } 48 | 49 | const hashElements = props 50 | .map(x => { 51 | if (message[x]) return message[x].trim(); 52 | }) 53 | .filter(x => typeof x === 'string'); 54 | 55 | debug( 56 | 'generateSessionId using props %j. Values on this message are %j', 57 | props, 58 | hashElements 59 | ); 60 | if (hashElements.length > 0) { 61 | return hasha(hashElements.join(''), { algorithm: 'md5' }); 62 | } else { 63 | return uuidv1(); 64 | } 65 | }; 66 | -------------------------------------------------------------------------------- /test/credentials.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "service_account", 3 | "project_id": "abc123", 4 | "private_key_id": "abc123", 5 | "private_key": "abc123", 6 | "client_email": "abc123", 7 | "client_id": "abc123", 8 | "auth_uri": "https://accounts.google.com/o/oauth2/auth", 9 | "token_uri": "https://accounts.google.com/o/oauth2/token", 10 | "auth_provider_x509_cert_url": "https://www.googleapis.com/oauth2/v1/certs", 11 | "client_x509_cert_url": "https://www.googleapis.com/robot/v1/metadata/x509/abc123" 12 | } 13 | -------------------------------------------------------------------------------- /test/dialogflow-mock-en.js: -------------------------------------------------------------------------------- 1 | class SessionsClient { 2 | constructor(opts) { 3 | this.opts = opts; 4 | } 5 | 6 | sessionPath() { 7 | return 'path123'; 8 | } 9 | 10 | detectIntent(request, cb) { 11 | cb(null, { 12 | responseId: '7f9da300-c16a-48be-b52e-f09157d34215', 13 | queryResult: { 14 | fulfillmentMessages: [ 15 | { 16 | platform: 'PLATFORM_UNSPECIFIED', 17 | text: { 18 | text: ['Okay how many apples?'], 19 | }, 20 | message: 'text', 21 | }, 22 | ], 23 | outputContexts: [], // incoming message from chat platform, before middleware processing 24 | queryText: 'I need apples', 25 | speechRecognitionConfidence: 0, 26 | action: 'pickFruit', 27 | parameters: { 28 | fields: { 29 | fruits: { 30 | stringValue: 'apple', 31 | kind: 'stringValue', 32 | }, 33 | }, 34 | }, 35 | allRequiredParamsPresent: true, 36 | fulfillmentText: 'Okay how many apples?', 37 | webhookSource: '', 38 | webhookPayload: null, 39 | intent: { 40 | inputContextNames: [], 41 | events: [], 42 | trainingPhrases: [], 43 | outputContexts: [], 44 | parameters: [], 45 | messages: [], 46 | defaultResponsePlatforms: [], 47 | followupIntentInfo: [], 48 | name: 'projects/botkit-dialogflow/agent/intents/4f01bbf2-41d7-41cc-9c9f-0969a8fa588c', 49 | displayName: 'add-to-list', 50 | priority: 0, 51 | isFallback: false, 52 | webhookState: 'WEBHOOK_STATE_UNSPECIFIED', 53 | action: '', 54 | resetContexts: false, 55 | rootFollowupIntentName: '', 56 | parentFollowupIntentName: '', 57 | mlDisabled: false, 58 | }, 59 | intentDetectionConfidence: 1, 60 | diagnosticInfo: { 61 | fields: {}, 62 | }, 63 | languageCode: 'en', 64 | }, 65 | webhookStatus: null, 66 | }); 67 | } 68 | } 69 | 70 | module.exports.SessionsClient = SessionsClient; 71 | -------------------------------------------------------------------------------- /test/dialogflow-mock-en2.js: -------------------------------------------------------------------------------- 1 | class SessionsClient { 2 | constructor(opts) { 3 | this.opts = opts; 4 | } 5 | 6 | sessionPath() { 7 | return 'path123'; 8 | } 9 | 10 | detectIntent(request, cb) { 11 | cb(null, { 12 | responseId: '261d37f0-34ee-11e8-bcca-67db967c2594', 13 | queryResult: { 14 | fulfillmentMessages: [ 15 | { 16 | platform: 'PLATFORM_UNSPECIFIED', 17 | text: { 18 | text: ['Good day!'], 19 | }, 20 | message: 'text', 21 | }, 22 | ], 23 | outputContexts: [], 24 | queryText: 'hi', 25 | speechRecognitionConfidence: 0, 26 | action: 'hello-intent', 27 | parameters: { 28 | fields: {}, 29 | }, 30 | allRequiredParamsPresent: true, 31 | fulfillmentText: 'Good day!', 32 | webhookSource: '', 33 | webhookPayload: null, 34 | intent: { 35 | inputContextNames: [], 36 | events: [], 37 | trainingPhrases: [], 38 | outputContexts: [], 39 | parameters: [], 40 | messages: [], 41 | defaultResponsePlatforms: [], 42 | followupIntentInfo: [], 43 | name: 'projects/botkit-middleware/agent/intents/a6bd6dd4-b934-4dc2-ac84-fee6b4c428d5', 44 | displayName: 'hello-intent', 45 | priority: 0, 46 | isFallback: false, 47 | webhookState: 'WEBHOOK_STATE_UNSPECIFIED', 48 | action: '', 49 | resetContexts: false, 50 | rootFollowupIntentName: '', 51 | parentFollowupIntentName: '', 52 | mlDisabled: false, 53 | }, 54 | intentDetectionConfidence: 1, 55 | diagnosticInfo: { 56 | fields: {}, 57 | }, 58 | languageCode: 'en', 59 | }, 60 | webhookStatus: null, 61 | }); 62 | } 63 | } 64 | 65 | module.exports.SessionsClient = SessionsClient; 66 | -------------------------------------------------------------------------------- /test/dialogflow-mock-fr.js: -------------------------------------------------------------------------------- 1 | class SessionsClient { 2 | constructor(opts) { 3 | this.opts = opts; 4 | } 5 | 6 | sessionPath() { 7 | return 'path123'; 8 | } 9 | 10 | detectIntent(request, cb) { 11 | cb(null, { 12 | responseId: '7cfc3ba7-cf87-4319-8c7a-0ba2f598e813', 13 | queryResult: { 14 | fulfillmentMessages: [ 15 | { 16 | platform: 'PLATFORM_UNSPECIFIED', 17 | text: { 18 | text: ['comment vas-tu aujourd\'hui'], 19 | }, 20 | message: 'text', 21 | }, 22 | ], 23 | outputContexts: [], 24 | queryText: 'bonjour', 25 | speechRecognitionConfidence: 0, 26 | action: 'hello-intent', 27 | parameters: { 28 | fields: {}, 29 | }, 30 | allRequiredParamsPresent: true, 31 | fulfillmentText: 'comment vas-tu aujourd\'hui', 32 | webhookSource: '', 33 | webhookPayload: null, 34 | intent: { 35 | inputContextNames: [], 36 | events: [], 37 | trainingPhrases: [], 38 | outputContexts: [], 39 | parameters: [], 40 | messages: [], 41 | defaultResponsePlatforms: [], 42 | followupIntentInfo: [], 43 | name: 'projects/botkit-middleware/agent/intents/a6bd6dd4-b934-4dc2-ac84-fee6b4c428d5', 44 | displayName: 'hello-intent', 45 | priority: 0, 46 | isFallback: false, 47 | webhookState: 'WEBHOOK_STATE_UNSPECIFIED', 48 | action: '', 49 | resetContexts: false, 50 | rootFollowupIntentName: '', 51 | parentFollowupIntentName: '', 52 | mlDisabled: false, 53 | }, 54 | intentDetectionConfidence: 1, 55 | diagnosticInfo: { 56 | fields: {}, 57 | }, 58 | languageCode: 'fr', 59 | }, 60 | webhookStatus: null, 61 | }); 62 | } 63 | } 64 | 65 | module.exports.SessionsClient = SessionsClient; 66 | -------------------------------------------------------------------------------- /test/test.action.js: -------------------------------------------------------------------------------- 1 | const Botkit = require('botkit'); 2 | const mockery = require('mockery'); 3 | const expect = require('chai').expect; 4 | const clone = require('clone'); 5 | 6 | describe('middleware.action()', function() { 7 | // Botkit params 8 | const controller = Botkit.slackbot(); 9 | const bot = controller.spawn({ 10 | token: 'abc123', 11 | }); 12 | 13 | // Setup message objects 14 | const defaultMessage = { 15 | type: 'direct_message', 16 | text: 'I need apples', 17 | user: 'test_user', 18 | channel: 'test_channel', 19 | }; 20 | 21 | let middleware; 22 | 23 | before(function() { 24 | mockery.enable({ 25 | useCleanCache: true, 26 | warnOnUnregistered: false, 27 | warnOnReplace: false, 28 | }); 29 | mockery.registerSubstitute('dialogflow', '../test/dialogflow-mock-en'); 30 | 31 | // Dialogflow middleware 32 | middleware = require('../src/botkit-middleware-dialogflow')({ 33 | version: 'v2', 34 | keyFilename: __dirname + '/credentials.json', 35 | minimumConfidence: 0.5 36 | }); 37 | }); 38 | 39 | after(function() { 40 | mockery.disable(); 41 | }); 42 | 43 | it('should trigger action returned in Dialogflow response', function(done) { 44 | const message = clone(defaultMessage); 45 | middleware.receive(bot, message, function(err, response) { 46 | const action = middleware.action(['pickFruit'], message); 47 | expect(action).is.true; 48 | done(); 49 | }); 50 | }); 51 | 52 | it('should not trigger action if confidence is not high enough', function(done) { 53 | const message = clone(defaultMessage); 54 | middleware.receive(bot, message, function(err, response) { 55 | const msg = clone(message); 56 | msg.confidence = 0.1; // under default threshold of 0.5 57 | 58 | const action = middleware.action(['pickFruit'], msg); 59 | expect(action).is.false; 60 | done(); 61 | }); 62 | }); 63 | 64 | it('should match action as a string', function(done) { 65 | const message = clone(defaultMessage); 66 | middleware.receive(bot, message, function(err, response) { 67 | const action = middleware.action('pickFruit', message); 68 | expect(action).is.true; 69 | done(); 70 | }); 71 | }); 72 | 73 | it('should match action as a string containing regex', function(done) { 74 | const message = clone(defaultMessage); 75 | middleware.receive(bot, message, function(err, response) { 76 | const action = middleware.action('pick(.*)', message); 77 | expect(action).is.true; 78 | done(); 79 | }); 80 | }); 81 | 82 | it('should match action as a string of mixed case', function(done) { 83 | const message = clone(defaultMessage); 84 | middleware.receive(bot, message, function(err, response) { 85 | const action = middleware.action('pickFRUIT', message); 86 | expect(action).is.true; 87 | done(); 88 | }); 89 | }); 90 | 91 | it('should not match action as a string if only a substring matches', function(done) { 92 | const message = clone(defaultMessage); 93 | middleware.receive(bot, message, function(err, response) { 94 | const action = middleware.action('pick', message); 95 | expect(action).is.false; 96 | done(); 97 | }); 98 | }); 99 | 100 | it('should match action as a RegExp', function(done) { 101 | const message = clone(defaultMessage); 102 | middleware.receive(bot, message, function(err, response) { 103 | const action = middleware.action(/^pick/, message); 104 | expect(action).is.true; 105 | done(); 106 | }); 107 | }); 108 | 109 | it('should match action as a string in an array', function(done) { 110 | const message = clone(defaultMessage); 111 | middleware.receive(bot, message, function(err, response) { 112 | const action = middleware.action(['blah', 'pickFruit'], message); 113 | expect(action).is.true; 114 | done(); 115 | }); 116 | }); 117 | 118 | it('should match action as a RegExp in an array', function(done) { 119 | const message = clone(defaultMessage); 120 | middleware.receive(bot, message, function(err, response) { 121 | const action = middleware.action(['blah', /^(pick)/], message); 122 | expect(action).is.true; 123 | done(); 124 | }); 125 | }); 126 | }); 127 | -------------------------------------------------------------------------------- /test/test.api.js: -------------------------------------------------------------------------------- 1 | const mockery = require('mockery'); 2 | const expect = require('chai').expect; 3 | 4 | describe('dialogflow api constructor', function() { 5 | before(function() { 6 | mockery.enable({ 7 | useCleanCache: true, 8 | warnOnUnregistered: false, 9 | warnOnReplace: false, 10 | }); 11 | }); 12 | 13 | after(function() { 14 | mockery.disable(); 15 | }); 16 | 17 | it('should pass config options through to SessionsClient constructor', function(done) { 18 | const dialogflowMock = { 19 | SessionsClient: function(opts) { 20 | return opts; 21 | }, 22 | }; 23 | mockery.registerMock('dialogflow', dialogflowMock); 24 | const api = require('../src/api'); 25 | 26 | const config = { 27 | version: 'v2', 28 | projectId: 'test', 29 | credentials: { x: 'y' }, 30 | email: 'a@b.com', 31 | port: 1234, 32 | promise: Promise, 33 | servicePath: 'abc', 34 | }; 35 | const app = api(config); 36 | 37 | expect(app.config).to.deep.equal(config); 38 | 39 | mockery.deregisterMock('dialogflow'); 40 | done(); 41 | }); 42 | }); 43 | -------------------------------------------------------------------------------- /test/test.grpc.js: -------------------------------------------------------------------------------- 1 | const Botkit = require('botkit'); 2 | const expect = require('chai').expect; 3 | 4 | describe('grpc layer', function() { 5 | // Botkit params 6 | const controller = Botkit.slackbot(); 7 | const bot = controller.spawn({ 8 | token: 'abc123', 9 | }); 10 | 11 | // Dialogflow middleware 12 | const middleware = require('../src/botkit-middleware-dialogflow')({ 13 | version: 'v2', 14 | keyFilename: __dirname + '/credentials.json', 15 | }); 16 | 17 | // incoming message from chat platform, before middleware processing 18 | const message = { 19 | type: 'direct_message', 20 | channel: 'D88V7BL2F', 21 | user: 'U891YCT42', 22 | text: 'hi', 23 | }; 24 | 25 | // response from DialogFlow api call 26 | const apiResponse = { 27 | responseId: '261d37f0-34ee-11e8-bcca-67db967c2594', 28 | queryResult: { 29 | // incoming message from chat platform, before middleware processing 30 | fulfillmentMessages: [ 31 | { 32 | platform: 'PLATFORM_UNSPECIFIED', 33 | text: { 34 | // incoming message from chat platform, before middleware processing 35 | text: ['Good day!'], 36 | }, 37 | message: 'text', 38 | }, 39 | ], 40 | outputContexts: [], 41 | queryText: 'hi', 42 | speechRecognitionConfidence: 0, 43 | action: 'hello-intent', 44 | parameters: { 45 | fields: {}, 46 | }, 47 | allRequiredParamsPresent: true, 48 | fulfillmentText: 'Good day!', 49 | webhookSource: '', 50 | webhookPayload: null, 51 | intent: { 52 | inputContextNames: [], 53 | events: [], 54 | trainingPhrases: [], 55 | outputContexts: [], 56 | parameters: [], 57 | messages: [], 58 | defaultResponsePlatforms: [], 59 | followupIntentInfo: [], 60 | name: 'projects/botkit-middleware/agent/intents/a6bd6dd4-b934-4dc2-ac84-fee6b4c428d5', 61 | displayName: 'hello-intent', 62 | priority: 0, 63 | isFallback: false, 64 | webhookState: 'WEBHOOK_STATE_UNSPECIFIED', 65 | action: '', 66 | resetContexts: false, 67 | rootFollowupIntentName: '', 68 | parentFollowupIntentName: '', 69 | mlDisabled: false, 70 | }, 71 | intentDetectionConfidence: 1, 72 | diagnosticInfo: { 73 | fields: {}, 74 | }, 75 | languageCode: 'en', 76 | }, 77 | webhookStatus: null, 78 | }; 79 | 80 | // Mock request 81 | const formattedSession = middleware.api.app.sessionPath('[PROJECT]', '[SESSION]'); 82 | const queryInput = {}; 83 | const request = { 84 | session: formattedSession, 85 | queryInput: queryInput, 86 | }; 87 | 88 | // Mock Grpc layer 89 | middleware.api.app._innerApiCalls.detectIntent = mockSimpleGrpcMethod(request, apiResponse); 90 | 91 | it('should make a call to Dialogflow and return an annotated message', function(done) { 92 | // eslint-disable-next-line 93 | middleware.receive(bot, message, function(err, response) { 94 | expect(message).to.deep.equal({ 95 | type: 'direct_message', 96 | channel: 'D88V7BL2F', 97 | user: 'U891YCT42', 98 | text: 'hi', 99 | intent: 'hello-intent', 100 | entities: {}, 101 | action: 'hello-intent', 102 | fulfillment: { 103 | text: 'Good day!', 104 | messages: [ 105 | { 106 | platform: 'PLATFORM_UNSPECIFIED', 107 | text: { 108 | text: ['Good day!'], 109 | }, 110 | message: 'text', 111 | }, 112 | ], 113 | }, 114 | confidence: 1, 115 | nlpResponse: apiResponse, 116 | }); 117 | done(); 118 | }); 119 | }); 120 | }); 121 | 122 | /** 123 | * Mocks a gRPC method call. 124 | * 125 | * @param {object} expectedRequest - the mocked request 126 | * @param {object} response - the mocked response 127 | * @param {error} error - the mocked error 128 | * @return {function} callback function 129 | */ 130 | function mockSimpleGrpcMethod(expectedRequest, response, error) { 131 | return function(actualRequest, options, callback) { 132 | if (error) { 133 | callback(error); 134 | } else if (response) { 135 | callback(null, response); 136 | } else { 137 | callback(null); 138 | } 139 | }; 140 | } 141 | -------------------------------------------------------------------------------- /test/test.hears.js: -------------------------------------------------------------------------------- 1 | const Botkit = require('botkit'); 2 | const mockery = require('mockery'); 3 | const expect = require('chai').expect; 4 | const clone = require('clone'); 5 | 6 | describe('middleware.hears()', function() { 7 | // Botkit params 8 | const controller = Botkit.slackbot(); 9 | const bot = controller.spawn({ 10 | token: 'abc123', 11 | }); 12 | 13 | // Setup message objects 14 | const defaultMessage = { 15 | type: 'direct_message', 16 | text: 'hi', 17 | user: 'test_user', 18 | channel: 'test_channel', 19 | }; 20 | 21 | let middleware; 22 | 23 | before(function() { 24 | mockery.enable({ 25 | useCleanCache: true, 26 | warnOnUnregistered: false, 27 | warnOnReplace: false, 28 | }); 29 | 30 | mockery.registerSubstitute('dialogflow', '../test/dialogflow-mock-en2'); 31 | 32 | // Dialogflow middleware 33 | middleware = require('../src/botkit-middleware-dialogflow')({ 34 | version: 'v2', 35 | keyFilename: __dirname + '/credentials.json', 36 | minimumConfidence: 0.5 37 | }); 38 | }); 39 | 40 | after(function() { 41 | mockery.disable(); 42 | }); 43 | 44 | it('should hear intent returned in Dialogflow response', function(done) { 45 | const message = clone(defaultMessage); 46 | middleware.receive(bot, message, function(err, response) { 47 | const heard = middleware.hears(['hello-intent'], message); 48 | expect(heard).is.true; 49 | done(); 50 | }); 51 | }); 52 | 53 | it('should not hear intent if confidence is not high enough', function(done) { 54 | const message = clone(defaultMessage); 55 | middleware.receive(bot, message, function(err, response) { 56 | const msg = clone(message); 57 | msg.confidence = 0.1; // under default threshold of 0.5 58 | 59 | const heard = middleware.hears(['hello-intent'], msg); 60 | expect(heard).is.false; 61 | done(); 62 | }); 63 | }); 64 | 65 | it('should match intent as a string', function(done) { 66 | const message = clone(defaultMessage); 67 | middleware.receive(bot, message, function(err, response) { 68 | const heard = middleware.hears('hello-intent', message); 69 | expect(heard).is.true; 70 | done(); 71 | }); 72 | }); 73 | 74 | it('should match intent as a string containing regex', function(done) { 75 | const message = clone(defaultMessage); 76 | middleware.receive(bot, message, function(err, response) { 77 | const heard = middleware.hears('hello(.*)', message); 78 | expect(heard).is.true; 79 | done(); 80 | }); 81 | }); 82 | 83 | it('should match intent as a string of mixed case', function(done) { 84 | const message = clone(defaultMessage); 85 | middleware.receive(bot, message, function(err, response) { 86 | const heard = middleware.hears('HELLO-intent', message); 87 | expect(heard).is.true; 88 | done(); 89 | }); 90 | }); 91 | 92 | it('should not match intent as a string if only a substring matches', function(done) { 93 | const message = clone(defaultMessage); 94 | middleware.receive(bot, message, function(err, response) { 95 | const heard = middleware.hears('hello-in', message); 96 | expect(heard).is.false; 97 | done(); 98 | }); 99 | }); 100 | 101 | it('should match intent as a RegExp', function(done) { 102 | const message = clone(defaultMessage); 103 | middleware.receive(bot, message, function(err, response) { 104 | const heard = middleware.hears(/^HEl.*/i, message); 105 | expect(heard).is.true; 106 | done(); 107 | }); 108 | }); 109 | 110 | it('should match intent as a string in an array', function(done) { 111 | const message = clone(defaultMessage); 112 | middleware.receive(bot, message, function(err, response) { 113 | const heard = middleware.hears(['blah', 'hello-intent'], message); 114 | expect(heard).is.true; 115 | done(); 116 | }); 117 | }); 118 | 119 | it('should match intent as a RegExp in an array', function(done) { 120 | const message = clone(defaultMessage); 121 | middleware.receive(bot, message, function(err, response) { 122 | const heard = middleware.hears(['blah', /^(hello)/], message); 123 | expect(heard).is.true; 124 | done(); 125 | }); 126 | }); 127 | }); 128 | -------------------------------------------------------------------------------- /test/test.options.js: -------------------------------------------------------------------------------- 1 | const mockery = require('mockery'); 2 | const expect = require('chai').expect; 3 | 4 | describe('options parser', function() { 5 | before(function() { 6 | mockery.enable({ 7 | useCleanCache: true, 8 | warnOnUnregistered: false, 9 | warnOnReplace: false, 10 | }); 11 | }); 12 | 13 | afterEach(() => { 14 | delete process.env.DIALOGFLOW_CLIENT_EMAIL; 15 | delete process.env.DIALOGFLOW_PRIVATE_KEY; 16 | delete process.env.DIALOGFLOW_PROJECT_ID; 17 | }); 18 | 19 | after(function() { 20 | mockery.disable(); 21 | }); 22 | 23 | it('should throw if v1 and token is missing', function(done) { 24 | const config = { version: 'v1' }; 25 | 26 | const checkOptions = require('../src/options').checkOptions; 27 | expect(() => checkOptions(config)).to.throw(Error, 'Dialogflow token must be provided for v1.'); 28 | done(); 29 | }); 30 | 31 | it('should throw if v2 and keyFilename and env variables are missing', function(done) { 32 | const config = { version: 'v2' }; 33 | 34 | const checkOptions = require('../src/options').checkOptions; 35 | expect(() => checkOptions(config)).to.throw( 36 | Error, 37 | 'projectId and credentials required, either via keyFile or environment variables.' 38 | ); 39 | done(); 40 | }); 41 | 42 | it('should throw if v2 and keyFilename and env variables are both supplied', function(done) { 43 | const config = { version: 'v2', keyFilename: __dirname + '/credentials.json' }; 44 | process.env.DIALOGFLOW_CLIENT_EMAIL = 'testemail'; 45 | process.env.DIALOGFLOW_PRIVATE_KEY = 'testkey'; 46 | process.env.DIALOGFLOW_PROJECT_ID = 'testproject'; 47 | 48 | const checkOptions = require('../src/options').checkOptions; 49 | expect(() => checkOptions(config)).to.throw( 50 | Error, 51 | 'Invalid configuration - cannot provide both keyfile and explicit credentials.' 52 | ); 53 | done(); 54 | }); 55 | 56 | it('should set the projectId if passed explicitly as an option', function(done) { 57 | const checkOptions = require('../src/options').checkOptions; 58 | 59 | const config = { 60 | version: 'v2', 61 | projectId: 'test', 62 | credentials: {}, 63 | }; 64 | 65 | const options = checkOptions(config); 66 | expect(options.projectId).to.equal(config.projectId); 67 | done(); 68 | }); 69 | 70 | it('should set the projectId if passed implicitly via keyfile', function(done) { 71 | const checkOptions = require('../src/options').checkOptions; 72 | 73 | const config = { 74 | version: 'v2', 75 | keyFilename: __dirname + '/credentials.json', 76 | }; 77 | const options = checkOptions(config); 78 | 79 | expect(options.projectId).to.equal('abc123'); 80 | done(); 81 | }); 82 | 83 | it('should set credentials and projectID through environment variables', function(done) { 84 | const checkOptions = require('../src/options').checkOptions; 85 | 86 | const config = { 87 | version: 'v2', 88 | projectId: 'testproject', 89 | }; 90 | 91 | process.env.DIALOGFLOW_CLIENT_EMAIL = 'testemail'; 92 | process.env.DIALOGFLOW_PRIVATE_KEY = 'testkey'; 93 | process.env.DIALOGFLOW_PROJECT_ID = 'testproject'; 94 | 95 | const options = checkOptions(config); 96 | 97 | expect(options.credentials).to.deep.equal({ 98 | private_key: 'testkey', 99 | client_email: 'testemail', 100 | }); 101 | 102 | expect(options.projectId).to.equal('testproject'); 103 | 104 | done(); 105 | }); 106 | }); 107 | -------------------------------------------------------------------------------- /test/test.receive.js: -------------------------------------------------------------------------------- 1 | const Botkit = require('botkit'); 2 | const mockery = require('mockery'); 3 | const expect = require('chai').expect; 4 | const clone = require('clone'); 5 | 6 | describe('middleware.receive() normalization into the message object', function() { 7 | // Botkit params 8 | const controller = Botkit.slackbot(); 9 | const bot = controller.spawn({ 10 | token: 'abc123', 11 | }); 12 | 13 | // incoming message from chat platform, before middleware processing 14 | const defaultMessage = { 15 | type: 'direct_message', 16 | channel: 'D88V7BL2F', 17 | user: 'U891YCT42', 18 | text: 'I need apples', 19 | }; 20 | 21 | let middleware; 22 | 23 | before(function() { 24 | mockery.enable({ 25 | useCleanCache: true, 26 | warnOnUnregistered: false, 27 | warnOnReplace: false, 28 | }); 29 | 30 | mockery.registerSubstitute('dialogflow', '../test/dialogflow-mock-en'); 31 | 32 | middleware = require('../src/botkit-middleware-dialogflow')({ 33 | version: 'v2', 34 | keyFilename: __dirname + '/credentials.json', 35 | }); 36 | }); 37 | 38 | after(function() { 39 | mockery.disable(); 40 | }); 41 | 42 | it('should make a call to the Dialogflow api', function(done) { 43 | const message = clone(defaultMessage); 44 | middleware.receive(bot, message, function(err, response) { 45 | expect(err).is.undefined; 46 | done(); 47 | }); 48 | }); 49 | 50 | it('should add custom fields to the message object', function(done) { 51 | const message = clone(defaultMessage); 52 | middleware.receive(bot, message, function(err, response) { 53 | expect(err).is.undefined; 54 | expect(message) 55 | .to.be.an('object') 56 | .that.includes.all.keys( 57 | 'nlpResponse', 58 | 'intent', 59 | 'entities', 60 | 'fulfillment', 61 | 'confidence', 62 | 'action' 63 | ); 64 | done(); 65 | }); 66 | }); 67 | 68 | it('should correctly add fields to the message', function(done) { 69 | const message = clone(defaultMessage); 70 | middleware.receive(bot, message, function(err, response) { 71 | expect(message).to.deep.include({ 72 | type: 'direct_message', 73 | channel: 'D88V7BL2F', 74 | user: 'U891YCT42', 75 | text: 'I need apples', 76 | intent: 'add-to-list', 77 | entities: { 78 | fruits: 'apple', 79 | }, 80 | action: 'pickFruit', 81 | fulfillment: { 82 | text: 'Okay how many apples?', 83 | messages: [ 84 | { 85 | platform: 'PLATFORM_UNSPECIFIED', 86 | text: { 87 | text: ['Okay how many apples?'], 88 | }, 89 | message: 'text', 90 | }, 91 | ], 92 | }, 93 | confidence: 1, 94 | }); 95 | done(); 96 | }); 97 | }); 98 | }); 99 | -------------------------------------------------------------------------------- /test/test.receive_language.js: -------------------------------------------------------------------------------- 1 | const Botkit = require('botkit'); 2 | const sinon = require('sinon'); 3 | const mockery = require('mockery'); 4 | const expect = require('chai').expect; 5 | const clone = require('clone'); 6 | 7 | describe('receive() text language support', function() { 8 | // Botkit params 9 | const controller = Botkit.slackbot(); 10 | const bot = controller.spawn({ 11 | token: 'abc123', 12 | }); 13 | 14 | // Setup message objects 15 | const defaultMessage = { 16 | type: 'direct_message', 17 | text: 'hi', 18 | user: 'test_user', 19 | channel: 'test_channel', 20 | }; 21 | 22 | const frenchMessage = { 23 | type: 'direct_message', 24 | text: 'bonjour', 25 | lang: 'fr', 26 | user: 'test_user', 27 | channel: 'test_channel', 28 | }; 29 | 30 | let middleware; 31 | 32 | before(function() { 33 | mockery.enable({ 34 | useCleanCache: true, 35 | warnOnUnregistered: false, 36 | warnOnReplace: false, 37 | }); 38 | 39 | mockery.registerSubstitute('dialogflow', '../test/dialogflow-mock-fr'); 40 | 41 | // Dialogflow middleware 42 | middleware = require('../src/botkit-middleware-dialogflow')({ 43 | version: 'v2', 44 | keyFilename: __dirname + '/credentials.json', 45 | }); 46 | }); 47 | 48 | after(function() { 49 | mockery.disable(); 50 | }); 51 | 52 | it('should call the Dialogflow API with en if no language is specified on message object', function(done) { 53 | const spy = sinon.spy(middleware.api.app, 'detectIntent'); 54 | 55 | middleware.receive(bot, clone(defaultMessage), function(err, response) { 56 | expect(spy.args[0][0].queryInput.text.languageCode).is.equal('en'); 57 | 58 | spy.restore(); 59 | done(); 60 | }); 61 | }); 62 | 63 | it('should pass lang set on the messsage through to the Dialogflow API call.', function(done) { 64 | const spy = sinon.spy(middleware.api.app, 'detectIntent'); 65 | 66 | middleware.receive(bot, clone(frenchMessage), function(err, response) { 67 | expect(spy.args[0][0].queryInput.text.languageCode).is.equal('fr'); 68 | 69 | spy.restore(); 70 | done(); 71 | }); 72 | }); 73 | 74 | it('should flow the language set on the message object through to the response', function(done) { 75 | const msg = clone(frenchMessage); 76 | const spy = sinon.spy(middleware.api.app, 'detectIntent'); 77 | 78 | middleware.receive(bot, msg, function(err, response) { 79 | expect(msg.lang).is.equal('fr'); 80 | 81 | spy.restore(); 82 | done(); 83 | }); 84 | }); 85 | }); 86 | -------------------------------------------------------------------------------- /test/test.receive_other.js: -------------------------------------------------------------------------------- 1 | const Botkit = require('botkit'); 2 | const mockery = require('mockery'); 3 | const expect = require('chai').expect; 4 | 5 | describe('middleware.receive() message types that should skip dialogflow', function() { 6 | // Botkit params 7 | const controller = Botkit.slackbot(); 8 | const bot = controller.spawn({ 9 | token: 'abc123', 10 | }); 11 | 12 | let middleware; 13 | 14 | before(function() { 15 | mockery.enable({ 16 | useCleanCache: true, 17 | warnOnUnregistered: false, 18 | warnOnReplace: false, 19 | }); 20 | 21 | mockery.registerSubstitute('dialogflow', '../test/dialogflow-mock-en'); 22 | 23 | // Dialogflow middleware 24 | middleware = require('../src/botkit-middleware-dialogflow')({ 25 | version: 'v2', 26 | keyFilename: __dirname + '/credentials.json', 27 | }); 28 | }); 29 | 30 | after(function() { 31 | mockery.disable(); 32 | }); 33 | 34 | it('should be a no-op if text field is missing', function(done) { 35 | const message = { 36 | type: 'user_typing', 37 | }; 38 | 39 | middleware.receive(bot, message, function(err, response) { 40 | expect(response).is.undefined; 41 | done(); 42 | }); 43 | }); 44 | 45 | it('should be a no-op if message is echo', function(done) { 46 | const message = { 47 | type: 'is_echo', 48 | }; 49 | 50 | middleware.receive(bot, message, function(err, response) { 51 | expect(response).is.undefined; 52 | done(); 53 | }); 54 | }); 55 | 56 | it('should be a no-op if text field is missing', function(done) { 57 | const message = { 58 | type: 'self_message', 59 | text: 'Hello!', 60 | }; 61 | 62 | middleware.receive(bot, message, function(err, response) { 63 | expect(response).is.undefined; 64 | done(); 65 | }); 66 | }); 67 | 68 | it('should be a no-op if type matches specific ignoreType config', function(done) { 69 | const bot2 = controller.spawn({ 70 | token: 'abc123', 71 | ignoreType: ['facebook_postback'], 72 | }); 73 | 74 | const message = { 75 | type: 'facebook_postback', 76 | text: 'payload', 77 | }; 78 | 79 | middleware.receive(bot2, message, function(err, response) { 80 | expect(response).is.undefined; 81 | done(); 82 | }); 83 | }); 84 | 85 | it('should be a no-op if type matches regex pattern for ignoreType config', function(done) { 86 | const bot2 = controller.spawn({ 87 | token: 'abc123', 88 | ignoreType: /^facebook/, 89 | }); 90 | 91 | const message = { 92 | type: 'facebook_postback', 93 | text: 'payload', 94 | }; 95 | 96 | middleware.receive(bot2, message, function(err, response) { 97 | expect(response).is.undefined; 98 | done(); 99 | }); 100 | }); 101 | }); 102 | -------------------------------------------------------------------------------- /test/test.util.js: -------------------------------------------------------------------------------- 1 | const expect = require('chai').expect; 2 | const util = require('../src/util'); 3 | 4 | describe('generateSessionId functions', function() { 5 | it('should return a hash when property is provided as a string', function(done) { 6 | const config = { sessionIdProps: 'blah' }; 7 | const message = { blah: 'test value' }; 8 | expect(util.generateSessionId(config, message)).to.equal('cc2d2adc8b1da820c1075a099866ceb4'); 9 | done(); 10 | }); 11 | 12 | it('should return a hash when property is provided as an array of strings', function(done) { 13 | const config = { sessionIdProps: ['blah'] }; 14 | const message = { blah: 'test value' }; 15 | expect(util.generateSessionId(config, message)).to.equal('cc2d2adc8b1da820c1075a099866ceb4'); 16 | done(); 17 | }); 18 | 19 | it('should return a hash when properties are missing, as long as at least one is available', function(done) { 20 | const config = { sessionIdProps: ['prop1', 'prop2'] }; 21 | const message = { prop1: 'test value' }; 22 | expect(util.generateSessionId(config, message)).to.equal('cc2d2adc8b1da820c1075a099866ceb4'); 23 | done(); 24 | }); 25 | 26 | it('should return a random uuid when all properties are missing from message object', function(done) { 27 | const config = { sessionIdProps: ['prop1', 'prop2'] }; 28 | const message = { prop3: 'test value' }; 29 | expect(util.generateSessionId(config, message)).to.contain('-'); // uuid's have seperators 30 | done(); 31 | }); 32 | }); 33 | -------------------------------------------------------------------------------- /test/v1/config.json: -------------------------------------------------------------------------------- 1 | { 2 | "url": "https://api.api.ai:443", 3 | "protocol": "20150910", 4 | "version": "v1" 5 | } 6 | -------------------------------------------------------------------------------- /test/v1/test.action.js: -------------------------------------------------------------------------------- 1 | const Botkit = require('botkit'); 2 | const nock = require('nock'); 3 | const expect = require('chai').expect; 4 | const clone = require('clone'); 5 | 6 | describe('v1/ action()', function() { 7 | // Dialogflow params 8 | const config = require('./config.json'); 9 | 10 | // Botkit params 11 | const controller = Botkit.slackbot(); 12 | const bot = controller.spawn(); 13 | 14 | // Dialogflow middleware 15 | const middleware = require('../../src/botkit-middleware-dialogflow')({ 16 | version: 'v1', 17 | token: 'abc', 18 | }); 19 | 20 | // incoming message from chat platform, before middleware processing 21 | const defaultMessage = { 22 | type: 'direct_message', 23 | text: 'pick an apple', 24 | user: 'test_user', 25 | channel: 'test_channel', 26 | }; 27 | 28 | // response from DialogFlow api call to /query endpoint 29 | const apiResponse = { 30 | id: '3622be70-cb49-4796-a4fa-71f16f7b5600', 31 | lang: 'en', 32 | result: { 33 | action: 'pickFruit', 34 | actionIncomplete: false, 35 | contexts: ['shop'], 36 | fulfillment: { 37 | messages: [ 38 | { 39 | platform: 'google', 40 | textToSpeech: 'Okay how many apples?', 41 | type: 'simple_response', 42 | }, 43 | { 44 | platform: 'google', 45 | textToSpeech: 'Okay. How many apples?', 46 | type: 'simple_response', 47 | }, 48 | { 49 | speech: 'Okay how many apples?', 50 | type: 0, 51 | }, 52 | ], 53 | speech: 'Okay how many apples?', 54 | }, 55 | metadata: { 56 | intentId: '21478be9-bea6-449b-bcca-c5f009c0a5a1', 57 | intentName: 'add-to-list', 58 | webhookForSlotFillingUsed: 'false', 59 | webhookUsed: 'false', 60 | }, 61 | parameters: { 62 | fruit: ['apples'], 63 | }, 64 | resolvedQuery: 'I need apples', 65 | score: 1, 66 | source: 'agent', 67 | }, 68 | sessionId: '12345', 69 | status: { 70 | code: 200, 71 | errorType: 'success', 72 | }, 73 | timestamp: '2017-09-19T21:16:44.832Z', 74 | }; 75 | 76 | beforeEach(function() { 77 | nock.disableNetConnect(); 78 | 79 | nock(config.url) 80 | .post('/' + config.version + '/query?v=' + config.protocol) 81 | .optionally() 82 | .reply(200, apiResponse); 83 | }); 84 | 85 | afterEach(function() { 86 | nock.cleanAll(); 87 | }); 88 | 89 | it('should trigger action returned in Dialogflow response', function(done) { 90 | const message = clone(defaultMessage); 91 | middleware.receive(bot, message, function(err, response) { 92 | const action = middleware.action(['pickFruit'], message); 93 | expect(action).is.true; 94 | done(); 95 | }); 96 | }); 97 | }); 98 | -------------------------------------------------------------------------------- /test/v1/test.hears.js: -------------------------------------------------------------------------------- 1 | const Botkit = require('botkit'); 2 | const nock = require('nock'); 3 | const expect = require('chai').expect; 4 | const clone = require('clone'); 5 | 6 | describe('v1/ hears()', function() { 7 | // Dialogflow params 8 | const config = require('./config.json'); 9 | 10 | // Botkit params 11 | const controller = Botkit.slackbot(); 12 | const bot = controller.spawn({ 13 | token: 'abc123', 14 | }); 15 | 16 | // Dialogflow middleware 17 | const middleware = require('../../src/botkit-middleware-dialogflow')({ 18 | version: 'v1', 19 | token: 'abc', 20 | }); 21 | 22 | // incoming message from chat platform, before middleware processing 23 | const defaultMessage = { 24 | type: 'direct_message', 25 | channel: 'D88V7BL2F', 26 | user: 'U891YCT42', 27 | text: 'hi', 28 | ts: '1522500856.000117', 29 | source_team: 'T8938ACLC', 30 | team: 'T8938ACLC', 31 | raw_message: { 32 | type: 'message', 33 | channel: 'D88V7BL2F', 34 | user: 'U891YCT42', 35 | text: 'hi', 36 | ts: '1522500856.000117', 37 | source_team: 'T8938ACLC', 38 | team: 'T8938ACLC', 39 | }, 40 | _pipeline: { stage: 'receive' }, 41 | }; 42 | 43 | // response from DialogFlow api call to /query endpoint 44 | const apiResponse = { 45 | id: '05a7ed32-6572-45a7-a27e-465959df5f9f', 46 | timestamp: '2018-03-31T14:16:54.369Z', 47 | lang: 'en', 48 | result: { 49 | source: 'agent', 50 | resolvedQuery: 'hi', 51 | action: '', 52 | actionIncomplete: false, 53 | parameters: {}, 54 | contexts: [], 55 | metadata: { 56 | intentId: 'bd8fdabb-2fd6-4018-a3a5-0c57c41f65c1', 57 | webhookUsed: 'false', 58 | webhookForSlotFillingUsed: 'false', 59 | intentName: 'hello-intent', 60 | }, 61 | fulfillment: { speech: '', messages: [{ type: 0, speech: '' }] }, 62 | score: 1, 63 | }, 64 | status: { code: 200, errorType: 'success', webhookTimedOut: false }, 65 | sessionId: '261d37f0-34ee-11e8-bcca-67db967c2594', 66 | }; 67 | 68 | beforeEach(function() { 69 | nock.disableNetConnect(); 70 | 71 | nock(config.url) 72 | .post('/' + config.version + '/query?v=' + config.protocol) 73 | .optionally() 74 | .reply(200, apiResponse); 75 | }); 76 | 77 | afterEach(function() { 78 | nock.cleanAll(); 79 | }); 80 | 81 | it('should hear intent returned in Dialogflow response', function(done) { 82 | const message = clone(defaultMessage); 83 | middleware.receive(bot, message, function(err, response) { 84 | const heard = middleware.hears(['hello-intent'], message); 85 | expect(heard).is.true; 86 | done(); 87 | }); 88 | }); 89 | }); 90 | -------------------------------------------------------------------------------- /test/v1/test.receive.js: -------------------------------------------------------------------------------- 1 | const Botkit = require('botkit'); 2 | const nock = require('nock'); 3 | const expect = require('chai').expect; 4 | const clone = require('clone'); 5 | 6 | describe('v1/ receive() text', function() { 7 | // Dialogflow params 8 | const config = require('./config.json'); 9 | 10 | // Botkit params 11 | const controller = Botkit.slackbot(); 12 | const bot = controller.spawn({ 13 | token: 'abc123', 14 | }); 15 | 16 | // Dialogflow middleware 17 | const middleware = require('../../src/botkit-middleware-dialogflow')({ 18 | version: 'v1', 19 | token: 'abc', 20 | }); 21 | 22 | // incoming message from chat platform, before middleware processing 23 | const defaultMessage = { 24 | type: 'direct_message', 25 | channel: 'D88V7BL2F', 26 | user: 'U891YCT42', 27 | text: 'hi', 28 | }; 29 | 30 | // response from DialogFlow api call to /query endpoint 31 | const expectedDfData = { 32 | id: '05a7ed32-6572-45a7-a27e-465959df5f9f', 33 | timestamp: '2018-03-31T14:16:54.369Z', 34 | lang: 'en', 35 | result: { 36 | source: 'agent', 37 | resolvedQuery: 'hi', 38 | action: '', 39 | actionIncomplete: false, 40 | parameters: {}, 41 | contexts: [], 42 | metadata: { 43 | intentId: 'bd8fdabb-2fd6-4018-a3a5-0c57c41f65c1', 44 | webhookUsed: 'false', 45 | webhookForSlotFillingUsed: 'false', 46 | intentName: 'hello-intent', 47 | }, 48 | fulfillment: { speech: '', messages: [{ type: 0, speech: '' }] }, 49 | score: 1, 50 | }, 51 | status: { code: 200, errorType: 'success', webhookTimedOut: false }, 52 | sessionId: '261d37f0-34ee-11e8-bcca-67db967c2594', 53 | }; 54 | 55 | beforeEach(function() { 56 | nock.disableNetConnect(); 57 | 58 | nock(config.url) 59 | .post('/' + config.version + '/query?v=' + config.protocol) 60 | .optionally() 61 | .reply(200, expectedDfData); 62 | }); 63 | 64 | afterEach(function() { 65 | nock.cleanAll(); 66 | }); 67 | 68 | it('should make a call to the Dialogflow api', function(done) { 69 | const message = clone(defaultMessage); 70 | middleware.receive(bot, message, function(err, response) { 71 | expect(nock.isDone()).is.true; 72 | done(); 73 | }); 74 | }); 75 | 76 | it('should add custom fields to the message object', function(done) { 77 | const message = clone(defaultMessage); 78 | middleware.receive(bot, message, function(err, response) { 79 | expect(message) 80 | .to.be.an('object') 81 | .that.includes.all.keys( 82 | 'nlpResponse', 83 | 'intent', 84 | 'entities', 85 | 'fulfillment', 86 | 'confidence', 87 | 'action' 88 | ); 89 | done(); 90 | }); 91 | }); 92 | 93 | it('should correctly include the Dialogflow API result on the nlpResponse key', function(done) { 94 | const message = clone(defaultMessage); 95 | middleware.receive(bot, message, function(err, response) { 96 | expect(message.nlpResponse).to.deep.equal(expectedDfData); 97 | done(); 98 | }); 99 | }); 100 | 101 | it('should correctly add fields to the message', function(done) { 102 | const message = clone(defaultMessage); 103 | middleware.receive(bot, message, function(err, response) { 104 | expect(message).to.deep.include({ 105 | type: 'direct_message', 106 | channel: 'D88V7BL2F', 107 | user: 'U891YCT42', 108 | text: 'hi', 109 | intent: 'hello-intent', 110 | entities: {}, 111 | action: '', 112 | fulfillment: { 113 | speech: '', 114 | messages: [ 115 | { 116 | type: 0, 117 | speech: '', 118 | }, 119 | ], 120 | }, 121 | confidence: 1, 122 | }); 123 | done(); 124 | }); 125 | }); 126 | }); 127 | -------------------------------------------------------------------------------- /test/v1/test.receive_language.js: -------------------------------------------------------------------------------- 1 | const Botkit = require('botkit'); 2 | const nock = require('nock'); 3 | const expect = require('chai').expect; 4 | const _ = require('lodash'); 5 | const clone = require('clone'); 6 | 7 | describe('v1/ receive() text language support', function() { 8 | // Botkit params 9 | const controller = Botkit.slackbot(); 10 | const bot = controller.spawn({ 11 | token: 'abc123', 12 | }); 13 | 14 | // Dialogflow middleware 15 | const middleware = require('../../src/botkit-middleware-dialogflow')({ 16 | version: 'v1', 17 | token: 'abc', 18 | }); 19 | 20 | // Setup message objects 21 | const defaultMessage = { 22 | type: 'direct_message', 23 | text: 'hi', 24 | user: 'test_user', 25 | channel: 'test_channel', 26 | }; 27 | 28 | const englishMessage = { 29 | type: 'direct_message', 30 | text: 'hi', 31 | lang: 'en', 32 | user: 'test_user', 33 | channel: 'test_channel', 34 | }; 35 | 36 | const frenchMessage = { 37 | type: 'direct_message', 38 | text: 'bonjour', 39 | lang: 'fr', 40 | user: 'test_user', 41 | channel: 'test_channel', 42 | }; 43 | 44 | // tests 45 | before(function() { 46 | nock.disableNetConnect(); 47 | }); 48 | 49 | after(function() { 50 | nock.cleanAll(); 51 | }); 52 | 53 | it('should call the Dialogflow API with en if no language is specified on message object', function(done) { 54 | nock('https://api.api.ai:443', { encodedQueryParams: true }) 55 | .post('/v1/query', _.matches({ lang: 'en', query: 'hi' })) 56 | .query({ v: '20150910' }) 57 | .reply(200); 58 | // .log(console.log); 59 | 60 | middleware.receive(bot, clone(defaultMessage), function(err, response) { 61 | expect(nock.isDone()).is.true; 62 | done(); 63 | }); 64 | }); 65 | 66 | // the language used by the nodejs client for Dialogflow is sticky over subsequent calls 67 | // Need to confirm we're resetting it. 68 | it('should call the API with correct language over subsequent calls in different languages', function(done) { 69 | nock('https://api.api.ai:443', { encodedQueryParams: true }) 70 | .post('/v1/query', _.matches({ lang: 'en', query: 'hi' })) 71 | .query({ v: '20150910' }) 72 | .reply(200); 73 | // .log(console.log); 74 | 75 | middleware.receive(bot, clone(englishMessage), function(err, response) { 76 | expect(nock.isDone()).is.true; 77 | }); 78 | 79 | nock('https://api.api.ai:443', { encodedQueryParams: true }) 80 | .post('/v1/query', _.matches({ lang: 'fr', query: 'bonjour' })) 81 | .query({ v: '20150910' }) 82 | .reply(200); 83 | // .log(console.log); 84 | 85 | middleware.receive(bot, clone(frenchMessage), function(err, response) { 86 | expect(nock.isDone()).is.true; 87 | }); 88 | 89 | nock('https://api.api.ai:443', { encodedQueryParams: true }) 90 | .post('/v1/query', _.matches({ lang: 'en', query: 'hi' })) 91 | .query({ v: '20150910' }) 92 | .reply(200); 93 | // .log(console.log); 94 | 95 | middleware.receive(bot, clone(defaultMessage), function(err, response) { 96 | expect(nock.isDone()).is.true; 97 | }); 98 | 99 | done(); 100 | }); 101 | 102 | it('should flow the language set on the message object through to the response', function(done) { 103 | nock('https://api.api.ai:443', { encodedQueryParams: true }) 104 | .post('/v1/query', _.matches({ lang: 'fr', query: 'bonjour' })) 105 | .query({ v: '20150910' }) 106 | .reply(200, { 107 | id: '7cfc3ba7-cf87-4319-8c7a-0ba2f598e813', 108 | timestamp: '2018-05-21T17:35:29.91Z', 109 | lang: 'fr', 110 | result: { 111 | source: 'agent', 112 | resolvedQuery: 'bonjour', 113 | action: '', 114 | actionIncomplete: false, 115 | parameters: {}, 116 | contexts: [], 117 | metadata: { 118 | intentId: 'bd8fdabb-2fd6-4018-a3a5-0c57c41f65c1', 119 | webhookUsed: 'false', 120 | webhookForSlotFillingUsed: 'false', 121 | intentName: 'hello-intent', 122 | }, 123 | fulfillment: { 124 | speech: 'comment vas-tu aujourd\'hui', 125 | messages: [{ type: 0, speech: 'comment vas-tu aujourd\'hui' }], 126 | }, 127 | score: 1, 128 | }, 129 | status: { code: 200, errorType: 'success' }, 130 | sessionId: '563be240-5d1d-11e8-9139-bfbb9dca30b5', 131 | }); 132 | // .log(console.log); 133 | 134 | const msg = clone(frenchMessage); 135 | middleware.receive(bot, msg, function(err, response) { 136 | expect(nock.isDone()).is.true; 137 | expect(msg.lang).is.equal('fr'); 138 | done(); 139 | }); 140 | }); 141 | }); 142 | --------------------------------------------------------------------------------