From 3920a47b2bb2a08452dc0d080dea15b31fc006b9 Mon Sep 17 00:00:00 2001 From: Amo Wu Date: Wed, 30 Dec 2015 01:11:06 +0800 Subject: [PATCH] v0.1.0 done --- README.md | 64 ++++++++++++++++++++++++++++++++++++++ config.sample.json | 4 +++ lambda.js | 35 +++++++++++++++++++++ lib/google.js | 77 ++++++++++++++++++++++++++++++++++++++++++++++ messages.json | 7 +++++ package.json | 21 +++++++++++++ role.example.json | 21 +++++++++++++ scripts/build.sh | 13 ++++++++ scripts/create.sh | 19 ++++++++++++ scripts/deploy.sh | 13 ++++++++ 10 files changed, 274 insertions(+) create mode 100644 README.md create mode 100644 config.sample.json create mode 100644 lambda.js create mode 100644 lib/google.js create mode 100644 messages.json create mode 100644 package.json create mode 100644 role.example.json create mode 100755 scripts/build.sh create mode 100755 scripts/create.sh create mode 100755 scripts/deploy.sh diff --git a/README.md b/README.md new file mode 100644 index 0000000..4b470c4 --- /dev/null +++ b/README.md @@ -0,0 +1,64 @@ +![slack-lambda-google](https://cloud.githubusercontent.com/assets/559351/12033086/4eb38aba-ae5a-11e5-8dbf-50e82f94424d.png) + +# Google God + +A serverless [Slack Slash Commands](https://api.slack.com/slash-commands) to integrate [Google Knowledge Graph API](https://developers.google.com/knowledge-graph/) using [AWS Lambda](https://aws.amazon.com/lambda/) and [AWS API Gateway](https://aws.amazon.com/api-gateway/). + +![screenshot](https://cloud.githubusercontent.com/assets/559351/12034616/835b2ad4-ae6e-11e5-8b89-fc7b486fd47e.gif) + +## Getting Started + +This project was built with Ryan Ray's repo [slack-lambda-weather](https://github.com/ryanray/slack-lambda-weather) and his [post](http://www.ryanray.me/serverless-slack-integrations). + +### Prerequisites + +* Install [AWS CLI](https://aws.amazon.com/cli/) +* Execution Role ARN for your AWS Lambda +* Create a `config.json` based on `config.sample.json`. This file is gitignored by default because this is where you would put any API key's and other secret info that your lambda may need. +* [Google Knowledge Graph Search API](https://developers.google.com/knowledge-graph/) API key, and paste it to `config.json`. + +### AWS Lambda + +> Lambda is based on EC2 and allows you to deploy and execute your code (Node.js, Java, Python) without having to provision servers. + +First build and create your Lambda function on AWS: + +```sh +npm run create LAMBDA_FUNCTION_NAME EXECUTION_ROLE_ARN +``` + +> Before you can create your Lambda you need to create an execution role. If you did any of the Lambda hello world tutorials in the AWS console you should already have a role created. Either way you need to goto the [IAM Management Console - Roles](https://console.aws.amazon.com/iam/home#roles). Get the ARN of `lambda_basic_execution` or create a new role based on `role.example.json` and get the ARN from that. The full ARN looks something like `arn:aws:iam::YOUR_ACCOUNT_ID:role/lambda_basic_execution`. + +Build and deploy to update your Lambda code: + +```sh +npm run deploy LAMBDA_FUNCTION_NAME +``` + +### AWS API Gateway + +> Lambda responds to events, which can come from a variety of sources. By default Lambda isn't accessable from a URL, but API Gateway allows you to map a URL and an HTTP method to trigger your Lambda code. You can setup GET, POST, PUT, etc... and map the parameters/body into a JSON payload that Lambda understands. + +1. Goto [AWS API Gateway](https://aws.amazon.com/api-gateway/) and Create new API - name it whatever you want - we'll do LambdaTest for now +2. Create a new resource, name it whatever +3. Create a POST method under your resource +4. Select Integration type with Lambda Function, and select your Lambda region, and enter your Lambda function name +5. Save and give API Gateway permission to invoke your Lambda function +6. Click on Integration Request > Mapping Templates +7. Add mapping Template for `application/x-www-form-urlencoded` and click checkmark +8. Change input passthrough to mapping template and paste this template [gist](https://gist.github.com/ryanray/668022ad2432e38493df) that can help you to convert `application/x-www-form-urlencoded` POST from Slack to Lambda's `application/json` format +9. Save and click Deploy API with a new stage, then you can see a public invoke URL + +### Slack + +1. Goto Slack App `https://YOUR_TEAN_DOMAIN.slack.com/apps/manage` +2. Search `Slash Commands` and add a new configuration +3. Choose a command, for this example enter `/google` in the command name input, click Add Slash Command Integration button. +4. Now you should be on the settings page, scroll down and copy the token to your `config.json` (NOTE: you don't want to expose the token to the public!). +5. Copy and paste your API Gateway invoke URL to URL field. + +## Contributing + +Improvements are welcome! Just fork, push your changes to a new branch, and create a pull request! + +![slack-lambda-google-ex](https://cloud.githubusercontent.com/assets/559351/12034120/721ea5a4-ae67-11e5-8d7a-297cbaa1a51d.png) \ No newline at end of file diff --git a/config.sample.json b/config.sample.json new file mode 100644 index 0000000..e0d9a8f --- /dev/null +++ b/config.sample.json @@ -0,0 +1,4 @@ +{ + "GOOGLE_API_KEY": "YOUR_GOOGLE_API_KEY", + "SLASH_COMMANDS_TOKEN": "YOUR_SLACK_SLASH_COMMAND" +} diff --git a/lambda.js b/lambda.js new file mode 100644 index 0000000..d92c4a5 --- /dev/null +++ b/lambda.js @@ -0,0 +1,35 @@ +var config = require('./config.json'); +var messages = require('./messages.json'); +var google = require('./lib/google.js'); + +/** + * Entrypoint for AWS Lambda + * @param event is the JSON payload that API Gateway transformed from slack's application/x-www-form-urlencoded request + * @param context has methods to let Lambda know when we're done - similar to http/express modules `response.send()` + */ +exports.handler = function (event, context) { + console.log(event); + + // Verify request came from slack + if (event.token !== config.SLASH_COMMANDS_TOKEN) { + return context.fail(messages.UNAUTHORIZED_TOKEN); + } + + google(event.text) + .then(function (response) { + context.succeed(response); + }) + .catch(function (error) { + console.error(error); + context.succeed({ + 'response_type': 'in_channel', + 'attachments': [ + { + 'fallback': messages.ERROR_FALLBACK, + 'text': messages.ERROR_TEXT, + 'color': 'danger' + } + ] + }); + }); +}; diff --git a/lib/google.js b/lib/google.js new file mode 100644 index 0000000..ee69d0c --- /dev/null +++ b/lib/google.js @@ -0,0 +1,77 @@ +var _ = require('lodash'); +var request = require('request-promise'); + +var config = require('../config.json'); +var messages = require('../messages.json'); + +// Language priority for search result +var languages = [ + 'zh-TW', + 'zh', + 'en', + 'ja' +]; +// Google API params +var params = { + 'limit': 1, + 'languages': 'zh,en,ja' +}; + +module.exports = function (text) { + return request('https://kgsearch.googleapis.com/v1/entities:search?' + + 'query=' + text.trim() + '&' + + 'key=' + config.GOOGLE_API_KEY + '&' + + 'limit=' + params.limit + '&' + + 'languages=' + params.languages) + .then(function (response) { + var data = JSON.parse(response); + + var result = _.get(data, ['itemListElement', 0, 'result']); + if (!result) { + return { + 'response_type': 'in_channel', + 'attachments': [ + { + 'fallback': messages.RESULT_NOT_FOUND, + 'text': messages.RESULT_NOT_FOUND_TEXT, + 'color': 'danger' + } + ] + }; + } + + var name = item(result, 'name', '@language'); + var title = _.get(name, '@value'); + + var detailedDescription = item(result, 'detailedDescription' ,'inLanguage'); + + return { + 'response_type': 'in_channel', + 'attachments': [ + { + 'fallback': title, + 'title': title, + 'title_link': _.get(detailedDescription, 'url'), + 'text': _.get(detailedDescription, 'articleBody'), + 'thumb_url': _.get(result, ['image', 'contentUrl']) + } + ] + }; + }); +}; + +/** + * Get only one item from search result by language priority list + * @param {Object} result search result + * @param {string} children item name + * @param {string} key item language property name + * @return {Object} filter object + */ +function item (result, children, key) { + var items = _.chain(result).get(children).value(); + var language = _.chain(languages).filter(function (lang) { + return _.find(items, _.matchesProperty(key, lang)); + }).first().value(); + + return _.find(items, _.matchesProperty(key, language)); +} diff --git a/messages.json b/messages.json new file mode 100644 index 0000000..d06ca3f --- /dev/null +++ b/messages.json @@ -0,0 +1,7 @@ +{ + "UNAUTHORIZED_TOKEN": "Unauthorized Request. Check Your Slash Commands Token.", + "ERROR_FALLBACK": "告訴你一件很恐怖的事⋯⋯", + "ERROR_TEXT": "◢▆▅▄▃崩╰(〒皿〒)╯潰▃▄▅▇◣", + "RESULT_NOT_FOUND_FALLBACK": "告訴你一件很恐怖的事⋯⋯", + "RESULT_NOT_FOUND_TEXT": "幹!我找不到⋯ಥ_ಥ" +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..a42a1b5 --- /dev/null +++ b/package.json @@ -0,0 +1,21 @@ +{ + "name": "slack-lambda-google", + "version": "0.1.0", + "description": "A serverless Slack Slash Commands to integrate Google Knowledge Graph API using AWS Lambda and AWS API Gateway", + "main": "lambda.js", + "scripts": { + "build": "./scripts/build.sh", + "create": "./scripts/create.sh", + "deploy": "./scripts/deploy.sh" + }, + "author": "Amo Wu ", + "license": "Apache-2.0", + "dependencies": { + "lodash": "^3.10.1", + "request-promise": "^0.4.3" + }, + "repository": { + "type": "git", + "url": "https://github.com/amowu/slack-lambda-google.git" + } +} diff --git a/role.example.json b/role.example.json new file mode 100644 index 0000000..4908486 --- /dev/null +++ b/role.example.json @@ -0,0 +1,21 @@ +{ + "Version": "2012-10-17", + "Statement": [ + { + "Effect": "Allow", + "Action": [ + "logs:CreateLogGroup", + "logs:CreateLogStream", + "logs:PutLogEvents" + ], + "Resource": "arn:aws:logs:*:*:*" + }, + { + "Action": [ + "lambda:InvokeFunction" + ], + "Effect": "Allow", + "Resource": "arn:aws:lambda:YOUR_REGION:YOUR_ACCOUNT_ID:function:*" + } + ] +} diff --git a/scripts/build.sh b/scripts/build.sh new file mode 100755 index 0000000..046d534 --- /dev/null +++ b/scripts/build.sh @@ -0,0 +1,13 @@ +#!/bin/sh -e + +if [ ! -f ./config.json ]; then + echo "Unable to build Lambda.zip, \"config.json\" is required!" + exit 1 +fi + +npm install + +rm -rf ./dist +mkdir -p dist + +zip -r -q dist/lambda.zip . -i "lib/*" "node_modules/*" "config.json" "messages.json" "lambda.js" "package.json" -x "*/.DS_Store" diff --git a/scripts/create.sh b/scripts/create.sh new file mode 100755 index 0000000..9f531a4 --- /dev/null +++ b/scripts/create.sh @@ -0,0 +1,19 @@ +#!/bin/sh -e + +[[ -z "$1" ]] && echo "Lambda function name must be provided, example: npm run create myLambdaFunction \"arn:aws:iam::YOUR_AWS_ACCOUNT_ID:role/lambda_basic_execution\"" && exit 1; +[[ -z "$2" ]] && echo "IAM Role ARN must be provided, example: npm run create myLambdaFunction \"arn:aws:iam::YOUR_AWS_ACCOUNT_ID:role/lambda_basic_execution\"" && exit 1; + +echo "build..." + +./scripts/build.sh + +echo "create..." + +aws lambda create-function \ + --function-name "$1" \ + --runtime nodejs \ + --role "$2" \ + --handler lambda.handler \ + --timeout 6 \ + --memory-size 512 \ + --zip-file fileb://$(pwd)/dist/lambda.zip diff --git a/scripts/deploy.sh b/scripts/deploy.sh new file mode 100755 index 0000000..bd885b2 --- /dev/null +++ b/scripts/deploy.sh @@ -0,0 +1,13 @@ +#!/bin/sh -e + +[[ -z "$1" ]] && echo "Lambda function name must be provided, example: npm run deploy myLambdaFunction" && exit 1; + +echo "build..." + +./scripts/build.sh + +echo "deploy..." + +aws lambda update-function-code \ + --function-name "$1" \ + --zip-file fileb://$(pwd)/dist/lambda.zip