How to Build Slack Bot? (Node JS | AWS Lambda & DynamoDB - AWS Serverless | New Slack Apps)
YouTube Tutorial
Go to https://api.slack.com/apps
Select create an app From scratch
Give it a name WALL-E
and select your namespace
Scroll down to App Credentials
a Display Information
Give it a short description Compactor robot
Upload app icon and change color
Go to Event Subscriptions
2. Create IAM User with Full Access
Create admin
user and place it in Admin
group
Download credentials
Configure AWS profile with aws configure
Test profile with aws sts get-caller-identity
3. Create IAM Role for AWS Lambda
Create IAM Policy AWSLambdaSlackAccess
{
"Version" : " 2012-10-17" ,
"Statement" : [
{
"Effect" : " Allow" ,
"Action" : " logs:CreateLogGroup" ,
"Resource" : " arn:aws:logs:us-east-1:424432388155:*"
},
{
"Effect" : " Allow" ,
"Action" : [
" logs:CreateLogStream" ,
" logs:PutLogEvents"
],
"Resource" : " arn:aws:logs:us-east-1:424432388155:log-group:/aws/lambda/wall-e:*"
},
{
"Effect" : " Allow" ,
"Action" : " dynamodb:*" ,
"Resource" : " arn:aws:dynamodb:us-east-1:424432388155:table/todos"
}
]
}
Create wall-e-role
for Lambda and attach AWSLambdaSlackAccess
IAM Policy
Optionally: aws iam get-role --role-name wall-e-role
4. Create AWS Lambda Function
Create wall-e
directory
Run npm init
from wall-e
directory
Create app.js
exports . handler = ( event , context , callback ) => {
const response = { 'message' : `Hello World!` } ;
callback ( null , response ) ;
} ;
FROM public.ecr.aws/lambda/nodejs:14
COPY *.js package*.json /var/task/
RUN npm install
CMD [ "app.handler" ]
Create ECR Repository wall-e
aws ecr get-login-password --region us-east-1 \
| docker login \
--username AWS \
--password-stdin 424432388155.dkr.ecr.us-east-1.amazonaws.com
docker build \
-t 424432388155.dkr.ecr.us-east-1.amazonaws.com/wall-e:v0.1.0 .
docker push \
424432388155.dkr.ecr.us-east-1.amazonaws.com/wall-e:v0.1.0
5. Deploy Lambda Using Container Image
Select Containerimage
from the console
Call function wall-e
Use an existing role wall-e-role
Select wall-e
ECR image
6. Create API Gateway for Lambda Function
Create HTTP API slack
Test Lambda with curl
curl -X POST https://uss0o5l3h3.execute-api.us-east-1.amazonaws.com/production/wall-e
Check logs
Use this URL for Slack
7. Use AWS Lambda for Slack Event Subscriptions
Create tests/event.json
file
{
"version" : " 2.0" ,
"routeKey" : " ANY /bot" ,
"rawPath" : " /default/bot" ,
"rawQueryString" : " " ,
"headers" : {
"accept" : " */*" ,
"accept-encoding" : " gzip,deflate" ,
"content-length" : " 129" ,
"content-type" : " application/json" ,
"host" : " 4o68t2fwke.execute-api.us-east-1.amazonaws.com" ,
"user-agent" : " Slackbot 1.0 (+https://api.slack.com/robots)" ,
"x-amzn-trace-id" : " Root=1-60f9f121-0e6b301236f5d57d46fbd0e1" ,
"x-forwarded-for" : " 3.94.92.68" ,
"x-forwarded-port" : " 443" ,
"x-forwarded-proto" : " https" ,
"x-slack-request-timestamp" : " 1626992929" ,
"x-slack-signature" : " v0=d12f7cb55add77074248241c2ec2d3c9fe4611e7879a965c92315edd8f0ec0cf"
},
"requestContext" : {
"accountId" : " 424432388155" ,
"apiId" : " 4o68t2fwke" ,
"domainName" : " 4o68t2fwke.execute-api.us-east-1.amazonaws.com" ,
"domainPrefix" : " 4o68t2fwke" ,
"http" : {
"method" : " POST" ,
"path" : " /default/bot" ,
"protocol" : " HTTP/1.1" ,
"sourceIp" : " 3.94.92.68" ,
"userAgent" : " Slackbot 1.0 (+https://api.slack.com/robots)"
},
"requestId" : " C5KdVjAlIAMEPzg=" ,
"routeKey" : " ANY /bot" ,
"stage" : " default" ,
"time" : " 22/Jul/2021:22:28:49 +0000" ,
"timeEpoch" : 1626992929961
},
"body" : " {\" token\" :\" UdG3UFNsPGoobvRzK5F2oIqe\" ,\" challenge\" :\" 6KaNtlamllYYaLZ7qhHxZbzyYut62TlDKu2wAZXp4rZlInRbcDTH\" ,\" type\" :\" url_verification\" }" ,
"isBase64Encoded" : false
}
Update body of the function
exports . handler = ( event , context , callback ) => {
const body = JSON . parse ( event . body ) ;
switch ( body . type ) {
case "url_verification" : callback ( null , body . challenge ) ; break ;
default : callback ( null ) ;
}
} ;
Package and upload docker image
docker build \
-t 424432388155.dkr.ecr.us-east-1.amazonaws.com/wall-e:v0.1.1 .
docker push \
424432388155.dkr.ecr.us-east-1.amazonaws.com/wall-e:v0.1.1
Redeploy AWS Lambda using v0.1.1 image tag
Go back to Slack and click Retry
8. Verify Slack Requests Using Signing Secret
Optionally: Create verify.js
Run with node verify.js
Compare string with event.json
Create security.js
const crypto = require ( "crypto" ) ;
exports . validateSlackRequest = ( event , signingSecret ) => {
const requestBody = event [ "body" ] ;
const headers = makeLower ( event . headers ) ;
const timestamp = headers [ "x-slack-request-timestamp" ] ;
const slackSignature = headers [ "x-slack-signature" ] ;
const baseString = 'v0:' + timestamp + ':' + requestBody ;
const hmac = crypto . createHmac ( "sha256" , signingSecret )
. update ( baseString )
. digest ( "hex" ) ;
const computedSlackSignature = "v0=" + hmac ;
const isValid = computedSlackSignature === slackSignature ;
return isValid ;
} ;
const makeLower = ( headers ) => {
let lowerCaseHeaders = { }
for ( const key in headers ) {
if ( headers . hasOwnProperty ( key ) ) {
lowerCaseHeaders [ key . toLowerCase ( ) ] = headers [ key ] . toLowerCase ( )
}
}
return lowerCaseHeaders
}
const security = require ( './security' ) ;
const signingSecret = process . env . SLACK_SIGNING_SECRET ;
exports . handler = ( event , context , callback ) => {
if ( security . validateSlackRequest ( event , signingSecret ) ) {
const body = JSON . parse ( event . body ) ;
switch ( body . type ) {
case "url_verification" : callback ( null , body . challenge ) ; break ;
default : callback ( null ) ;
}
}
else callback ( "verification failed" ) ;
} ;
Add Environment variable to Lambda SLACK_SIGNING_SECRET
(next video aws lambda secrets manager integration)
Create unit test security.test.js
Install Jest npm i --save-dev jest
Update test command to jest
Run tests npm test
9. Process Slack Messages
Create private Slack channel earth
Subscribe to following events
message.groups
message.channels
Install app to workspace
Optionally: set Always Show My Bot as Online
from App Home
Go to OAuth & Permissions
to check automatically added permissions
Add event_callback
event
case "event_callback" : processRequest ( body , callback ) ; break ;
Create processRequest
method
const processRequest = ( body , callback ) => {
switch ( body . event . type ) {
case "message" : processMessages ( body , callback ) ; break ;
default : callback ( null ) ;
}
} ;
Create processMessages
method in app.js
const processMessages = ( body , callback ) => {
console . debug ( "message:" , body . event . text ) ;
callback ( null ) ;
} ;
Update Dockerfile
to use npm ci --production
Package and upload docker image
docker build \
-t 424432388155.dkr.ecr.us-east-1.amazonaws.com/wall-e:v0.1.2 .
docker push \
424432388155.dkr.ecr.us-east-1.amazonaws.com/wall-e:v0.1.2
Update AWS Lambda to use v0.1.2
tag
Optionally: Delete all log streams
Inviite WALL-E
to earth
channel
Post hello
message
Open CloudWatch logs
10. Save Messages Using AWS Lambda to DynamoDB
Create todos
DynamoDB table with uuid
primary key
Replace message.groups
and message.channels
with app_mention
event
Add chat:write
instead of message.*
Reinstall app
Install aws-sdk
, uuid
, and axios
const AWS = require ( 'aws-sdk' ) ;
const { v4 : uuidv4 } = require ( 'uuid' ) ;
AWS . config . update ( { region : 'us-east-1' } ) ;
const ddb = new AWS . DynamoDB ( { apiVersion : '2012-08-10' } ) ;
exports . saveItem = ( item , callback ) => {
const params = {
TableName : 'todos' ,
Item : {
'uuid' : { S : uuidv4 ( ) } ,
'item' : { S : item }
}
} ;
ddb . putItem ( params , ( error , data ) => {
if ( error ) {
callback ( new Error ( error ) ) ;
} else {
callback ( null ) ;
}
} ) ;
} ;
case "app_mention" : processAppMention ( body , callback ) ; break ;
const axios = require ( 'axios' ) ;
const db = require ( './db' ) ;
Create token
variable and add it to Lambda
const token = process . env . SLACK_BOT_TOKEN ;
Create processAppMention
method
const processAppMention = ( body , callback ) => {
const item = body . event . text . split ( ":" ) . pop ( ) . trim ( ) ;
db . saveItem ( item , ( error , result ) => {
if ( error !== null ) {
callback ( error )
} else {
const message = {
channel : body . event . channel ,
text : `Item: \`${ item } \` is saved to *Amazon DynamoDB*!`
} ;
axios ( {
method : 'post' ,
url : 'https://slack.com/api/chat.postMessage' ,
headers : { 'Content-Type' : 'application/json; charset=utf-8' , 'Authorization' : `Bearer ${ token } ` } ,
data : message
} )
. then ( ( response ) => {
callback ( null ) ;
} )
. catch ( ( error ) => {
callback ( "failed to process app_mention" ) ;
} ) ;
}
} ) ;
} ;
Package and upload docker image
docker build \
-t 424432388155.dkr.ecr.us-east-1.amazonaws.com/wall-e:v0.1.3 .
docker push \
424432388155.dkr.ecr.us-east-1.amazonaws.com/wall-e:v0.1.3
Redeploy Lambda using v0.1.3
tag
Post @WALL-E todo: Save the Planet!
message
Open DynamoDB todos
table
Delete ECR wall-e
Delete IAM User admin
Delete IAM Role wall-e-role
Delete IAM Policy AWSLambdaSlackAccess
Delete DynamoDB table todos
Delete Lambda wall-e
Delete CloudWatch log groups /aws/lambda/wall-e
Delete API Gateway slack
Delete docker images docker rmi -f $(docker images -a -q)
Delete slack bot wall-e
Delete earth
Slack channel