├── .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 | [](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 |
--------------------------------------------------------------------------------