├── .npmignore ├── docs ├── test_step_1.png ├── test_step_2.png ├── test_step_3.png ├── test_step_4.png ├── 1_name_slack_app.png ├── 6_slash_commands.png ├── slack_channels.png ├── 1_create_slack_app.png ├── 1_slack_bot_scopes.png ├── 1_configure_slack_app.png ├── 6_added_slash_command.png ├── 6_create_new_command.png ├── 7_interactivity_form.png ├── slack_channel_modal.png ├── 1_slack_app_permissions.png ├── 7_interactivity_toggle.png ├── slack_app_architecture.jpg └── 1_installation_verification.png ├── .gitignore ├── CODE_OF_CONDUCT.md ├── package.json ├── tsconfig.json ├── LICENSE ├── cdk.json ├── lib ├── app-stack.sample-lambda.ts ├── app-stack.https-client.ts ├── app-stack.slack-auth.ts └── app-stack.ts ├── bin └── amazon-interactive-slack-app-starter-kit.ts ├── CONTRIBUTING.md └── README.md /.npmignore: -------------------------------------------------------------------------------- 1 | *.ts 2 | !*.d.ts 3 | 4 | # CDK asset staging directory 5 | .cdk.staging 6 | cdk.out 7 | -------------------------------------------------------------------------------- /docs/test_step_1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/amazon-interactive-slack-app-starter-kit/HEAD/docs/test_step_1.png -------------------------------------------------------------------------------- /docs/test_step_2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/amazon-interactive-slack-app-starter-kit/HEAD/docs/test_step_2.png -------------------------------------------------------------------------------- /docs/test_step_3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/amazon-interactive-slack-app-starter-kit/HEAD/docs/test_step_3.png -------------------------------------------------------------------------------- /docs/test_step_4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/amazon-interactive-slack-app-starter-kit/HEAD/docs/test_step_4.png -------------------------------------------------------------------------------- /docs/1_name_slack_app.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/amazon-interactive-slack-app-starter-kit/HEAD/docs/1_name_slack_app.png -------------------------------------------------------------------------------- /docs/6_slash_commands.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/amazon-interactive-slack-app-starter-kit/HEAD/docs/6_slash_commands.png -------------------------------------------------------------------------------- /docs/slack_channels.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/amazon-interactive-slack-app-starter-kit/HEAD/docs/slack_channels.png -------------------------------------------------------------------------------- /docs/1_create_slack_app.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/amazon-interactive-slack-app-starter-kit/HEAD/docs/1_create_slack_app.png -------------------------------------------------------------------------------- /docs/1_slack_bot_scopes.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/amazon-interactive-slack-app-starter-kit/HEAD/docs/1_slack_bot_scopes.png -------------------------------------------------------------------------------- /docs/1_configure_slack_app.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/amazon-interactive-slack-app-starter-kit/HEAD/docs/1_configure_slack_app.png -------------------------------------------------------------------------------- /docs/6_added_slash_command.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/amazon-interactive-slack-app-starter-kit/HEAD/docs/6_added_slash_command.png -------------------------------------------------------------------------------- /docs/6_create_new_command.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/amazon-interactive-slack-app-starter-kit/HEAD/docs/6_create_new_command.png -------------------------------------------------------------------------------- /docs/7_interactivity_form.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/amazon-interactive-slack-app-starter-kit/HEAD/docs/7_interactivity_form.png -------------------------------------------------------------------------------- /docs/slack_channel_modal.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/amazon-interactive-slack-app-starter-kit/HEAD/docs/slack_channel_modal.png -------------------------------------------------------------------------------- /docs/1_slack_app_permissions.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/amazon-interactive-slack-app-starter-kit/HEAD/docs/1_slack_app_permissions.png -------------------------------------------------------------------------------- /docs/7_interactivity_toggle.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/amazon-interactive-slack-app-starter-kit/HEAD/docs/7_interactivity_toggle.png -------------------------------------------------------------------------------- /docs/slack_app_architecture.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/amazon-interactive-slack-app-starter-kit/HEAD/docs/slack_app_architecture.jpg -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.js 2 | !jest.config.js 3 | *.d.ts 4 | node_modules 5 | 6 | # CDK asset staging directory 7 | .cdk.staging 8 | cdk.out 9 | .idea/** 10 | -------------------------------------------------------------------------------- /docs/1_installation_verification.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/amazon-interactive-slack-app-starter-kit/HEAD/docs/1_installation_verification.png -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | ## Code of Conduct 2 | This project has adopted the [Amazon Open Source Code of Conduct](https://aws.github.io/code-of-conduct). 3 | For more information see the [Code of Conduct FAQ](https://aws.github.io/code-of-conduct-faq) or contact 4 | opensource-codeofconduct@amazon.com with any additional questions or comments. 5 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "amazon-interactive-slack-app-starter-kit", 3 | "version": "0.1.0", 4 | "bin": { 5 | "amazon-interactive-slack-app-starter-kit": "bin/amazon-interactive-slack-app-starter-kit.js" 6 | }, 7 | "scripts": { 8 | "build": "tsc", 9 | "watch": "tsc -w", 10 | "cdk": "cdk" 11 | }, 12 | "devDependencies": { 13 | "@types/axios": "^0.14.0", 14 | "@types/node": "10.17.27", 15 | "@types/tsscmp": "^1.0.0", 16 | "aws-cdk": "2.19.0", 17 | "ts-node": "^9.0.0", 18 | "typescript": "~3.9.7" 19 | }, 20 | "dependencies": { 21 | "@slack/bolt": "^3.21.1", 22 | "@types/aws-sdk": "^2.7.0", 23 | "aws-cdk-lib": "2.147.3", 24 | "axios": "^1.7.5", 25 | "constructs": "^10.0.0", 26 | "source-map-support": "^0.5.16", 27 | "tsscmp": "^1.0.6" 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES2018", 4 | "module": "commonjs", 5 | "lib": [ 6 | "es2018" 7 | ], 8 | "declaration": true, 9 | "strict": true, 10 | "noImplicitAny": true, 11 | "strictNullChecks": true, 12 | "noImplicitThis": true, 13 | "alwaysStrict": true, 14 | "noUnusedLocals": false, 15 | "noUnusedParameters": false, 16 | "noImplicitReturns": true, 17 | "noFallthroughCasesInSwitch": false, 18 | "inlineSourceMap": true, 19 | "inlineSources": true, 20 | "experimentalDecorators": true, 21 | "strictPropertyInitialization": false, 22 | "typeRoots": [ 23 | "./node_modules/@types" 24 | ], 25 | "baseUrl": "./", 26 | "paths": { 27 | "/opt/nodejs/utils": ["lib/shared/nodejs/utils"] 28 | } 29 | }, 30 | "exclude": [ 31 | "node_modules", 32 | "cdk.out" 33 | ] 34 | } 35 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of 4 | this software and associated documentation files (the "Software"), to deal in 5 | the Software without restriction, including without limitation the rights to 6 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 7 | the Software, and to permit persons to whom the Software is furnished to do so. 8 | 9 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 10 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 11 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 12 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 13 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 14 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 15 | 16 | -------------------------------------------------------------------------------- /cdk.json: -------------------------------------------------------------------------------- 1 | { 2 | "app": "npx ts-node --prefer-ts-exts bin/amazon-interactive-slack-app-starter-kit.ts", 3 | "watch": { 4 | "include": [ 5 | "**" 6 | ], 7 | "exclude": [ 8 | "README.md", 9 | "cdk*.json", 10 | "**/*.d.ts", 11 | "**/*.js", 12 | "tsconfig.json", 13 | "package*.json", 14 | "yarn.lock", 15 | "node_modules", 16 | "test" 17 | ] 18 | }, 19 | "context": { 20 | "@aws-cdk/aws-apigateway:usagePlanKeyOrderInsensitiveId": true, 21 | "@aws-cdk/core:stackRelativeExports": true, 22 | "@aws-cdk/aws-rds:lowercaseDbIdentifier": true, 23 | "@aws-cdk/aws-lambda:recognizeVersionProps": true, 24 | "@aws-cdk/aws-cloudfront:defaultSecurityPolicyTLSv1.2_2021": true, 25 | "@aws-cdk-containers/ecs-service-extensions:enableDefaultLogDriver": true, 26 | "@aws-cdk/aws-ec2:uniqueImdsv2TemplateName": true, 27 | "@aws-cdk/aws-iam:minimizePolicies": true, 28 | "@aws-cdk/core:target-partitions": [ 29 | "aws", 30 | "aws-cn" 31 | ] 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /lib/app-stack.sample-lambda.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 3 | * SPDX-License-Identifier: MIT-0 4 | * 5 | * Permission is hereby granted, free of charge, to any person obtaining a copy of this 6 | * software and associated documentation files (the "Software"), to deal in the Software 7 | * without restriction, including without limitation the rights to use, copy, modify, 8 | * merge, publish, distribute, sublicense, and/or sell copies of the Software, and to 9 | * permit persons to whom the Software is furnished to do so. 10 | * 11 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, 12 | * INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A 13 | * PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT 14 | * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 15 | * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE 16 | * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 17 | */ 18 | 19 | exports.handler = async function (event: any, context: any) { 20 | await new Promise((resolve) => { 21 | setTimeout(() => { 22 | resolve(true) 23 | }, 1000) // Use a small value here to prevent unnecessary charges 24 | }) 25 | 26 | return { 27 | input: event.input, 28 | result: 'some result' 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /lib/app-stack.https-client.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 3 | * SPDX-License-Identifier: MIT-0 4 | * 5 | * Permission is hereby granted, free of charge, to any person obtaining a copy of this 6 | * software and associated documentation files (the "Software"), to deal in the Software 7 | * without restriction, including without limitation the rights to use, copy, modify, 8 | * merge, publish, distribute, sublicense, and/or sell copies of the Software, and to 9 | * permit persons to whom the Software is furnished to do so. 10 | * 11 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, 12 | * INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A 13 | * PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT 14 | * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 15 | * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE 16 | * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 17 | */ 18 | 19 | import { SecretsManager } from 'aws-sdk' 20 | import * as axios from 'axios' 21 | 22 | const secretsClient = new SecretsManager() 23 | 24 | type HttpClientParameters = { 25 | url: string, 26 | body: any 27 | } 28 | 29 | exports.handler = async function (parameters: HttpClientParameters, context: any) { 30 | // Get the Bot Token Secret 31 | const secretResult = await secretsClient.getSecretValue({ SecretId: process.env.SLACK_SECRETS_NAME as string }).promise() 32 | const secretObject = JSON.parse(secretResult.SecretString as string) 33 | 34 | const headers = { Authorization: `Bearer ${secretObject.botToken}` } 35 | 36 | // Post message in Slack channel 37 | const response = await axios.default.post(parameters.url, parameters.body, { headers }) 38 | if (response.data === 'ok' || response.data.ok) { 39 | return response.data 40 | } 41 | 42 | throw new Error('Failed to update message in Slack channel') 43 | } 44 | -------------------------------------------------------------------------------- /bin/amazon-interactive-slack-app-starter-kit.ts: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | /* 3 | * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 4 | * SPDX-License-Identifier: MIT-0 5 | * 6 | * Permission is hereby granted, free of charge, to any person obtaining a copy of this 7 | * software and associated documentation files (the "Software"), to deal in the Software 8 | * without restriction, including without limitation the rights to use, copy, modify, 9 | * merge, publish, distribute, sublicense, and/or sell copies of the Software, and to 10 | * permit persons to whom the Software is furnished to do so. 11 | * 12 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, 13 | * INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A 14 | * PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT 15 | * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 16 | * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE 17 | * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 18 | */ 19 | 20 | import 'source-map-support/register' 21 | import * as cdk from 'aws-cdk-lib' 22 | import { AppStack } from '../lib/app-stack' 23 | 24 | const app = new cdk.App() 25 | new AppStack(app, 'AmazonInteractiveSlackAppStarterKitStack', { 26 | /* If you don't specify 'env', this stack will be environment-agnostic. 27 | * Account/Region-dependent features and context lookups will not work, 28 | * but a single synthesized template can be deployed anywhere. */ 29 | 30 | /* Uncomment the next line to specialize this stack for the AWS Account 31 | * and Region that are implied by the current CLI configuration. */ 32 | // env: { account: process.env.CDK_DEFAULT_ACCOUNT, region: process.env.CDK_DEFAULT_REGION }, 33 | 34 | /* Uncomment the next line if you know exactly what Account and Region you 35 | * want to deploy the stack to. */ 36 | // env: { account: '123456789012', region: 'us-east-1' }, 37 | 38 | /* For more information, see https://docs.aws.amazon.com/cdk/latest/guide/environments.html */ 39 | }) 40 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing Guidelines 2 | 3 | Thank you for your interest in contributing to our project. Whether it's a bug report, new feature, correction, or additional 4 | documentation, we greatly value feedback and contributions from our community. 5 | 6 | Please read through this document before submitting any issues or pull requests to ensure we have all the necessary 7 | information to effectively respond to your bug report or contribution. 8 | 9 | 10 | ## Reporting Bugs/Feature Requests 11 | 12 | We welcome you to use the GitHub issue tracker to report bugs or suggest features. 13 | 14 | When filing an issue, please check existing open, or recently closed, issues to make sure somebody else hasn't already 15 | reported the issue. Please try to include as much information as you can. Details like these are incredibly useful: 16 | 17 | * A reproducible test case or series of steps 18 | * The version of our code being used 19 | * Any modifications you've made relevant to the bug 20 | * Anything unusual about your environment or deployment 21 | 22 | 23 | ## Contributing via Pull Requests 24 | Contributions via pull requests are much appreciated. Before sending us a pull request, please ensure that: 25 | 26 | 1. You are working against the latest source on the *main* branch. 27 | 2. You check existing open, and recently merged, pull requests to make sure someone else hasn't addressed the problem already. 28 | 3. You open an issue to discuss any significant work - we would hate for your time to be wasted. 29 | 30 | To send us a pull request, please: 31 | 32 | 1. Fork the repository. 33 | 2. Modify the source; please focus on the specific change you are contributing. If you also reformat all the code, it will be hard for us to focus on your change. 34 | 3. Ensure local tests pass. 35 | 4. Commit to your fork using clear commit messages. 36 | 5. Send us a pull request, answering any default questions in the pull request interface. 37 | 6. Pay attention to any automated CI failures reported in the pull request, and stay involved in the conversation. 38 | 39 | GitHub provides additional document on [forking a repository](https://help.github.com/articles/fork-a-repo/) and 40 | [creating a pull request](https://help.github.com/articles/creating-a-pull-request/). 41 | 42 | 43 | ## Finding contributions to work on 44 | Looking at the existing issues is a great way to find something to contribute on. As our projects, by default, use the default GitHub issue labels (enhancement/bug/duplicate/help wanted/invalid/question/wontfix), looking at any 'help wanted' issues is a great place to start. 45 | 46 | 47 | ## Code of Conduct 48 | This project has adopted the [Amazon Open Source Code of Conduct](https://aws.github.io/code-of-conduct). 49 | For more information see the [Code of Conduct FAQ](https://aws.github.io/code-of-conduct-faq) or contact 50 | opensource-codeofconduct@amazon.com with any additional questions or comments. 51 | 52 | 53 | ## Security issue notifications 54 | If you discover a potential security issue in this project we ask that you notify AWS/Amazon Security via our [vulnerability reporting page](http://aws.amazon.com/security/vulnerability-reporting/). Please do **not** create a public github issue. 55 | 56 | 57 | ## Licensing 58 | 59 | See the [LICENSE](LICENSE) file for our project's licensing. We will ask you to confirm the licensing of your contribution. 60 | -------------------------------------------------------------------------------- /lib/app-stack.slack-auth.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 3 | * SPDX-License-Identifier: MIT-0 4 | * 5 | * Permission is hereby granted, free of charge, to any person obtaining a copy of this 6 | * software and associated documentation files (the "Software"), to deal in the Software 7 | * without restriction, including without limitation the rights to use, copy, modify, 8 | * merge, publish, distribute, sublicense, and/or sell copies of the Software, and to 9 | * permit persons to whom the Software is furnished to do so. 10 | * 11 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, 12 | * INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A 13 | * PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT 14 | * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 15 | * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE 16 | * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 17 | */ 18 | 19 | import { App, AwsLambdaReceiver, LogLevel } from '@slack/bolt' 20 | import { SecretsManager } from 'aws-sdk' 21 | 22 | const secretsClient = new SecretsManager() 23 | 24 | let awsLambdaReceiver: AwsLambdaReceiver | null = null 25 | let app: App | null = null 26 | let requestDetails: RequestDetails | null = null 27 | 28 | type RequestDetails = { 29 | channelId: string 30 | userName: string 31 | action: string 32 | actionBase?: string 33 | responseUrl: string 34 | inputValue?: string 35 | } 36 | 37 | exports.handler = async (event: any, context: any, callback: any) => { 38 | requestDetails = null 39 | if (!awsLambdaReceiver) { 40 | await initBolt() 41 | configureApp(callback) 42 | } 43 | const handler = await awsLambdaReceiver!.start() 44 | const response = await handler(event, context, callback) 45 | 46 | if (response.statusCode != 200) { 47 | throw new Error('Failed to validate slack message') 48 | } 49 | 50 | return requestDetails 51 | } 52 | 53 | async function initBolt() { 54 | // Get the Slack Secrets 55 | const secretResult = await secretsClient.getSecretValue({ SecretId: process.env.SLACK_SECRETS_NAME as string }).promise() 56 | const secretObject = JSON.parse(secretResult.SecretString!) 57 | 58 | awsLambdaReceiver = new AwsLambdaReceiver({ 59 | signingSecret: secretObject.signingSecret 60 | }) 61 | 62 | app = new App({ 63 | token: secretObject.botToken, 64 | receiver: awsLambdaReceiver, 65 | logLevel: LogLevel.DEBUG 66 | }) 67 | } 68 | 69 | function configureApp(callback: any) { 70 | if (!app) { 71 | throw Error('Bolt app not initialized while trying to configure it') 72 | } 73 | 74 | // @ts-ignore 75 | app.use(async ({ body, ack, next }) => { 76 | // eslint-disable-next-line @typescript-eslint/no-non-null-assertion 77 | await ack!() 78 | 79 | // Convert body to a consistent format 80 | requestDetails = parseRequest(body) 81 | }) 82 | } 83 | 84 | function parseRequest(body: any): RequestDetails { 85 | if (body.user_id) { 86 | return { 87 | channelId: body.channel_id, 88 | userName: body.user_name, 89 | action: 'welcome', 90 | responseUrl: body.response_url 91 | } 92 | } else { 93 | return { 94 | channelId: body.channel.id, 95 | userName: body.user.username, 96 | action: body.actions[0].action_id, 97 | actionBase: body.actions[0].action_id.split('/')[0], 98 | responseUrl: body.response_url, 99 | inputValue: body.state?.values?.form_input?.input_value?.value ?? null 100 | } 101 | } 102 | } 103 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Amazon Interactive Slack App Starter Kit 2 | 3 | This starter kit will help you started with building an interactive Slack app leveraging CDK. More details about why to use the solution in this repository may be found in this [blog post](https://aws.amazon.com/blogs/compute/developing-a-serverless-slack-app-using-aws-step-functions-and-aws-lambda/). 4 | 5 | This README will guide you to configure this solution with your own AWS account. 6 | 7 | ## Table of Contents 8 | 9 | - [Architecture](#architecture) 10 | - [Prerequisites](#prerequisites) 11 | - [Deployment Instructions](#deployment-instructions) 12 | - [1. Request Slack App Token](#1-request-slack-app-token) 13 | - [2. Clone this repository](#2-clone-this-repository) 14 | - [3. Bootstrap AWS Environment](#3-bootstrap-aws-environment) 15 | - [4. Build and deploy serverless resources](#4-build-and-deploy-serverless-resources) 16 | - [5. Configure resources with Slack App secrets and Slack users](#5-configure-resources-with-slack-app-secrets-and-slack-users) 17 | - [6. Register slash command to invoke Slack App](#6-register-slash-command-to-invoke-slack-app) 18 | - [7. Register Interactivity URL (pointing to API Gateway)](#7-register-interactivity-url-pointing-to-api-gateway) 19 | - [Testing Instructions](#testing-instructions) 20 | - [Destroying Resources](#destroying-resources) 21 | - [Useful Commands](#useful-commands) 22 | - [Security](#security) 23 | - [License](#license) 24 | 25 | ## Architecture 26 | 27 | ![Interactive Slack App Architecture](docs/slack_app_architecture.jpg?raw=true "Architecture") 28 | 29 | ## Prerequisites 30 | * [AWS CDK](https://docs.aws.amazon.com/cdk/v2/guide/getting_started.html#getting_started_install) version 2.19.0 or later 31 | * [Node](https://nodejs.org/en/download/current/) version 16+ 32 | * [Docker-cli](https://docs.docker.com/get-docker/) 33 | * [Git](https://git-scm.com/download) 34 | * A personal or company Slack account with permissions to create applications 35 | * The Slack Channel ID of a channel in your Workspace for integration with the Slack App 36 | * To get the Channel ID, open the context menu on the Slack channel and select View channel details. The modal displays your Channel ID at the bottom: 37 | 38 | ![Slack Channels](docs/slack_channels.png?raw=true "Slack Channel") 39 | 40 | ![Slack Channel Modal](docs/slack_channel_modal.png?raw=true "Slack Channel Modal") 41 | 42 | To learn more, visit Slack's [Getting Started with Bolt for JavaScript](https://slack.dev/bolt-js/tutorial/getting-started) page. 43 | 44 | --- 45 | 46 | ## Deployment Instructions 47 | 48 | ### 1. Request Slack App Token 49 | To create the Slack App within your Slack Workspace, navigate to Slack's [Your Apps](https://api.slack.com/apps) page and choose the **Create New App** button. 50 | 51 | Select the _From scratch_ option within the _Create an app_ dialog: 52 | 53 | ![Create an app dialog](docs/1_create_slack_app.png?raw=true "Create an app dialog") 54 | 55 | Enter a name for your Slack App and your Workspace, then choose **Create App**: 56 | 57 | ![Name app & choose workspace dialog](docs/1_name_slack_app.png?raw=true "Name app & choose workspace dialog") 58 | 59 | You see the configuration page for your new Slack App. 60 | 61 | ![Configuration page](docs/1_configure_slack_app.png?raw=true "Configuration page") 62 | 63 | To add permissions to your Slack App using _OAuth scopes_, navigate to the **OAuth & Permissions** sidebar and scroll to the _Bot Token Scopes_ section. Choose **Add an OAuth Scope** and add the `chat:write` and `commands` _OAuth scopes_. 64 | 65 | ![Bot Token Scopes](docs/1_slack_bot_scopes.png?raw=true "Bot Token Scopes") 66 | 67 | Install the Slack App to your Workspace to generate a Slack Bot token. Navigate to the **Basic Information** sidebar, then choose **Install to Workspace**. 68 | 69 | From the _Application installation verification_ page, choose **Allow** to complete the installation. 70 | 71 | ![Application installation verification](docs/1_installation_verification.png?raw=true "Application installation verification") 72 | 73 | After installation, the configuration page for your Slack App shows a _Success_ banner. Navigate back to the **OAuth & Permissions** sidebar to view your Slack App token. 74 | 75 | ![OAuth & Permissions page](docs/1_slack_app_permissions.png?raw=true "OAuth & Permissions page") 76 | 77 | To learn more about Token Types, visit Slack's [Access tokens](https://api.slack.com/authentication/token-types#bot) page. 78 | 79 | ### 2. Clone this repository 80 | From a command prompt on your computer, clone this repository in a directory of your choice: 81 | 82 | ```bash 83 | git clone https://github.com/aws-samples/amazon-interactive-slack-app-starter-kit.git 84 | ``` 85 | 86 | Within the `/lib` directory, the `app-stack.ts` file outlines all the resources to be deployed. Additionally, using `NodeJsFunction` resources enables full TypeScript transpiling and bundling for a Lambda function. 87 | 88 | Download the project dependencies using the NPM install command: 89 | 90 | ```bash 91 | npm install 92 | ``` 93 | 94 | ### 3. Bootstrap AWS Environment 95 | 96 | Before you deploy the CDK resources to your AWS account, bootstrap the AWS environment in the AWS Region of your choice with this command: 97 | 98 | ```bash 99 | cdk bootstrap 100 | ``` 101 | 102 | By running the preceding command, you prepare your AWS account and AWS Region with resources to perform deployments. You only need to bootstrap your AWS account and AWS Region once. 103 | 104 | ### 4. Build and deploy serverless resources 105 | Now that you have the code base cloned and the AWS environment configured, it’s time to deploy the AWS resources. 106 | 107 | You will run the following command from the root of the project to start the deployment. Ensure you have docker running as it will bundle your Lambda resources. 108 | 109 | ```bash 110 | cdk deploy 111 | ``` 112 | 113 | **Note:** You may specify a target Region using the argument `--region ` 114 | 115 | You must accept the security changes being made to your account because of the new resources being deployed. 116 | 117 | Once the deployment completes, observe the output from `cdk deploy` which looks like: 118 | 119 | ```bash 120 | AmazonInteractiveSlackAppStarterKitStack: creating CloudFormation changeset... 121 | 122 | ✅ AmazonInteractiveSlackAppStarterKitStack 123 | 124 | ✨ Deployment time: 158.28s 125 | 126 | Outputs: 127 | AmazonInteractiveSlackAppStarterKitStack.SlackAppApiEndpointXXXX = https://XXXXXXXX.execute-api.us-east-1.amazonaws.com/prod/ 128 | Stack ARN: 129 | arn:aws:cloudformation:us-east-1:XXXXXXXXXX:stack/AmazonInteractiveSlackAppStarterKitStack/123e4567-e89b-12d3-a456-426652340000 130 | 131 | ✨ Total time: 173.81s 132 | ``` 133 | 134 | **Note:** Record the API Gateway URL generated from your deployment as it is needed for registration with the Slack App configuration later on. 135 | 136 | ### 5. Configure resources with Slack App secrets and Slack users 137 | With the CDK resources deployed, you need to configure the newly generated SSM Parameter and Secrets Parameter with your specific application values. 138 | 139 | To update the SSM Parameter containing the Slack channel ID, perform the following steps: 140 | 141 | 1. Go to the [Parameter Store console](https://console.aws.amazon.com/systems-manager/parameters/?region=us-east-1&tab=Table) for your Region 142 | 143 | 1. Choose the parameter with the name **SlackChannelIdParameter** 144 | 145 | 1. Choose the **Edit** button 146 | 147 | 1. Enter your Channel ID from the prerequisites section into the **Value** text field. 148 | 149 | 1. Choose the **Save changes** button 150 | 151 | Next, you must update the Secrets Parameter containing the OAuth token and signing secret for your Slack app using the following steps: 152 | 153 | 1. Go to the [Secrets Manager console](https://console.aws.amazon.com/secretsmanager/listsecrets) for your Region 154 | 155 | 1. Choose the secret starting with **SlackSecretsXXXXXXXX** 156 | 157 | 1. Select the **Retrieve secret value** button to reveal the secret’s details 158 | 159 | 1. Choose the **Edit** button 160 | 161 | 1. Select the **Plaintext** tab, and enter the following value. Be sure to substitute your own values where appropriate 162 | 163 | `{"signingSecret":"","botToken":""}` 164 | 165 | 1. Choose the **Save** button after finishing with your changes 166 | 167 | Lastly, configure your slack user to have permissions to invoke the Slack App. 168 | 169 | To configure your Slack user, proceed with the following steps: 170 | 171 | 1. Navigate to your account settings for your organization’s Slack account. The URL will look like: 172 | 173 | `https://.slack.com/account/settings#username` 174 | 175 | 1. Copy the value under **Username** as this is needed in the next few steps 176 | 177 | 1. Go to [DynamoDB Tables console](https://console.aws.amazon.com/dynamodbv2/home#tables) and be sure to choose the correct Region to which you deployed your resources. 178 | 179 | 1. Choose the **AmazonInteractiveSlackAppStarterKitStack-BotUsersTableXXXXXXX** 180 | 181 | 1. Select the **Explore table items** button 182 | 183 | 1. Select the **Create item** button 184 | 185 | 1. Choose the JSON view option in the top right corner 186 | 187 | 1. In the **Attributes** text entry, provide the following. Be sure to substitute your own values where appropriate. 188 | 189 | ```json 190 | { 191 | "slackUserName": "", 192 | "permittedActions": [ 193 | "sample-lambda", 194 | "sample-sm" 195 | ] 196 | } 197 | ``` 198 | 199 | 9. To save, choose the **Create item** button 200 | 201 | Your Slack user now has the permissions to run commands to invoke the Slack App. 202 | 203 | ### 6. Register slash command to invoke Slack App 204 | Now, you must create an _entry point_ for your Slack App by registering a **Slash Command** for your Slack App. The _Slash Command_ is a keyword which informs Slack to invoke a specific function of your backend application. For this exercise, register the _Slash Command_, `/my-slack-bot`. 205 | 206 | To register the `/my-slack-bot` _Slash Command_, navigate to the _Application Configuration_ page for your Slack App: 207 | 208 | https://api.slack.com/apps > My Slack Bot. 209 | 210 | Go to the _Slash Commands_ sidebar, then choose the _Create New Command_ button: 211 | 212 | ![Slash Commands page](docs/6_slash_commands.png?raw=true "Slash Commands page") 213 | 214 | Complete the _Create New Command_ registration form. For the text field labeled _Request URL_, enter the API Gateway URL created from the deployment of your serverless resources from the preceding section. Note that this URL must follow the pattern `https://.execute-api..amazonaws.com//slack/events`, as the Slack Bolt SDK binds to the `/slack/events` endpoint: 215 | 216 | ![Create New Command form](docs/6_create_new_command.png?raw=true "Create New Command form") 217 | 218 | Once completed, select the _Save_ button. Upon creation, your browser is returned to the _Slash Commands_ configuration page for your Slack App with a _Success_ banner at the top of the page: 219 | 220 | ![Slash Commands page](docs/6_added_slash_command.png?raw=true "Slash Commands page") 221 | 222 | To learn more about Slash Commands, visit Slack's [Enabling interactivity with Slash Commands](https://api.slack.com/interactivity/slash-commands) page. 223 | 224 | ### 7. Register Interactivity URL (pointing to API Gateway) 225 | With the _entry point_ for your Slack App configured, you must now configure your Slack workspace to interact with your backend application. Since your Slack App supports actions _beyond_ the invocation stage, you need to inform Slack to direct subsequent interactivity to the backend. You must register your backend API with your Slack App's _Interactivity & Shortcuts_ configuration. 226 | 227 | Navigate to the _Application Configuration_ page for your Slack App: 228 | 229 | https://api.slack.com/apps > My Slack Bot. 230 | 231 | Go to the **Interactivity & Shortcuts** sidebar and enable interactivity by choosing the _Interactivity_ toggle: 232 | 233 | ![Interactivity toggle](docs/7_interactivity_toggle.png?raw=true "Interactivity toggle") 234 | 235 | For the text field labeled _Request URL_, enter the API Gateway URL created from the deployment of your serverless resources. Note that this URL must follow the pattern `https://.execute-api..amazonaws.com//slack/events`, as the Slack Bolt SDK binds to the `/slack/events` endpoint: 236 | 237 | ![Interactivity form](docs/7_interactivity_form.png?raw=true "Interactivity form") 238 | 239 | Once entered, choose the _Save Changes_ button. Upon creation, a _Success_ banner appears at the top of the page. 240 | 241 | ## Testing Instructions 242 | 1. Start the Slack App by invoking the `/my-slack-bot` slash command 243 | 244 | ![Invoke Slash Command](docs/test_step_1.png?raw=true "Invoke Slash Command") 245 | 246 | 1. From the My Slack Bot action menu, select **Sample Lambda** 247 | 248 | ![Select Sample Lambda](docs/test_step_2.png?raw=true "Select Sample Lambda") 249 | 250 | 1. Enter command input, select **Submit** button, then observe the response (this input value applies to the sample Lambda function) 251 | 252 | ![Select Submit Button](docs/test_step_3.png?raw=true "Select Submit Button") 253 | 254 | 1. Observe the execution output posted to the Slack channel 255 | 256 | ![Observe Output](docs/test_step_4.png?raw=true "Observe Output") 257 | 258 | **Note**: You can test the State Machine execution by selecting **Sample State Machine** in step #2 259 | 260 | ## Destroying Resources 261 | To avoid additional charges to your account, run the following command from the project’s root directory: 262 | 263 | ```bash 264 | cdk destroy 265 | ``` 266 | 267 | CDK prompts you to confirm if you want to delete the resources. Enter “y” to confirm. The process removes all the resources created. 268 | 269 | --- 270 | 271 | ## Useful Commands 272 | 273 | * `npm run build` compile typescript to js 274 | * `npm run watch` watch for changes and compile 275 | * `npm run test` perform the jest unit tests 276 | * `cdk deploy` deploy this stack to your default AWS account/region 277 | * `cdk diff` compare deployed stack with current state 278 | * `cdk synth` emits the synthesized CloudFormation template 279 | * `cdk destroy` removes all stack resources 280 | 281 | ## Security 282 | 283 | See [CONTRIBUTING](CONTRIBUTING.md#security-issue-notifications) for more information. 284 | 285 | ## License 286 | 287 | This library is licensed under the MIT-0 License. See the LICENSE file. 288 | -------------------------------------------------------------------------------- /lib/app-stack.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 3 | * SPDX-License-Identifier: MIT-0 4 | * 5 | * Permission is hereby granted, free of charge, to any person obtaining a copy of this 6 | * software and associated documentation files (the "Software"), to deal in the Software 7 | * without restriction, including without limitation the rights to use, copy, modify, 8 | * merge, publish, distribute, sublicense, and/or sell copies of the Software, and to 9 | * permit persons to whom the Software is furnished to do so. 10 | * 11 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, 12 | * INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A 13 | * PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT 14 | * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 15 | * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE 16 | * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 17 | */ 18 | 19 | import { RemovalPolicy, Stack, StackProps } from 'aws-cdk-lib' 20 | import { Construct } from 'constructs' 21 | import { PassthroughBehavior, RestApi, StepFunctionsIntegration } from 'aws-cdk-lib/aws-apigateway' 22 | import { Secret } from 'aws-cdk-lib/aws-secretsmanager' 23 | import { NodejsFunction } from 'aws-cdk-lib/aws-lambda-nodejs' 24 | import { AttributeType, BillingMode, Table } from 'aws-cdk-lib/aws-dynamodb' 25 | import { StringParameter } from 'aws-cdk-lib/aws-ssm' 26 | import { Choice, Condition, CustomState, Fail, IChainable, IntegrationPattern, JsonPath, LogLevel, Pass, Result, StateMachine, StateMachineType, Succeed, TaskInput, Wait, WaitTime } from 'aws-cdk-lib/aws-stepfunctions' 27 | import { SfnStateMachine } from 'aws-cdk-lib/aws-events-targets' 28 | import { DynamoAttributeValue, DynamoGetItem, LambdaInvoke, StepFunctionsStartExecution } from 'aws-cdk-lib/aws-stepfunctions-tasks' 29 | import { EventBus, Rule } from 'aws-cdk-lib/aws-events' 30 | import { LogGroup } from 'aws-cdk-lib/aws-logs' 31 | 32 | export class AppStack extends Stack { 33 | private readonly channelIdParameter: StringParameter 34 | private readonly slackAuthFunction: NodejsFunction 35 | private readonly httpsClientFunction: NodejsFunction 36 | private readonly botUsersTable: Table 37 | private readonly commandEventBus: EventBus 38 | 39 | constructor(scope: Construct, id: string, props?: StackProps) { 40 | super(scope, id, props) 41 | 42 | // Secrets and Parameters 43 | const slackSecrets = new Secret(this, 'SlackSecrets', { 44 | generateSecretString: { 45 | secretStringTemplate: JSON.stringify({ 46 | botToken: '', 47 | signingSecret: '' 48 | }), 49 | generateStringKey: 'signingSecret' 50 | }, 51 | }) 52 | 53 | this.channelIdParameter = new StringParameter(this, 'SlackChannelIdParameter', { 54 | parameterName: 'SlackChannelIdParameter', 55 | description: 'The permitted slack channel ID for Slack Bot requets', 56 | stringValue: '' 57 | }) 58 | 59 | // DB Tables 60 | this.botUsersTable = new Table(this, 'BotUsersTable', { 61 | billingMode: BillingMode.PROVISIONED, 62 | readCapacity: 1, 63 | writeCapacity: 1, 64 | removalPolicy: RemovalPolicy.DESTROY, 65 | partitionKey: { name: 'slackUserName', type: AttributeType.STRING } 66 | }) 67 | 68 | // Lambda Functions 69 | const bundlingConfig = { 70 | externalModules: [ 71 | 'aws-sdk' 72 | ] 73 | } 74 | this.slackAuthFunction = new NodejsFunction(this, 'slack-auth', { 75 | environment: { 76 | SLACK_SECRETS_NAME: slackSecrets.secretName 77 | }, 78 | bundling: bundlingConfig 79 | }) 80 | this.httpsClientFunction = new NodejsFunction(this, 'https-client', { 81 | environment: { 82 | SLACK_SECRETS_NAME: slackSecrets.secretName 83 | }, 84 | bundling: bundlingConfig 85 | }) 86 | const sampleLambdaFunction = new NodejsFunction(this, 'sample-lambda') 87 | 88 | // EventBridge 89 | this.commandEventBus = new EventBus(this, 'CommandEventBus', { 90 | eventBusName: 'command-event-bus' 91 | }) 92 | 93 | // State Machines 94 | const sampleStateMachine = this.buildSampleStateMachine() 95 | const requestValidatorStateMachine = this.buildRequestValidatorStateMachine() 96 | this.buildWelcomeProcessor() 97 | this.buildFormProcessor('SampleLambdaProcessor', 'Sample Lambda', 'sample-lambda') 98 | this.buildLambdaFormSubmitProcessor('SampleLambdaSubmitProcessor', 'Sample Lambda', 'sample-lambda/submit', sampleLambdaFunction) 99 | this.buildFormProcessor('SampleStateMachineProcessor', 'Sample State Machine', 'sample-sm') 100 | this.buildStateMachineFormSubmitProcessor('SampleStateMachineSubmitProcessor', 'Sample State Machine', 'sample-sm/submit', sampleStateMachine) 101 | 102 | // Grant Permissions 103 | slackSecrets.grantRead(this.slackAuthFunction) 104 | this.channelIdParameter.grantRead(requestValidatorStateMachine) 105 | slackSecrets.grantRead(this.httpsClientFunction) 106 | this.commandEventBus.grantPutEventsTo(requestValidatorStateMachine) 107 | 108 | // API Gateway 109 | const boltApi = new RestApi(this, 'SlackAppApi', { 110 | restApiName: 'Interactive Slack App Starter Kit API', 111 | description: 'This is the API service for the Interactive Slack App Starter Kit.', 112 | }) 113 | // POST /slack/events 114 | boltApi.root 115 | .addProxy({ 116 | defaultIntegration: StepFunctionsIntegration.startExecution(requestValidatorStateMachine, { 117 | passthroughBehavior: PassthroughBehavior.NEVER, 118 | requestTemplates: { 119 | 'application/x-www-form-urlencoded': ` 120 | { 121 | "stateMachineArn": "${requestValidatorStateMachine.stateMachineArn}", 122 | "input": "{\\"body\\": \\"$input.path('$')\\", \\"headers\\": {\\"X-Slack-Signature\\": \\"$input.params().header.get('X-Slack-Signature')\\", \\"X-Slack-Request-Timestamp\\": \\"$input.params().header.get('X-Slack-Request-Timestamp')\\", \\"Content-Type\\": \\"application/x-www-form-urlencoded\\"}}" 123 | } 124 | ` 125 | }, 126 | integrationResponses: [{ 127 | statusCode: '200', 128 | responseTemplates: { 129 | 'application/json': ` 130 | #set($context.responseOverride.status = 204) 131 | {} 132 | ` 133 | } 134 | }] 135 | }) 136 | }) 137 | } 138 | 139 | private buildRequestValidatorStateMachine(): StateMachine { 140 | const logGroup = new LogGroup(this, 'RequestValidatorStateMachineLogGroup') 141 | 142 | // Validate Slack message 143 | const validateSlackMessage = new LambdaInvoke(this, 'Validate Slack Message', { 144 | lambdaFunction: this.slackAuthFunction, 145 | resultSelector: { 'request.$': '$.Payload' } 146 | }) 147 | .addCatch(new Fail(this, 'Validate Slack Message Failure'), { 148 | errors: ['States.ALL'] 149 | }) 150 | 151 | // Get Channel ID value 152 | const getChannelId = new CustomState(this, 'Get Channel ID Value', { 153 | stateJson: { 154 | Type: 'Task', 155 | Resource: 'arn:aws:states:::aws-sdk:ssm:getParameter', 156 | Parameters: { 157 | Name: this.channelIdParameter.parameterName 158 | }, 159 | ResultPath: '$.getParameterResult' 160 | } 161 | }) 162 | 163 | // Get Slack User (executes after Validate Channel ID) 164 | const getSlackUser = new DynamoGetItem(this, 'Get Slack User', { 165 | key: { slackUserName: DynamoAttributeValue.fromString(JsonPath.stringAt('$.request.userName')) }, 166 | table: this.botUsersTable, 167 | resultPath: '$.getUserResult' 168 | }) 169 | 170 | const sendUnauthorizedUserMessage = this.sendEphemeralMessage('Unauthorized User Message', { text: 'You are not authorized to use this command here', response_type: 'ephemeral' }) 171 | 172 | // Validate Channel ID 173 | const validateChannelId = new Choice(this, 'Validate Channel ID') 174 | .when(Condition.stringEqualsJsonPath('$.getParameterResult.Parameter.Value', '$.request.channelId'), getSlackUser) 175 | .otherwise(sendUnauthorizedUserMessage) 176 | 177 | // Send to Command EventBus 178 | const sendToCommandEventBus = new CustomState(this, 'Send to Command EventBus', { 179 | stateJson: { 180 | Type: 'Task', 181 | Resource: 'arn:aws:states:::events:putEvents', 182 | Parameters: { 183 | Entries: [ 184 | { 185 | 'Detail.$': '$.request', 186 | 'DetailType.$': '$.request.action', 187 | EventBusName: this.commandEventBus.eventBusName, 188 | Source: 'slack-app' 189 | } 190 | ] 191 | } 192 | } 193 | }) 194 | const mapPermittedActions = new Pass(this, 'Map User Actions', { 195 | inputPath: '$.getUserResult.Item.permittedActions.SS', 196 | resultPath: '$.request.permittedActions' 197 | }) 198 | .next(sendToCommandEventBus) 199 | 200 | // Filter User Permitted Actions 201 | const filterPermittedActions = new Pass(this, 'Filter User Actions', { 202 | inputPath: '$.getUserResult.Item.permittedActions.SS[?(@ == $.request.actionBase)]', 203 | resultPath: '$.permittedActionsFilter' 204 | }) 205 | .next(new Choice(this, 'Validate Permitted Actions') 206 | .when(Condition.isPresent('$.permittedActionsFilter[0]'), mapPermittedActions) 207 | .otherwise(sendUnauthorizedUserMessage) 208 | ) 209 | 210 | // Validate Slack user 211 | const validateSlackUser = new Choice(this, 'Validate Slack User') 212 | // A user was found in the table AND the action is the welcome screen 213 | .when(Condition.and(Condition.isPresent('$.getUserResult.Item'), Condition.stringEquals('$.request.action', 'welcome')), mapPermittedActions) 214 | // A user was found in the table AND the action is not the welcome screen 215 | .when(Condition.and(Condition.isPresent('$.getUserResult.Item'), Condition.not(Condition.stringEquals('$.request.action', 'welcome'))), filterPermittedActions) 216 | .otherwise(sendUnauthorizedUserMessage) 217 | getSlackUser.next(validateSlackUser) 218 | 219 | // Coordinate states 220 | const definition = validateSlackMessage 221 | .next(getChannelId) 222 | .next(validateChannelId) 223 | 224 | return new StateMachine(this, 'RequestValidator', { 225 | definition, 226 | stateMachineType: StateMachineType.EXPRESS, 227 | logs: { 228 | destination: logGroup, 229 | level: LogLevel.ALL, 230 | includeExecutionData: true 231 | } 232 | }) 233 | } 234 | 235 | private sendEphemeralMessage(id: string, body: any, responseUrlPath: string = 'request.responseUrl'): LambdaInvoke { 236 | return new LambdaInvoke(this, id, { 237 | lambdaFunction: this.httpsClientFunction, 238 | payload: TaskInput.fromObject({ 'url.$': `$.${responseUrlPath}`, body }), 239 | resultPath: JsonPath.DISCARD 240 | }) 241 | } 242 | 243 | private postMessageInChannel(id: string, blocks: any[]): LambdaInvoke { 244 | return new LambdaInvoke(this, id, { 245 | lambdaFunction: this.httpsClientFunction, 246 | payload: TaskInput.fromObject({ 247 | url: 'https://slack.com/api/chat.postMessage', 248 | body: { 249 | 'channel.$': '$.detail.channelId', 250 | blocks 251 | } 252 | }), 253 | resultPath: '$.postMessageResult' 254 | }) 255 | } 256 | 257 | private updatePostedMessage(id: string, blocks: any[]): LambdaInvoke { 258 | return new LambdaInvoke(this, id, { 259 | lambdaFunction: this.httpsClientFunction, 260 | payload: TaskInput.fromObject({ 261 | url: 'https://slack.com/api/chat.update', 262 | body: { 263 | 'channel.$': '$.detail.channelId', 264 | 'ts.$': '$.postMessageResult.Payload.ts', 265 | blocks 266 | } 267 | }), 268 | resultPath: JsonPath.DISCARD 269 | }) 270 | } 271 | 272 | private buildSampleStateMachine() { 273 | const waitX = new Wait(this, 'Wait X Seconds', { 274 | time: WaitTime.secondsPath('$.waitSeconds'), 275 | }) 276 | const pass = new Pass(this, 'Add Result', { 277 | result: Result.fromString('some result'), 278 | resultPath: '$.result', 279 | }) 280 | const jobSuccess = new Succeed(this, 'Job Succcess') 281 | const definition = waitX.next(pass).next(jobSuccess) 282 | 283 | const logGroup = new LogGroup(this, 'SampleStateMachineLogGroup') 284 | 285 | return new StateMachine(this, 'SampleStateMachine', { 286 | definition, 287 | stateMachineType: StateMachineType.EXPRESS, 288 | logs: { 289 | destination: logGroup, 290 | level: LogLevel.ALL, 291 | includeExecutionData: true 292 | } 293 | }) 294 | } 295 | 296 | private buildWelcomeProcessor() { 297 | const sendWelcomeBlocks = this.sendEphemeralMessage('Send Welcome Blocks to Slack', { 298 | blocks: [ 299 | { 300 | type: 'section', 301 | text: { 302 | type: 'mrkdwn', 303 | text: 'Hello! I\'m a slack bot here to help you.\n\n *Please select an action:*' 304 | } 305 | }, 306 | { 307 | type: 'divider' 308 | }, 309 | { 310 | type: 'actions', 311 | elements: [ 312 | { 313 | type: 'button', 314 | text: { 315 | type: 'plain_text', 316 | text: 'Sample Lambda', 317 | emoji: true 318 | }, 319 | action_id: 'sample-lambda' 320 | }, 321 | { 322 | type: 'button', 323 | text: { 324 | type: 'plain_text', 325 | text: 'Sample State Machine', 326 | emoji: true 327 | }, 328 | action_id: 'sample-sm' 329 | } 330 | ] 331 | } 332 | ] 333 | }, 'detail.responseUrl') 334 | 335 | this.buildStateMachineProcessor('WelcomeStateMachineProcessor', sendWelcomeBlocks, 'welcome') 336 | } 337 | 338 | private buildFormProcessor(id: string, title: string, action: string) { 339 | const definition = new Pass(this, `${id} Build Form Blocks Request`, { 340 | result: Result.fromArray([ 341 | { 342 | type: 'header', 343 | text: { 344 | type: 'plain_text', 345 | text: title, 346 | emoji: true 347 | } 348 | }, 349 | { 350 | type: 'section', 351 | text: { 352 | type: 'mrkdwn', 353 | text: 'Here\'s a description about what kind of input this form expects.' 354 | } 355 | }, 356 | { 357 | type: 'input', 358 | block_id: 'form_input', 359 | element: { 360 | type: 'plain_text_input', 361 | action_id: 'input_value' 362 | }, 363 | label: { 364 | type: 'plain_text', 365 | text: 'Input Value', 366 | emoji: true 367 | }, 368 | }, 369 | { 370 | type: 'actions', 371 | elements: [ 372 | { 373 | action_id: `${action}/submit`, 374 | type: 'button', 375 | style: 'primary', 376 | text: { 377 | type: 'plain_text', 378 | text: 'Submit', 379 | emoji: true 380 | }, 381 | value: `${action}/submit` 382 | } 383 | ] 384 | } 385 | ]), 386 | resultPath: '$.blocks' 387 | }) 388 | .next(this.sendEphemeralMessage(`${id} Send Form Blocks to Slack`, { 389 | blocks: JsonPath.listAt('$.blocks') 390 | }, 'detail.responseUrl')) 391 | 392 | this.buildStateMachineProcessor(id, definition, action) 393 | } 394 | 395 | private buildLambdaFormSubmitProcessor(id: string, title: string, action: string, lambdaFunction: NodejsFunction) { 396 | const deleteFormMessage = this.sendEphemeralMessage(`${id} Delete Form Message`, { 397 | delete_original: true 398 | }, 'detail.responseUrl') 399 | 400 | const headerBlocks = this.buildHeaderBlocks(title) 401 | 402 | const sendRunningStatusMessage = this.postMessageInChannel( 403 | `${id} Send Running Status Message to Slack`, 404 | [...headerBlocks, ...this.buildStatusBlocks('running')] 405 | ) 406 | 407 | const sendFailureStatusMessage = this.updatePostedMessage( 408 | `${id} Send Failure Status Message to Slack`, 409 | [...headerBlocks, ...this.buildStatusBlocks('failed'), ...this.buildDetailsBlocks('$.lambdaError.Cause')] 410 | ) 411 | 412 | const sendSuccessStatusMessage = this.updatePostedMessage( 413 | `${id} Send Success Status Message to Slack`, 414 | [...headerBlocks, ...this.buildStatusBlocks('success'), ...this.buildDetailsBlocks('$.lambdaResult.Payload')] 415 | ) 416 | 417 | const executeLambdaFunction = new LambdaInvoke(this, `${id} Execute Lambda Function`, { 418 | lambdaFunction, 419 | payload: TaskInput.fromObject({ 'input.$': '$.detail.inputValue' }), 420 | resultPath: '$.lambdaResult' 421 | }) 422 | .addCatch(sendFailureStatusMessage, { 423 | errors: ['States.ALL'], 424 | resultPath: '$.lambdaError' 425 | }) 426 | 427 | const definition = deleteFormMessage 428 | .next(sendRunningStatusMessage) 429 | .next(executeLambdaFunction) 430 | .next(sendSuccessStatusMessage) 431 | 432 | this.buildStateMachineProcessor(id, definition, action) 433 | } 434 | 435 | private buildStateMachineFormSubmitProcessor(id: string, title: string, action: string, stateMachine: StateMachine) { 436 | const deleteFormMessage = this.sendEphemeralMessage(`${id} Delete Form Message`, { 437 | delete_original: true 438 | }, 'detail.responseUrl') 439 | 440 | const headerBlocks = this.buildHeaderBlocks(title) 441 | 442 | const sendRunningStatusMessage = this.postMessageInChannel( 443 | `${id} Send Running Status Message to Slack`, 444 | [...headerBlocks, ...this.buildStatusBlocks('running')] 445 | ) 446 | 447 | const sendFailureStatusMessage = this.updatePostedMessage( 448 | `${id} Send Failure Status Message to Slack`, 449 | [...headerBlocks, ...this.buildStatusBlocks('failed'), ...this.buildDetailsBlocks('$.stateMachineError.Cause')] 450 | ) 451 | 452 | const sendSuccessStatusMessage = this.updatePostedMessage( 453 | `${id} Send Success Status Message to Slack`, 454 | [...headerBlocks, ...this.buildStatusBlocks('success'), ...this.buildDetailsBlocks('$.stateMachineResult.Output')] 455 | ) 456 | 457 | const executeStateMachine = new StepFunctionsStartExecution(this, `${id} Execute State Machine`, { 458 | stateMachine, 459 | integrationPattern: IntegrationPattern.RUN_JOB, 460 | input: TaskInput.fromObject({ 461 | waitSeconds: 1, 462 | 'input.$': '$.detail.inputValue' 463 | }), 464 | resultPath: '$.stateMachineResult' 465 | }) 466 | .addCatch(sendFailureStatusMessage, { 467 | errors: ['States.ALL'], 468 | resultPath: '$.stateMachineError' 469 | }) 470 | 471 | const definition = deleteFormMessage 472 | .next(sendRunningStatusMessage) 473 | .next(executeStateMachine) 474 | .next(sendSuccessStatusMessage) 475 | 476 | this.buildStateMachineProcessor(id, definition, action, false) 477 | } 478 | 479 | private buildStateMachineProcessor(id: string, definition: IChainable, action: string, express: boolean = true): StateMachine { 480 | let stateMachine: StateMachine 481 | 482 | if (express) { 483 | const logGroup = new LogGroup(this, `${id}LogGroup`) 484 | 485 | stateMachine = new StateMachine(this, id, { 486 | definition, 487 | stateMachineType: StateMachineType.EXPRESS, 488 | logs: { 489 | destination: logGroup, 490 | level: LogLevel.ALL, 491 | includeExecutionData: true 492 | } 493 | }) 494 | } else { 495 | stateMachine = new StateMachine(this, id, { definition }) 496 | } 497 | 498 | const rule = new Rule(this, `${id}Rule`, { 499 | eventBus: this.commandEventBus 500 | }) 501 | rule.addEventPattern({ 502 | source: ['slack-app'], 503 | detailType: [action] 504 | }) 505 | rule.addTarget(new SfnStateMachine(stateMachine)) 506 | 507 | return stateMachine 508 | } 509 | 510 | private buildHeaderBlocks(title: string): any[] { 511 | return [ 512 | { 513 | type: 'header', 514 | text: { 515 | type: 'plain_text', 516 | text: title, 517 | emoji: true 518 | } 519 | }, 520 | { 521 | type: 'section', 522 | text: { 523 | type: 'mrkdwn', 524 | text: JsonPath.format('@{} initiated this workflow', JsonPath.stringAt('$.detail.userName')) 525 | } 526 | }, 527 | { 528 | type: 'divider' 529 | } 530 | ] 531 | } 532 | 533 | private buildStatusBlocks(status: 'running' | 'success' | 'failed'): any[] { 534 | const block = { 535 | type: 'section', 536 | text: { 537 | type: 'mrkdwn', 538 | text: '' 539 | } 540 | } 541 | 542 | if (status === 'running') { 543 | block.text.text = '▶️ Execution has started...' 544 | } else if (status === 'success') { 545 | block.text.text = '✅ Execution was successful!' 546 | } else { 547 | block.text.text = '⛔️ Execution has failed. Please see details below' 548 | } 549 | 550 | return [block] 551 | } 552 | 553 | private buildDetailsBlocks(contentPath: string): any[] { 554 | return [ 555 | { 556 | type: 'section', 557 | text: { 558 | type: 'mrkdwn', 559 | text: '*Response Details*' 560 | } 561 | }, 562 | { 563 | type: 'section', 564 | text: { 565 | type: 'mrkdwn', 566 | text: JsonPath.format('```{}```', JsonPath.stringAt(contentPath)) 567 | } 568 | } 569 | ] 570 | } 571 | } 572 | --------------------------------------------------------------------------------