├── .bluemix ├── deploy.json ├── pipeline-DEPLOY.sh ├── pipeline.yml └── toolchain.yml ├── .eslintignore ├── .eslintrc ├── .gitignore ├── .travis.yml ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── READMEarchive.md ├── channels ├── facebook │ ├── README.md │ ├── READMEarchive.md │ ├── batched_messages │ │ └── index.js │ ├── multiple_post │ │ └── index.js │ ├── post │ │ └── index.js │ ├── receive │ │ └── index.js │ └── setup.sh └── slack │ ├── README.md │ ├── READMEarchive.md │ ├── deploy │ └── index.js │ ├── engage_oauth.sh │ ├── multiple_post │ └── index.js │ ├── post │ └── index.js │ ├── receive │ └── index.js │ └── setup.sh ├── context ├── load-context.js ├── save-context.js └── setup.sh ├── conversation ├── call-conversation.js └── setup.sh ├── deploy ├── README.md ├── channels │ └── slack │ │ └── verify-slack.js ├── check-deploy-exists.js ├── create-cloudant-database.js ├── create-cloudant-lite-instance.js ├── populate-actions.js ├── setup.sh └── update-auth-document.js ├── package.json ├── providers.json ├── readme_images ├── connector_pipeline.png └── conv-connector-arch-with-multipost.jpg ├── setup.sh ├── starter-code ├── normalize-for-channel │ ├── normalize-conversation-for-facebook.js │ └── normalize-conversation-for-slack.js ├── normalize-for-conversation │ ├── normalize-facebook-for-conversation.js │ └── normalize-slack-for-conversation.js ├── post-conversation.js ├── post-normalize.js ├── pre-conversation.js ├── pre-normalize.js └── setup.sh └── test ├── end-to-end ├── breakdown.sh ├── setup.sh ├── test.end-to-end.deploy-slack.js ├── test.end-to-end.prechecks.js ├── test.end-to-end.with-facebook.js └── test.end-to-end.with-slack.js ├── integration ├── channels │ ├── facebook │ │ ├── breakdown.sh │ │ ├── middle.js │ │ ├── setup.sh │ │ └── test.channel.facebook.js │ └── slack │ │ ├── breakdown.sh │ │ ├── send-attached-message-multipost.js │ │ ├── send-attached-message-response.js │ │ ├── send-attached-message.js │ │ ├── send-text.js │ │ ├── setup.sh │ │ └── test.channel.slack.js ├── context │ ├── breakdown.sh │ ├── middle-for-context.js │ ├── setup.sh │ └── test.context.js ├── conversation │ └── test.conversation.js ├── deploy │ ├── breakdown.sh │ ├── channels │ │ └── slack │ │ │ └── test.verify-slack.js │ ├── setup.sh │ └── test.populate-actions.js └── starter-code │ ├── breakdown.sh │ ├── mock-conversation-facebook-data.js │ ├── mock-conversation-generic-data.js │ ├── mock-conversation-slack-data.js │ ├── mock-conversation-text.js │ ├── setup.sh │ ├── test.starter-code.facebook.js │ └── test.starter-code.slack.js ├── resources ├── .unit.env └── payloads │ ├── test.unit.auth.conversation.json │ ├── test.unit.auth.facebook.json │ ├── test.unit.auth.slack.json │ ├── test.unit.context.json │ ├── test.unit.deploy.cf-endpoint-payloads.json │ ├── test.unit.deploy.json │ └── test.unit.starter-code.json ├── scripts ├── clean.sh └── run_tests.sh ├── unit ├── authdb │ ├── test.load-auth.js │ └── test.save-auth.js ├── channels │ ├── facebook │ │ ├── test.channel.facebook.batched_messages.js │ │ ├── test.channel.facebook.multiple_post.js │ │ ├── test.channel.facebook.post.js │ │ └── test.channel.facebook.receive.js │ └── slack │ │ ├── test.channel.slack.deploy.js │ │ ├── test.channel.slack.multiple_post.js │ │ ├── test.channel.slack.post.js │ │ └── test.channel.slack.receive.js ├── context │ ├── test.load-context.js │ └── test.save-context.js ├── conversation │ └── test.conversation.js ├── deploy │ ├── channels │ │ └── slack │ │ │ └── test.verify-slack.js │ ├── test.check-deploy-exists.js │ ├── test.create-cloudant-database.js │ ├── test.create-cloudant-lite-instance.js │ ├── test.populate-actions.js │ └── test.update-auth-document.js └── starter-code │ ├── normalize-for-channel │ ├── test.starter-code.normalize-conversation-for-facebook.js │ └── test.starter-code.normalize-conversation-for-slack.js │ └── normalize-for-conversation │ ├── test.starter-code.normalize-facebook-for-conversation.js │ └── test.starter-code.normalize-slack-for-conversation.js └── utils ├── cloudant-utils.js └── helper-methods.js /.bluemix/deploy.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "http://json-schema.org/draft-04/schema#", 3 | "title": "Conversation Connector", 4 | "description": "", 5 | "longDescription": "The Delivery Pipeline automates the continuous deployment of a Conversation connector.", 6 | "type": "object", 7 | "properties": { 8 | "prod-region": { 9 | "description": "The Bluemix region. Currently, only US South is supported.", 10 | "type": "string" 11 | }, 12 | "prod-organization": { 13 | "description": "The Bluemix organization where you want to deploy.", 14 | "type": "string" 15 | }, 16 | "prod-space": { 17 | "description": "The Bluemix space where you want to deploy.", 18 | "type": "string" 19 | }, 20 | "deploy-name": { 21 | "description": "The deployment name that will identify the deployed Cloud Functions components. Any characters other than A-Z and 0-9 are discarded.", 22 | "type": "string" 23 | }, 24 | "facebook-secret": { 25 | "description": "The Facebook app secret from https://developers.facebook.com/apps/.", 26 | "type": "string" 27 | }, 28 | "facebook-access-token": { 29 | "description": "The Facebook page access token from https://developers.facebook.com/apps/.", 30 | "type": "string" 31 | }, 32 | "facebook-verification-token": { 33 | "description": "The Facebook verification token that will be used to verify webhook settings.", 34 | "type": "string" 35 | }, 36 | "conversation-username": { 37 | "description": "The Conversation service username from the workspace credentials.", 38 | "type": "string" 39 | }, 40 | "conversation-password": { 41 | "description": "The Conversation service password from the workspace credentials.", 42 | "type": "string" 43 | }, 44 | "conversation-workspace": { 45 | "description": "ID of the Conversation workspace you want to deploy.", 46 | "type": "string" 47 | } 48 | }, 49 | "required": ["prod-region", "prod-organization", "prod-space", "deployment-name", "facebook-secret", "facebook-access-token", "facebook-verification-token", "conversation-username", "conversation-password", "conversation-workspace"], 50 | "form": [{ 51 | "type": "validator", 52 | "url": "/devops/setup/bm-helper/helper.html" 53 | }, 54 | { 55 | "type": "table", 56 | "columnCount": 3, 57 | "widths": ["33%", "33%", "33%"], 58 | "items": [ 59 | { 60 | "type": "label", 61 | "title": "Region (must be US South)" 62 | }, 63 | { 64 | "type": "label", 65 | "title": "Organization" 66 | }, 67 | { 68 | "type": "label", 69 | "title": "Space" 70 | }, 71 | { 72 | "type": "select", 73 | "key": "prod-region" 74 | }, 75 | { 76 | "type": "select", 77 | "key": "prod-organization" 78 | }, 79 | { 80 | "type": "select", 81 | "key": "prod-space", 82 | "readonly": false 83 | } 84 | ] 85 | }, 86 | { 87 | "type": "text", 88 | "readonly": false, 89 | "title": "Deployment name", 90 | "key": "deployment-name" 91 | }, 92 | { 93 | "type": "text", 94 | "readonly": false, 95 | "title": "Facebook App Secret", 96 | "key": "facebook-secret" 97 | }, 98 | { 99 | "type": "text", 100 | "readonly": false, 101 | "title": "Facebook Page Access Token", 102 | "key": "facebook-access-token" 103 | }, 104 | { 105 | "type": "text", 106 | "readonly": false, 107 | "title": "Facebook Verification Token", 108 | "key": "facebook-verification-token" 109 | }, 110 | { 111 | "type": "text", 112 | "readonly": false, 113 | "title": "Conversation Service Username", 114 | "key": "conversation-username" 115 | }, 116 | { 117 | "type": "text", 118 | "readonly": false, 119 | "title": "Conversation Service Password", 120 | "key": "conversation-password" 121 | }, 122 | { 123 | "type": "text", 124 | "readonly": false, 125 | "title": "Conversation Workspace ID", 126 | "key": "conversation-workspace" 127 | } 128 | ] 129 | } 130 | -------------------------------------------------------------------------------- /.bluemix/pipeline-DEPLOY.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | echo 'Installing nvm (Node.js Version Manager)...' 3 | npm config delete prefix 4 | curl -o- https://raw.githubusercontent.com/creationix/nvm/v0.31.2/install.sh | bash 2>&1 5 | . ~/.nvm/nvm.sh 6 | 7 | echo 'Installing Node.js 7.9.0...' 8 | nvm install 7.9.0 1>/dev/null 9 | npm install --progress false --loglevel error 1>/dev/null 10 | 11 | echo 'Retrieving Cloud Functions authorization key...' 12 | 13 | # Retrieve the Cloud Functions authorization key 14 | CF_ACCESS_TOKEN=`cat ~/.cf/config.json | jq -r .AccessToken | awk '{print $2}'` 15 | 16 | export CLOUDFUNCTIONS_API_HOST=openwhisk.ng.bluemix.net 17 | 18 | CLOUDFUNCTIONS_KEYS=`curl -XPOST -k -d "{ \"accessToken\" : \"$CF_ACCESS_TOKEN\", \"refreshToken\" : \"$CF_ACCESS_TOKEN\" }" \ 19 | -H 'Content-Type:application/json' https://$CLOUDFUNCTIONS_API_HOST/bluemix/v2/authenticate` 20 | 21 | SPACE_KEY=`echo $CLOUDFUNCTIONS_KEYS | jq -r '.namespaces[] | select(.name == "'$CF_ORG'_'$CF_SPACE'") | .key'` 22 | SPACE_UUID=`echo $CLOUDFUNCTIONS_KEYS | jq -r '.namespaces[] | select(.name == "'$CF_ORG'_'$CF_SPACE'") | .uuid'` 23 | CLOUDFUNCTIONS_AUTH=$SPACE_UUID:$SPACE_KEY 24 | 25 | # Configure the Cloud Functions CLI 26 | bx wsk property set --apihost $CLOUDFUNCTIONS_API_HOST --auth "${CLOUDFUNCTIONS_AUTH}" 27 | 28 | ./setup.sh -s 29 | -------------------------------------------------------------------------------- /.bluemix/pipeline.yml: -------------------------------------------------------------------------------- 1 | --- 2 | stages: 3 | - name: Build Stage 4 | inputs: 5 | - type: git 6 | branch: master 7 | service: ${REPO} 8 | triggers: 9 | - type: commit 10 | properties: 11 | - name: PROD_REGION_ID 12 | value: ${PROD_REGION_ID} 13 | jobs: 14 | - name: Build 15 | type: builder 16 | artifact_dir: '' 17 | build_type: shell 18 | script: |- 19 | #!/bin/bash 20 | if [ "${PROD_REGION_ID}" != "ibm:yp:us-south" ]; then 21 | echo "Deployment is currently supported only in the US South region." 22 | exit 1 23 | fi 24 | sudo apt-get -qq update 25 | sudo apt-get -qqy install curl 26 | curl http://stedolan.github.io/jq/download/linux64/jq -o jq 27 | chmod +x jq 28 | curl -s -O https://openwhisk.ng.bluemix.net/cli/go/download/linux/amd64/wsk 29 | chmod +x wsk 30 | echo "AUTH=$WSK_AUTH" > ~/.wskprops 31 | echo "APIHOST=$WSK_HOST" >> ~/.wskprops 32 | - name: Deploy Stage 33 | inputs: 34 | - type: job 35 | stage: Build Stage 36 | job: Build 37 | triggers: 38 | - type: stage 39 | properties: 40 | - name: CF_APP_NAME 41 | value: ${CF_APP_NAME} 42 | - name: FACEBOOK_SECRET 43 | value: ${FACEBOOK_SECRET} 44 | - name: FACEBOOK_ACCESS_TOKEN 45 | value: ${FACEBOOK_ACCESS_TOKEN} 46 | - name: FACEBOOK_VERIFICATION_TOKEN 47 | value: ${FACEBOOK_VERIFICATION_TOKEN} 48 | - name: CONVERSATION_USERNAME 49 | value: ${CONVERSATION_USERNAME} 50 | - name: CONVERSATION_PASSWORD 51 | value: ${CONVERSATION_PASSWORD} 52 | - name: CONVERSATION_WORKSPACE 53 | value: ${CONVERSATION_WORKSPACE} 54 | - name: BUTTON_DEPLOY 55 | value: "1" 56 | jobs: 57 | - name: Deploy 58 | type: deployer 59 | target: 60 | region_id: ${PROD_REGION_ID} 61 | organization: ${PROD_ORG_NAME} 62 | space: ${PROD_SPACE_NAME} 63 | application: ${CF_APP_NAME} 64 | script: |- 65 | #!/bin/bash 66 | export CF_APP_NAME=`echo "$CF_APP_NAME" | LANG=C sed 's/[^a-zA-Z0-9]//g'` 67 | export PATH=$(pwd):$PATH 68 | chmod a+x ./.bluemix/pipeline-DEPLOY.sh 69 | ./.bluemix/pipeline-DEPLOY.sh -------------------------------------------------------------------------------- /.bluemix/toolchain.yml: -------------------------------------------------------------------------------- 1 | --- 2 | name: "Conversation Connector Toolchain" 3 | description: "Deploy your own Conversation connector." 4 | required: 5 | - deploy 6 | - connector-repo 7 | 8 | # Github repos 9 | connector-repo: 10 | service_id: githubpublic 11 | parameters: 12 | repo_name: "conversation-connector" 13 | repo_url: https://github.com/watson-developer-cloud/conversation-connector 14 | type: clone 15 | has_issues: false 16 | 17 | # Pipelines 18 | connector-build: 19 | service_id: pipeline 20 | parameters: 21 | name: "conversation-connector" 22 | ui-pipeline: true 23 | configuration: 24 | content: $file(pipeline.yml) 25 | env: 26 | REPO: "connector-repo" 27 | CF_APP_NAME: "{{deploy.parameters.deployment-name}}" 28 | PROD_SPACE_NAME: "{{deploy.parameters.prod-space}}" 29 | PROD_ORG_NAME: "{{deploy.parameters.prod-organization}}" 30 | PROD_REGION_ID: "{{deploy.parameters.prod-region}}" 31 | FACEBOOK_SECRET: "{{deploy.parameters.facebook-secret}}" 32 | FACEBOOK_ACCESS_TOKEN: "{{deploy.parameters.facebook-access-token}}" 33 | FACEBOOK_VERIFICATION_TOKEN: "{{deploy.parameters.facebook-verification-token}}" 34 | CONVERSATION_USERNAME: "{{deploy.parameters.conversation-username}}" 35 | CONVERSATION_PASSWORD: "{{deploy.parameters.conversation-password}}" 36 | CONVERSATION_WORKSPACE: "{{deploy.parameters.conversation-workspace}}" 37 | execute: true 38 | services: ["connector-repo"] 39 | hidden: [form, description] 40 | 41 | #Deployment 42 | deploy: 43 | schema: 44 | $ref: deploy.json 45 | service-category: pipeline 46 | # Parameters the user will type 47 | parameters: 48 | prod-region: "{{region}}" 49 | prod-organization: "{{organization}}" 50 | prod-space: "{{space}}" 51 | deployment-name: "{{name}}" 52 | facebook-secret: "" 53 | facebook-access-token: "" 54 | facebook-verification-token: "" 55 | conversation-username: "" 56 | conversation-password: "" 57 | conversation-workspace: "" -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | coverage 2 | node_modules -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "airbnb-base", 3 | "plugins": [ 4 | "import", 5 | "prettier" 6 | ], 7 | "rules": { 8 | "arrow-body-style": ["error", "always"], 9 | "arrow-parens": ["error", "as-needed"], 10 | "comma-dangle": ["error", "never"], 11 | "no-underscore-dangle": ["off"], 12 | "no-unused-vars": ["error", { "varsIgnorePattern": "main" }], 13 | "no-use-before-define": ["error", {"functions": false}], 14 | "prettier/prettier": ["warn", {"singleQuote": true}], 15 | "strict": ["off"] 16 | }, 17 | "env": { 18 | "mocha": true 19 | } 20 | } -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # DS_Store 2 | .DS_Store 3 | 4 | # Logs 5 | logs 6 | *.log 7 | npm-debug.log* 8 | 9 | # Runtime data 10 | pids 11 | *.pid 12 | *.seed 13 | 14 | # Production system credentials file 15 | providers.json 16 | 17 | # Ignore changes in slack and OW binding files so devs can use their own slack apps and namespaces 18 | test/resources/openwhisk-bindings.json 19 | test/resources/slack-bindings.json 20 | 21 | # Directory for instrumented libs generated by jscoverage/JSCover 22 | lib-cov 23 | 24 | # Coverage directory used by tools like istanbul 25 | coverage 26 | 27 | # nyc test coverage 28 | .nyc_output 29 | 30 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 31 | .grunt 32 | 33 | # node-waf configuration 34 | .lock-wscript 35 | 36 | # Compiled binary addons (http://nodejs.org/api/addons.html) 37 | build/Release 38 | 39 | # Dependency directories 40 | node_modules 41 | jspm_packages 42 | 43 | # Optional npm cache directory 44 | .npm 45 | 46 | # Optional REPL history 47 | .node_repl_history 48 | 49 | # Ignore files/folders created by IDEs 50 | .idea/ 51 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - "7" 4 | before_install: 5 | - sudo apt-get -qq update 6 | - sudo apt-get -qqy install jq curl 7 | - curl -L -o - 'https://cli.run.pivotal.io/stable?release=linux64-binary&version=6.25.0&source=github-rel' > cf.tar.gz 8 | - tar zxf cf.tar.gz cf 9 | - curl -s -O https://openwhisk.ng.bluemix.net/cli/go/download/linux/amd64/wsk 10 | - chmod +x wsk 11 | - echo "AUTH=$WSK_AUTH" > ~/.wskprops 12 | - echo "APIHOST=$WSK_HOST" >> ~/.wskprops 13 | before_script: 14 | - export PATH=$PATH:$PWD 15 | install: 16 | - npm install 17 | script: 18 | - eslint . 19 | - npm test 20 | - node ./node_modules/.bin/istanbul check-coverage --statements 90 --branches 90 --lines 90 --functions 90 21 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Questions 2 | 3 | If you are having problems using the product or have a question about the IBM 4 | Watson Services, please ask a question on 5 | [dW Answers](https://developer.ibm.com/answers/questions/ask/?topics=watson) 6 | or [Stack Overflow](http://stackoverflow.com/questions/ask?tags=ibm-watson). 7 | 8 | # Code 9 | 10 | In an effort to have clear uniform code across a code base being developed by multiple individuals we will adopt the [Airbnb style guidelines](https://github.com/airbnb/javascript). 11 | 12 | The above is provided primarily for reference and while it is encouraged reading, we will use eslint and the prettier formatter to automatically apply most of these rules. 13 | 14 | Travis will automatically run eslint and prettier when a PR is made. However, it is a best practice to run both prettier and eslint locally to work out any issues before a PR is submitted. 15 | 16 | # Issues 17 | 18 | If you encounter an issue with the product, you are welcome to submit 19 | a [bug report](https://github.com/watson-developer-cloud/conversation-connector/issues). 20 | Before that, please search for similar issues. It's possible somebody has 21 | already encountered this issue. 22 | 23 | # Pull Requests 24 | 25 | If you want to contribute to the repository, follow these steps: 26 | 27 | 1. Fork the repo. 28 | 2. Develop and test your code changes: `npm install && npm test`. 29 | 3. Travis-CI will run the tests once your changes are merged. 30 | 4. Add a test for your changes. Only refactoring and documentation changes require no new tests. 31 | 5. Make the test pass. 32 | 6. Commit your changes. 33 | 7. Push to your fork and submit a pull request. 34 | 35 | # Developer's Certificate of Origin 1.1 36 | 37 | By making a contribution to this project, I certify that: 38 | 39 | (a) The contribution was created in whole or in part by me and I 40 | have the right to submit it under the open source license 41 | indicated in the file; or 42 | 43 | (b) The contribution is based upon previous work that, to the best 44 | of my knowledge, is covered under an appropriate open source 45 | license and I have the right under that license to submit that 46 | work with modifications, whether created in whole or in part 47 | by me, under the same open source license (unless I am 48 | permitted to submit under a different license), as indicated 49 | in the file; or 50 | 51 | (c) The contribution was provided directly to me by some other 52 | person who certified (a), (b) or (c) and I have not modified 53 | it. 54 | 55 | (d) I understand and agree that this project and the contribution 56 | are public and that a record of the contribution (including all 57 | personal information I submit with it, including my sign-off) is 58 | maintained indefinitely and may be redistributed consistent with 59 | this project or the open source license(s) involved. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | The Conversation connector is no longer supported for new deployments. Instead, Watson Assistant now provides new integrations with Slack, Facebook Messenger, and other channels. See [Adding integrations](https://console.bluemix.net/docs/services/assistant/add-integrations.html) for more details. 2 | 3 | The archived instructions can be found [here](READMEarchive.md). 4 | -------------------------------------------------------------------------------- /channels/facebook/README.md: -------------------------------------------------------------------------------- 1 | The Conversation connector is no longer supported for new deployments. Instead, Watson Assistant now provides new integrations with Slack, Facebook Messenger, and other channels. See [Adding integrations](https://console.bluemix.net/docs/services/assistant/add-integrations.html) for more details. 2 | 3 | The archived instructions can be found [here](READMEarchive.md). 4 | -------------------------------------------------------------------------------- /channels/facebook/READMEarchive.md: -------------------------------------------------------------------------------- 1 | # Deploying a Facebook Messenger app 2 | 3 | Deploying your workspace to a Facebook Messenger app requires modifying several configuration files and running scripts. 4 | 5 | **Note:** This process is intended to connect an existing Watson Conversation workspace to a Facebook app. If you have not yet built a workspace, you must do so first. For more information, see the [Conversation documentation](https://console.bluemix.net/docs/services/conversation/index.html#about). 6 | 7 | **Note:** The manual deployment process is supported only on Linux/UNIX and macOS systems. 8 | 9 | 1. Clone or download this GitHub repository to your local file system. 10 | 11 | 1. If you have not done so already, install the following prerequisite software: 12 | 13 | - The IBM [Cloud Functions CLI](https://console.ng.bluemix.net/openwhisk/learn/cli) 14 | - The [Node.js runtime](https://nodejs.org/), including the npm package manager 15 | 16 | 1. Log in using the following command: 17 | 18 | `bx login` 19 | 20 | Select the IBM Cloud organization where you want to deploy. 21 | 22 | **Note:** Currently, only the US South region is supported. 23 | 24 | 1. Target the space where you want to deploy by running the following command: 25 | 26 | `bx target --cf` 27 | 28 | 1. Go to https://console.ng.bluemix.net/openwhisk/learn/cli and then do the following: 29 | 30 | 1. Click the account information in the upper right corner, and confirm that the organization and space shown are correct. 31 | 32 | 1. Copy the command in step 3 ("Log in to IBM Cloud"), and run it in your CLI. 33 | 34 | 1. Go to [https://developers.facebook.com/apps/](https://developers.facebook.com/apps/). Log in with your Facebook credentials if necessary. 35 | 36 | 1. Click **Add a New App**. Specify a display name for your app and your contact email address, and then click **Create App ID**. 37 | 38 | **Note:** If you have already created the app you want to use, select it from the **My Apps** menu. 39 | 40 | 1. In the navigation pane, click **Settings -> Basic**. 41 | 42 | 1. Click **Show** and then copy the displayed app secret to the clipboard. 43 | 44 | 1. In the root directory of your local copy of the repository, edit the `providers.json` file. Paste the app secret value into the `app_secret` field of the `facebook` object. 45 | 46 | 1. In the Facebook app page, go to the bottom of the navigation pane and click the plus sign (**`+`**) next to **PRODUCTS**. 47 | 48 | 1. Under **Add a Product**, go to the **Messenger** tile and click **Set Up**. 49 | 50 | 1. In the Messenger settings, scroll down to **Token Generation**. Click **Select a Page** and choose the Facebook page you want to use for your app. 51 | 52 | **Note:** If you don't already have a page for your app, click **Create a new page**. After you finish creating the page, return to the [Facebook apps page](https://developers.facebook.com/apps/) and navigate back to the Messenger settings for your app. You can then select the page you created. 53 | 54 | 1. Copy the page access token. In `providers.json`, paste the value into the corresponding field of the `facebook` object. 55 | 56 | 1. In `providers.json`, add a value for `verfication_token`. This can be any string you want to use as a verification token. Make a record of this value, which you will need later. (Facebook will use this verification token to verify your webhook URL.) 57 | 58 | 1. In `providers.json`, make sure the channel `name` is set to `facebook`. 59 | 60 | 1. In `providers.json`, edit the pipeline `name` to specify a name for your deployment. This name will help you find your Cloud Functions assets later. Only alphanumeric characters (A-Z and 0-9) are kept. 61 | 62 | 1. In `providers.json`, edit the `conversation` object to specify the username, password, and workspace ID of the workspace you are deploying. 63 | 64 | 1. Run `npm install`. 65 | 66 | 1. `cd` to the root of the repository and run `./setup.sh -s`. This creates the Facebook Cloud Functions package, as well as all the other packages in your namespace. 67 | 68 | 1. Copy the generated request URL. You can copy this directly from the terminal window after the script completes (look for a message that begins `Your Request URL is:`). If you need to find the endpoint URL later, follow these steps: 69 | 70 | 1. Go to https://console.ng.bluemix.net/openwhisk/actions. 71 | 72 | 1. In the list of actions, click on _facebook/receive>. 73 | 74 | 1. In the left-hand pane, click **Endpoints**. 75 | 76 | 1. Confirm that **Enable as Web Action** is selected. Copy the provided URL to the clipboard. **Note**: You will need to replace `.json` with `.text` when you paste this value into the Facebook webhook settings. 77 | 78 | 1. In the Facebook app settings, go to the Messenger settings and scroll to the **Webhooks** section. Click **Setup Webhooks**. 79 | 80 | 1. In the **New Page Subscription** window, paste the request URL from the clipboard into the **Callback URL** field. (If the URL you pasted ends with `.json`, change this to `.text`.) In the **Verify Token** field, specify the same Facebook verification token that you created earlier. Under **Subscription Fields**, select **messages** and **messaging_postbacks**. Then click **Verify and Save**. 81 | 82 | 1. After the verification finishes, go back to the **Webhooks** section in the Messenger settings and click **Select a Page**. Select the same page you selected during token generation, and then click **Subscribe**. 83 | 84 | **Note:** Subscribe to only one page. Multiple-page subscriptions are not currently supported. 85 | 86 | That's it. You're all set. You can now go to Facebook Messenger, search for your Facebook bot (or the Facebook page you subscribed to), and talk to it! 87 | -------------------------------------------------------------------------------- /channels/facebook/post/index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright IBM Corp. 2017 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the License); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an AS IS BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | 'use strict'; 18 | 19 | const assert = require('assert'); 20 | const request = require('request'); 21 | const omit = require('object.omit'); 22 | 23 | /** 24 | * Receives a Facebook POST JSON object and sends the object to the Facebook API. 25 | * 26 | * @params - Facebook post parameters as outlined by https://graph.facebook.com/v2.6/me/messages 27 | * 28 | * @return - status of post request sent to Facebook POST API 29 | */ 30 | function main(params) { 31 | return new Promise((resolve, reject) => { 32 | try { 33 | validateParameters(params); 34 | } catch (e) { 35 | reject(e.message); 36 | } 37 | 38 | const auth = params.raw_input_data.auth; 39 | 40 | assert( 41 | auth.facebook && auth.facebook.page_access_token, 42 | 'auth.facebook.page_access_token not found.' 43 | ); 44 | 45 | const facebookParams = extractFacebookParams(params); 46 | const postUrl = params.url || 'https://graph.facebook.com/v2.6/me/messages'; 47 | 48 | return postFacebook( 49 | facebookParams, 50 | postUrl, 51 | auth.facebook.page_access_token 52 | ) 53 | .then(resolve) 54 | .catch(reject); 55 | }); 56 | } 57 | 58 | /** 59 | * Posts Conversation response to the message sender using the Facebook API https://graph.facebook.com/v2.6/me/messages 60 | * as a default. If a different url is specified in params.url then it will post to that instead. 61 | * 62 | * @param {JSON} Facebook post parameters 63 | * @postUrl {string} Url for posting the response 64 | * @accessToken {string} auth token to send with the post request 65 | * 66 | * @return - status of post request sent to Facebook POST API 67 | */ 68 | function postFacebook(params, postUrl, accessToken) { 69 | return new Promise((resolve, reject) => { 70 | request( 71 | { 72 | url: postUrl, 73 | qs: { access_token: accessToken }, 74 | method: 'POST', 75 | json: params 76 | }, 77 | (error, response) => { 78 | if (error) { 79 | reject(error.message); 80 | } 81 | if (response) { 82 | if (response.statusCode === 200) { 83 | // Facebook expects a "200" string/text response instead of a JSON. 84 | // With Cloud Functions if we have to return a string/text, then we'd have to specify 85 | // the field "text" and assign it a value that we'd like to return. In this case, 86 | // the value to be returned is a statusCode. 87 | resolve({ 88 | text: response.statusCode, 89 | params, 90 | url: postUrl 91 | }); 92 | } 93 | reject( 94 | `Action returned with status code ${response.statusCode}, message: ${response.statusMessage}` 95 | ); 96 | } 97 | reject(`An unexpected error occurred when sending POST to ${postUrl}.`); 98 | } 99 | ); 100 | }); 101 | } 102 | 103 | /** 104 | * Extracts and converts the input parameters to JSON that Facebook understands. 105 | * 106 | * @params The parameters passed into the action 107 | * 108 | * @return JSON containing all and only the parameter that Facebook /v2.6/me/messages 109 | * graph API needs 110 | */ 111 | function extractFacebookParams(params) { 112 | const facebookParams = omit(params, [ 113 | 'page_access_token', 114 | 'app_secret', 115 | 'verification_token', 116 | 'raw_input_data', 117 | 'raw_output_data', 118 | 'sub_pipeline', 119 | 'batched_messages' 120 | ]); 121 | 122 | return facebookParams; 123 | } 124 | 125 | /** 126 | * Validates the required parameters for running this action. 127 | * 128 | * @params The parameters passed into the action 129 | */ 130 | function validateParameters(params) { 131 | // Required: Channel identifier 132 | assert(params.recipient && params.recipient.id, 'Recepient id not provided.'); 133 | 134 | // Required: Message object or sender_action 135 | assert( 136 | params.message || params.sender_action, 137 | 'Must provide message object or sender_action.' 138 | ); 139 | 140 | // Required: raw_input_data and Facebook Auth 141 | assert( 142 | params.raw_input_data && 143 | params.raw_input_data.auth && 144 | params.raw_input_data.auth.facebook, 145 | 'Facebook auth not provided.' 146 | ); 147 | } 148 | 149 | module.exports = { 150 | main, 151 | name: 'facebook/post', 152 | postFacebook, 153 | validateParameters 154 | }; 155 | -------------------------------------------------------------------------------- /channels/facebook/setup.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | # Pipeline name 4 | # eg: my-flex-pipeline_ 5 | PIPELINE_NAME=$1 6 | 7 | PACKAGE_NAME="${PIPELINE_NAME}facebook" 8 | 9 | # Bind the cloudant url, authdbname and auth_key as annotations to the package. 10 | # facebook/receive and facebook/post will need these to load auth 11 | bx wsk package update $PACKAGE_NAME \ 12 | -p sub_pipeline "${PIPELINE_NAME%_}" \ 13 | -p batched_messages "${PACKAGE_NAME}/batched_messages" 14 | 15 | bx wsk action update $PACKAGE_NAME/receive ./receive/index.js -a web-export true 16 | bx wsk action update $PACKAGE_NAME/post ./post/index.js 17 | bx wsk action update $PACKAGE_NAME/batched_messages ./batched_messages/index.js 18 | bx wsk action update $PACKAGE_NAME/multiple_post ./multiple_post/index.js 19 | -------------------------------------------------------------------------------- /channels/slack/README.md: -------------------------------------------------------------------------------- 1 | The Conversation connector is no longer supported for new deployments. Instead, Watson Assistant now provides new integrations with Slack, Facebook Messenger, and other channels. See [Adding integrations](https://console.bluemix.net/docs/services/assistant/add-integrations.html) for more details. 2 | 3 | The archived instructions can be found [here](READMEarchive.md). 4 | -------------------------------------------------------------------------------- /channels/slack/READMEarchive.md: -------------------------------------------------------------------------------- 1 | # Deploying a Slack app 2 | 3 | You can use either of two methods to deploy your workspace to a Slack app: 4 | 5 | - Use the [Watson Assistant tool](https://console.bluemix.net/docs/services/conversation/conversation-connector.html#deploying-to-slack-using-the-watson-assistant-tool) if you want to quickly deploy your app with just a few clicks. 6 | 7 | - Use [manual deployment](#manual-deployment) if you want to deploy your app by modifying configuration files and running scripts. You might want to use this method if you are customizing the Conversation connector, or if you need to repair or update components of an existing deployment. 8 | 9 | **Note:** This process is intended to connect an existing Watson Assistant workspace to a Slack app. If you have not yet built a workspace, you must do so first. For more information, see the [Watson Assistant documentation](https://console.bluemix.net/docs/services/conversation/index.html). 10 | 11 | ## Manual deployment 12 | 13 | **Note:** This process is supported only on Linux/UNIX and macOS systems. 14 | 15 | 1. Clone or download this GitHub repository to your local file system. 16 | 17 | 1. If you have not done so already, install the following prerequisite software: 18 | 19 | - The IBM [Cloud Functions CLI](https://console.ng.bluemix.net/openwhisk/learn/cli) 20 | - The [Node.js runtime](https://nodejs.org/), including the npm package manager 21 | 22 | 1. Log in using the following command: 23 | 24 | `bx login` 25 | 26 | Select the IBM Cloud organization where you want to deploy. 27 | 28 | **Note:** Currently, only the US South region is supported. 29 | 30 | 1. Target the space where you want to deploy by running the following command: 31 | 32 | `bx target --cf` 33 | 34 | 1. Go to https://console.ng.bluemix.net/openwhisk/learn/cli and then do the following: 35 | 36 | 1. Click the account information in the upper right corner, and confirm that the organization and space shown are correct. 37 | 38 | 1. Copy the command in step 3 ("Log in to IBM Cloud"), and run it in your CLI. 39 | 40 | 1. Go to [https://slack.com](https://slack.com) and make sure you are signed in to the Slack workspace where you want to deploy your bot. 41 | 42 | 1. Go to [https://api.slack.com/apps/](https://api.slack.com/apps/). Sign in with your Slack credentials if necessary. 43 | 44 | 1. Click **Create an App**. 45 | 46 | 1. Specify an app name, select a development Slack workspace, and then click **Create App**. 47 | 48 | **Note:** If you have already created the app you want to use, select it from the **Your Apps** list. 49 | 50 | 1. In the navigation menu, click **Bot Users**. 51 | 52 | 1. On the Bot User page, click **Add a Bot User**. Verify the display name and bot username, and then toggle **Always Show My Bot as Online** to **On**. Click **Add Bot User** and then **Save Changes**. 53 | 54 | 1. Click **Event Subscriptions**. Toggle the **Enable Events** switch to **On**. 55 | 56 | 1. Scroll down to **Subscribe to Bot Events** and click **Add Bot User Event**. You must select at least one event. For most bots, the following events are good choices: 57 | 58 | - `message.im` 59 | - `message.channels` 60 | - `message.mpim` 61 | - `message.groups` 62 | 63 | Click **Save Changes**. 64 | 65 | 1. Click **Basic Information** and scroll down to **App Credentials**. 66 | 67 | 1. In the root directory of your local copy of the repository, edit the `providers.json` file. 68 | 69 | 1. On the Slack page, copy the client ID, client secret, and verification token, and paste them into the corresponding fields in the `slack` object in the `providers.json` file. 70 | 71 | 1. In `providers.json`, make sure the channel `name` is set to `slack`. 72 | 73 | 1. In `providers.json`, edit the pipeline `name` to specify a name for your deployment. This name will help you find your Cloud Functions assets later. Only alphanumeric characters (A-Z and 0-9) are kept. 74 | 75 | 1. In `providers.json`, edit the `conversation` object to specify the username, password, and workspace ID of the workspace you are deploying. Save the changes to the file. 76 | 77 | 1. Run `npm install`. 78 | 79 | 1. Run `./setup.sh`. This creates the required Cloud Functions packages in your Cloud Functions space. 80 | 81 | **Note:** If you are not already signed in to your Slack workspace, you will be redirected to the sign-in page so you can enter your Slack workspace credentials. If this happens, you must run the `setup.sh` script again after you sign in. 82 | 83 | 1. When the script pauses, copy the generated Slack redirect URL from the terminal window (look for a message that begins `Your Slack Redirect URL is:`. Leave the script paused for now. 84 | 85 | 1. In the Slack app settings in your browser, click **OAuth & Permissions**. 86 | 87 | 1. Under **Redirect URLs**, click **Add a new Redirect URL**, and paste in the redirect URL you copied from the terminal window. Click **Save URLs**. 88 | 89 | 1. Go back to the terminal window and copy the request URL (look for a message that begins `Your Request URL is:`). If you need to find the endpoint URL later, follow these steps: 90 | 91 | 1. Go to https://console.ng.bluemix.net/openwhisk/editor. 92 | 93 | 1. Under **My Actions**, click _slack/receive>. 94 | 95 | 1. Click **View Action Details**. 96 | 97 | 1. Confirm that **Enable as Web Action** is selected. Copy the URL from the **Web Action URL**. 98 | 99 | 1. In the Slack app settings, click **Event Subscriptions**. 100 | 101 | 1. Under **Enable Events**, paste in the request URL you copied from the terminal window. Toggle the switch at the top to **On**. After Slack has verified the URL, click **Save Changes**. 102 | 103 | 1. **Optional:** If you want to enable interactive components, click **Interactive Components**, and then click **Enable Interactive Components**. In the **Request URL** field, paste the same request URL you specified in the previous step, and then click **Enable Interactive Components**. 104 | 105 | 1. Go back to the terminal window and press Enter to resume the script. 106 | 107 | 1. When prompted in your browser, sign in to your Slack workspace and authorize the app. 108 | 109 | That's it. You're all set. You can now go to your Slack team and talk to your bot! 110 | -------------------------------------------------------------------------------- /channels/slack/engage_oauth.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | PIPELINE=$1 4 | 5 | PIPELINE_NAME=`echo $PIPELINE | jq -r .name` 6 | SLACK_CLIENT_ID=`echo $PIPELINE | jq -r .channel.slack.client_id` 7 | SLACK_CLIENT_SECRET=`echo $PIPELINE | jq -r .channel.slack.client_secret` 8 | 9 | SLACK_REDIRECT_URL="https://openwhisk.ng.bluemix.net/api/v1/web/$(bx wsk namespace list | tail -n +2 | head -n 1)/${PIPELINE_NAME}_slack/deploy.http" 10 | 11 | signature=`node -e "const crypto = require('crypto'); console.log(crypto.createHmac('sha256', process.argv[1]).update('authorize').digest('hex'));" "${SLACK_CLIENT_ID}&${SLACK_CLIENT_SECRET}"` 12 | 13 | state=$(node -e 'console.log(JSON.stringify({ 14 | signature: process.argv[1], 15 | redirect_url: process.argv[2] 16 | }));' $signature ${SLACK_REDIRECT_URL}) 17 | 18 | AUTHORIZE_URL="/oauth/authorize?client_id=${SLACK_CLIENT_ID}&scope=bot+chat:write:bot&redirect_uri=${SLACK_REDIRECT_URL}&state=${state}" 19 | 20 | AUTH_REDIRECT_URL="https://slack.com/signin?redir=$(node -e 'console.log(encodeURIComponent(process.argv[1]));' ${AUTHORIZE_URL})" 21 | 22 | python -m webbrowser -t ${AUTH_REDIRECT_URL} 23 | -------------------------------------------------------------------------------- /channels/slack/post/index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright IBM Corp. 2017 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the License); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an AS IS BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | 'use strict'; 18 | 19 | const assert = require('assert'); 20 | const request = require('request'); 21 | 22 | const mustUrlEncodeUrls = [ 23 | 'https://slack.com/api/chat.postMessage', 24 | 'https://slack.com/api/chat.update', 25 | 'https://slack.com/api/chat.postEphemeral' 26 | ]; 27 | const slackResponseHookUrl = 'hooks.slack.com'; 28 | 29 | /** 30 | * Receives a Slack POST JSON object and sends the object to the Slack API. 31 | * 32 | * @param {JSON} params - Slack post parameters as outlined by 33 | * https://api.slack.com/methods/chat.postMessage 34 | * @return {Promise} - status of post request send to Slack POST API 35 | */ 36 | function main(params) { 37 | return new Promise((resolve, reject) => { 38 | validateParameters(params); 39 | const postUrl = params.url || 'https://slack.com/api/chat.postMessage'; 40 | 41 | const auth = params.raw_input_data.auth; 42 | 43 | const postParams = extractSlackParameters(useAuth(params, auth)); 44 | 45 | return postSlack(postParams, postUrl).then(resolve).catch(reject); 46 | }); 47 | } 48 | 49 | /** 50 | * Sends a message to Slack as the bot by sending a POST request to the specified endpoint. 51 | * 52 | * @param {JSON} slackParams - Slack parameters to be sent to the Slack server 53 | * @param {string} postUrl - Slack server endpoint to sent POST to 54 | * @return {Promise} - return status of the POST 55 | */ 56 | function postSlack(slackParams, postUrl) { 57 | return new Promise((resolve, reject) => { 58 | request( 59 | { 60 | url: postUrl, 61 | method: 'POST', 62 | form: modifyPostParams(slackParams, postUrl) 63 | }, 64 | (error, response) => { 65 | if (error) { 66 | reject(error); 67 | } else if (response && response.statusCode === 200) { 68 | resolve(slackParams); 69 | } else { 70 | reject( 71 | `Action returned with status code ${response.statusCode}, message: ${response.statusMessage}` 72 | ); 73 | } 74 | } 75 | ); 76 | }); 77 | } 78 | 79 | /** 80 | * When POSTing messages using one of the three endpoints listed in mustEncodeUrls, 81 | * the attachments must be url-encoded or stringified. 82 | * For more information, see https://api.slack.com/interactive-messages, under 83 | * "Using chat.postMessage, chat.postEphemeral, chat.update, and chat.unfurl". 84 | * 85 | * @param {JSON} params - The parameters passed into the action 86 | * @param {string} postUrl - the POST url which determines if the parameters should be encoded 87 | * @return {JSON} - result parameters after encoding 88 | */ 89 | function modifyPostParams(params, postUrl) { 90 | let slackParams = Object.assign({}, params); 91 | if (slackParams.attachments && mustUrlEncodeUrls.indexOf(postUrl) >= 0) { 92 | slackParams.attachments = JSON.stringify(slackParams.attachments); 93 | } else if (postUrl.indexOf(slackResponseHookUrl) >= 0) { 94 | // if the post URL is a hook URL provided by Slack, then this is an interactive message update, 95 | // and the parameters need to be stringified before being sent 96 | slackParams = JSON.stringify(slackParams); 97 | } 98 | return slackParams; 99 | } 100 | 101 | /** 102 | * Extracts and converts the input parameters to JSON that Slack understands. 103 | * 104 | * @params The parameters passed into the action 105 | * 106 | * @return JSON containing all and only the parameter that Slack chat.postMessage API needs 107 | */ 108 | function extractSlackParameters(params) { 109 | const noIncludeKeys = ['raw_input_data', 'raw_output_data', 'url']; 110 | const slackParams = {}; 111 | 112 | Object.keys(params).forEach(key => { 113 | if (noIncludeKeys.indexOf(key) < 0) { 114 | slackParams[key] = params[key]; 115 | } 116 | }); 117 | slackParams.as_user = slackParams.as_user || 'true'; 118 | return slackParams; 119 | } 120 | 121 | /** 122 | * Validates the required parameters for running this action. 123 | * 124 | * @params The parameters passed into the action 125 | */ 126 | function validateParameters(params) { 127 | // Required: Slack channel 128 | assert(params.channel, 'Channel not provided.'); 129 | 130 | // Required: Bot ID 131 | assert( 132 | params.raw_input_data && params.raw_input_data.bot_id, 133 | 'Bot ID not provided.' 134 | ); 135 | 136 | // Required: auth 137 | assert( 138 | params.raw_input_data && params.raw_input_data.auth, 139 | 'No raw_input_data.auth found.' 140 | ); 141 | } 142 | 143 | /** 144 | * Uses the loaded auth info to attach bot_access_token 145 | * to the payload before posting the response to Slack. 146 | * 147 | * @params - {JSON} Slack params to be updated with the auth token 148 | * @auth - {JSON} loaded auth data 149 | * 150 | * @return updated Slack params containing the token required for posting 151 | */ 152 | function useAuth(params, auth) { 153 | const returnParams = params; 154 | 155 | const botId = params.raw_input_data.bot_id; 156 | assert(botId, 'No bot user found in parameters.'); 157 | const botAccessToken = auth && 158 | auth.slack && 159 | auth.slack.bot_users && 160 | auth.slack.bot_users[botId] && 161 | auth.slack.bot_users[botId].bot_access_token; 162 | assert(botAccessToken, 'bot_access_token absent in auth.'); 163 | 164 | returnParams.token = botAccessToken; 165 | return returnParams; 166 | } 167 | 168 | module.exports = { 169 | main, 170 | name: 'slack/post', 171 | validateParameters, 172 | postSlack, 173 | modifyPostParams 174 | }; 175 | -------------------------------------------------------------------------------- /channels/slack/setup.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | # Pipeline name 4 | # eg: my-flex-pipeline_ 5 | PIPELINE_NAME=$1 6 | 7 | PACKAGE_NAME="${PIPELINE_NAME}slack" 8 | 9 | bx wsk package update $PACKAGE_NAME 10 | 11 | bx wsk action update $PACKAGE_NAME/receive receive/index.js -a web-export true 12 | bx wsk action update $PACKAGE_NAME/post post/index.js 13 | bx wsk action update $PACKAGE_NAME/deploy deploy/index.js -a web-export true 14 | bx wsk action update $PACKAGE_NAME/multiple_post ./multiple_post/index.js 15 | 16 | echo "Your Slack Redirect URL is: https://openwhisk.ng.bluemix.net/api/v1/web/$(bx wsk namespace list | tail -n +2 | head -n 1)/${PIPELINE_NAME}slack/deploy.http" 17 | -------------------------------------------------------------------------------- /context/setup.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | PIPELINE_NAME=$1 4 | CLOUDANT_URL=$2 5 | CLOUDANT_CONTEXT_DBNAME=$3 6 | 7 | # create context database 8 | if [ "${CLOUDANT_URL}" != "" ]; then 9 | for i in {1..10}; do 10 | e=`curl -s -XPUT "${CLOUDANT_URL}/${CLOUDANT_CONTEXT_DBNAME}" | jq -r .error` 11 | if [ "$e" != "null" ]; then 12 | if [ "$e" == "conflict" -o "$e" == "file_exists" ]; then 13 | break 14 | fi 15 | echo "create context database returned with error [$e], retrying..." 16 | sleep 5 17 | else 18 | break 19 | fi 20 | done 21 | fi 22 | 23 | bx wsk package update ${PIPELINE_NAME}context 24 | 25 | bx wsk action update ${PIPELINE_NAME}context/load-context load-context.js 26 | bx wsk action update ${PIPELINE_NAME}context/save-context save-context.js 27 | -------------------------------------------------------------------------------- /conversation/call-conversation.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright IBM Corp. 2017 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the License); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an AS IS BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | const assert = require('assert'); 18 | 19 | const ConversationV1 = require('watson-developer-cloud/conversation/v1'); 20 | 21 | /** 22 | * 23 | * This action takes a user query and runs it against the Conversation service specified in the 24 | * package bindings. 25 | * 26 | * @param Whisk actions accept a single parameter, 27 | * which must be a JSON object. 28 | * 29 | * At minimum, the params variable must contain: 30 | { 31 | "conversation":{ 32 | "input":{ 33 | "text":"How is the weather?" 34 | } 35 | } 36 | } 37 | * It should be noted that the full Conversation message API can be specified in the Conversation 38 | * object. username, password, and workspace_id will be picked up via the package bindings and be 39 | * available at the root of the params object. 40 | * 41 | * @return which must be a JSON object. 42 | * It will be the output of this action. 43 | * 44 | */ 45 | function main(params) { 46 | return new Promise((resolve, reject) => { 47 | validateParams(params); 48 | 49 | const auth = params.raw_input_data.auth; 50 | 51 | assert(auth.conversation, 'conversation object absent in auth data.'); 52 | assert( 53 | auth.conversation.username, 54 | 'conversation username absent in auth.conversation' 55 | ); 56 | assert( 57 | auth.conversation.password, 58 | 'conversation password absent in auth.conversation' 59 | ); 60 | assert( 61 | auth.conversation.workspace_id, 62 | 'conversation workspace_id absent in auth.conversation' 63 | ); 64 | 65 | const conversation = new ConversationV1({ 66 | username: auth.conversation.username, 67 | password: auth.conversation.password, 68 | url: params.url, 69 | version: params.version || 'v1', 70 | version_date: params.version_date || '2018-07-10' 71 | }); 72 | const payload = Object.assign({}, params.conversation); 73 | payload.workspace_id = auth.conversation.workspace_id; 74 | 75 | conversation.message(payload, (err, response) => { 76 | if (err) { 77 | reject(err); 78 | } else { 79 | const conversationOutput = { 80 | conversation: response, 81 | raw_input_data: params.raw_input_data 82 | }; 83 | conversationOutput.raw_input_data.conversation = params.conversation; 84 | resolve(conversationOutput); 85 | } 86 | }); 87 | }); 88 | } 89 | 90 | /** 91 | * Verify the params required to call conversation exist and are in the appropriate format 92 | * @params {JSON} parameters passed into the action 93 | */ 94 | function validateParams(params) { 95 | // Check if we have a message in the proper format 96 | assert( 97 | params.conversation && 98 | params.conversation.input && 99 | params.conversation.input.text, 100 | 'No message supplied to send to the Conversation service.' 101 | ); 102 | 103 | // Required: channel raw input data 104 | assert( 105 | params.raw_input_data && 106 | params.raw_input_data.provider && 107 | params.raw_input_data[params.raw_input_data.provider], 108 | 'No channel raw input data found.' 109 | ); 110 | 111 | // Required: auth 112 | assert(params.raw_input_data.auth, 'No auth found.'); 113 | } 114 | 115 | module.exports = { 116 | main, 117 | name: 'conversation/call-conversation', 118 | validateParams 119 | }; 120 | -------------------------------------------------------------------------------- /conversation/setup.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | # Pipeline name 4 | # eg: my-flex-pipeline_ 5 | PIPELINE_NAME=$1 6 | 7 | PACKAGE_NAME="${PIPELINE_NAME}conversation" 8 | 9 | bx wsk package update $PACKAGE_NAME \ 10 | -p version "v1" \ 11 | -p version_date "2018-07-10" 12 | 13 | bx wsk action update $PACKAGE_NAME/call-conversation call-conversation.js 14 | -------------------------------------------------------------------------------- /deploy/README.md: -------------------------------------------------------------------------------- 1 | # Watson Cloud Functions Deploy Directory 2 | 3 | This directory contains scripts and actions used to implement the automated Conversation connector deployment. If you want to implement your own deployment process, you can use this code as an example. 4 | 5 | If you just want to deploy an instance of the Conversation connector, see the [main README](../README.md). 6 | 7 | ## Setup 8 | 9 | To set up artifacts in this directory, return to the root directory and run `./setup.sh -n`. The `-n` flag will create OpenWhisk actions in this directory into your namespace. Note that these artifacts will be updated in your namespace but without a deployment name, as `-n` will ignore deployment names. 10 | 11 | ## Architecture 12 | 13 | Artifact deployment is split into 4 different endpoints: `check-deploy-exists`, `populate-actions`, `verify-${channel}`, and `${channel}/deploy`. 14 | 15 | ### check-deploy-exists 16 | 17 | This action accepts a Bluemix access token, an OpenWhisk namespace, and a deployment name. It returns whether the deployment already exists in the user's OpenWhisk namespace. The user's OpenWhisk instance is determined by the Bluemix token as well as the OpenWhisk namespace sting provided. 18 | 19 | The inputs to this action look like so: 20 | 21 | ``` 22 | { 23 | state: { 24 | auth: { 25 | access_token: 'my_bluemix_access_token', 26 | refresh_token: 'my_bluemix_refresh_token' 27 | }, 28 | wsk: { 29 | namespace: 'myorganization_myspace' 30 | }, 31 | name: 'my_deployment_name' 32 | } 33 | } 34 | ``` 35 | 36 | The output is a JSON object containing the status `code` as well as a success or error `message`. 37 | 38 | ### populate-actions 39 | 40 | This action provides to the user all non-channel related artifacts, and uploads them onto the user's Bluemix or OpenWhisk accounts. This will create the following: 41 | * **Bluemix**: CloudantNoSQLDB (Lite/Free Plan) service instance; within this instance: 42 | * *contextdb* Context database 43 | * *authdb* Authentications database; within this database: 44 | * a single document storing the authentication keys of the current deployment 45 | * **OpenWhisk action**: ${deployment-name}_starter-code/pre-normalize 46 | * **OpenWhisk action**: ${deployment-name}_context/load-context 47 | * **OpenWhisk action**: ${deployment-name}_starter-code/pre-conversation 48 | * **OpenWhisk action**: ${deployment-name}_conversation/call-conversation 49 | * **OpenWhisk action**: ${deployment-name}_starter-code/post-conversation 50 | * **OpenWhisk action**: ${deployment-name}_context/save-context 51 | * **OpenWhisk action**: ${deployment-name}_starter-code/post-normalize 52 | 53 | The `${deployment-name}` is the name of the deployment specified by the input parameters. 54 | 55 | The inputs to this action look like so: 56 | 57 | ``` 58 | { 59 | state: { 60 | auth: { 61 | access_token: 'my_bluemix_access_token', 62 | refresh_token: 'my_bluemix_refresh_token' 63 | }, 64 | wsk: { 65 | namespace: 'myorganization_myspace' 66 | }, 67 | conversation: { 68 | guid: 'my_conversation_service_guid', 69 | workspace_id: 'my_conversation_service_workspace_id' 70 | }, 71 | name: 'my_deployment_name' 72 | } 73 | } 74 | ``` 75 | 76 | The output is a JSON object containing the status `code` as well as a success or error `message`. 77 | 78 | ### verify-${channel} 79 | 80 | This action provides to the user all channel-specific artifacts, and uploads them onto the user's Bluemix or OpenWhisk accounts. This will create or modify the following: 81 | * **Bluemix**: inside the previously created CloudantNoSQLDB *authdb* database, the same document will be updatead to include the channel-specific credentials, such as authentication keys or access tokens 82 | * **OpenWhisk action**: ${deployment-name}_${channel}/deploy 83 | * **OpenWhisk action**: ${deployment-name}_${channel}/receive 84 | * **OpenWhisk action**: ${deployment-name}_starter-code/normalize-${channel}-for-conversation 85 | * **OpenWhisk action**: ${deployment-name}_starter-code/normalize-conversation-for-${channel} 86 | * **OpenWhisk action**: ${deployment-name}_${channel}/post 87 | 88 | The `${deployment-name}` is the name of the deployment specified by the input parameters. 89 | The `${channel}` is determined by the exactly version of `verify-${channel}` action invoked. 90 | 91 | The inputs to this action look like so: 92 | 93 | ``` 94 | { 95 | state: { 96 | auth: { 97 | access_token: 'my_bluemix_access_token', 98 | refresh_token: 'my_bluemix_refresh_token' 99 | }, 100 | wsk: { 101 | namespace: 'myorganization_myspace' 102 | }, 103 | conversation: { 104 | guid: 'my_conversation_service_guid', 105 | workspace_id: 'my_conversation_service_workspace_id' 106 | }, 107 | ${channel}: { 108 | /* all channel's authentication keys go in here */ 109 | }. 110 | name: 'my_deployment_name' 111 | } 112 | } 113 | ``` 114 | The ouputs of this action look like so: 115 | 116 | ``` 117 | { 118 | code: 200, 119 | message: 'OK', 120 | request_url: 'https://some_request_url.com', 121 | redirect_url: 'https://some_redirect_url.com', 122 | authorize_url: 'https://some_redirect_url.com' 123 | } 124 | ``` 125 | 126 | The Request URL, or Webhook URL, is used by channel or chat service to send its received messages to. \ 127 | The Redirect URL is the URL the chat server redirects to after the user agrees to authenticate during the OAuth process. \ 128 | The Authorize URL is the URL the user needs to go to in order to start the OAuth process. If you have a UI with a button that starts the OAuth process, use this URL for your "Authorize ${channel}" button. 129 | 130 | ### ${channel}/deploy 131 | 132 | After your user goes through the OAuth process, the chat service's server will automatically invoke this action. You will automatically receive a status `code` and `message` depicting success or error. 133 | -------------------------------------------------------------------------------- /deploy/check-deploy-exists.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright IBM Corp. 2017 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the License); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an AS IS BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | 'use strict'; 18 | 19 | const assert = require('assert'); 20 | const openwhisk = require('openwhisk'); 21 | const request = require('request'); 22 | 23 | const apihost = 'openwhisk.ng.bluemix.net'; 24 | 25 | /** 26 | * Accepts a user's Bluemix tokens and org-space, as well as a deployment name, 27 | * and returns whether the deployment already exists in the user's Cloud Functions namespace. 28 | * 29 | * @param {JSON} params - Parameters passed into the action 30 | * @return {Promise} - status code and message depicting success or error 31 | */ 32 | function main(params) { 33 | return new Promise((resolve, reject) => { 34 | try { 35 | validateParameters(params); 36 | } catch (e) { 37 | reject({ 38 | code: 400, 39 | message: e.message 40 | }); 41 | } 42 | 43 | const accessToken = params.state.auth.access_token; 44 | const refreshToken = params.state.auth.refresh_token; 45 | const wskNamespace = params.state.wsk.namespace; 46 | const deployName = params.state.name; 47 | 48 | checkRegex(deployName) 49 | .then(() => { 50 | return getCloudFunctionsFromBluemix( 51 | accessToken, 52 | refreshToken, 53 | wskNamespace 54 | ); 55 | }) 56 | .catch(error => { 57 | if (typeof error === 'string') { 58 | reject({ 59 | code: 400, 60 | message: error 61 | }); 62 | } else { 63 | const returnError = error; 64 | returnError.code = 400; 65 | returnError.message = returnError.message || returnError.error; 66 | reject(returnError); 67 | } 68 | }) 69 | .then(ow => { 70 | return ow.actions.get({ name: deployName, namespace: wskNamespace }); 71 | }) 72 | .then( 73 | () => { 74 | reject({ 75 | code: 400, 76 | message: `Deployment "${deployName}" already exists.` 77 | }); 78 | }, 79 | () => { 80 | resolve({ 81 | code: 200, 82 | message: 'OK' 83 | }); 84 | } 85 | ); 86 | }); 87 | } 88 | 89 | /** 90 | * Checks whether the deployment name is valid to use. 91 | * 92 | * @param {string} deploymentName - deployment name 93 | * @return {Promise} - resolve if deployment name is valid, reject otherwise 94 | */ 95 | function checkRegex(deploymentName) { 96 | return new Promise((resolve, reject) => { 97 | const matchString = /^([a-zA-Z0-9][a-zA-Z0-9-]{0,255})$/; 98 | 99 | if (!matchString.test(deploymentName)) { 100 | reject( 101 | 'Deployment name contains invalid characters. Please use only the following characters in your deployment name: "a-z A-Z 0-9 -". Additionally, your deployment name cannot start with a -, and your name cannot be longer than 256 characters.' 102 | ); 103 | } else { 104 | resolve(deploymentName); 105 | } 106 | }); 107 | } 108 | 109 | /** 110 | * Get the user's Cloud Functions credentials using his Bluemix access and refresh tokens. 111 | * 112 | * @param {string} accessToken - Bluemix access token 113 | * @param {string} refreshToken - Bluemix refresh token 114 | * @param {string} namespace - Bluemix organization_space 115 | * @return {JSON} - Cloud Functions credentials: { apihost, apikey } 116 | */ 117 | function getCloudFunctionsFromBluemix(accessToken, refreshToken, namespace) { 118 | const url = `https://${apihost}/bluemix/v2/authenticate`; 119 | 120 | const postData = { accessToken, refreshToken }; 121 | 122 | return new Promise((resolve, reject) => { 123 | request.post( 124 | { 125 | headers: { 'Content-Type': 'application/json' }, 126 | form: postData, 127 | url 128 | }, 129 | (error, response, body) => { 130 | if (error) { 131 | reject(error.message); 132 | } else { 133 | const jsonBody = JSON.parse(body); 134 | if (jsonBody.error) { 135 | reject(jsonBody.error); 136 | } else { 137 | for (let i = 0; i < jsonBody.namespaces.length; i += 1) { 138 | if (jsonBody.namespaces[i].name === namespace) { 139 | const namespaceKeys = jsonBody.namespaces[i]; 140 | resolve( 141 | openwhisk({ 142 | api_key: `${namespaceKeys.uuid}:${namespaceKeys.key}`, 143 | apihost, 144 | namespace 145 | }) 146 | ); 147 | } 148 | } 149 | 150 | reject(`Could not find user namespace: ${namespace}.`); 151 | } 152 | } 153 | } 154 | ); 155 | }); 156 | } 157 | 158 | /** 159 | * Validates the required parameters for running this action. 160 | * 161 | * @param {JSON} params - the parameters passed into the action 162 | */ 163 | function validateParameters(params) { 164 | // Required: state object 165 | assert(params.state, "Could not get user's input information."); 166 | 167 | // Required: Bluemix authentication 168 | assert( 169 | params.state.auth && 170 | params.state.auth.access_token && 171 | params.state.auth.refresh_token, 172 | "Could not get user's Bluemix credentials." 173 | ); 174 | 175 | // Required: Cloud Functions namespace 176 | assert( 177 | params.state.wsk && params.state.wsk.namespace, 178 | "Could not get user's Bluemix credentials." 179 | ); 180 | 181 | // Required: Name of deployment 182 | assert(params.state.name, 'Could not get deployment name.'); 183 | } 184 | 185 | module.exports = main; 186 | -------------------------------------------------------------------------------- /deploy/create-cloudant-database.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright IBM Corp. 2017 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the License); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an AS IS BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | 'use strict'; 18 | 19 | const assert = require('assert'); 20 | const request = require('request'); 21 | 22 | const RETRY_TIMEOUT = 400; 23 | 24 | /** 25 | * Accepts a cloudant instance object and a database name, 26 | * and creates a cloudant database within. 27 | * 28 | * @param {JSON} params - Parameters passed into the action 29 | * @return {Promise} - status code and message depicting success or error 30 | */ 31 | function main(params) { 32 | return new Promise((resolve, reject) => { 33 | try { 34 | validateParameters(params); 35 | } catch (error) { 36 | reject({ code: 400, message: error.message }); 37 | } 38 | 39 | const account = params.cloudant.username; 40 | const password = params.cloudant.password; 41 | const dbName = params.db_name; 42 | 43 | return retriableCreateCloudantDatabase(account, password, dbName) 44 | .then(() => { 45 | resolve({ code: 200, message: 'OK' }); 46 | }) 47 | .catch(error => { 48 | reject({ code: 400, message: error.error }); 49 | }); 50 | }); 51 | } 52 | 53 | /** 54 | * Try creating a cloudant database, retry if service timed out. 55 | * 56 | * @param {string} account - cloudant account name 57 | * @param {string} password - cloudant password 58 | * @param {string} dbName - cloudant database name 59 | * @return {string} - response to cloudant database creation 60 | */ 61 | function retriableCreateCloudantDatabase(account, password, dbName) { 62 | return new Promise((resolve, reject) => { 63 | const url = `https://${account}:${password}@${account}.cloudant.com/${dbName}`; 64 | 65 | return request( 66 | { 67 | method: 'PUT', 68 | url 69 | }, 70 | (error, response, body) => { 71 | if (error) { 72 | const errorString = typeof error === 'string' 73 | ? JSON.parse(error).error 74 | : error.error; 75 | if (errorString === 'service_unavailable') { 76 | sleep(RETRY_TIMEOUT) 77 | .then(() => { 78 | return retriableCreateCloudantDatabase( 79 | account, 80 | password, 81 | dbName 82 | ); 83 | }) 84 | .then(resolve) 85 | .catch(reject); 86 | } else { 87 | reject(error); 88 | } 89 | } else if (response.statusCode >= 500) { 90 | sleep(RETRY_TIMEOUT) 91 | .then(() => { 92 | return retriableCreateCloudantDatabase(account, password, dbName); 93 | }) 94 | .then(resolve) 95 | .catch(reject); 96 | } else if (response.statusCode < 200 || response.statusCode >= 400) { 97 | const responseBody = JSON.parse(response.body); 98 | if (responseBody.error === 'file_exists') { 99 | resolve({}); 100 | } else { 101 | reject(responseBody); 102 | } 103 | } else { 104 | resolve(body); 105 | } 106 | } 107 | ); 108 | }); 109 | } 110 | 111 | /** 112 | * Validates the required parameters for running this action. 113 | * 114 | * @param {JSON} params - the parameters passed into the action 115 | */ 116 | function validateParameters(params) { 117 | assert(params.cloudant, 'No cloudant object provided.'); 118 | assert(params.db_name, 'No database name provided.'); 119 | } 120 | 121 | /** 122 | * Sleep for a supplied amount of milliseconds. 123 | * 124 | * @param {integer} ms - number of milliseconds to sleep 125 | * @return {Promise} - Promise resolve 126 | */ 127 | function sleep(ms) { 128 | return new Promise(resolve => { 129 | setTimeout(resolve, ms); 130 | }); 131 | } 132 | 133 | module.exports = main; 134 | -------------------------------------------------------------------------------- /deploy/setup.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | bx wsk action update create-cloudant-lite-instance create-cloudant-lite-instance.js 4 | bx wsk action update create-cloudant-database create-cloudant-database.js 5 | bx wsk action update update-auth-document update-auth-document.js 6 | bx wsk action update check-deploy-exists check-deploy-exists.js -a web-export true 7 | bx wsk action update populate-actions populate-actions.js -a web-export true 8 | bx wsk action update verify-slack channels/slack/verify-slack.js -a web-export true 9 | -------------------------------------------------------------------------------- /deploy/update-auth-document.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright IBM Corp. 2017 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the License); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an AS IS BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | 'use strict'; 18 | 19 | const assert = require('assert'); 20 | const request = require('request'); 21 | 22 | const RETRY_TIMEOUT = 400; 23 | 24 | /** 25 | * Accepts cloudant authdb instance, database, document information, as well as auth doc, 26 | * and stores the auth doc into the cloudant database. 27 | * 28 | * @param {JSON} params - Parameters passed into the action 29 | * @return {Promise} - status code and message depicting success or error 30 | */ 31 | function main(params) { 32 | return new Promise((resolve, reject) => { 33 | try { 34 | validateParameters(params); 35 | } catch (error) { 36 | reject({ code: 400, message: error.message }); 37 | } 38 | 39 | const account = params.cloudant.username; 40 | const password = params.cloudant.password; 41 | const dbName = params.db_name; 42 | const authKey = params.auth_key; 43 | const pipeline = params.pipeline; 44 | 45 | const cloudantUrl = `https://${account}:${password}@${account}.cloudant.com/${dbName}/${authKey}`; 46 | 47 | return retriableLoadDocument(cloudantUrl) 48 | .then(doc => { 49 | return updateAuthDocument(doc, pipeline); 50 | }) 51 | .then(doc => { 52 | return retriableInsertDocument(cloudantUrl, doc); 53 | }) 54 | .then(() => { 55 | resolve({ code: 200, message: 'OK' }); 56 | }) 57 | .catch(error => { 58 | reject({ code: 400, message: error.error }); 59 | }); 60 | }); 61 | } 62 | 63 | /** 64 | * Try loading a cloudant database document, retry if service timed out. 65 | * 66 | * @param {string} url - cloudant document url 67 | * @return {string} - document loaded 68 | */ 69 | function retriableLoadDocument(url) { 70 | return new Promise((resolve, reject) => { 71 | return request( 72 | { 73 | url 74 | }, 75 | (error, response, body) => { 76 | if (error) { 77 | const errorString = typeof error === 'string' 78 | ? JSON.parse(error).error 79 | : error.error; 80 | if (errorString === 'service_unavailable') { 81 | sleep(RETRY_TIMEOUT) 82 | .then(() => { 83 | return retriableLoadDocument(url); 84 | }) 85 | .then(resolve) 86 | .catch(reject); 87 | } else { 88 | reject(error); 89 | } 90 | } else if (response.statusCode >= 500) { 91 | sleep(RETRY_TIMEOUT) 92 | .then(() => { 93 | return retriableLoadDocument(url); 94 | }) 95 | .then(resolve) 96 | .catch(reject); 97 | } else if (response.statusCode === 404) { 98 | resolve({}); 99 | } else if (response.statusCode < 200 || response.statusCode >= 400) { 100 | reject(JSON.parse(response.body)); 101 | } else { 102 | resolve(JSON.parse(body)); 103 | } 104 | } 105 | ); 106 | }); 107 | } 108 | /** 109 | * Try inserting a cloudant database document, retry if service timed out. 110 | * 111 | * @param {string} url - cloudant document url 112 | * @param {JSON} doc - auth documentation 113 | * @return {string} - response to cloudant document insertion 114 | */ 115 | function retriableInsertDocument(url, doc) { 116 | return new Promise((resolve, reject) => { 117 | return request( 118 | { 119 | method: 'PUT', 120 | form: JSON.stringify(doc), 121 | url 122 | }, 123 | (error, response, body) => { 124 | if (error) { 125 | const errorString = typeof error === 'string' 126 | ? JSON.parse(error).error 127 | : error.error; 128 | if (errorString === 'service_unavailable') { 129 | sleep(RETRY_TIMEOUT) 130 | .then(() => { 131 | return retriableInsertDocument(url, doc); 132 | }) 133 | .then(resolve) 134 | .catch(reject); 135 | } else { 136 | reject(error); 137 | } 138 | } else if (response.statusCode >= 500) { 139 | sleep(RETRY_TIMEOUT) 140 | .then(() => { 141 | return retriableInsertDocument(url, doc); 142 | }) 143 | .then(resolve) 144 | .catch(reject); 145 | } else if (response.statusCode < 200 || response.statusCode >= 400) { 146 | reject(JSON.parse(response.body)); 147 | } else { 148 | resolve(JSON.parse(body)); 149 | } 150 | } 151 | ); 152 | }); 153 | } 154 | 155 | /** 156 | * Update auth document 157 | * 158 | * @param {JSON} doc - old auth doc 159 | * @param {JSON} pipeline - updated JSON pipeline 160 | * @return {JSON} - new auth doc 161 | */ 162 | function updateAuthDocument(doc, pipeline) { 163 | return new Promise(resolve => { 164 | const newDoc = doc; 165 | 166 | // Save Conversation info from pipeline object 167 | newDoc.conversation = pipeline.conversation; 168 | 169 | // Save only deployment channel info (based on name) from pipeline object 170 | if (pipeline.channel && pipeline.channel.name) { 171 | newDoc[pipeline.channel.name] = pipeline.channel[pipeline.channel.name]; 172 | } 173 | 174 | resolve(newDoc); 175 | }); 176 | } 177 | 178 | /** 179 | * Sleep for a supplied amount of milliseconds. 180 | * 181 | * @param {integer} ms - number of milliseconds to sleep 182 | * @return {Promise} - Promise resolve 183 | */ 184 | function sleep(ms) { 185 | return new Promise(resolve => { 186 | setTimeout(resolve, ms); 187 | }); 188 | } 189 | 190 | /** 191 | * Validates the required parameters for running this action. 192 | * 193 | * @param {JSON} params - the parameters passed into the action 194 | */ 195 | function validateParameters(params) { 196 | assert(params.cloudant, 'No cloudant object provided.'); 197 | assert(params.db_name, 'No database name provided.'); 198 | assert(params.pipeline, 'No pipeline provided.'); 199 | assert(params.auth_key, 'No auth key provided.'); 200 | } 201 | 202 | module.exports = main; 203 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "conversation-connector", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "./test/scripts/run_tests.sh" 8 | }, 9 | "repository": { 10 | "type": "git", 11 | "url": "https://github.com/watson-developer-cloud/conversation-connector.git" 12 | }, 13 | "author": "", 14 | "license": "Apache-2.0", 15 | "devDependencies": { 16 | "assert": "^1.4.1", 17 | "eslint": "^3.18.0", 18 | "eslint-config-airbnb-base": "^11.1.2", 19 | "eslint-plugin-import": "^2.2.0", 20 | "eslint-plugin-prettier": "^2.0.1", 21 | "istanbul": "^0.4.5", 22 | "mocha": "^3.2.0", 23 | "nock": "^9.0.13", 24 | "prettier": "^0.22.0", 25 | "proxyquire": "^1.8.0", 26 | "sinon": "^3.2.1", 27 | "underscore": "^1.8.3" 28 | }, 29 | "dependencies": { 30 | "@cloudant/cloudant": "^1.8.0", 31 | "merge": "^1.2.0", 32 | "object.omit": "^2.0.1", 33 | "object.pick": "^1.2.0", 34 | "openwhisk": "^3.5.1", 35 | "request": "^2.81.0", 36 | "request-promise": "^4.2.1", 37 | "uuid": "^3.1.0", 38 | "watson-developer-cloud": "^2.29.0" 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /providers.json: -------------------------------------------------------------------------------- 1 | { 2 | "bluemix": { 3 | "api_host": "", 4 | "api_key": "", 5 | "org": "", 6 | "space": "" 7 | }, 8 | "pipeline": [ 9 | { 10 | "channel": { 11 | "facebook": { 12 | "app_secret": "xxxxxx", 13 | "page_access_token": "xxxxxx", 14 | "verification_token": "xxxxxx" 15 | }, 16 | "name": "slack", 17 | "slack": { 18 | "client_id": "xxxx", 19 | "client_secret": "xxxx", 20 | "verification_token": "xxxx" 21 | } 22 | }, 23 | "conversation": { 24 | "username": "xxxx", 25 | "password": "xxxx", 26 | "workspace_id": "xxxx" 27 | }, 28 | "name": "xxxx" 29 | } 30 | ] 31 | } 32 | -------------------------------------------------------------------------------- /readme_images/connector_pipeline.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/watson-developer-cloud/conversation-connector/c33641c24f27614ece13e8de85b04ac32b5b7a30/readme_images/connector_pipeline.png -------------------------------------------------------------------------------- /readme_images/conv-connector-arch-with-multipost.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/watson-developer-cloud/conversation-connector/c33641c24f27614ece13e8de85b04ac32b5b7a30/readme_images/conv-connector-arch-with-multipost.jpg -------------------------------------------------------------------------------- /starter-code/normalize-for-conversation/normalize-facebook-for-conversation.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright IBM Corp. 2017 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the License); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an AS IS BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | 'use strict'; 18 | 19 | const assert = require('assert'); 20 | 21 | /** 22 | * Converts facebook channel-formatted JSON data into Conversation SDK formatted JSON input. 23 | * 24 | * @param {JSON} params - Each individual messaging entry/event 25 | * { 26 | "sender": { "id": "1637923046xxxxxx" }, 27 | "recipient": { "id": "268440730xxxxxx" }, 28 | "timestamp": 1501786719609, 29 | "message": { 30 | "mid": "mid.$cAACu1giyQ85j2rwNfVdqxxxxxxxx", 31 | "seq": 3054, 32 | "text": "find a restaurant" 33 | } 34 | } 35 | * @return {JSON} - JSON data formatted as input for Conversation SDK 36 | */ 37 | function main(params) { 38 | return new Promise((resolve, reject) => { 39 | validateParameters(params); 40 | 41 | const auth = params.auth; 42 | 43 | getTextFromPayload(params) 44 | .then(result => { 45 | const conversationJson = { 46 | conversation: { 47 | input: { 48 | text: result 49 | } 50 | }, 51 | raw_input_data: { 52 | facebook: params.facebook, 53 | provider: 'facebook', 54 | auth, 55 | // This cloudant_key lives till context/saveContext so the action can perform 56 | // operations in the Cloudant db. 57 | // Other channels must add a similar parameter 58 | // which uniquely identifies a conversation for a user. 59 | cloudant_context_key: generateCloudantKey(params, auth) 60 | } 61 | }; 62 | resolve(conversationJson); 63 | }) 64 | .catch(reject); 65 | }); 66 | } 67 | 68 | /** 69 | * Function checks for regular text message or a postback event payload 70 | * and sends it to watson conversation 71 | * @param {JSON} params - Params coming into the action 72 | * @return {JSON} - Text that is to be sent to conversation 73 | */ 74 | function getTextFromPayload(params) { 75 | return new Promise((resolve, reject) => { 76 | // 1. Message Type Event 77 | // Extract text from message event to send it to Conversation 78 | const messageEventPayload = params.facebook.message && 79 | params.facebook.message.text; 80 | 81 | // 2. Postback type event. Usually detected on button clicks 82 | // Extract text (postback payload) from postback event to send it to Conversation 83 | const postbackEventPayload = params.facebook.postback && 84 | params.facebook.postback.payload; 85 | 86 | /** 87 | * You can add code to handle other facebook events HERE 88 | */ 89 | 90 | if (messageEventPayload) { 91 | resolve(messageEventPayload); 92 | } else if (postbackEventPayload) { 93 | resolve(postbackEventPayload); 94 | } else { 95 | reject( 96 | 'Neither message.text event detected nor postback.payload event detected. Please add appropriate code to handle a different facebook event.' 97 | ); 98 | } 99 | }); 100 | } 101 | 102 | /** 103 | * Validates the required parameters for running this action. 104 | * 105 | * @param {JSON} params - the parameters passed into the action 106 | */ 107 | function validateParameters(params) { 108 | // Required: the provider must be known and supplied 109 | assert(params.provider, "Provider not supplied or isn't Facebook."); 110 | // Required: JSON data for the channel provider must be supplied 111 | assert(params.facebook, 'Facebook JSON data is missing.'); 112 | // Required: auth 113 | assert(params.auth, 'No auth found.'); 114 | } 115 | 116 | /** 117 | * Builds and returns a Cloudant database key from Facebook input parameters. 118 | * 119 | * @param {JSON} params - The parameters passed into the action 120 | * @return {string} - cloudant database key 121 | */ 122 | function generateCloudantKey(params, auth) { 123 | const fbSenderId = params.facebook && 124 | params.facebook.sender && 125 | params.facebook.sender.id; 126 | const fbWorkspaceId = auth.conversation.workspace_id; 127 | const fbRecipientId = params.facebook && 128 | params.facebook.recipient && 129 | params.facebook.recipient.id; 130 | 131 | return `facebook_${fbSenderId}_${fbWorkspaceId}_${fbRecipientId}`; 132 | } 133 | 134 | module.exports = { 135 | main, 136 | name: 'starter-code/normalize-facebook-for-conversation', 137 | validateParameters 138 | }; 139 | -------------------------------------------------------------------------------- /starter-code/normalize-for-conversation/normalize-slack-for-conversation.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright IBM Corp. 2017 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the License); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an AS IS BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | 'use strict'; 18 | 19 | const assert = require('assert'); 20 | 21 | /** 22 | * Converts Slack channel-formatted JSON data into Conversation SDK formatted JSON input. 23 | * 24 | * @param {JSON} params - input parameters in Slack's event subscription format 25 | * @return {JSON} - JSON data formatted as input for Conversation SDK 26 | */ 27 | function main(params) { 28 | return new Promise(resolve => { 29 | validateParameters(params); 30 | const auth = params.auth; 31 | 32 | resolve({ 33 | conversation: { 34 | input: { 35 | text: getSlackInputMessage(params) 36 | } 37 | }, 38 | raw_input_data: { 39 | slack: params.slack, 40 | provider: 'slack', 41 | bot_id: params.bot_id, 42 | auth, 43 | cloudant_context_key: generateCloudantKey(params, auth) 44 | } 45 | }); 46 | }); 47 | } 48 | 49 | /** 50 | * Gets and extracts the Slack input message from either a text or interactive message. 51 | * 52 | * @param {JSON} params - The parameters passed into the action 53 | * @return {string} - Slack input message 54 | */ 55 | function getSlackInputMessage(params) { 56 | const slackEvent = params.slack.event; 57 | const slackEventMessage = slackEvent && slackEvent.message; 58 | 59 | const textMessage = slackEvent && (slackEvent.text || slackEventMessage.text); 60 | if (textMessage) { 61 | return textMessage; 62 | } 63 | 64 | const payloadAction = params.slack.payload && 65 | JSON.parse(params.slack.payload) && 66 | JSON.parse(params.slack.payload).actions && 67 | JSON.parse(params.slack.payload).actions[0]; 68 | const buttonMessage = payloadAction && payloadAction.value; 69 | if (buttonMessage) { 70 | return buttonMessage; 71 | } 72 | 73 | const menuMessage = payloadAction && 74 | payloadAction.selected_options && 75 | payloadAction.selected_options 76 | .reduce( 77 | (array, x) => { 78 | if (x.value) { 79 | return array.concat(x.value); 80 | } 81 | return array; 82 | }, 83 | [] 84 | ) 85 | .join(' '); 86 | return menuMessage; 87 | } 88 | 89 | /** 90 | * Builds and returns a Cloudant database key from Slack input parameters. 91 | * 92 | * @param {JSON} params - The parameters passed into the action 93 | * @return {string} - cloudant database key 94 | */ 95 | function generateCloudantKey(params, auth) { 96 | assert( 97 | auth.conversation && auth.conversation.workspace_id, 98 | 'auth.conversation.workspace_id absent!' 99 | ); 100 | 101 | const slackEvent = params.slack.event; 102 | const slackPayload = params.slack.payload && JSON.parse(params.slack.payload); 103 | 104 | const slackTeamId = slackPayload 105 | ? slackPayload.team && slackPayload.team.id 106 | : params.slack.team_id; 107 | const slackUserId = slackPayload 108 | ? slackPayload.user && slackPayload.user.id 109 | : slackEvent && 110 | (slackEvent.user || (slackEvent.message && slackEvent.message.user)); 111 | const slackChannelId = slackPayload 112 | ? slackPayload.channel && slackPayload.channel.id 113 | : slackEvent && slackEvent.channel; 114 | const conversationWorkspaceId = auth.conversation.workspace_id; 115 | 116 | return `slack_${slackTeamId}_${conversationWorkspaceId}_${slackUserId}_${slackChannelId}`; 117 | } 118 | 119 | /** 120 | * Validates the required parameters for running this action. 121 | * 122 | * @param {JSON} params - the parameters passed into the action 123 | */ 124 | function validateParameters(params) { 125 | // Required: channel provider must be slack 126 | assert(params.provider, "Provider not supplied or isn't Slack."); 127 | assert.equal( 128 | params.provider, 129 | 'slack', 130 | "Provider not supplied or isn't Slack." 131 | ); 132 | 133 | // Required: JSON data for the channel provider 134 | assert(params.slack, 'Slack JSON data is missing.'); 135 | 136 | // Required: either the Slack event subscription (text message) 137 | // or the callback ID (interactive message) 138 | const messageType = params.slack.type || 139 | (params.slack.payload && 140 | JSON.parse(params.slack.payload) && 141 | JSON.parse(params.slack.payload).callback_id); 142 | assert(messageType, 'No Slack message type specified.'); 143 | 144 | const slackPayload = params.slack.payload && JSON.parse(params.slack.payload); 145 | 146 | // Required: Slack team ID 147 | const slackTeamId = params.slack.team_id || 148 | (slackPayload.team && slackPayload.team.id); 149 | assert(slackTeamId, 'Slack team ID not found.'); 150 | 151 | const slackEvent = params.slack.event; 152 | const slackEventMessage = slackEvent && slackEvent.message; 153 | 154 | // Required: Slack user ID 155 | const slackUserId = slackEvent 156 | ? slackEvent.user || slackEventMessage.user 157 | : slackPayload && slackPayload.user && slackPayload.user.id; 158 | assert(slackUserId, 'Slack user ID not found.'); 159 | 160 | // Required: Slack channel 161 | const slackChannel = slackEvent 162 | ? slackEvent.channel 163 | : slackPayload && slackPayload.channel && slackPayload.channel.id; 164 | assert(slackChannel, 'Slack channel not found.'); 165 | 166 | // Required: Slack message 167 | let slackMessage = slackEvent && (slackEvent.text || slackEventMessage.text); 168 | if (!slackMessage) { 169 | const payloadAction = slackPayload && 170 | slackPayload.actions && 171 | slackPayload.actions[0]; 172 | 173 | slackMessage = payloadAction && 174 | (payloadAction.value || 175 | (payloadAction.selected_options && 176 | payloadAction.selected_options[0] && 177 | payloadAction.selected_options[0].value)); 178 | } 179 | assert(slackMessage, 'No Slack message text provided.'); 180 | 181 | // Required: auth 182 | assert(params.auth, 'No auth found.'); 183 | } 184 | 185 | module.exports = { 186 | main, 187 | name: 'starter-code/normalize-slack-for-conversation', 188 | validateParameters 189 | }; 190 | -------------------------------------------------------------------------------- /starter-code/post-conversation.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright IBM Corp. 2017 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the License); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an AS IS BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | 'use strict'; 18 | 19 | /** 20 | * Here the user can edit the Conversation SDK output message as he desires. 21 | * 22 | * @param {JSON} params - output JSON sent by Conversation module 23 | * @return {JSON} - modified output JSON to be sent to normalization and to channel/post 24 | */ 25 | function main(params) { 26 | return Promise.resolve(params); 27 | } 28 | 29 | module.exports = main; 30 | -------------------------------------------------------------------------------- /starter-code/post-normalize.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright IBM Corp. 2017 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the License); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an AS IS BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | 'use strict'; 18 | 19 | /** 20 | * The user can edit data after the normalization step in the sequence 21 | * 22 | * @param {JSON} params - output JSON to be posted back to the channel 23 | * @return {JSON} - modified output JSON to be sent to channel/post 24 | */ 25 | function main(params) { 26 | return Promise.resolve(params); 27 | } 28 | 29 | module.exports = main; 30 | -------------------------------------------------------------------------------- /starter-code/pre-conversation.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright IBM Corp. 2017 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the License); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an AS IS BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | 'use strict'; 18 | 19 | /** 20 | * Here the user can edit the channel input message before send to Conversation module. 21 | * 22 | * @param {JSON} params - input JSON sent by the channel module 23 | * @return {JSON} - modified input JSON to be sent to the conversation module 24 | */ 25 | function main(params) { 26 | return Promise.resolve(params); 27 | } 28 | 29 | module.exports = main; 30 | -------------------------------------------------------------------------------- /starter-code/pre-normalize.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright IBM Corp. 2017 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the License); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an AS IS BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | 'use strict'; 18 | 19 | /** 20 | * The user can edit data before the normalization step in the sequence 21 | * 22 | * @param {JSON} params - message sent from channel/receive 23 | * @return {JSON} - modified channel data to be sent to normalization and conversation module 24 | */ 25 | function main(params) { 26 | return Promise.resolve(params); 27 | } 28 | 29 | module.exports = main; 30 | -------------------------------------------------------------------------------- /starter-code/setup.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | PIPELINE_NAME=$1 4 | 5 | bx wsk package update ${PIPELINE_NAME}starter-code 6 | 7 | for file in `find . -type f -name '*.js'`; do 8 | file_basename=`basename ${file}` 9 | file_basename=${file_basename%.*} 10 | bx wsk action update ${PIPELINE_NAME}starter-code/${file_basename} ${file} 11 | done 12 | -------------------------------------------------------------------------------- /test/end-to-end/breakdown.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | DEPLOY_NAME=$1 4 | 5 | bx wsk action delete $1-endtoend-slack-nocontext 6 | bx wsk action delete $1-endtoend-slack-nocontext_postsequence 7 | bx wsk action delete $1-endtoend-slack-nocontext_slack/receive 8 | bx wsk action delete $1-endtoend-slack-nocontext_slack/post 9 | bx wsk action delete $1-endtoend-slack-nocontext_slack/multiple_post 10 | bx wsk action delete $1-endtoend-slack-nocontext_conversation/call-conversation 11 | bx wsk action delete $1-endtoend-slack-nocontext_starter-code/pre-conversation 12 | bx wsk action delete $1-endtoend-slack-nocontext_starter-code/post-conversation 13 | bx wsk action delete $1-endtoend-slack-nocontext_starter-code/pre-normalize 14 | bx wsk action delete $1-endtoend-slack-nocontext_starter-code/post-normalize 15 | bx wsk action delete $1-endtoend-slack-nocontext_starter-code/normalize-slack-for-conversation 16 | bx wsk action delete $1-endtoend-slack-nocontext_starter-code/normalize-conversation-for-slack 17 | bx wsk package delete $1-endtoend-slack-nocontext_slack 18 | bx wsk package delete $1-endtoend-slack-nocontext_conversation 19 | bx wsk package delete $1-endtoend-slack-nocontext_starter-code 20 | 21 | bx wsk action delete $1-endtoend-slack-withcontext 22 | bx wsk action delete $1-endtoend-slack-withcontext_postsequence 23 | bx wsk action delete $1-endtoend-slack-withcontext_slack/receive 24 | bx wsk action delete $1-endtoend-slack-withcontext_slack/post 25 | bx wsk action delete $1-endtoend-slack-withcontext_slack/multiple_post 26 | bx wsk action delete $1-endtoend-slack-withcontext_conversation/call-conversation 27 | bx wsk action delete $1-endtoend-slack-withcontext_context/load-context 28 | bx wsk action delete $1-endtoend-slack-withcontext_context/save-context 29 | bx wsk action delete $1-endtoend-slack-withcontext_starter-code/pre-conversation 30 | bx wsk action delete $1-endtoend-slack-withcontext_starter-code/post-conversation 31 | bx wsk action delete $1-endtoend-slack-withcontext_starter-code/pre-normalize 32 | bx wsk action delete $1-endtoend-slack-withcontext_starter-code/post-normalize 33 | bx wsk action delete $1-endtoend-slack-withcontext_starter-code/normalize-slack-for-conversation 34 | bx wsk action delete $1-endtoend-slack-withcontext_starter-code/normalize-conversation-for-slack 35 | bx wsk package delete $1-endtoend-slack-withcontext_slack 36 | bx wsk package delete $1-endtoend-slack-withcontext_conversation 37 | bx wsk package delete $1-endtoend-slack-withcontext_context 38 | bx wsk package delete $1-endtoend-slack-withcontext_starter-code 39 | 40 | curl -s -XDELETE ${__TEST_CLOUDANT_URL}/contextdb 41 | curl -s -XDELETE ${__TEST_CLOUDANT_URL}/authdb 42 | curl -s -XPUT ${__TEST_CLOUDANT_URL}/contextdb 43 | curl -s -XPUT ${__TEST_CLOUDANT_URL}/authdb 44 | 45 | 46 | bx wsk action delete test-pipeline-facebook | grep -v 'ok' 47 | bx wsk action delete test-pipeline-context-facebook | grep -v 'ok' 48 | 49 | bx target -o ${__TEST_DEPLOYUSER_ORG} -s ${__TEST_DEPLOYUSER_SPACE} 50 | bx wsk property set --apihost ${__OW_API_HOST} --auth ${__TEST_DEPLOYUSER_WSK_API_KEY} --namespace ${__TEST_DEPLOYUSER_WSK_NAMESPACE} 51 | 52 | # Clean all artifacts created in the user-deploy namespace 53 | IFS=$'\n' 54 | while [ $(bx wsk action list | tail -n +2 | wc -l | awk '{print $1}') -gt 0 ]; do 55 | for line in `bx wsk action list | tail -n +2`; do 56 | actionName=${line%% *} 57 | execution=${line##* } 58 | bx wsk action delete $actionName 59 | done 60 | done 61 | 62 | for line in `bx wsk trigger list | tail -n +2`; do 63 | triggerName=${line%% *} 64 | execution=${line##* } 65 | bx wsk trigger delete $triggerName 66 | done 67 | 68 | for line in `bx wsk rule list | tail -n +2`; do 69 | ruleName=${line%% *} 70 | execution=${line##* } 71 | bx wsk rule delete $ruleName 72 | done 73 | 74 | for line in `bx wsk package list | tail -n +2`; do 75 | packageName=${line%% *} 76 | execution=${line##* } 77 | bx wsk package delete $packageName 78 | done 79 | IFS=$' \t\n' 80 | 81 | bx target -o ${__TEST_BX_USER_ORG} -s ${__TEST_BX_USER_SPACE} 82 | bx wsk property set --apihost ${__OW_API_HOST} --auth ${__OW_API_KEY} --namespace ${__OW_NAMESPACE} 83 | -------------------------------------------------------------------------------- /test/end-to-end/test.end-to-end.prechecks.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright IBM Corp. 2017 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the License); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an AS IS BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | 'use strict'; 18 | 19 | const assert = require('assert'); 20 | const openwhisk = require('openwhisk'); 21 | 22 | const safeExtractErrorMessage = require('./../utils/helper-methods.js').safeExtractErrorMessage; 23 | 24 | const pipelineName = process.env.__TEST_PIPELINE_NAME; 25 | 26 | describe('End-to-End tests: Conversation & Starter-code prerequisites', () => { 27 | const ow = openwhisk(); 28 | const requiredActions = [ 29 | `${pipelineName}_starter-code/pre-conversation`, 30 | `${pipelineName}_starter-code/post-conversation`, 31 | `${pipelineName}_conversation/call-conversation`, 32 | `${pipelineName}_starter-code/pre-normalize`, 33 | `${pipelineName}_starter-code/post-normalize`, 34 | `${pipelineName}_context/load-context`, 35 | `${pipelineName}_context/save-context` 36 | ]; 37 | 38 | requiredActions.forEach(action => { 39 | it(`${action} is deployed in Cloud Functions namespace`, () => { 40 | return ow.actions.get({ name: action }).then( 41 | () => {}, 42 | error => { 43 | assert(false, `${action}, ${safeExtractErrorMessage(error)}`); 44 | } 45 | ); 46 | }); 47 | }); 48 | }); 49 | -------------------------------------------------------------------------------- /test/integration/channels/facebook/breakdown.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | PIPELINE_NAME=$1 4 | PACKAGE_NAME="$1_facebook" 5 | 6 | bx wsk action delete ${PACKAGE_NAME}/middle | grep -v 'ok' 7 | bx wsk action delete ${PACKAGE_NAME}/integration-pipeline | grep -v 'ok' 8 | bx wsk action delete ${PIPELINE_NAME}postsequence | grep -v 'ok' 9 | -------------------------------------------------------------------------------- /test/integration/channels/facebook/middle.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright IBM Corp. 2017 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the License); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an AS IS BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | 'use strict'; 18 | 19 | /** 20 | * An action representing a "black box" from and to the channel specified. 21 | * 22 | * @params Paramters sent to this action by channel/receive: 23 | * { 24 | * facebook: { 25 | * ... 26 | * }, 27 | * provider: 'facebook' 28 | * } 29 | * 30 | * @return Return parameters required by facebook/post 31 | */ 32 | function main(params) { 33 | try { 34 | validateParams(params); 35 | } catch (e) { 36 | return Promise.reject(e.message); 37 | } 38 | 39 | return { 40 | recipient: { 41 | id: params.facebook.sender.id 42 | }, 43 | message: params.facebook.message, 44 | raw_input_data: { 45 | auth: params.auth 46 | } 47 | }; 48 | } 49 | 50 | /** 51 | * Validates the required parameters for running this action. 52 | * 53 | * @params - the parameters passed into the action 54 | */ 55 | function validateParams(params) { 56 | // Required: The channel provider communicating with this action 57 | if (!params.provider || params.provider !== 'facebook') { 58 | throw new Error('No facebook channel provider supplied.'); 59 | } 60 | // Required: The parameters of the channel provider 61 | if (!params.facebook) { 62 | throw new Error('No facebook data or event parameters provided.'); 63 | } 64 | 65 | // Required: Auth 66 | if (!params.auth) { 67 | throw new Error('No auth provided.'); 68 | } 69 | } 70 | 71 | module.exports = main; 72 | -------------------------------------------------------------------------------- /test/integration/channels/facebook/setup.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | PIPELINE_NAME="$1_" 4 | PACKAGE_NAME="$1_facebook" 5 | 6 | bx wsk action update ${PACKAGE_NAME}/middle ./test/integration/channels/facebook/middle.js | grep -v 'ok' 7 | 8 | bx wsk action update ${PACKAGE_NAME}/integration-pipeline --sequence ${PACKAGE_NAME}/middle,${PACKAGE_NAME}/multiple_post | grep -v 'ok' 9 | 10 | postSequence="${PIPELINE_NAME}starter-code/post-normalize,${PACKAGE_NAME}/post" 11 | 12 | bx wsk action update ${PIPELINE_NAME}postsequence --sequence ${postSequence} | grep -v 'ok' 13 | -------------------------------------------------------------------------------- /test/integration/channels/slack/breakdown.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | PACKAGE_NAME="$1" 4 | 5 | 6 | # send and receive text 7 | PIPELINE_SEND_TEXT="$1-integration-slack-send-text" 8 | 9 | CLOUDANT_AUTH_KEY="${PIPELINE_SEND_TEXT}" 10 | 11 | curl -s -XDELETE ${__TEST_CLOUDANT_URL}/authdb/${CLOUDANT_AUTH_KEY}?rev=$(curl -s ${__TEST_CLOUDANT_URL}/authdb/${CLOUDANT_AUTH_KEY} | jq -r ._rev) 12 | 13 | bx wsk action delete ${PIPELINE_SEND_TEXT} 14 | bx wsk action delete ${PIPELINE_SEND_TEXT}_postsequence 15 | bx wsk action delete ${PIPELINE_SEND_TEXT}_slack/send-text 16 | bx wsk action delete ${PIPELINE_SEND_TEXT}_slack/post 17 | bx wsk action delete ${PIPELINE_SEND_TEXT}_slack/multiple_post 18 | bx wsk action delete ${PIPELINE_SEND_TEXT}_slack/receive 19 | 20 | bx wsk package delete ${PIPELINE_SEND_TEXT}_slack 21 | 22 | 23 | # send text and receive an interactive message 24 | PIPELINE_SEND_ATTACHED_MESSAGE="$1-integration-slack-send-attached-message" 25 | 26 | CLOUDANT_AUTH_KEY="${PIPELINE_SEND_ATTACHED_MESSAGE}" 27 | 28 | curl -s -XDELETE ${__TEST_CLOUDANT_URL}/authdb/${CLOUDANT_AUTH_KEY}?rev=$(curl -s ${__TEST_CLOUDANT_URL}/authdb/${CLOUDANT_AUTH_KEY} | jq -r ._rev) 29 | 30 | bx wsk action delete ${PIPELINE_SEND_ATTACHED_MESSAGE} 31 | bx wsk action delete ${PIPELINE_SEND_ATTACHED_MESSAGE}_postsequence 32 | bx wsk action delete ${PIPELINE_SEND_ATTACHED_MESSAGE}_slack/send-attached-message 33 | bx wsk action delete ${PIPELINE_SEND_ATTACHED_MESSAGE}_slack/post 34 | bx wsk action delete ${PIPELINE_SEND_ATTACHED_MESSAGE}_slack/multiple_post 35 | bx wsk action delete ${PIPELINE_SEND_ATTACHED_MESSAGE}_slack/receive 36 | 37 | bx wsk package delete ${PIPELINE_SEND_ATTACHED_MESSAGE}_slack 38 | 39 | 40 | # send interactive click and receive a click response 41 | PIPELINE_SEND_ATTACHED_RESPONSE="$1-integration-slack-send-attached-response" 42 | 43 | CLOUDANT_AUTH_KEY="${PIPELINE_SEND_ATTACHED_RESPONSE}" 44 | 45 | curl -s -XDELETE ${__TEST_CLOUDANT_URL}/authdb/${CLOUDANT_AUTH_KEY}?rev=$(curl -s ${__TEST_CLOUDANT_URL}/authdb/${CLOUDANT_AUTH_KEY} | jq -r ._rev) 46 | 47 | bx wsk action delete ${PIPELINE_SEND_ATTACHED_RESPONSE} 48 | bx wsk action delete ${PIPELINE_SEND_ATTACHED_RESPONSE}_postsequence 49 | bx wsk action delete ${PIPELINE_SEND_ATTACHED_RESPONSE}_slack/send-attached-message-response 50 | bx wsk action delete ${PIPELINE_SEND_ATTACHED_RESPONSE}_slack/post 51 | bx wsk action delete ${PIPELINE_SEND_ATTACHED_RESPONSE}_slack/multiple_post 52 | bx wsk action delete ${PIPELINE_SEND_ATTACHED_RESPONSE}_slack/receive 53 | 54 | bx wsk package delete ${PIPELINE_SEND_ATTACHED_RESPONSE}_slack 55 | 56 | 57 | # Request and receive an interactive message requiring multipost 58 | PIPELINE_SEND_ATTACHED_MULTIPOST="$1-integration-slack-send-attached-multipost" 59 | 60 | CLOUDANT_AUTH_KEY="${PIPELINE_SEND_ATTACHED_MULTIPOST}" 61 | 62 | curl -s -XDELETE ${__TEST_CLOUDANT_URL}/authdb/${CLOUDANT_AUTH_KEY}?rev=$(curl -s ${__TEST_CLOUDANT_URL}/authdb/${CLOUDANT_AUTH_KEY} | jq -r ._rev) 63 | 64 | bx wsk action delete ${PIPELINE_SEND_ATTACHED_MULTIPOST}_slack/send-attached-message-multipost 65 | bx wsk action delete ${PIPELINE_SEND_ATTACHED_MULTIPOST}_slack/post 66 | bx wsk action delete ${PIPELINE_SEND_ATTACHED_MULTIPOST}_slack/multiple_post 67 | bx wsk action delete ${PIPELINE_SEND_ATTACHED_MULTIPOST}_slack/receive 68 | bx wsk action delete ${PIPELINE_SEND_ATTACHED_MULTIPOST}_postsequence 69 | bx wsk action delete ${PIPELINE_SEND_ATTACHED_MULTIPOST} 70 | 71 | bx wsk package delete ${PIPELINE_SEND_ATTACHED_MULTIPOST}_slack 72 | -------------------------------------------------------------------------------- /test/integration/channels/slack/send-attached-message-multipost.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright IBM Corp. 2018 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the License); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an AS IS BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | 'use strict'; 18 | 19 | const assert = require('assert'); 20 | 21 | /** 22 | * Mock action receiving a Slack event from slack/receive, 23 | * and sending an attachment to slack/post. 24 | * 25 | * @param {JSON} params - Slack event subscription 26 | * @return {JSON} - Slack POST parameters of text message 27 | */ 28 | function main(params) { 29 | return new Promise(resolve => { 30 | validateParameters(params); 31 | const auth = params.auth; 32 | 33 | const message = [ 34 | { 35 | text: 'Here is your multi-modal response.' 36 | }, 37 | { 38 | attachments: [ 39 | { 40 | title: 'Image title', 41 | pretext: 'Image description', 42 | image_url: 'https://s.w-x.co/240x180_twc_default.png' 43 | } 44 | ] 45 | }, 46 | { 47 | attachments: [ 48 | { 49 | text: 'Choose your location', 50 | callback_id: 'Choose your location', 51 | actions: [ 52 | { 53 | name: 'Location 1', 54 | type: 'button', 55 | text: 'Location 1', 56 | value: 'Location 1' 57 | }, 58 | { 59 | name: 'Location 2', 60 | type: 'button', 61 | text: 'Location 2', 62 | value: 'Location 2' 63 | }, 64 | { 65 | name: 'Location 3', 66 | type: 'button', 67 | text: 'Location 3', 68 | value: 'Location 3' 69 | } 70 | ] 71 | } 72 | ] 73 | } 74 | ]; 75 | 76 | resolve({ 77 | channel: params.slack.event.channel, 78 | message, 79 | raw_input_data: { 80 | bot_id: params.bot_id, 81 | provider: 'slack', 82 | slack: params.slack, 83 | auth 84 | } 85 | }); 86 | }); 87 | } 88 | 89 | /** 90 | * Validates the required parameters for running this action. 91 | * 92 | * @param {JSON} params - the parameters passed into the action 93 | */ 94 | function validateParameters(params) { 95 | // Required: The channel provider communicating with this action 96 | assert( 97 | params.provider && params.provider === 'slack', 98 | 'No Slack channel provider provided.' 99 | ); 100 | 101 | // Required: The parameters of the channel provider 102 | assert(params.slack, 'No Slack data provided.'); 103 | 104 | // Required: Slack event data 105 | assert(params.slack.event, 'No Slack event data provided.'); 106 | 107 | // Required: Slack channel 108 | assert(params.slack.event.channel, 'No Slack channel provided.'); 109 | 110 | // Required: Slack input text 111 | assert(params.slack.event.text, 'No Slack input text provided.'); 112 | 113 | // Required: Bot ID 114 | assert(params.bot_id, 'No bot ID provided.'); 115 | 116 | // Required: auth 117 | assert(params.auth, 'No auth provided.'); 118 | } 119 | 120 | module.exports = main; 121 | -------------------------------------------------------------------------------- /test/integration/channels/slack/send-attached-message-response.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright IBM Corp. 2017 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the License); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an AS IS BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | 'use strict'; 18 | 19 | const assert = require('assert'); 20 | 21 | /** 22 | * Mock action receiving a Slack attachment payload from slack/receive, 23 | * and sending an attached response to slack/post. 24 | * 25 | * @param {JSON} params - Slack event subscription 26 | * @return {JSON} - Slack POST parameters of text message 27 | */ 28 | function main(params) { 29 | return new Promise(resolve => { 30 | validateParameters(params); 31 | const auth = params.auth; 32 | 33 | const payload = JSON.parse(params.slack.payload); 34 | 35 | resolve({ 36 | channel: payload.channel.id, 37 | text: payload.original_message.text, 38 | attachments: [ 39 | { 40 | text: 'Message coming from Slack integration test.' 41 | } 42 | ], 43 | raw_input_data: { 44 | bot_id: params.bot_id, 45 | provider: 'slack', 46 | slack: params.slack, 47 | auth 48 | } 49 | }); 50 | }); 51 | } 52 | 53 | /** 54 | * Validates the required parameters for running this action. 55 | * 56 | * @param {JSON} params - the parameters passed into the action 57 | */ 58 | function validateParameters(params) { 59 | // Required: The channel provider communicating with this action 60 | assert( 61 | params.provider && params.provider === 'slack', 62 | 'No Slack channel provided supplied.' 63 | ); 64 | 65 | // Required: The parameters of the channel provider 66 | assert(params.slack, 'No Slack data or event parameters provided.'); 67 | 68 | // Required: Slack attached message payload 69 | assert( 70 | params.slack.payload && JSON.parse(params.slack.payload), 71 | 'No Slack data payload provided.' 72 | ); 73 | 74 | const payload = JSON.parse(params.slack.payload); 75 | 76 | // Required: Slack channel 77 | assert(payload.channel.id, 'No Slack channel provided.'); 78 | 79 | // Required: Slack original message 80 | assert(payload.original_message, 'No Slack original message provided.'); 81 | 82 | // Required: Bot ID 83 | assert(params.bot_id, 'No bot ID provided.'); 84 | 85 | // Required: auth 86 | assert(params.auth, 'No auth provided.'); 87 | } 88 | 89 | module.exports = main; 90 | -------------------------------------------------------------------------------- /test/integration/channels/slack/send-attached-message.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright IBM Corp. 2017 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the License); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an AS IS BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | 'use strict'; 18 | 19 | const assert = require('assert'); 20 | 21 | /** 22 | * Mock action receiving a Slack event from slack/receive, 23 | * and sending an attachment to slack/post. 24 | * 25 | * @param {JSON} params - Slack event subscription 26 | * @return {JSON} - Slack POST parameters of text message 27 | */ 28 | function main(params) { 29 | return new Promise(resolve => { 30 | validateParameters(params); 31 | const auth = params.auth; 32 | 33 | const attachments = [ 34 | { 35 | actions: [ 36 | { 37 | name: 'test_option_one', 38 | text: 'Test Option One', 39 | type: 'button', 40 | value: 'test option one' 41 | }, 42 | { 43 | name: 'test_option_two', 44 | text: 'Test Option Two', 45 | type: 'button', 46 | value: 'test option two' 47 | }, 48 | { 49 | name: 'test_option_three', 50 | text: 'Test Option Three', 51 | type: 'button', 52 | value: 'test option three' 53 | } 54 | ], 55 | fallback: 'Buttons not working...', 56 | callback_id: 'test_integration_options' 57 | } 58 | ]; 59 | 60 | resolve({ 61 | channel: params.slack.event.channel, 62 | text: params.slack.event.text, 63 | attachments, 64 | raw_input_data: { 65 | bot_id: params.bot_id, 66 | provider: 'slack', 67 | slack: params.slack, 68 | auth 69 | } 70 | }); 71 | }); 72 | } 73 | 74 | /** 75 | * Validates the required parameters for running this action. 76 | * 77 | * @param {JSON} params - the parameters passed into the action 78 | */ 79 | function validateParameters(params) { 80 | // Required: The channel provider communicating with this action 81 | assert( 82 | params.provider && params.provider === 'slack', 83 | 'No Slack channel provider provided.' 84 | ); 85 | 86 | // Required: The parameters of the channel provider 87 | assert(params.slack, 'No Slack data provided.'); 88 | 89 | // Required: Slack event data 90 | assert(params.slack.event, 'No Slack event data provided.'); 91 | 92 | // Required: Slack channel 93 | assert(params.slack.event.channel, 'No Slack channel provided.'); 94 | 95 | // Required: Slack input text 96 | assert(params.slack.event.text, 'No Slack input text provided.'); 97 | 98 | // Required: Bot ID 99 | assert(params.bot_id, 'No bot ID provided.'); 100 | 101 | // Required: auth 102 | assert(params.auth, 'No auth provided.'); 103 | } 104 | 105 | module.exports = main; 106 | -------------------------------------------------------------------------------- /test/integration/channels/slack/send-text.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright IBM Corp. 2017 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the License); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an AS IS BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | 'use strict'; 18 | 19 | const assert = require('assert'); 20 | 21 | /** 22 | * Mock action receiving a Slack event from slack/receive, 23 | * and sending a text message to slack/post. 24 | * 25 | * @param {JSON} params - Slack event subscription 26 | * @return {JSON} - Slack POST parameters of text message 27 | */ 28 | function main(params) { 29 | return new Promise(resolve => { 30 | validateParameters(params); 31 | const auth = params.auth; 32 | 33 | resolve({ 34 | channel: params.slack.event.channel, 35 | text: params.slack.event.text, 36 | raw_input_data: { 37 | bot_id: params.bot_id, 38 | provider: 'slack', 39 | slack: params.slack, 40 | auth 41 | } 42 | }); 43 | }); 44 | } 45 | 46 | /** 47 | * Validates the required parameters for running this action. 48 | * 49 | * @param {JSON} params - the parameters passed into the action 50 | */ 51 | function validateParameters(params) { 52 | // Required: The channel provider communicating with this action 53 | assert( 54 | params.provider && params.provider === 'slack', 55 | 'No Slack channel provider provided.' 56 | ); 57 | 58 | // Required: The parameters of the channel provider 59 | assert(params.slack, 'No Slack data provided.'); 60 | 61 | // Required: Slack event data 62 | assert(params.slack.event, 'No Slack event data provided.'); 63 | 64 | // Required: Slack channel 65 | assert(params.slack.event.channel, 'No Slack channel provided.'); 66 | 67 | // Required: Slack input text 68 | assert(params.slack.event.text, 'No Slack input text provided.'); 69 | 70 | // Required: Bot ID 71 | assert(params.bot_id, 'No bot ID provided.'); 72 | 73 | // Required: Auth 74 | assert(params.auth, 'No auth provided.'); 75 | } 76 | 77 | module.exports = main; 78 | -------------------------------------------------------------------------------- /test/integration/channels/slack/setup.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | PACKAGE_NAME="$1" 4 | 5 | retriableCreateDbDoc() { 6 | DOC="$1" 7 | URL="$2" 8 | 9 | for i in {1..10}; do 10 | out=$(curl -s -XPUT -d "$DOC" "$URL") 11 | e=$(echo $out | jq -r .error) 12 | if [ -z "$e" -o "$e" == "null" ]; then 13 | break 14 | else 15 | if [ "$e" == "conflict" -o "$e" == "file_exists" ]; then 16 | break 17 | fi 18 | echo "PUT url [$URL] returned with error [$e], retrying ($i)..." 19 | sleep 5 20 | fi 21 | done 22 | } 23 | 24 | AUTH_DOC=$(node -e 'const params = process.env; 25 | const doc = { 26 | slack: { 27 | client_id: params.__TEST_SLACK_CLIENT_ID, 28 | client_secret: params.__TEST_SLACK_CLIENT_SECRET, 29 | verification_token: params.__TEST_SLACK_VERIFICATION_TOKEN, 30 | bot_users: { 31 | "bot-id": { 32 | access_token: params.__TEST_SLACK_ACCESS_TOKEN, 33 | bot_access_token: params.__TEST_SLACK_BOT_ACCESS_TOKEN 34 | } 35 | } 36 | } 37 | }; 38 | console.log(JSON.stringify(doc)); 39 | ') 40 | 41 | 42 | # send and receive text 43 | PIPELINE_SEND_TEXT="$1-integration-slack-send-text" 44 | 45 | CLOUDANT_AUTH_KEY="${PIPELINE_SEND_TEXT}" 46 | 47 | retriableCreateDbDoc ${AUTH_DOC} ${__TEST_CLOUDANT_URL}/authdb/${CLOUDANT_AUTH_KEY} 48 | 49 | bx wsk package update ${PIPELINE_SEND_TEXT}_slack \ 50 | -a cloudant_auth_key "${CLOUDANT_AUTH_KEY}" \ 51 | -a cloudant_url "${__TEST_CLOUDANT_URL}" \ 52 | -a cloudant_auth_dbname "authdb" \ 53 | -a cloudant_context_dbname "contextdb" 54 | 55 | bx wsk action update ${PIPELINE_SEND_TEXT}_slack/receive ./channels/slack/receive/index.js -a web-export true 56 | bx wsk action update ${PIPELINE_SEND_TEXT}_slack/post ./channels/slack/post/index.js 57 | bx wsk action update ${PIPELINE_SEND_TEXT}_slack/multiple_post ./channels/slack/multiple_post/index.js 58 | 59 | bx wsk action update ${PIPELINE_SEND_TEXT}_slack/send-text ./test/integration/channels/slack/send-text.js 60 | bx wsk action update ${PIPELINE_SEND_TEXT} --sequence ${PIPELINE_SEND_TEXT}_slack/send-text,${PIPELINE_SEND_TEXT}_slack/multiple_post 61 | 62 | PIPELINE_SEND_TEXT_POST_SEQUENCE="${PIPELINE_SEND_TEXT}_slack/post" 63 | bx wsk action update ${PIPELINE_SEND_TEXT}_postsequence --sequence ${PIPELINE_SEND_TEXT_POST_SEQUENCE} 64 | 65 | # send text and receive an interactive message 66 | PIPELINE_SEND_ATTACHED_MESSAGE="$1-integration-slack-send-attached-message" 67 | 68 | CLOUDANT_AUTH_KEY="${PIPELINE_SEND_ATTACHED_MESSAGE}" 69 | 70 | retriableCreateDbDoc ${AUTH_DOC} ${__TEST_CLOUDANT_URL}/authdb/${CLOUDANT_AUTH_KEY} 71 | 72 | bx wsk package update ${PIPELINE_SEND_ATTACHED_MESSAGE}_slack \ 73 | -a cloudant_auth_key "${CLOUDANT_AUTH_KEY}" \ 74 | -a cloudant_url "${__TEST_CLOUDANT_URL}" \ 75 | -a cloudant_auth_dbname "authdb" \ 76 | -a cloudant_context_dbname "contextdb" 77 | 78 | bx wsk action update ${PIPELINE_SEND_ATTACHED_MESSAGE}_slack/receive ./channels/slack/receive/index.js -a web-export true 79 | bx wsk action update ${PIPELINE_SEND_ATTACHED_MESSAGE}_slack/post ./channels/slack/post/index.js 80 | bx wsk action update ${PIPELINE_SEND_ATTACHED_MESSAGE}_slack/multiple_post ./channels/slack/multiple_post/index.js 81 | 82 | bx wsk action update ${PIPELINE_SEND_ATTACHED_MESSAGE}_slack/send-attached-message ./test/integration/channels/slack/send-attached-message.js 83 | bx wsk action update ${PIPELINE_SEND_ATTACHED_MESSAGE} --sequence ${PIPELINE_SEND_ATTACHED_MESSAGE}_slack/send-attached-message,${PIPELINE_SEND_ATTACHED_MESSAGE}_slack/multiple_post 84 | 85 | PIPELINE_SEND_ATTACHED_MESSAGE_POST_SEQUENCE="${PIPELINE_SEND_ATTACHED_MESSAGE}_slack/post" 86 | bx wsk action update ${PIPELINE_SEND_ATTACHED_MESSAGE}_postsequence --sequence ${PIPELINE_SEND_ATTACHED_MESSAGE_POST_SEQUENCE} 87 | 88 | # send interactive click and receive a click response 89 | PIPELINE_SEND_ATTACHED_RESPONSE="$1-integration-slack-send-attached-response" 90 | 91 | CLOUDANT_AUTH_KEY="${PIPELINE_SEND_ATTACHED_RESPONSE}" 92 | 93 | retriableCreateDbDoc ${AUTH_DOC} ${__TEST_CLOUDANT_URL}/authdb/${CLOUDANT_AUTH_KEY} 94 | 95 | bx wsk package update ${PIPELINE_SEND_ATTACHED_RESPONSE}_slack \ 96 | -a cloudant_auth_key "${CLOUDANT_AUTH_KEY}" \ 97 | -a cloudant_url "${__TEST_CLOUDANT_URL}" \ 98 | -a cloudant_auth_dbname "authdb" \ 99 | -a cloudant_context_dbname "contextdb" 100 | 101 | bx wsk action update ${PIPELINE_SEND_ATTACHED_RESPONSE}_slack/receive ./channels/slack/receive/index.js -a web-export true 102 | bx wsk action update ${PIPELINE_SEND_ATTACHED_RESPONSE}_slack/post ./channels/slack/post/index.js 103 | bx wsk action update ${PIPELINE_SEND_ATTACHED_RESPONSE}_slack/multiple_post ./channels/slack/multiple_post/index.js 104 | 105 | bx wsk action update ${PIPELINE_SEND_ATTACHED_RESPONSE}_slack/send-attached-message-response ./test/integration/channels/slack/send-attached-message-response.js 106 | bx wsk action update ${PIPELINE_SEND_ATTACHED_RESPONSE} --sequence ${PIPELINE_SEND_ATTACHED_RESPONSE}_slack/send-attached-message-response,${PIPELINE_SEND_ATTACHED_RESPONSE}_slack/multiple_post 107 | 108 | PIPELINE_SEND_ATTACHED_RESPONSE_POST_SEQUENCE="${PIPELINE_SEND_ATTACHED_RESPONSE}_slack/post" 109 | bx wsk action update ${PIPELINE_SEND_ATTACHED_RESPONSE}_postsequence --sequence ${PIPELINE_SEND_ATTACHED_RESPONSE_POST_SEQUENCE} 110 | 111 | # Request and receive an interactive message requiring multipost 112 | PIPELINE_SEND_ATTACHED_MULTIPOST="$1-integration-slack-send-attached-multipost" 113 | 114 | CLOUDANT_AUTH_KEY="${PIPELINE_SEND_ATTACHED_MULTIPOST}" 115 | 116 | retriableCreateDbDoc ${AUTH_DOC} ${__TEST_CLOUDANT_URL}/authdb/${CLOUDANT_AUTH_KEY} 117 | 118 | bx wsk package update ${PIPELINE_SEND_ATTACHED_MULTIPOST}_slack \ 119 | -a cloudant_auth_key "${CLOUDANT_AUTH_KEY}" \ 120 | -a cloudant_url "${__TEST_CLOUDANT_URL}" \ 121 | -a cloudant_auth_dbname "authdb" \ 122 | -a cloudant_context_dbname "contextdb" 123 | 124 | bx wsk action update ${PIPELINE_SEND_ATTACHED_MULTIPOST}_slack/receive ./channels/slack/receive/index.js -a web-export true 125 | bx wsk action update ${PIPELINE_SEND_ATTACHED_MULTIPOST}_slack/post ./channels/slack/post/index.js 126 | bx wsk action update ${PIPELINE_SEND_ATTACHED_MULTIPOST}_slack/multiple_post ./channels/slack/multiple_post/index.js 127 | 128 | bx wsk action update ${PIPELINE_SEND_ATTACHED_MULTIPOST}_slack/send-attached-message-multipost ./test/integration/channels/slack/send-attached-message-multipost.js 129 | bx wsk action update ${PIPELINE_SEND_ATTACHED_MULTIPOST} --sequence ${PIPELINE_SEND_ATTACHED_MULTIPOST}_slack/send-attached-message-multipost,${PIPELINE_SEND_ATTACHED_MULTIPOST}_slack/multiple_post 130 | 131 | PIPELINE_SEND_ATTACHED_MULTIPOST_POST_SEQUENCE="${PIPELINE_SEND_ATTACHED_MULTIPOST}_slack/post" 132 | bx wsk action update ${PIPELINE_SEND_ATTACHED_MULTIPOST}_postsequence --sequence ${PIPELINE_SEND_ATTACHED_MULTIPOST_POST_SEQUENCE} 133 | -------------------------------------------------------------------------------- /test/integration/context/breakdown.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | PACKAGE_NAME="$1_context" 4 | 5 | bx wsk action delete ${PACKAGE_NAME}/integration-pipeline | grep -v 'ok' 6 | bx wsk action delete ${PACKAGE_NAME}/middle-for-context | grep -v 'ok' 7 | -------------------------------------------------------------------------------- /test/integration/context/middle-for-context.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright IBM Corp. 2017 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the License); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an AS IS BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | 'use strict'; 18 | 19 | /** 20 | * Mock action needed for integration tests of the context package actions. 21 | * 22 | * @param {JSON} params - parameters sent by context/load-context 23 | * @return {JSON} - parameters sent to context/save-context 24 | */ 25 | function main(params) { 26 | try { 27 | validateParams(params); 28 | } catch (e) { 29 | return Promise.reject(e.message); 30 | } 31 | 32 | // Add a mock response like it would come back from call-conversation. 33 | // Add provider-specific fields which normalize-conversation-for-channel would add. 34 | let conversationData; 35 | let conversationId; 36 | if ( 37 | params.raw_input_data.cloudant_context_key === 38 | 'slack_TXXXXXXXX_abcd-123_U2147483697_D024BE91M' 39 | ) { 40 | // This corresponds to the first turn of a multi-turn request so 41 | // set the conversation id accordingly. 42 | conversationId = '2'; 43 | } else { 44 | conversationId = '1'; 45 | } 46 | if (Object.keys(params.conversation.context).length > 0) { 47 | // If the Conversation context is not empty, it's not the first turn. 48 | // Respond with a dummy payload for a second turn. 49 | // For testing purposes we can just reply with the "turn on lights" example. 50 | conversationData = { 51 | entities: [], 52 | context: { 53 | conversation_id: conversationId, 54 | system: { 55 | branch_exited_reason: 'completed', 56 | dialog_request_counter: 2, 57 | branch_exited: true, 58 | dialog_turn_counter: 2, 59 | dialog_stack: [ 60 | { 61 | dialog_node: 'root' 62 | } 63 | ] 64 | } 65 | }, 66 | intents: [], 67 | output: { 68 | text: ['Ok. Turning on the lights.'], 69 | nodes_visited: ['node_2_1473880041309'], 70 | log_messages: [] 71 | }, 72 | input: { 73 | text: 'Turn on lights' 74 | } 75 | }; 76 | } else { 77 | // Context is empty so reply with a dummy welcome message. 78 | 79 | conversationData = { 80 | entities: [], 81 | context: { 82 | conversation_id: conversationId, 83 | system: { 84 | branch_exited_reason: 'completed', 85 | dialog_request_counter: 1, 86 | branch_exited: true, 87 | dialog_turn_counter: 1, 88 | dialog_stack: [ 89 | { 90 | dialog_node: 'root' 91 | } 92 | ] 93 | } 94 | }, 95 | intents: [], 96 | output: { 97 | text: [ 98 | 'Hi. It looks like a nice drive today. What would you like me to do? ' 99 | ], 100 | nodes_visited: ['node_1_1473880041309'], 101 | log_messages: [] 102 | }, 103 | input: { 104 | text: '' 105 | } 106 | }; 107 | } 108 | const rawInputData = params.raw_input_data; 109 | const responseJson = { 110 | channel: rawInputData[rawInputData.provider].event.channel, 111 | text: conversationData.output.text.join(' '), 112 | raw_input_data: rawInputData, 113 | conversation: conversationData 114 | }; 115 | return responseJson; 116 | } 117 | 118 | /** 119 | * Validates the required parameters for running this action. 120 | * 121 | * @params The parameters passed into the action 122 | */ 123 | function validateParams(params) { 124 | if (!params.conversation.context) { 125 | throw new Error('No context in params.'); 126 | } 127 | } 128 | 129 | module.exports = main; 130 | -------------------------------------------------------------------------------- /test/integration/context/setup.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | PACKAGE_NAME="$1_context" 4 | 5 | bx wsk action update ${PACKAGE_NAME}/middle-for-context ./test/integration/context/middle-for-context.js | grep -v 'ok' 6 | bx wsk action update ${PACKAGE_NAME}/integration-pipeline --sequence ${PACKAGE_NAME}/load-context,${PACKAGE_NAME}/middle-for-context,${PACKAGE_NAME}/save-context | grep -v 'ok' 7 | -------------------------------------------------------------------------------- /test/integration/context/test.context.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright IBM Corp. 2017 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the License); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an AS IS BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | 'use strict'; 18 | 19 | /** 20 | * Context Package Integration Tests (load-context and save-context) 21 | */ 22 | 23 | const assert = require('assert'); 24 | const openwhisk = require('openwhisk'); 25 | 26 | const envParams = process.env; 27 | 28 | const pipelineName = envParams.__TEST_PIPELINE_NAME; 29 | 30 | const actionName = `${pipelineName}_context/integration-pipeline`; 31 | 32 | describe('context package integration tests', () => { 33 | const ow = openwhisk(); 34 | 35 | let params; 36 | let expectedResult; 37 | 38 | beforeEach(() => { 39 | params = { 40 | conversation: { 41 | input: { 42 | text: '' 43 | } 44 | }, 45 | raw_input_data: { 46 | slack: { 47 | bot_access_token: 'xoxb-197154261526-JFcrTQgmN9pubm8nYMcXXXXX', 48 | access_token: 'xoxp-186464790945-187081019828-196275302867-9dcd597b2590c1f371dcf938f4eXXXXX', 49 | team_id: 'TXXXXXXXX', 50 | event: { 51 | channel: 'D024BE91L', 52 | ts: '1355517523.000005', 53 | text: '', 54 | type: 'message', 55 | user: 'U2147483697' 56 | }, 57 | api_app_id: 'AXXXXXXXXX', 58 | authed_users: ['UXXXXXXX1', 'UXXXXXXX2'], 59 | event_time: 1234567890, 60 | token: 'XXYYZZ', 61 | type: 'event_callback', 62 | event_id: 'Ev08MFMKH6' 63 | }, 64 | provider: 'slack', 65 | cloudant_context_key: 'slack_TXXXXXXXX_abcd-123_U2147483697_D024BE91L' 66 | } 67 | }; 68 | 69 | expectedResult = { 70 | raw_input_data: params.raw_input_data, 71 | channel: params.raw_input_data.slack.event.channel, 72 | text: 'Hi. It looks like a nice drive today. What would you like me to do? ', 73 | conversation: { 74 | entities: [], 75 | context: { 76 | conversation_id: '1', 77 | system: { 78 | branch_exited_reason: 'completed', 79 | dialog_request_counter: 1, 80 | branch_exited: true, 81 | dialog_turn_counter: 1, 82 | dialog_stack: [ 83 | { 84 | dialog_node: 'root' 85 | } 86 | ] 87 | } 88 | }, 89 | intents: [], 90 | output: { 91 | text: [ 92 | 'Hi. It looks like a nice drive today. What would you like me to do? ' 93 | ], 94 | nodes_visited: ['node_1_1473880041309'], 95 | log_messages: [] 96 | }, 97 | input: { 98 | text: '' 99 | } 100 | } 101 | }; 102 | }); 103 | 104 | it('validate actions work properly for single turn', () => { 105 | return ow.actions 106 | .invoke({ 107 | name: actionName, 108 | blocking: true, 109 | result: true, 110 | params 111 | }) 112 | .then(success => { 113 | assert.deepEqual(success, expectedResult); 114 | }) 115 | .catch(err => { 116 | assert(false, err); 117 | }); 118 | }) 119 | .timeout(15000) 120 | .retries(4); 121 | 122 | it('validate actions work properly for multiple turns', () => { 123 | // Get the json params for the multi turn case. 124 | params.raw_input_data.slack.event.channel = 'D024BE91L'; 125 | params.raw_input_data.cloudant_context_key = 'slack_TXXXXXXXX_abcd-123_U2147483697_D024BE91M'; 126 | 127 | // The expected responses from the system. 128 | expectedResult.conversation.context.conversation_id = '2'; 129 | 130 | return ow.actions 131 | .invoke({ 132 | name: actionName, 133 | blocking: true, 134 | result: true, 135 | params 136 | }) 137 | .then(result => { 138 | assert.deepEqual(result, expectedResult); 139 | 140 | // Update params text for the second turn. 141 | params.conversation.input.text = 'Turn on lights'; 142 | 143 | // Update the expected JSON for the second turn. 144 | expectedResult.text = 'Ok. Turning on the lights.'; 145 | expectedResult.conversation.context.system.dialog_request_counter = 2; 146 | expectedResult.conversation.context.system.dialog_turn_counter = 2; 147 | expectedResult.conversation.output.text[0] = expectedResult.text; 148 | expectedResult.conversation.output.nodes_visited[ 149 | 0 150 | ] = 'node_2_1473880041309'; 151 | expectedResult.conversation.input.text = params.conversation.input.text; 152 | 153 | // Invoke the context sequence actions again. 154 | // The context package should read the updated context from the previous turn. 155 | return ow.actions.invoke({ 156 | name: actionName, 157 | result: true, 158 | blocking: true, 159 | params 160 | }); 161 | }) 162 | .then(result => { 163 | assert.deepEqual(result, expectedResult); 164 | }) 165 | .catch(err => { 166 | assert(false, err); 167 | }); 168 | }) 169 | .timeout(20000) 170 | .retries(4); 171 | }); 172 | -------------------------------------------------------------------------------- /test/integration/conversation/test.conversation.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright IBM Corp. 2017 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the License); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an AS IS BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | 'use strict'; 18 | 19 | const assert = require('assert'); 20 | const openwhisk = require('openwhisk'); 21 | 22 | const envParams = process.env; 23 | 24 | const pipelineName = envParams.__TEST_PIPELINE_NAME; 25 | 26 | describe('conversation integration tests', () => { 27 | // Setup the ow module for the upcoming calls 28 | const ow = openwhisk(); 29 | 30 | let params = {}; 31 | 32 | beforeEach(() => { 33 | params = { 34 | conversation: { 35 | input: { 36 | text: 'Turn on lights' 37 | } 38 | }, 39 | raw_input_data: { 40 | slack: { 41 | event: { 42 | text: 'Turn on lights' 43 | } 44 | }, 45 | provider: 'slack', 46 | auth: { 47 | conversation: { 48 | username: envParams.__TEST_CONVERSATION_USERNAME, 49 | password: envParams.__TEST_CONVERSATION_PASSWORD, 50 | workspace_id: envParams.__TEST_CONVERSATION_WORKSPACE_ID 51 | } 52 | } 53 | } 54 | }; 55 | 56 | // merge the two objects, deep copying packageBindings so it doesn't get changed between tests 57 | // and we only have to read it once 58 | params = Object.assign(params, { 59 | username: envParams.__TEST_CONVERSATION_USERNAME, 60 | password: envParams.__TEST_CONVERSATION_PASSWORD, 61 | workspace_id: envParams.__TEST_CONVERSATION_WORKSPACE_ID, 62 | version_date: envParams.__TEST_CONVERSATION_VERSION_DATE, 63 | version: envParams.__TEST_CONVERSATION_VERSION 64 | }); 65 | }); 66 | 67 | it('call using OpenWhisk module ', () => { 68 | const name = `${pipelineName}_conversation/call-conversation`; 69 | const blocking = true; 70 | const result = true; 71 | 72 | // Call Conversation once to initiate the Conversation. The car-dashboard we are using for tests 73 | // always responds with a welcome message to the original user query. 74 | return ow.actions 75 | .invoke({ name, blocking, result, params }) 76 | .then(response1 => { 77 | params.conversation.context = response1.context; 78 | 79 | // After getting the context, call Conversation again to get the true response to the user's 80 | // query (and not just the welcome message) 81 | return ow.actions.invoke({ name, blocking, result, params }); 82 | }) 83 | .then(response2 => { 84 | assert.equal( 85 | response2.conversation.output.text[0], 86 | 'Hi. It looks like a nice drive today. What would you like me to do? ', 87 | 'response from conversation does not contain expected answer' 88 | ); 89 | }) 90 | .catch(e => { 91 | assert(false, e); 92 | }); 93 | }) 94 | .timeout(20000) 95 | .retries(4); 96 | }); 97 | -------------------------------------------------------------------------------- /test/integration/deploy/breakdown.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | bx target -o ${__TEST_DEPLOYUSER_ORG} -s ${__TEST_DEPLOYUSER_SPACE} 4 | bx wsk property set --apihost ${__OW_API_HOST} --auth ${__TEST_DEPLOYUSER_WSK_API_KEY} --namespace ${__TEST_DEPLOYUSER_WSK_NAMESPACE} 5 | 6 | # Clean all artifacts created in the user-deploy namespace 7 | IFS=$'\n' 8 | while [ $(bx wsk action list | tail -n +2 | wc -l | awk '{print $1}') -gt 0 ]; do 9 | for line in `bx wsk action list | tail -n +2`; do 10 | actionName=${line%% *} 11 | execution=${line##* } 12 | bx wsk action delete $actionName 13 | done 14 | done 15 | 16 | for line in `bx wsk package list | tail -n +2`; do 17 | packageName=${line%% *} 18 | execution=${line##* } 19 | bx wsk package delete $packageName 20 | done 21 | 22 | for line in `bx wsk trigger list | tail -n +2`; do 23 | triggerName=${line%% *} 24 | execution=${line##* } 25 | bx wsk trigger delete $triggerName 26 | done 27 | 28 | for line in `bx wsk rule list | tail -n +2`; do 29 | ruleName=${line%% *} 30 | execution=${line##* } 31 | bx wsk rule delete $ruleName 32 | done 33 | IFS=$' \t\n' 34 | 35 | bx target -o ${__TEST_BX_USER_ORG} -s ${__TEST_BX_USER_SPACE} 36 | bx wsk property set --apihost ${__OW_API_HOST} --auth ${__OW_API_KEY} --namespace ${__OW_NAMESPACE} 37 | -------------------------------------------------------------------------------- /test/integration/deploy/channels/slack/test.verify-slack.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright IBM Corp. 2017 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the License); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an AS IS BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | 'use strict'; 18 | 19 | /** 20 | * Deploy Verify Slack Actions Integration Tests 21 | */ 22 | 23 | const assert = require('assert'); 24 | const crypto = require('crypto'); 25 | const openwhisk = require('openwhisk'); 26 | const _ = require('underscore'); 27 | 28 | const actionPopulateActions = 'populate-actions'; 29 | const actionVerifySlack = 'verify-slack'; 30 | 31 | const apihost = 'openwhisk.ng.bluemix.net'; 32 | 33 | describe('deploy verify-slack integration tests', () => { 34 | const ow = openwhisk(); 35 | 36 | let params; 37 | 38 | beforeEach(() => { 39 | params = { 40 | state: { 41 | auth: { 42 | access_token: process.env.__TEST_DEPLOYUSER_ACCESS_TOKEN, 43 | refresh_token: process.env.__TEST_DEPLOYUSER_REFRESH_TOKEN 44 | }, 45 | wsk: { 46 | namespace: process.env.__TEST_DEPLOYUSER_WSK_NAMESPACE 47 | }, 48 | slack: { 49 | client_id: process.env.__TEST_SLACK_CLIENT_ID, 50 | client_secret: process.env.__TEST_SLACK_CLIENT_SECRET, 51 | verification_token: process.env.__TEST_SLACK_VERIFICATION_TOKEN 52 | }, 53 | conversation: { 54 | guid: process.env.__TEST_DEPLOYUSER_CONVERSATION_GUID, 55 | workspace_id: process.env.__TEST_DEPLOYUSER_CONVERSATION_WORKSPACEID 56 | } 57 | } 58 | }; 59 | }); 60 | 61 | it('validate verify-slack works', () => { 62 | const deploymentName = 'test-integration-verifyslack'; 63 | params.state.name = deploymentName; 64 | 65 | const requestUrl = `https://${process.env.__OW_API_HOST}/api/v1/web/${params.state.wsk.namespace}/${params.state.name}_slack/receive.json`; 66 | const redirectUrl = `https://${process.env.__OW_API_HOST}/api/v1/web/${params.state.wsk.namespace}/${params.state.name}_slack/deploy.http`; 67 | 68 | const state = JSON.stringify({ 69 | signature: createHmacKey( 70 | params.state.slack.client_id, 71 | params.state.slack.client_secret 72 | ), 73 | redirect_url: redirectUrl 74 | }); 75 | 76 | const authUrl = `/oauth/authorize?client_id=${params.state.slack.client_id}&scope=bot+chat:write:bot&redirect_uri=${redirectUrl}&state=${state}`; 77 | const redirAuthUrl = `https://slack.com/signin?redir=${encodeURIComponent(authUrl)}`; 78 | 79 | const expectedPostSequenceActions = [ 80 | `/${process.env.__TEST_DEPLOYUSER_WSK_NAMESPACE}/${deploymentName}_starter-code/post-normalize`, 81 | `/${process.env.__TEST_DEPLOYUSER_WSK_NAMESPACE}/${deploymentName}_slack/post` 82 | ]; 83 | 84 | const expectedMainSequenceActions = [ 85 | `/${process.env.__TEST_DEPLOYUSER_WSK_NAMESPACE}/${deploymentName}_starter-code/pre-normalize`, 86 | `/${process.env.__TEST_DEPLOYUSER_WSK_NAMESPACE}/${deploymentName}_starter-code/normalize-slack-for-conversation`, 87 | `/${process.env.__TEST_DEPLOYUSER_WSK_NAMESPACE}/${deploymentName}_context/load-context`, 88 | `/${process.env.__TEST_DEPLOYUSER_WSK_NAMESPACE}/${deploymentName}_starter-code/pre-conversation`, 89 | `/${process.env.__TEST_DEPLOYUSER_WSK_NAMESPACE}/${deploymentName}_conversation/call-conversation`, 90 | `/${process.env.__TEST_DEPLOYUSER_WSK_NAMESPACE}/${deploymentName}_starter-code/post-conversation`, 91 | `/${process.env.__TEST_DEPLOYUSER_WSK_NAMESPACE}/${deploymentName}_context/save-context`, 92 | `/${process.env.__TEST_DEPLOYUSER_WSK_NAMESPACE}/${deploymentName}_starter-code/normalize-conversation-for-slack`, 93 | `/${process.env.__TEST_DEPLOYUSER_WSK_NAMESPACE}/${deploymentName}_slack/multiple_post` 94 | ]; 95 | 96 | const supplierWsk = openwhisk({ 97 | api_key: process.env.__TEST_DEPLOYUSER_WSK_API_KEY, 98 | namespace: process.env.__TEST_DEPLOYUSER_WSK_NAMESPACE, 99 | apihost 100 | }); 101 | 102 | return ow.actions 103 | .invoke({ 104 | name: actionPopulateActions, 105 | blocking: true, 106 | result: true, 107 | params 108 | }) 109 | .then(result => { 110 | assert.deepEqual(result, { code: 200, message: 'OK' }); 111 | }) 112 | .then(() => { 113 | return ow.actions.invoke({ 114 | name: actionVerifySlack, 115 | blocking: true, 116 | result: true, 117 | params 118 | }); 119 | }) 120 | .then(result => { 121 | assert.deepEqual(result, { 122 | code: 200, 123 | message: 'OK', 124 | request_url: requestUrl, 125 | redirect_url: redirectUrl, 126 | authorize_url: redirAuthUrl 127 | }); 128 | }) 129 | .then(() => { 130 | return validateSequenceCreation(deploymentName, supplierWsk); 131 | }) 132 | .then(action => { 133 | assert( 134 | _.isEqual(action.exec.components, expectedMainSequenceActions), 135 | 'main sequence does not contain expected actions.' 136 | ); 137 | }) 138 | .then(() => { 139 | return validateSequenceCreation( 140 | `${deploymentName}_postsequence`, 141 | supplierWsk 142 | ); 143 | }) 144 | .then(action => { 145 | assert( 146 | _.isEqual(action.exec.components, expectedPostSequenceActions), 147 | 'post sequence does not contain expected actions.' 148 | ); 149 | }) 150 | .catch(error => { 151 | assert(false, error); 152 | }); 153 | }) 154 | .timeout(30000) 155 | .retries(4); 156 | 157 | function createHmacKey(clientId, clientSecret) { 158 | const hmacKey = `${clientId}&${clientSecret}`; 159 | return crypto 160 | .createHmac('sha256', hmacKey) 161 | .update('authorize') 162 | .digest('hex'); 163 | } 164 | 165 | function validateSequenceCreation(pipelineName, supplierWsk) { 166 | return supplierWsk.actions.get(pipelineName); 167 | } 168 | }); 169 | -------------------------------------------------------------------------------- /test/integration/deploy/setup.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | bx target -o ${__TEST_DEPLOYUSER_ORG} -s ${__TEST_DEPLOYUSER_SPACE} 4 | bx wsk property set --apihost ${__OW_API_HOST} --auth ${__TEST_DEPLOYUSER_WSK_API_KEY} --namespace ${__TEST_DEPLOYUSER_WSK_NAMESPACE} 5 | 6 | # Clean the user-deploy namespace to ensure the deploy integration tests are performed 7 | # with the user starting with no artifacts 8 | IFS=$'\n' 9 | while [ $(bx wsk action list | tail -n +2 | wc -l | awk '{print $1}') -gt 0 ]; do 10 | for line in `bx wsk action list | tail -n +2`; do 11 | actionName=${line%% *} 12 | execution=${line##* } 13 | bx wsk action delete $actionName 14 | done 15 | done 16 | 17 | for line in `bx wsk package list | tail -n +2`; do 18 | packageName=${line%% *} 19 | execution=${line##* } 20 | bx wsk package delete $packageName 21 | done 22 | 23 | for line in `bx wsk trigger list | tail -n +2`; do 24 | triggerName=${line%% *} 25 | execution=${line##* } 26 | bx wsk trigger delete $triggerName 27 | done 28 | 29 | for line in `bx wsk rule list | tail -n +2`; do 30 | ruleName=${line%% *} 31 | execution=${line##* } 32 | bx wsk rule delete $ruleName 33 | done 34 | IFS=$' \t\n' 35 | 36 | bx target -o ${__TEST_BX_USER_ORG} -s ${__TEST_BX_USER_SPACE} 37 | bx wsk property set --apihost ${__OW_API_HOST} --auth ${__OW_API_KEY} --namespace ${__OW_NAMESPACE} 38 | -------------------------------------------------------------------------------- /test/integration/deploy/test.populate-actions.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright IBM Corp. 2017 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the License); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an AS IS BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | 'use strict'; 18 | 19 | /** 20 | * Deploy Populate Actions Integration Tests 21 | */ 22 | 23 | const assert = require('assert'); 24 | const openwhisk = require('openwhisk'); 25 | 26 | const actionPopulateActions = 'populate-actions'; 27 | 28 | describe('deploy populate-actions integration tests', () => { 29 | const ow = openwhisk(); 30 | 31 | let params; 32 | 33 | beforeEach(() => { 34 | params = { 35 | state: { 36 | auth: { 37 | access_token: process.env.__TEST_DEPLOYUSER_ACCESS_TOKEN, 38 | refresh_token: process.env.__TEST_DEPLOYUSER_REFRESH_TOKEN 39 | }, 40 | wsk: { 41 | namespace: process.env.__TEST_DEPLOYUSER_WSK_NAMESPACE 42 | }, 43 | conversation: { 44 | guid: process.env.__TEST_DEPLOYUSER_CONVERSATION_GUID, 45 | workspace_id: process.env.__TEST_DEPLOYUSER_CONVERSATION_WORKSPACEID 46 | } 47 | } 48 | }; 49 | }); 50 | 51 | it('validate populate-actions works', () => { 52 | const deploymentName = 'test-integration-populateactions'; 53 | params.state.name = deploymentName; 54 | 55 | return ow.actions 56 | .invoke({ 57 | name: actionPopulateActions, 58 | blocking: true, 59 | result: true, 60 | params 61 | }) 62 | .then(result => { 63 | assert.deepEqual(result, { code: 200, message: 'OK' }); 64 | }) 65 | .catch(error => { 66 | assert(false, error); 67 | }); 68 | }); 69 | }); 70 | -------------------------------------------------------------------------------- /test/integration/starter-code/breakdown.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | PACKAGE_NAME="$1_starter-code" 4 | 5 | bx wsk action delete ${PACKAGE_NAME}/integration-pipeline-slack | grep -v 'ok' 6 | bx wsk action delete ${PACKAGE_NAME}/integration-pipeline-slack-with-slack-data | grep -v 'ok' 7 | bx wsk action delete ${PACKAGE_NAME}/integration-pipeline-slack-with-generic-data | grep -v 'ok' 8 | 9 | bx wsk action delete ${PACKAGE_NAME}/integration-pipeline-facebook | grep -v 'ok' 10 | bx wsk action delete ${PACKAGE_NAME}/integration-pipeline-facebook-with-facebook-data | grep -v 'ok' 11 | bx wsk action delete ${PACKAGE_NAME}/integration-pipeline-facebook-with-generic-data | grep -v 'ok' 12 | 13 | bx wsk action delete ${PACKAGE_NAME}/mock-conversation-text | grep -v 'ok' 14 | bx wsk action delete ${PACKAGE_NAME}/mock-conversation-slack-data | grep -v 'ok' 15 | bx wsk action delete ${PACKAGE_NAME}/mock-conversation-generic-data | grep -v 'ok' 16 | bx wsk action delete ${PACKAGE_NAME}/mock-conversation-facebook-data | grep -v 'ok' 17 | -------------------------------------------------------------------------------- /test/integration/starter-code/mock-conversation-facebook-data.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright IBM Corp. 2017 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the License); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an AS IS BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | 'use strict'; 18 | 19 | const assert = require('assert'); 20 | 21 | /** 22 | * Mock action receiving input from normalize-channel-for-conversation, 23 | * and sending a Facebook-specific JSON in Conversation response 24 | * to normalize-conversation-for-channel. 25 | * 26 | * @param {JSON} params - input with Conversation (call-conversation) input 27 | * @return {JSON} - text output from Conversation (call-conversation) 28 | */ 29 | function main(params) { 30 | return new Promise(resolve => { 31 | validateParameters(params); 32 | 33 | const facebookData = { 34 | attachment: { 35 | type: 'template', 36 | payload: { 37 | elements: [ 38 | { 39 | title: 'Output text from mock-conversation.', 40 | buttons: [ 41 | { 42 | type: 'postback', 43 | title: 'Enter T-Shirt Store', 44 | payload: 'List all t-shirts' 45 | } 46 | ], 47 | subtitle: 'I can help you find a t-shirt', 48 | image_url: 'https://encrypted-tbn0.gstatic.com/images?q=tbn:ANd9GcTDQKvGUWTu5hStYHbjH8J3fZi6JgYqw6WY3CrfjB680uLjy2FF9A' 49 | } 50 | ], 51 | template_type: 'generic', 52 | image_aspect_ratio: 'square' 53 | } 54 | } 55 | }; 56 | 57 | const returnParameters = { 58 | conversation: { 59 | output: { 60 | text: ['Output text from mock-conversation.'] 61 | }, 62 | context: { 63 | conversation_id: '06aae48c-a5a9-4bbc-95eb-2ddd26db9a7b', 64 | system: { 65 | branch_exited_reason: 'completed', 66 | dialog_request_counter: 1, 67 | branch_exited: true, 68 | dialog_turn_counter: 1, 69 | dialog_stack: [ 70 | { 71 | dialog_node: 'root' 72 | } 73 | ], 74 | _node_output_map: { 75 | 'Anything else': [0] 76 | } 77 | } 78 | } 79 | } 80 | }; 81 | returnParameters.raw_input_data = Object.assign({}, params.raw_input_data); 82 | returnParameters.raw_input_data.conversation = Object.assign( 83 | {}, 84 | params.conversation 85 | ); 86 | returnParameters.conversation.output.facebook = facebookData; 87 | resolve(returnParameters); 88 | }); 89 | } 90 | 91 | /** 92 | * Validates the required parameters for running this action. 93 | * 94 | * @param {JSON} params - the parameters passed into the action 95 | */ 96 | function validateParameters(params) { 97 | // Required: Conversation object 98 | assert(params.conversation, 'Conversation data not provided.'); 99 | 100 | // Required: Conversation input text 101 | assert( 102 | params.conversation.input && params.conversation.input.text, 103 | 'Input text not provided.' 104 | ); 105 | 106 | // Required: channel raw data 107 | assert( 108 | params.raw_input_data && 109 | params.raw_input_data.provider && 110 | params.raw_input_data[params.raw_input_data.provider], 111 | 'No channel raw input data provided.' 112 | ); 113 | } 114 | 115 | module.exports = main; 116 | -------------------------------------------------------------------------------- /test/integration/starter-code/mock-conversation-generic-data.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright IBM Corp. 2017 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the License); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an AS IS BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | 'use strict'; 18 | 19 | const assert = require('assert'); 20 | 21 | /** 22 | * Mock action receiving input from normalize-channel-for-conversation, 23 | * and sending a generic multi-modal Conversation response 24 | * to normalize-conversation-for-channel. 25 | * 26 | * @param {JSON} params - input with Conversation (call-conversation) input 27 | * @return {JSON} - multi-modal output from Conversation (call-conversation) 28 | */ 29 | function main(params) { 30 | return new Promise(resolve => { 31 | validateParameters(params); 32 | 33 | const genericData = [ 34 | { 35 | response_type: 'pause', 36 | time: 1000, 37 | typing: true 38 | }, 39 | { 40 | response_type: 'text', 41 | text: 'Output text from mock-conversation.' 42 | }, 43 | { 44 | response_type: 'image', 45 | source: 'https://a.slack-edge.com/66f9/img/api/attachment_image.png', 46 | title: 'Image title', 47 | description: 'Image description' 48 | }, 49 | { 50 | response_type: 'option', 51 | title: 'Choose your location', 52 | options: [ 53 | { 54 | label: 'Location 1', 55 | value: 'Location 1' 56 | }, 57 | { 58 | label: 'Location 2', 59 | value: 'Location 2' 60 | }, 61 | { 62 | label: 'Location 3', 63 | value: 'Location 3' 64 | } 65 | ] 66 | } 67 | ]; 68 | 69 | const returnParameters = { 70 | conversation: { 71 | output: { 72 | generic: genericData 73 | }, 74 | context: { 75 | conversation_id: '06aae48c-a5a9-4bbc-95eb-2ddd26db9a7b', 76 | system: { 77 | branch_exited_reason: 'completed', 78 | dialog_request_counter: 1, 79 | branch_exited: true, 80 | dialog_turn_counter: 1, 81 | dialog_stack: [ 82 | { 83 | dialog_node: 'root' 84 | } 85 | ], 86 | _node_output_map: { 87 | 'Anything else': [0] 88 | } 89 | } 90 | } 91 | } 92 | }; 93 | returnParameters.raw_input_data = Object.assign({}, params.raw_input_data); 94 | returnParameters.raw_input_data.conversation = Object.assign( 95 | {}, 96 | params.conversation 97 | ); 98 | resolve(returnParameters); 99 | }); 100 | } 101 | 102 | /** 103 | * Validates the required parameters for running this action. 104 | * 105 | * @param {JSON} params - the parameters passed into the action 106 | */ 107 | function validateParameters(params) { 108 | // Required: Conversation object 109 | assert(params.conversation, 'Conversation data not provided.'); 110 | 111 | // Required: Conversation input text 112 | assert( 113 | params.conversation.input && params.conversation.input.text, 114 | 'Input text not provided.' 115 | ); 116 | 117 | // Required: channel raw data 118 | assert( 119 | params.raw_input_data && 120 | params.raw_input_data.provider && 121 | params.raw_input_data[params.raw_input_data.provider], 122 | 'No channel raw input data provided.' 123 | ); 124 | } 125 | 126 | module.exports = main; 127 | -------------------------------------------------------------------------------- /test/integration/starter-code/mock-conversation-slack-data.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright IBM Corp. 2017 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the License); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an AS IS BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | 'use strict'; 18 | 19 | const assert = require('assert'); 20 | 21 | /** 22 | * Mock action receiving input from normalize-channel-for-conversation, 23 | * and sending a Slack-specific JSON in Conversation response 24 | * to normalize-conversation-for-channel. 25 | * 26 | * @param {JSON} params - input with Conversation (call-conversation) input 27 | * @return {JSON} - text output from Conversation (call-conversation) 28 | */ 29 | function main(params) { 30 | return new Promise(resolve => { 31 | validateParameters(params); 32 | 33 | const slackData = { 34 | text: 'Output text from mock-conversation.', 35 | attachments: [ 36 | { 37 | actions: [ 38 | { 39 | name: 'test_option_one', 40 | text: 'Test Option One', 41 | type: 'button', 42 | value: 'test option one' 43 | }, 44 | { 45 | name: 'test_option_two', 46 | text: 'Test Option Two', 47 | type: 'button', 48 | value: 'test option two' 49 | }, 50 | { 51 | name: 'test_option_three', 52 | text: 'Test Option Three', 53 | type: 'button', 54 | value: 'test option three' 55 | } 56 | ], 57 | fallback: 'Buttons not working...', 58 | callback_id: 'test_integration_options' 59 | } 60 | ] 61 | }; 62 | 63 | const returnParameters = { 64 | conversation: { 65 | output: { 66 | text: ['Output text from mock-conversation.'] 67 | }, 68 | context: { 69 | conversation_id: '06aae48c-a5a9-4bbc-95eb-2ddd26db9a7b', 70 | system: { 71 | branch_exited_reason: 'completed', 72 | dialog_request_counter: 1, 73 | branch_exited: true, 74 | dialog_turn_counter: 1, 75 | dialog_stack: [ 76 | { 77 | dialog_node: 'root' 78 | } 79 | ], 80 | _node_output_map: { 81 | 'Anything else': [0] 82 | } 83 | } 84 | } 85 | } 86 | }; 87 | returnParameters.raw_input_data = Object.assign({}, params.raw_input_data); 88 | returnParameters.raw_input_data.conversation = Object.assign( 89 | {}, 90 | params.conversation 91 | ); 92 | returnParameters.conversation.output.slack = slackData; 93 | resolve(returnParameters); 94 | }); 95 | } 96 | 97 | /** 98 | * Validates the required parameters for running this action. 99 | * 100 | * @param {JSON} params - the parameters passed into the action 101 | */ 102 | function validateParameters(params) { 103 | // Required: Conversation object 104 | assert(params.conversation, 'Conversation data not provided.'); 105 | 106 | // Required: Conversation input text 107 | assert( 108 | params.conversation.input && params.conversation.input.text, 109 | 'Input text not provided.' 110 | ); 111 | 112 | // Required: channel raw data 113 | assert( 114 | params.raw_input_data && 115 | params.raw_input_data.provider && 116 | params.raw_input_data[params.raw_input_data.provider], 117 | 'No channel raw input data provided.' 118 | ); 119 | } 120 | 121 | module.exports = main; 122 | -------------------------------------------------------------------------------- /test/integration/starter-code/mock-conversation-text.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright IBM Corp. 2017 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the License); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an AS IS BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | 'use strict'; 18 | 19 | const assert = require('assert'); 20 | 21 | /** 22 | * Mock action receiving input from normalize-channel-for-conversation, 23 | * and sending a text Conversation response to normalize-conversation-for-channel. 24 | * 25 | * @param {JSON} params - input with Conversation (call-conversation) input 26 | * @return {JSON} - text output from Conversation (call-conversation) 27 | */ 28 | function main(params) { 29 | return new Promise(resolve => { 30 | validateParameters(params); 31 | 32 | const returnParameters = { 33 | conversation: { 34 | output: { 35 | text: ['Output text from mock-conversation.'] 36 | }, 37 | context: { 38 | conversation_id: '06aae48c-a5a9-4bbc-95eb-2ddd26db9a7b', 39 | system: { 40 | branch_exited_reason: 'completed', 41 | dialog_request_counter: 1, 42 | branch_exited: true, 43 | dialog_turn_counter: 1, 44 | dialog_stack: [ 45 | { 46 | dialog_node: 'root' 47 | } 48 | ], 49 | _node_output_map: { 50 | 'Anything else': [0] 51 | } 52 | } 53 | } 54 | }, 55 | raw_input_data: params.raw_input_data 56 | }; 57 | returnParameters.raw_input_data.conversation = params.conversation; 58 | resolve(returnParameters); 59 | }); 60 | } 61 | 62 | /** 63 | * Validates the required parameters for running this action. 64 | * 65 | * @param {JSON} params - the parameters passed into the action 66 | */ 67 | function validateParameters(params) { 68 | // Required: Conversation object 69 | assert(params.conversation, 'Conversation data not provided.'); 70 | 71 | // Required: Conversation input text 72 | assert( 73 | params.conversation.input && params.conversation.input.text, 74 | 'Input text not provided.' 75 | ); 76 | 77 | // Required: channel raw data 78 | assert( 79 | params.raw_input_data && 80 | params.raw_input_data.provider && 81 | params.raw_input_data[params.raw_input_data.provider], 82 | 'No channel raw input data provided.' 83 | ); 84 | } 85 | 86 | module.exports = main; 87 | -------------------------------------------------------------------------------- /test/integration/starter-code/setup.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | PACKAGE_NAME="$1_starter-code" 4 | 5 | bx wsk action update ${PACKAGE_NAME}/mock-conversation-text ./test/integration/starter-code/mock-conversation-text.js | grep -v 'ok' 6 | bx wsk action update ${PACKAGE_NAME}/mock-conversation-slack-data ./test/integration/starter-code/mock-conversation-slack-data.js | grep -v 'ok' 7 | bx wsk action update ${PACKAGE_NAME}/mock-conversation-generic-data ./test/integration/starter-code/mock-conversation-generic-data.js | grep -v 'ok' 8 | bx wsk action update ${PACKAGE_NAME}/mock-conversation-facebook-data ./test/integration/starter-code/mock-conversation-facebook-data.js | grep -v 'ok' 9 | 10 | bx wsk action update ${PACKAGE_NAME}/integration-pipeline-slack --sequence ${PACKAGE_NAME}/pre-normalize,${PACKAGE_NAME}/normalize-slack-for-conversation,${PACKAGE_NAME}/pre-conversation,${PACKAGE_NAME}/mock-conversation-text,${PACKAGE_NAME}/post-conversation,${PACKAGE_NAME}/normalize-conversation-for-slack,${PACKAGE_NAME}/post-normalize | grep -v 'ok' 11 | bx wsk action update ${PACKAGE_NAME}/integration-pipeline-slack-with-slack-data --sequence ${PACKAGE_NAME}/pre-normalize,${PACKAGE_NAME}/normalize-slack-for-conversation,${PACKAGE_NAME}/pre-conversation,${PACKAGE_NAME}/mock-conversation-slack-data,${PACKAGE_NAME}/post-conversation,${PACKAGE_NAME}/normalize-conversation-for-slack,${PACKAGE_NAME}/post-normalize | grep -v 'ok' 12 | bx wsk action update ${PACKAGE_NAME}/integration-pipeline-slack-with-generic-data --sequence ${PACKAGE_NAME}/pre-normalize,${PACKAGE_NAME}/normalize-slack-for-conversation,${PACKAGE_NAME}/pre-conversation,${PACKAGE_NAME}/mock-conversation-generic-data,${PACKAGE_NAME}/post-conversation,${PACKAGE_NAME}/normalize-conversation-for-slack,${PACKAGE_NAME}/post-normalize | grep -v 'ok' 13 | 14 | bx wsk action update ${PACKAGE_NAME}/integration-pipeline-facebook --sequence ${PACKAGE_NAME}/pre-normalize,${PACKAGE_NAME}/normalize-facebook-for-conversation,${PACKAGE_NAME}/pre-conversation,${PACKAGE_NAME}/mock-conversation-text,${PACKAGE_NAME}/post-conversation,${PACKAGE_NAME}/normalize-conversation-for-facebook,${PACKAGE_NAME}/post-normalize | grep -v 'ok' 15 | bx wsk action update ${PACKAGE_NAME}/integration-pipeline-facebook-with-facebook-data --sequence ${PACKAGE_NAME}/pre-normalize,${PACKAGE_NAME}/normalize-facebook-for-conversation,${PACKAGE_NAME}/pre-conversation,${PACKAGE_NAME}/mock-conversation-facebook-data,${PACKAGE_NAME}/post-conversation,${PACKAGE_NAME}/normalize-conversation-for-facebook,${PACKAGE_NAME}/post-normalize | grep -v 'ok' 16 | bx wsk action update ${PACKAGE_NAME}/integration-pipeline-facebook-with-generic-data --sequence ${PACKAGE_NAME}/pre-normalize,${PACKAGE_NAME}/normalize-facebook-for-conversation,${PACKAGE_NAME}/pre-conversation,${PACKAGE_NAME}/mock-conversation-generic-data,${PACKAGE_NAME}/post-conversation,${PACKAGE_NAME}/normalize-conversation-for-facebook,${PACKAGE_NAME}/post-normalize | grep -v 'ok' 17 | -------------------------------------------------------------------------------- /test/resources/.unit.env: -------------------------------------------------------------------------------- 1 | __OW_API_HOST=openwhisk.ng.bluemix.net 2 | __OW_API_KEY=xxxxx 3 | __OW_NAMESPACE=xxx_xxx 4 | 5 | __TEST_SLACK_CLIENT_ID=xxxxxxxxxxxx:xxxxxxxxxxxx 6 | __TEST_SLACK_CLIENT_SECRET=xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx 7 | __TEST_SLACK_VERIFICATION_TOKEN=xxxxxxxxxxxxxxxxxxxxxxxx 8 | __TEST_SLACK_ACCESS_TOKEN=xxxx-xxxxxxxxxxxx-xxxxxxxxxxxx-xxxxxxxxxxxx-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx 9 | __TEST_SLACK_BOT_ACCESS_TOKEN=xxxx-xxxxxxxxxxxx-xxxxxxxxxxxxxxxxxxxxxxxx 10 | __TEST_SLACK_BOT_USER_ID=xxxxxxxxx 11 | __TEST_SLACK_CHANNEL=DXXXXXXXX 12 | 13 | __TEST_FACEBOOK_PAGE_ACCESS_TOKEN=xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx 14 | __TEST_FACEBOOK_SENDER_ID=xxxxxxxxxxxxxxxx 15 | 16 | __TEST_CONVERSATION_USERNAME=xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx 17 | __TEST_CONVERSATION_PASSWORD=xxxxxxxxxxxx 18 | __TEST_CONVERSATION_WORKSPACE_ID=xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx 19 | -------------------------------------------------------------------------------- /test/resources/payloads/test.unit.auth.conversation.json: -------------------------------------------------------------------------------- 1 | { 2 | "cloudant": { 3 | "dbname": "authdb", 4 | "url": "https://pinkunicorns.cloudant.com" 5 | }, 6 | "loadAuth": { 7 | "slack":{ 8 | "key": "85239056-0789-4838-9c31-39f1d431c99e", 9 | "tests": { 10 | "allOk": { 11 | "request": { 12 | "params": { 13 | "raw_input_data": { 14 | "provider": "slack", 15 | "slack": { 16 | "authed_users": [ 17 | "U5T4J7PFG" 18 | ] 19 | } 20 | } 21 | } 22 | }, 23 | "response": { 24 | "conversation": { 25 | "password": "xx1AFqNxxxxx", 26 | "username": "8d71b8fd-xxxx-1111-xxxx-2256216d19fc", 27 | "workspace_id": "e808d814-9143-4dce-aec7-68af0xxxxxxx" 28 | }, 29 | "slack": { 30 | "access_token": "xoxp-176389621495-176389622103-175070850417-2f7cc0xxxxxxxxxxxxxxxxxxxxxxx", 31 | "bot_access_token": "xoxb-1764574xxxxxxxxxxxxxxxxxxxxxxxxxx", 32 | "client_id": "176389621495.1750xxxxxxxxxxx", 33 | "client_secret": "8e1075aed344xxxxxxxxxxxx", 34 | "redirect_uri": "https://forcloudfunctions-prod.mybluemix.net/slack/deploy.json", 35 | "verification_token": "BrfPhxxxxxxxxxxxxx" 36 | } 37 | } 38 | } 39 | } 40 | }, 41 | "facebook": { 42 | "key": "60458739-7df6-4dde-9677-37fc526b107e", 43 | "tests": { 44 | "allOk": { 45 | "request": { 46 | "params": { 47 | "raw_input_data": { 48 | "provider": "facebook", 49 | "slack": { 50 | "authed_users": [ 51 | "U5T4J7PFG" 52 | ] 53 | } 54 | } 55 | } 56 | }, 57 | "response": { 58 | "conversation": { 59 | "password": "xx1AFqNxxxxx", 60 | "username": "8d71b8fd-xxxx-1111-xxxx-2256216d19fc", 61 | "workspace_id": "e808d814-9143-4dce-aec7-68af0xxxxxxx" 62 | }, 63 | "slack": { 64 | "access_token": "xoxp-176389621495-176389622103-175070850417-2f7cc0xxxxxxxxxxxxxxxxxxxxxxx", 65 | "bot_access_token": "xoxb-1764574xxxxxxxxxxxxxxxxxxxxxxxxxx", 66 | "client_id": "176389621495.1750xxxxxxxxxxx", 67 | "client_secret": "8e1075aed344xxxxxxxxxxxx", 68 | "redirect_uri": "https://forcloudfunctions-prod.mybluemix.net/slack/deploy.json", 69 | "verification_token": "BrfPhxxxxxxxxxxxxx" 70 | } 71 | } 72 | } 73 | } 74 | } 75 | } 76 | } -------------------------------------------------------------------------------- /test/resources/payloads/test.unit.auth.facebook.json: -------------------------------------------------------------------------------- 1 | { 2 | "cloudant": { 3 | "dbname": "authdb", 4 | "url": "https://pinkunicorns.cloudant.com" 5 | }, 6 | "facebook/deploy": { 7 | "saveFacebookAuth": { 8 | "key": "facebook_1481847138543615", 9 | "tests":{ 10 | "newDeploymentAllOk": { 11 | "request": { 12 | "pageId": "1481847138543615", 13 | "params": { 14 | "facebook_app_secret": "6b4603517ca566396a92a168xxxxxxxx", 15 | "facebook_verification_token": "test_token", 16 | "facebook_page_access_token": "XXXXXXXXCwmgBAOZAgRmIlSQwNvZCXVrrTwa34dQtHmkHfxj1Z4gW87mzov9Eqvvt9CvJxU38wHCC0IN85rZAjYQkxxxxxxxx", 17 | "conversation_workspace_id": "e808d814-9143-4dce-aec7-68af0xxxxxxx", 18 | "conversation_username": "8d71b8fd-xxxx-1111-xxxx-2256216d19fc", 19 | "conversation_password": "xx1AFqNxxxxx" 20 | } 21 | }, 22 | "response": [ 23 | {}, 24 | { 25 | "ok":true, 26 | "id": "facebook_U56DFBX7H","rev":"17-1501225f77b403b7f334f1b82db352ef" 27 | } 28 | ] 29 | } 30 | } 31 | } 32 | } 33 | } -------------------------------------------------------------------------------- /test/resources/payloads/test.unit.auth.slack.json: -------------------------------------------------------------------------------- 1 | { 2 | "cloudant": { 3 | "dbname": "authdb", 4 | "url": "https://pinkunicorns.cloudant.com" 5 | }, 6 | "slack/deploy": { 7 | "saveSlackAuth": { 8 | "key": "85239056-0789-4838-9c31-39f1d431c99e", 9 | "tests":{ 10 | "newDeploymentAllOk": { 11 | "request": { 12 | "params": { 13 | "code": "code", 14 | "state":{ 15 | "slack": { 16 | "client_id": "176389621495.1750xxxxxxxxxxx", 17 | "client_secret": "8e1075aed344xxxxxxxxxxxx", 18 | "redirect_uri": "https://forcloudfunctions-prod.mybluemix.net/slack/deploy.json", 19 | "verification_token": "BrfPhxxxxxxxxxxxxx" 20 | }, 21 | "conversation": { 22 | "workspace_id": "e808d814-9143-4dce-aec7-68af0xxxxxxx", 23 | "username": "8d71b8fd-xxxx-1111-xxxx-2256216d19fc", 24 | "password": "xx1AFqNxxxxx" 25 | } 26 | } 27 | }, 28 | "slackOAuthBody": { 29 | "access_token": "xoxp-176389621495-176389622103-175070850417-2f7cc0xxxxxxxxxxxxxxxxxxxxxxx", 30 | "bot":{ 31 | "bot_access_token": "xoxb-1764574xxxxxxxxxxxxxxxxxxxxxxxxxx", 32 | "bot_user_id": "U56DFBX7H" 33 | } 34 | } 35 | }, 36 | "response": [ 37 | {}, 38 | { 39 | "ok":true, 40 | "id": "85239056-0789-4838-9c31-39f1d431c99e","rev":"17-1501225f77b403b7f334f1b82db352ef" 41 | } 42 | ] 43 | } 44 | } 45 | } 46 | }, 47 | "slack/receive": { 48 | "loadSlackAuth": { 49 | "key": "85239056-0789-4838-9c31-39f1d431c99e", 50 | "tests":{ 51 | "allOk": { 52 | "request": { 53 | "params": { 54 | "raw_input_data": { 55 | "provider": "slack", 56 | "provider": "slack", 57 | "slack": { 58 | "authed_users": [ 59 | "U5T4J7PFG" 60 | ] 61 | } 62 | } 63 | } 64 | }, 65 | "response": { 66 | "slack": { 67 | "client_id": "176389621495.1750xxxxxxxxxxx", 68 | "client_secret": "8e1075aed344xxxxxxxxxxxx", 69 | "redirect_uri": "https://forcloudfunctions-prod.mybluemix.net/slack/deploy.json", 70 | "verification_token": "BrfPhxxxxxxxxxxxxx", 71 | "bot_access_token": "xoxb-1764574xxxxxxxxxxxxxxxxxxxxxxxxxx", 72 | "access_token": "xoxp-176389621495-176389622103-175070850417-2f7cc0xxxxxxxxxxxxxxxxxxxxxxx" 73 | }, 74 | "conversation": { 75 | "workspace_id": "e808d814-9143-4dce-aec7-68af0xxxxxxx", 76 | "username": "8d71b8fd-xxxx-1111-xxxx-2256216d19fc", 77 | "password": "xx1AFqNxxxxx" 78 | } 79 | } 80 | } 81 | } 82 | } 83 | }, 84 | "slack/post": { 85 | "loadSlackAuth": { 86 | "key": "85239056-0789-4838-9c31-39f1d431c99e", 87 | "tests":{ 88 | "allOk": { 89 | "request": { 90 | "params": { 91 | "raw_input_data": { 92 | "provider": "slack", 93 | "slack": { 94 | "authed_users": [ 95 | "U5T4J7PFG" 96 | ] 97 | } 98 | } 99 | } 100 | }, 101 | "response": { 102 | "slack": { 103 | "client_id": "176389621495.1750xxxxxxxxxxx", 104 | "client_secret": "8e1075aed344xxxxxxxxxxxx", 105 | "redirect_uri": "https://forcloudfunctions-prod.mybluemix.net/slack/deploy.json", 106 | "verification_token": "BrfPhxxxxxxxxxxxxx", 107 | "bot_access_token": "xoxb-1764574xxxxxxxxxxxxxxxxxxxxxxxxxx", 108 | "access_token": "xoxp-176389621495-176389622103-175070850417-2f7cc0xxxxxxxxxxxxxxxxxxxxxxx" 109 | }, 110 | "conversation": { 111 | "workspace_id": "e808d814-9143-4dce-aec7-68af0xxxxxxx", 112 | "username": "8d71b8fd-xxxx-1111-xxxx-2256216d19fc", 113 | "password": "xx1AFqNxxxxx" 114 | } 115 | } 116 | } 117 | } 118 | } 119 | } 120 | } -------------------------------------------------------------------------------- /test/resources/payloads/test.unit.deploy.cf-endpoint-payloads.json: -------------------------------------------------------------------------------- 1 | { 2 | "get_conversation_creds": { 3 | "resources": [ 4 | { 5 | "entity": { 6 | "credentials": { 7 | "username": "sample_username", 8 | "password": "sample_password" 9 | } 10 | } 11 | } 12 | ] 13 | }, 14 | "get_organization_id": { 15 | "resources": [ 16 | { 17 | "entity": { 18 | "name": "sampleorganization" 19 | }, 20 | "metadata": { 21 | "guid": "sampleorgguid" 22 | } 23 | } 24 | ] 25 | }, 26 | "get_space_id": { 27 | "resources": [ 28 | { 29 | "entity": { 30 | "name": "samplespace" 31 | }, 32 | "metadata": { 33 | "guid": "samplespaceguid" 34 | } 35 | } 36 | ] 37 | }, 38 | "get_service_id": { 39 | "resources": [ 40 | { 41 | "entity": { 42 | "unique_id": "cloudant" 43 | }, 44 | "metadata": { 45 | "guid": "serviceguid" 46 | } 47 | } 48 | ] 49 | }, 50 | "get_service_plan_id": { 51 | "resources": [ 52 | { 53 | "entity": { 54 | "unique_id": "cloudant-lite" 55 | }, 56 | "metadata": { 57 | "guid": "planguid" 58 | } 59 | } 60 | ] 61 | }, 62 | "get_instance_id": { 63 | "resources": [ 64 | { 65 | "entity": { 66 | "service_plan_guid": "planguid", 67 | "space_guid": "samplespaceguid" 68 | }, 69 | "metadata": { 70 | "guid": "instanceguid" 71 | } 72 | } 73 | ] 74 | }, 75 | "get_instance_key_url": { 76 | "resources": [ 77 | { 78 | "entity": { 79 | "name": "conversation-connector-key", 80 | "credentials": { 81 | "url": "https://api.ng.bluemix.net/v2/sample_instance_key_url" 82 | } 83 | } 84 | } 85 | ] 86 | }, 87 | "get_cloudant_db": { 88 | "update_seq": "xxxx", 89 | "db_name": "contextdb", 90 | "sizes": { 91 | "file": 1, 92 | "external": 2, 93 | "active": 3 94 | }, 95 | "purge_seq": 0, 96 | "other": { 97 | "data_size": 4 98 | }, 99 | "doc_del_count": 0, 100 | "doc_count": 5, 101 | "disk_size": 6, 102 | "disk_format_version": 7, 103 | "data_size": 8, 104 | "compact_running": false, 105 | "instance_start_time": "9" 106 | }, 107 | "create_cloudant_instance": { 108 | "metadata": { 109 | "guid": "sampleguid" 110 | } 111 | }, 112 | "create_cloudant_instance_key": { 113 | "entity": { 114 | "credentials": { 115 | "url": "https://api.ng.bluemix.net/v2/sample_instance_key_url" 116 | } 117 | } 118 | }, 119 | "create_context_database": { 120 | "update_seq": "xxxx", 121 | "db_name": "contextdb", 122 | "sizes": { 123 | "file": 1, 124 | "external": 2, 125 | "active": 3 126 | }, 127 | "purge_seq": 0, 128 | "other": { 129 | "data_size": 4 130 | }, 131 | "doc_del_count": 0, 132 | "doc_count": 5, 133 | "disk_size": 6, 134 | "disk_format_version": 7, 135 | "data_size": 8, 136 | "compact_running": false, 137 | "instance_start_time": "9" 138 | }, 139 | "get_bad_resource": { 140 | "resources": [ 141 | { 142 | "entity": { 143 | "name": "badresource" 144 | }, 145 | "metadata": { 146 | "guid": "sampleorgguid" 147 | } 148 | } 149 | ] 150 | }, 151 | "get_no_resource": { 152 | "resources": [] 153 | } 154 | } -------------------------------------------------------------------------------- /test/resources/payloads/test.unit.deploy.json: -------------------------------------------------------------------------------- 1 | { 2 | "slack": { 3 | "cloudant": { 4 | "dbname": "authdb", 5 | "url": "https://pinkunicorns.cloudant.com" 6 | }, 7 | "saveAuth": { 8 | "newDeploymentAllOk": { 9 | "request": { 10 | "auth": { 11 | "access_token": "xoxp-176389621495-176389622103-175070850417-2f7cc0xxxxxxxxxxxxxxxxxxxxxxx", 12 | "bot_access_token": "xoxb-1764574xxxxxxxxxxxxxxxxxxxxxxxxxx", 13 | "client_id": "176389621495.1750xxxxxxxxxxx", 14 | "client_secret": "8e1075aed344xxxxxxxxxxxx", 15 | "redirect_uri": "https://forcloudfunctions-prod.mybluemix.net/slack/deploy.json", 16 | "verification_token": "BrfPhxxxxxxxxxxxxx" 17 | }, 18 | "key": "slack_b123" 19 | }, 20 | "response": [ 21 | {}, 22 | { 23 | "access_token": "xoxp-176389621495-176389622103-175070850417-2f7cc0xxxxxxxxxxxxxxxxxxxxxxx", 24 | "bot_access_token": "xoxb-1764574xxxxxxxxxxxxxxxxxxxxxxxxxx", 25 | "client_id": "176389621495.1750xxxxxxxxxxx", 26 | "client_secret": "8e1075aed344xxxxxxxxxxxx", 27 | "redirect_uri": "https://forcloudfunctions-prod.mybluemix.net/slack/deploy.json", 28 | "verification_token": "BrfPhxxxxxxxxxxxxx" 29 | } 30 | ] 31 | } 32 | }, 33 | "main": { 34 | "allOk": { 35 | "request": { 36 | "auth": { 37 | "access_token": "xoxp-176389621495-176389622103-175070850417-2f7cc0xxxxxxxxxxxxxxxxxxxxxxx", 38 | "bot_access_token": "xoxb-1764574xxxxxxxxxxxxxxxxxxxxxxxxxx", 39 | "client_id": "176389621495.1750xxxxxxxxxxx", 40 | "client_secret": "8e1075aed344xxxxxxxxxxxx", 41 | "redirect_uri": "https://forcloudfunctions-prod.mybluemix.net/slack/deploy.json", 42 | "verification_token": "BrfPhxxxxxxxxxxxxx" 43 | }, 44 | "key": "slack_U56DFBX7H" 45 | }, 46 | "response": [ 47 | {}, 48 | { 49 | "access_token": "xoxp-176389621495-176389622103-175070850417-2f7cc0xxxxxxxxxxxxxxxxxxxxxxx", 50 | "bot_access_token": "xoxb-1764574xxxxxxxxxxxxxxxxxxxxxxxxxx", 51 | "client_id": "176389621495.1750xxxxxxxxxxx", 52 | "client_secret": "8e1075aed344xxxxxxxxxxxx", 53 | "redirect_uri": "https://forcloudfunctions-prod.mybluemix.net/slack/deploy.json", 54 | "verification_token": "BrfPhxxxxxxxxxxxxx" 55 | } 56 | ] 57 | } 58 | } 59 | } 60 | } -------------------------------------------------------------------------------- /test/resources/payloads/test.unit.starter-code.json: -------------------------------------------------------------------------------- 1 | { 2 | "conversationResponseJson":{ 3 | "duration":537, 4 | "name":"call-conversation", 5 | "subject":"dgterry@us.ibm.com", 6 | "activationId":"1f6e9f7c7f744b578b8f8caf3166f1a8", 7 | "publish":false, 8 | "annotations":[ 9 | { 10 | "key":"limits", 11 | "value":{ 12 | "timeout":60000, 13 | "memory":256, 14 | "logs":10 15 | } 16 | }, 17 | { 18 | "key":"path", 19 | "value":"forcloudfunctions_prod/conversation/call-conversation" 20 | } 21 | ], 22 | "version":"0.0.77", 23 | "response":{ 24 | "result":{ 25 | "entities":[ 26 | { 27 | "entity":"appliance", 28 | "location":[ 29 | 8, 30 | 14 31 | ], 32 | "value":"lights", 33 | "confidence":1 34 | } 35 | ], 36 | "context":{ 37 | "appl_action":"on", 38 | "default_counter":0, 39 | "volumeonoff":"off", 40 | "musiconoff":"off", 41 | "system":{ 42 | "branch_exited_reason":"completed", 43 | "dialog_request_counter":2, 44 | "branch_exited":true, 45 | "dialog_turn_counter":2, 46 | "dialog_stack":[ 47 | { 48 | "dialog_node":"root" 49 | } 50 | ], 51 | "_node_output_map":{ 52 | "Start And Initialize Context":[ 53 | 0, 54 | 0 55 | ], 56 | "node_12_1484715413335":[ 57 | 0, 58 | 0 59 | ] 60 | } 61 | }, 62 | "heateronoff":"off", 63 | "AConoff":"off", 64 | "wipersonoff":"off", 65 | "conversation_id":"e2f462fb-0437-4c37-9237-74f5a7b0d01b", 66 | "lightonoff":"on" 67 | }, 68 | "intents":[ 69 | { 70 | "intent":"turn_on", 71 | "confidence":1 72 | } 73 | ], 74 | "output":{ 75 | "log_messages":[ 76 | 77 | ], 78 | "text":[ 79 | "I'll turn on the lights for you." 80 | ], 81 | "nodes_visited":[ 82 | "Entry Point For On Off Commands", 83 | "node_2_1467232480480", 84 | "Appliance On Off Check" 85 | ], 86 | "action":{ 87 | "lights_on":"" 88 | } 89 | }, 90 | "input":{ 91 | "text":"Turn on lights" 92 | } 93 | }, 94 | "success":true, 95 | "status":"success" 96 | }, 97 | "end":1493916794530, 98 | "logs":[ 99 | 100 | ], 101 | "start":1493916793993, 102 | "namespace":"forcloudfunctions_prod" 103 | }, 104 | "normalizedParamsJson":{ 105 | "slack":{ 106 | "team_id":"TXXXXXXXX", 107 | "event":{ 108 | "channel":"D024BE91L", 109 | "ts":"1355517523.000005", 110 | "text":"Turn on lights", 111 | "type":"message", 112 | "user":"U2147483697" 113 | }, 114 | "api_app_id":"AXXXXXXXXX", 115 | "authed_users":[ 116 | "UXXXXXXX1", 117 | "UXXXXXXX2" 118 | ], 119 | "event_time":1234567890, 120 | "token":"XXYYZZ", 121 | "type":"event_callback", 122 | "event_id":"Ev08MFMKH6" 123 | }, 124 | "provider":"slack", 125 | "input":{ 126 | "text":"Turn on lights" 127 | } 128 | }, 129 | "paramsJson":{ 130 | "slack":{ 131 | "team_id":"TXXXXXXXX", 132 | "event":{ 133 | "channel":"D024BE91L", 134 | "ts":"1355517523.000005", 135 | "text":"Turn on lights", 136 | "type":"message", 137 | "user":"U2147483697" 138 | }, 139 | "api_app_id":"AXXXXXXXXX", 140 | "authed_users":[ 141 | "UXXXXXXX1", 142 | "UXXXXXXX2" 143 | ], 144 | "event_time":1234567890, 145 | "token":"XXYYZZ", 146 | "type":"event_callback", 147 | "event_id":"Ev08MFMKH6" 148 | }, 149 | "provider":"slack" 150 | } 151 | } -------------------------------------------------------------------------------- /test/scripts/clean.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | # Cleans all deploy artifacts in the user's namespace corresponding to the pipeline name passed as argument. 4 | # Cloud Functions artifacts are named like: ${pipeline_name}_slack, ${pipeline_name}_context, and so on ... 5 | 6 | PIPNAME=$1 7 | 8 | WSK_API_HOST=`bx wsk property get --apihost | tr "\t" "\n" | tail -n 1` 9 | WSK_API_KEY=`bx wsk property get --auth | tr "\t" "\n" | tail -n 1` 10 | WSK_NAMESPACE=`bx wsk namespace list | tail -n +2 | head -n 1` 11 | 12 | bx wsk action delete "/${WSK_NAMESPACE}/${PIPNAME}_slack/deploy" 13 | bx wsk action delete "/${WSK_NAMESPACE}/${PIPNAME}_slack/receive" 14 | bx wsk action delete "/${WSK_NAMESPACE}/${PIPNAME}_slack/post" 15 | 16 | bx wsk action delete "/${WSK_NAMESPACE}/${PIPNAME}_facebook/receive" 17 | bx wsk action delete "/${WSK_NAMESPACE}/${PIPNAME}_facebook/post" 18 | bx wsk action delete "/${WSK_NAMESPACE}/${PIPNAME}_facebook/batched_messages" 19 | bx wsk action delete "/${WSK_NAMESPACE}/${PIPNAME}_facebook/multiple_post" 20 | 21 | bx wsk action delete "/${WSK_NAMESPACE}/${PIPNAME}_starter-code/pre-conversation" 22 | bx wsk action delete "/${WSK_NAMESPACE}/${PIPNAME}_starter-code/post-conversation" 23 | bx wsk action delete "/${WSK_NAMESPACE}/${PIPNAME}_starter-code/pre-normalize" 24 | bx wsk action delete "/${WSK_NAMESPACE}/${PIPNAME}_starter-code/post-normalize" 25 | bx wsk action delete "/${WSK_NAMESPACE}/${PIPNAME}_starter-code/normalize-slack-for-conversation" 26 | bx wsk action delete "/${WSK_NAMESPACE}/${PIPNAME}_starter-code/normalize-facebook-for-conversation" 27 | bx wsk action delete "/${WSK_NAMESPACE}/${PIPNAME}_starter-code/normalize-conversation-for-slack" 28 | bx wsk action delete "/${WSK_NAMESPACE}/${PIPNAME}_starter-code/normalize-conversation-for-facebook" 29 | 30 | bx wsk action delete "/${WSK_NAMESPACE}/${PIPNAME}_context/load-context" 31 | bx wsk action delete "/${WSK_NAMESPACE}/${PIPNAME}_context/save-context" 32 | 33 | bx wsk action delete "/${WSK_NAMESPACE}/${PIPNAME}_conversation/call-conversation" 34 | 35 | bx wsk package delete "/${WSK_NAMESPACE}/${PIPNAME}_facebook" 36 | bx wsk package delete "/${WSK_NAMESPACE}/${PIPNAME}_slack" 37 | bx wsk package delete "/${WSK_NAMESPACE}/${PIPNAME}_starter-code" 38 | bx wsk package delete "/${WSK_NAMESPACE}/${PIPNAME}_context" 39 | bx wsk package delete "/${WSK_NAMESPACE}/${PIPNAME}_conversation" 40 | -------------------------------------------------------------------------------- /test/unit/channels/facebook/test.channel.facebook.post.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright IBM Corp. 2017 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the License); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an AS IS BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | 'use strict'; 18 | 19 | /** 20 | * Facebook channel post action unit tests. 21 | */ 22 | 23 | const assert = require('assert'); 24 | const nock = require('nock'); 25 | 26 | const envParams = process.env; 27 | 28 | process.env.__OW_ACTION_NAME = `/${envParams.__OW_NAMESPACE}/pipeline_pkg/action-to-test`; 29 | 30 | const facebookPost = require('./../../../../channels/facebook/post/index.js'); 31 | 32 | const defaultPostUrl = 'https://graph.facebook.com/v2.6/me/messages'; 33 | const badUri = 'badlink.hi'; 34 | const movedUri = 'http://www.ibm.com'; 35 | 36 | const errorBadUri = `Invalid URI "${badUri}"`; 37 | const errorMovedPermanently = 'Action returned with status code 301, message: Moved Permanently'; 38 | const errorNoPageAccessToken = 'auth.facebook.page_access_token not found.'; 39 | const errorNoRecipientId = 'Recepient id not provided.'; 40 | const errorNoMessageText = 'Must provide message object or sender_action.'; 41 | 42 | describe('Facebook Post Unit Tests', () => { 43 | let postParams = {}; 44 | let func; 45 | 46 | const facebookHost = 'https://graph.facebook.com'; 47 | 48 | const expectedResult = { 49 | text: 200, 50 | url: defaultPostUrl, 51 | params: { 52 | message: { 53 | text: 'Hello, World!' 54 | }, 55 | recipient: { 56 | id: envParams.__TEST_FACEBOOK_SENDER_ID 57 | } 58 | } 59 | }; 60 | 61 | beforeEach(() => { 62 | postParams = { 63 | page_access_token: envParams.__TEST_FACEBOOK_PAGE_ACCESS_TOKEN, 64 | message: { 65 | text: 'Hello, World!' 66 | }, 67 | recipient: { 68 | id: envParams.__TEST_FACEBOOK_SENDER_ID 69 | }, 70 | raw_input_data: { 71 | provider: 'facebook', 72 | auth: { 73 | facebook: { 74 | app_secret: envParams.__TEST_FACEBOOK_APP_SECRET, 75 | verification_token: envParams.__TEST_FACEBOOK_VERIFICATION_TOKEN, 76 | page_access_token: envParams.__TEST_FACEBOOK_PAGE_ACCESS_TOKEN 77 | } 78 | } 79 | } 80 | }; 81 | 82 | createFacebookMock(); 83 | }); 84 | 85 | it('validate facebook/post works as intended', () => { 86 | func = facebookPost.main; 87 | 88 | return func(postParams).then( 89 | result => { 90 | assert.deepEqual(expectedResult, result); 91 | }, 92 | error => { 93 | assert(false, error); 94 | } 95 | ); 96 | }); 97 | 98 | it('validate error when bad uri supplied', () => { 99 | func = facebookPost.postFacebook; 100 | 101 | return func(postParams, badUri, postParams.page_access_token).then( 102 | result => { 103 | assert(false, result); 104 | }, 105 | error => { 106 | nock.cleanAll(); 107 | assert(error, errorBadUri); 108 | } 109 | ); 110 | }); 111 | 112 | it('validate error when not 200 uri supplied', () => { 113 | func = facebookPost.postFacebook; 114 | 115 | return func(postParams, movedUri, postParams.page_access_token).then( 116 | result => { 117 | assert(false, result); 118 | }, 119 | error => { 120 | nock.cleanAll(); 121 | assert(error, errorMovedPermanently); 122 | } 123 | ); 124 | }); 125 | 126 | it('validate error when page access token absent in auth', () => { 127 | func = facebookPost.main; 128 | delete postParams.raw_input_data.auth.facebook.page_access_token; 129 | 130 | return func(postParams).then( 131 | result => { 132 | assert(false, result); 133 | }, 134 | error => { 135 | assert.equal(error.message, errorNoPageAccessToken); 136 | } 137 | ); 138 | }); 139 | 140 | it('validate error when no recipient Id provided', () => { 141 | delete postParams.recipient; 142 | func = facebookPost.main; 143 | 144 | return func(postParams).then( 145 | result => { 146 | assert(false, result); 147 | }, 148 | error => { 149 | assert.equal(error, errorNoRecipientId); 150 | } 151 | ); 152 | }); 153 | 154 | it('validate error when no message text or sender_action provided', () => { 155 | delete postParams.message; 156 | func = facebookPost.main; 157 | return func(postParams).then( 158 | result => { 159 | assert(false, result); 160 | }, 161 | error => { 162 | assert.equal(error, errorNoMessageText); 163 | } 164 | ); 165 | }); 166 | 167 | function createFacebookMock() { 168 | return nock(facebookHost) 169 | .post('/v2.6/me/messages') 170 | .query({ 171 | access_token: envParams.__TEST_FACEBOOK_PAGE_ACCESS_TOKEN 172 | }) 173 | .reply(200, {}); 174 | } 175 | }); 176 | -------------------------------------------------------------------------------- /test/unit/channels/slack/test.channel.slack.post.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright IBM Corp. 2017 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the License); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an AS IS BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | 'use strict'; 18 | 19 | /** 20 | * Slack channel post action unit tests. 21 | */ 22 | 23 | const assert = require('assert'); 24 | const nock = require('nock'); 25 | 26 | const envParams = process.env; 27 | 28 | const slackPost = require('./../../../../channels/slack/post/index.js'); 29 | 30 | const text = 'Message coming from slack/post unit test.'; 31 | 32 | const mockError = 'mock-error'; 33 | const errorNoChannel = 'Channel not provided.'; 34 | const errorSlackResponse = 'Action returned with status code 400, message: null'; 35 | const errorNoText = 'Message text not provided.'; 36 | 37 | describe('Slack Post Unit Tests', () => { 38 | let options; 39 | let expectedResult; 40 | 41 | let func; 42 | 43 | const botId = 'bot-id'; 44 | 45 | const slackHost = 'https://slack.com'; 46 | 47 | beforeEach(() => { 48 | options = { 49 | channel: envParams.__TEST_SLACK_CHANNEL, 50 | text, 51 | raw_input_data: { 52 | bot_id: botId, 53 | provider: 'slack', 54 | auth: { 55 | slack: { 56 | verification_token: envParams.__TEST_SLACK_VERIFICATION_TOKEN, 57 | access_token: envParams.__TEST_SLACK_ACCESS_TOKEN, 58 | bot_access_token: envParams.__TEST_SLACK_BOT_ACCESS_TOKEN, 59 | bot_users: {} 60 | } 61 | } 62 | } 63 | }; 64 | options.raw_input_data.auth.slack.bot_users[botId] = { 65 | access_token: envParams.__TEST_SLACK_ACCESS_TOKEN, 66 | bot_access_token: envParams.__TEST_SLACK_BOT_ACCESS_TOKEN 67 | }; 68 | 69 | expectedResult = { 70 | as_user: 'true', 71 | text: 'Message coming from slack/post unit test.', 72 | channel: envParams.__TEST_SLACK_CHANNEL, 73 | token: envParams.__TEST_SLACK_BOT_ACCESS_TOKEN 74 | }; 75 | 76 | createSlackMock(); 77 | }); 78 | 79 | it('validate slack/post works as intended', () => { 80 | func = slackPost.main; 81 | 82 | return func(options).then( 83 | result => { 84 | assert.deepEqual(result, expectedResult); 85 | }, 86 | error => { 87 | assert(false, error); 88 | } 89 | ); 90 | }); 91 | 92 | it('validate slack/post works with attachments', () => { 93 | const attachments = [{ text: 'Message coming from slack/post unit test.' }]; 94 | options.attachments = attachments; 95 | expectedResult.attachments = attachments; 96 | 97 | func = slackPost.main; 98 | 99 | return func(options).then( 100 | result => { 101 | assert.deepEqual(result, expectedResult); 102 | }, 103 | error => { 104 | assert(false, error); 105 | } 106 | ); 107 | }); 108 | 109 | it('validate post params modified differently for slack hooks url', () => { 110 | const postParams = slackPost.modifyPostParams( 111 | expectedResult, 112 | 'https://hooks.slack.com/sample_page' 113 | ); 114 | 115 | assert.deepEqual(postParams, JSON.stringify(expectedResult)); 116 | }); 117 | 118 | it('validate error when slack server throws error', () => { 119 | nock.cleanAll(); 120 | nock(slackHost).post('/api/chat.postMessage').replyWithError(mockError); 121 | 122 | return slackPost 123 | .main(options) 124 | .then(() => { 125 | assert(false, 'Action succeeded unexpectedly.'); 126 | }) 127 | .catch(error => { 128 | assert.equal(error.message, mockError); 129 | }); 130 | }); 131 | 132 | it('validate error when slack server throws response error', () => { 133 | nock.cleanAll(); 134 | nock(slackHost).post('/api/chat.postMessage').reply(400, mockError); 135 | 136 | return slackPost 137 | .main(options) 138 | .then(() => { 139 | assert(false, 'Action succeeded unexpectedly.'); 140 | }) 141 | .catch(error => { 142 | assert.equal(error, errorSlackResponse); 143 | }); 144 | }); 145 | 146 | it('validate error when no channel provided', () => { 147 | delete options.channel; 148 | func = slackPost.validateParameters; 149 | 150 | try { 151 | func(options); 152 | } catch (e) { 153 | assert.equal(e.message, errorNoChannel); 154 | } 155 | }); 156 | 157 | it('validate error when no message text provided', () => { 158 | delete options.text; 159 | 160 | func = slackPost.validateParameters; 161 | 162 | try { 163 | func(options); 164 | } catch (e) { 165 | assert.equal(e.message, errorNoText); 166 | } 167 | }); 168 | 169 | function createSlackMock() { 170 | return nock(slackHost).post('/api/chat.postMessage').reply(200, {}); 171 | } 172 | }); 173 | -------------------------------------------------------------------------------- /test/unit/deploy/test.check-deploy-exists.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright IBM Corp. 2017 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the License); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an AS IS BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | 'use strict'; 18 | 19 | /** 20 | * Unit tests for deploy endpoints, checking deploy name exists. 21 | */ 22 | 23 | const assert = require('assert'); 24 | const nock = require('nock'); 25 | const pick = require('object.pick'); 26 | 27 | const checkDeployExistsAction = require('./../../../deploy/check-deploy-exists.js'); 28 | 29 | const owHost = 'https://openwhisk.ng.bluemix.net'; 30 | 31 | const userNamespace = 'sampleorganization_samplespace'; 32 | const deployName = 'sample-deployment-name'; 33 | const mockError = 'mock-error'; 34 | const errorNoNamespaceFound = `Could not find user namespace: ${userNamespace}.`; 35 | 36 | describe('Check-Deploy-Exists Unit Tests', () => { 37 | let params; 38 | let bxAuthPayload; 39 | let owMock; 40 | 41 | beforeEach(() => { 42 | params = { 43 | state: { 44 | auth: { 45 | access_token: 'sample_access_token', 46 | refresh_token: 'sample_refresh_token' 47 | }, 48 | wsk: { 49 | namespace: userNamespace 50 | }, 51 | name: deployName 52 | } 53 | }; 54 | 55 | bxAuthPayload = { 56 | subject: 'mockuser@ibm.com', 57 | developer_guid: 'DXXXXXXXXX', 58 | namespaces: [ 59 | { 60 | name: userNamespace, 61 | uuid: 'sampleuuid', 62 | key: 'samplekey' 63 | } 64 | ] 65 | }; 66 | 67 | owMock = createCloudFunctionsMock(); 68 | }); 69 | 70 | it('validate main works', () => { 71 | return checkDeployExistsAction(params) 72 | .then(results => { 73 | assert.deepEqual(results, { code: 200, message: 'OK' }); 74 | }) 75 | .catch(error => { 76 | assert(false, error); 77 | }); 78 | }); 79 | 80 | it('validate error when not enough input parameters', () => { 81 | delete params.state.auth; 82 | 83 | return checkDeployExistsAction(params) 84 | .then(() => { 85 | assert(false, 'Action succeeded unexpectedly.'); 86 | }) 87 | .catch(error => { 88 | assert.deepEqual(error, { 89 | code: 400, 90 | message: "Could not get user's Bluemix credentials." 91 | }); 92 | }); 93 | }); 94 | 95 | it('validate error when Cloud Functions host throws object error', () => { 96 | nock.cleanAll(); 97 | 98 | owMock.post('/bluemix/v2/authenticate').replyWithError(mockError); 99 | 100 | return checkDeployExistsAction(params) 101 | .then(() => { 102 | assert(false, 'Action succeeded unexpectedly.'); 103 | }) 104 | .catch(error => { 105 | assert.deepEqual(error, { code: 400, message: mockError }); 106 | }); 107 | }); 108 | 109 | it('validate error when Cloud Functions host throws response error', () => { 110 | nock.cleanAll(); 111 | 112 | owMock 113 | .post('/bluemix/v2/authenticate') 114 | .reply(400, { error: { error: mockError } }); 115 | 116 | return checkDeployExistsAction(params) 117 | .then(() => { 118 | assert(false, 'Action succeeded unexpectedly.'); 119 | }) 120 | .catch(error => { 121 | assert.deepEqual(pick(error, ['code', 'message']), { 122 | code: 400, 123 | message: mockError 124 | }); 125 | }); 126 | }); 127 | 128 | it('validate error when Cloud Functions host does not find the correct namespace', () => { 129 | bxAuthPayload.namespaces[0].name = 'bad_namespace'; 130 | 131 | nock.cleanAll(); 132 | 133 | owMock.post('/bluemix/v2/authenticate').reply(200, bxAuthPayload); 134 | 135 | return checkDeployExistsAction(params) 136 | .then(() => { 137 | assert(false, 'Action succeeded unexpectedly.'); 138 | }) 139 | .catch(error => { 140 | assert.deepEqual(error, { code: 400, message: errorNoNamespaceFound }); 141 | }); 142 | }); 143 | 144 | it('validate error when ow deployment exists', () => { 145 | nock.cleanAll(); 146 | 147 | owMock 148 | .post('/bluemix/v2/authenticate') 149 | .reply(200, bxAuthPayload) 150 | .get(uri => { 151 | return uri.indexOf(`/api/v1/namespaces/${userNamespace}/actions`) === 0; 152 | }) 153 | .reply(200, { exec: { code: 'sample_code' } }); 154 | 155 | return checkDeployExistsAction(params) 156 | .then(() => { 157 | assert(false, 'Action succeeded unexpectedly.'); 158 | }) 159 | .catch(error => { 160 | assert.deepEqual(error, { 161 | code: 400, 162 | message: `Deployment "${deployName}" already exists.` 163 | }); 164 | }); 165 | }); 166 | 167 | it('validate error when deploy name contains invalid characters', () => { 168 | params.state.name = 'invalid*deployment*name'; 169 | 170 | return checkDeployExistsAction(params) 171 | .then(() => { 172 | assert(false, 'Action succeeded unexpectedly.'); 173 | }) 174 | .catch(error => { 175 | assert.deepEqual(error, { 176 | code: 400, 177 | message: 'Deployment name contains invalid characters. Please use only the following characters in your deployment name: "a-z A-Z 0-9 -". Additionally, your deployment name cannot start with a -, and your name cannot be longer than 256 characters.' 178 | }); 179 | }); 180 | }); 181 | 182 | function createCloudFunctionsMock() { 183 | return ( 184 | nock(owHost) 185 | // get Cloud Functions credentials from bx tokens 186 | .post('/bluemix/v2/authenticate') 187 | .reply(200, bxAuthPayload) 188 | // get action from user namespace 189 | .get(uri => { 190 | return uri.indexOf(`/api/v1/namespaces/${userNamespace}/actions`) === 191 | 0; 192 | }) 193 | .replyWithError(mockError) 194 | ); 195 | } 196 | }); 197 | -------------------------------------------------------------------------------- /test/unit/deploy/test.create-cloudant-database.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright IBM Corp. 2017 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the License); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an AS IS BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | 'use strict'; 18 | 19 | /** 20 | * Unit tests for deploy endpoints, creating a Cloudant database. 21 | */ 22 | 23 | const assert = require('assert'); 24 | const nock = require('nock'); 25 | 26 | const deployCreateCloudantDbAction = require('./../../../deploy/create-cloudant-database.js'); 27 | 28 | const sampleUsername = 'sample_username'; 29 | const samplePassword = 'sample_password'; 30 | const sampleDbName = 'sample_database_name'; 31 | const host = `https://${sampleUsername}:${samplePassword}@${sampleUsername}.cloudant.com`; 32 | 33 | const mockError = 'mock-error'; 34 | 35 | describe('Create Cloudant Databade Unit Tests', () => { 36 | let params; 37 | let mock; 38 | 39 | beforeEach(() => { 40 | params = { 41 | cloudant: { 42 | username: sampleUsername, 43 | password: samplePassword 44 | }, 45 | db_name: sampleDbName 46 | }; 47 | 48 | mock = createCloudantHostMock(); 49 | }); 50 | 51 | it('validate main works', () => { 52 | mock.put(`/${sampleDbName}`).reply(200, { ok: 'true' }); 53 | 54 | return deployCreateCloudantDbAction(params) 55 | .then(result => { 56 | assert(mock.isDone()); 57 | assert.deepEqual(result, { code: 200, message: 'OK' }); 58 | }) 59 | .catch(error => { 60 | assert(false, error); 61 | }); 62 | }); 63 | 64 | it('validate error when not enough input parameters', () => { 65 | delete params.cloudant; 66 | 67 | return deployCreateCloudantDbAction(params) 68 | .then(() => { 69 | assert(false, 'Action succeeded unexpectedly.'); 70 | }) 71 | .catch(error => { 72 | assert.deepEqual(error, { 73 | code: 400, 74 | message: 'No cloudant object provided.' 75 | }); 76 | }); 77 | }); 78 | 79 | it('validate system works with database already existing', () => { 80 | mock 81 | .put(`/${sampleDbName}`) 82 | .reply(400, JSON.stringify({ error: 'file_exists' })); 83 | 84 | return deployCreateCloudantDbAction(params) 85 | .then(result => { 86 | assert(mock.isDone()); 87 | assert.deepEqual(result, { code: 200, message: 'OK' }); 88 | }) 89 | .catch(error => { 90 | assert(false, error); 91 | }); 92 | }); 93 | 94 | it('validate system works with retries (error object)', () => { 95 | mock 96 | .put(`/${sampleDbName}`) 97 | .replyWithError({ error: 'service_unavailable' }) 98 | .put(`/${sampleDbName}`) 99 | .reply(200, { ok: 'true' }); 100 | 101 | return deployCreateCloudantDbAction(params) 102 | .then(result => { 103 | assert(mock.isDone()); 104 | assert.deepEqual(result, { code: 200, message: 'OK' }); 105 | }) 106 | .catch(error => { 107 | assert(false, error); 108 | }); 109 | }); 110 | 111 | it('validate system works with retries (response object)', () => { 112 | mock 113 | .put(`/${sampleDbName}`) 114 | .reply(500, 'service_unavailable') 115 | .put(`/${sampleDbName}`) 116 | .reply(200, { ok: 'true' }); 117 | 118 | return deployCreateCloudantDbAction(params) 119 | .then(result => { 120 | assert(mock.isDone()); 121 | assert.deepEqual(result, { code: 200, message: 'OK' }); 122 | }) 123 | .catch(error => { 124 | assert(false, error); 125 | }); 126 | }); 127 | 128 | it('validate error when error is thrown by cloudant (error object)', () => { 129 | mock.put(`/${sampleDbName}`).replyWithError({ error: mockError }); 130 | 131 | return deployCreateCloudantDbAction(params) 132 | .then(() => { 133 | assert(false, 'Action succeeded unexpectedly.'); 134 | }) 135 | .catch(error => { 136 | assert.deepEqual(error, { code: 400, message: mockError }); 137 | }); 138 | }); 139 | 140 | it('validate error when error is thrown by cloudant (response object)', () => { 141 | mock 142 | .put(`/${sampleDbName}`) 143 | .reply(400, JSON.stringify({ error: mockError })); 144 | 145 | return deployCreateCloudantDbAction(params) 146 | .then(() => { 147 | assert(false, 'Action succeeded unexpectedly.'); 148 | }) 149 | .catch(error => { 150 | assert.deepEqual(error, { code: 400, message: mockError }); 151 | }); 152 | }); 153 | 154 | function createCloudantHostMock() { 155 | return nock(host); 156 | } 157 | }); 158 | -------------------------------------------------------------------------------- /test/unit/starter-code/normalize-for-conversation/test.starter-code.normalize-facebook-for-conversation.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright IBM Corp. 2017 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the License); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an AS IS BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | 'use strict'; 18 | 19 | /** 20 | * Unit Tests for normalizing facebook JSON parameters to conversation SDK parameters. 21 | */ 22 | 23 | const assert = require('assert'); 24 | 25 | const envParams = process.env; 26 | 27 | process.env.__OW_ACTION_NAME = `/${process.env.__OW_NAMESPACE}/pipeline_pkg/action-to-test`; 28 | 29 | const actionNormFacebookForConversation = require('./../../../../starter-code/normalize-for-conversation/normalize-facebook-for-conversation.js'); 30 | 31 | const errorBadSupplier = "Provider not supplied or isn't Facebook."; 32 | const errorNoFacebookData = 'Facebook JSON data is missing.'; 33 | const errorNoMsgOrPostbackTypeEvent = 'Neither message.text event detected nor postback.payload event detected. Please add appropriate code to handle a different facebook event.'; 34 | const text = 'hello, world!'; 35 | 36 | describe('Starter Code Normalize-Facebook-For-Conversation Unit Tests', () => { 37 | let textMsgParams; 38 | let textMsgResult; 39 | let buttonClickParams; 40 | let buttonClickResult; 41 | let func; 42 | 43 | const auth = { 44 | conversation: { 45 | workspace_id: envParams.__TEST_CONVERSATION_WORKSPACE_ID 46 | } 47 | }; 48 | 49 | beforeEach(() => { 50 | textMsgParams = { 51 | facebook: { 52 | sender: { 53 | id: 'user_id' 54 | }, 55 | recipient: { 56 | id: 'page_id' 57 | }, 58 | message: { 59 | text: 'hello, world!' 60 | } 61 | }, 62 | provider: 'facebook', 63 | auth 64 | }; 65 | 66 | buttonClickParams = { 67 | facebook: { 68 | sender: { 69 | id: 'user_id' 70 | }, 71 | recipient: { 72 | id: 'page_id' 73 | }, 74 | postback: { 75 | payload: 'hello, world!', 76 | title: 'Click here' 77 | } 78 | }, 79 | provider: 'facebook', 80 | auth 81 | }; 82 | 83 | textMsgResult = { 84 | conversation: { 85 | input: { 86 | text 87 | } 88 | }, 89 | raw_input_data: { 90 | facebook: textMsgParams.facebook, 91 | provider: 'facebook', 92 | auth, 93 | cloudant_context_key: `facebook_user_id_${envParams.__TEST_CONVERSATION_WORKSPACE_ID}_page_id` 94 | } 95 | }; 96 | 97 | buttonClickResult = { 98 | conversation: { 99 | input: { 100 | text 101 | } 102 | }, 103 | raw_input_data: { 104 | facebook: buttonClickParams.facebook, 105 | provider: 'facebook', 106 | auth, 107 | cloudant_context_key: `facebook_user_id_${envParams.__TEST_CONVERSATION_WORKSPACE_ID}_page_id` 108 | } 109 | }; 110 | }); 111 | 112 | it('validate normalizing works for a regular text message', () => { 113 | func = actionNormFacebookForConversation.main; 114 | 115 | return func(textMsgParams).then( 116 | result => { 117 | assert.deepEqual(result, textMsgResult); 118 | }, 119 | error => { 120 | assert(false, error); 121 | } 122 | ); 123 | }); 124 | 125 | it('validate normalizing works for an event when a button is clicked', () => { 126 | func = actionNormFacebookForConversation.main; 127 | 128 | return func(buttonClickParams).then( 129 | result => { 130 | assert.deepEqual(result, buttonClickResult); 131 | }, 132 | error => { 133 | assert(false, error); 134 | } 135 | ); 136 | }); 137 | 138 | it('validate error when neither message type event nor postback type event detected', () => { 139 | delete textMsgParams.facebook.message; 140 | 141 | func = actionNormFacebookForConversation.main; 142 | 143 | return func(textMsgParams).then( 144 | result => { 145 | assert(false, result); 146 | }, 147 | error => { 148 | assert.equal(error, errorNoMsgOrPostbackTypeEvent); 149 | } 150 | ); 151 | }); 152 | 153 | it('validate error when provider missing', () => { 154 | delete textMsgParams.provider; 155 | 156 | func = actionNormFacebookForConversation.validateParameters; 157 | try { 158 | func(textMsgParams); 159 | } catch (e) { 160 | assert.equal(e.message, errorBadSupplier); 161 | } 162 | }); 163 | 164 | it('validate error when facebook data missing', () => { 165 | delete textMsgParams.facebook; 166 | 167 | func = actionNormFacebookForConversation.validateParameters; 168 | try { 169 | func(textMsgParams); 170 | } catch (e) { 171 | assert.equal(e.message, errorNoFacebookData); 172 | } 173 | }); 174 | }); 175 | -------------------------------------------------------------------------------- /test/utils/cloudant-utils.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright IBM Corp. 2017 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the License); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an AS IS BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | 'use strict'; 18 | 19 | const request = require('request'); 20 | /** 21 | * Utility functions for Cloudant 22 | */ 23 | 24 | /** 25 | * Clears the context database by deleting and re-creating the db. 26 | * @param {string} dbName Name of the context database 27 | * @param {string} cloudant_url Cloudant instance base url 28 | * @return {object} response body/error json 29 | */ 30 | function clearContextDb(cloudantUrl, dbName) { 31 | return retriableDestroyDatabase(cloudantUrl, dbName).then(() => { 32 | return retriableCreateDatabase(cloudantUrl, dbName); 33 | }); 34 | } 35 | 36 | function retriableDestroyDatabase(cloudantUrl, dbName) { 37 | return new Promise((resolve, reject) => { 38 | return request( 39 | { 40 | method: 'DELETE', 41 | url: `${cloudantUrl}/${dbName}` 42 | }, 43 | (error, response, body) => { 44 | if (error) { 45 | const errorString = typeof error === 'string' 46 | ? JSON.parse(error).error 47 | : error.error; 48 | if (errorString === 'service_unavailable') { 49 | sleep(500) 50 | .then(() => { 51 | return retriableCreateDatabase(cloudantUrl, dbName); 52 | }) 53 | .then(resolve) 54 | .catch(reject); 55 | } else { 56 | reject(error); 57 | } 58 | } else if (response.statusCode >= 500) { 59 | sleep(500) 60 | .then(() => { 61 | return retriableCreateDatabase(cloudantUrl, dbName); 62 | }) 63 | .then(resolve) 64 | .catch(reject); 65 | } else if (response.statusCode < 200 || response.statusCode >= 400) { 66 | const responseBody = JSON.parse(response.body); 67 | reject(responseBody); 68 | } else { 69 | resolve(body); 70 | } 71 | } 72 | ); 73 | }); 74 | } 75 | 76 | function retriableCreateDatabase(cloudantUrl, dbName) { 77 | return new Promise((resolve, reject) => { 78 | return request( 79 | { 80 | method: 'PUT', 81 | url: `${cloudantUrl}/${dbName}` 82 | }, 83 | (error, response, body) => { 84 | if (error) { 85 | const errorString = typeof error === 'string' 86 | ? JSON.parse(error).error 87 | : error.error; 88 | if (errorString === 'service_unavailable') { 89 | sleep(500) 90 | .then(() => { 91 | return retriableCreateDatabase(cloudantUrl, dbName); 92 | }) 93 | .then(resolve) 94 | .catch(reject); 95 | } else { 96 | reject(error); 97 | } 98 | } else if (response.statusCode >= 500) { 99 | sleep(500) 100 | .then(() => { 101 | return retriableCreateDatabase(cloudantUrl, dbName); 102 | }) 103 | .then(resolve) 104 | .catch(reject); 105 | } else if (response.statusCode < 200 || response.statusCode >= 400) { 106 | const responseBody = JSON.parse(response.body); 107 | if (responseBody.error === 'file_exists') { 108 | resolve({}); 109 | } else { 110 | reject(responseBody); 111 | } 112 | } else { 113 | resolve(body); 114 | } 115 | } 116 | ); 117 | }); 118 | } 119 | 120 | function sleep(ms) { 121 | return new Promise(resolve => { 122 | setTimeout(resolve, ms); 123 | }); 124 | } 125 | 126 | module.exports = { clearContextDb }; 127 | -------------------------------------------------------------------------------- /test/utils/helper-methods.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright IBM Corp. 2017 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the License); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an AS IS BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | 'use strict'; 18 | 19 | /** 20 | * Takes an error object or string and returns a string containing the core error message. 21 | * 22 | * @param {string/object} error Error string or object 23 | * @return {string} Error string 24 | */ 25 | function safeExtractErrorMessage(error) { 26 | const errorMessage = typeof error === 'string' ? error : error.message; 27 | const owError = extractCloudFunctionsErrorMessage(errorMessage); 28 | 29 | return owError || errorMessage; 30 | 31 | /** 32 | * Cloud Functions error messages are in the form: 33 | * POST Returned HTTP () --> "The request resource does not exist." 34 | * and this method will extract the core error message after the -->. 35 | * 36 | * @param {string} eMessage Detailed Cloud Functions error message. 37 | * @return {string} Core error message. 38 | */ 39 | function extractCloudFunctionsErrorMessage(eMessage) { 40 | const arrow = '--> '; 41 | if (eMessage.indexOf(arrow) < 0) return null; 42 | 43 | return eMessage.substring( 44 | eMessage.indexOf(arrow) + arrow.length + 1, 45 | eMessage.length - 1 46 | ); 47 | } 48 | } 49 | 50 | module.exports = { safeExtractErrorMessage }; 51 | --------------------------------------------------------------------------------