Creating a slack game using Serverless architecture & AWS - part 2
Tags: serverless, aws, javascript
Published: 2020-03-03 18:29:07

In the [previous article](https://dev.to/femioladeji/creating-a-slack-game-using-serverless-architecture-aws-part-1-4jhn), we did a brief introduction to what serverless architecture is. We created a development environment with three lambda functions for starting a game, submitting words and ending the game. In this part, we’ll finalize what we started by making our app available online on AWS and create a Slack app so that people can interact with the game directly via slack.

We'll have to deploy our app to AWS and a new script command has to be added to package.json for that. The `--stage` option is used to specify the stage the app will be deployed to. You can use this to create a staging environment to test features and perform QA to prevent bugs from being deployed to prod.
```json
"scripts": {
   "dev": "serverless offline start",
   "production": "serverless deploy --stage prod"
 }
```
The db.js file was configured to work with local DynamoDB but we need to modify it to support AWS DynamoDB also. The `option` object when running in the production environment can be an empty object but in development, it needs to be updated as in the snippet below. The snippet will replace the option declaration that was initially in the file.
```js
let option = {};
 
if (process.env.NODE_ENV === 'development') {
 option = {
   region: 'localhost',
   endpoint: 'http://localhost:8000'
 };
}
```

Let’s run the `yarn production` command to get it deployed online. If the deployment was successful you should get the URL for two endpoints (start_game and submit lambda functions).

With the endpoints, we can get started with the creation of the app on Slack. Go to [https://api.slack.com/apps](https://api.slack.com/apps) and click on the Create New App.
![](https://i.ibb.co/9HkQJML/slack-1.jpg)
To get our apps fully working, we need some slack permissions.

The first permission we need is to use the slash command. It will be used to start a new game. Click on the slash command link on the basic information tab. Enter any slash command of your choice and use the `/start` endpoint link as the redirect URL and save the entered details.
![](https://i.ibb.co/nDfkcCN/Screenshot-2019-12-02-at-00-58-21.png)

Go back to the basic information tab and click on `event subscription`. We need this to know when users submit words. In order to enable this, let’s modify the submit lambda function to handle slack's test POST request using the snippet below
```js
module.exports.submit = async (event, _context, callback) => {
  const { body } = event;
  const { event: message, challenge } = JSON.parse(body);
  if (challenge) {
    // this is for slack verification
    return respond(callback, 200, challenge);
  }
  if (!message.thread_ts || message.text.trim().split(' ').length > 1) {
    return callback(null, { statusCode: 200 });
  }
  try {
    const id = `${message.team}${message.channel}`;
    await db.addWords(id, {
      user: message.user,
      word: message.text,
    });
    return callback(null, { statusCode: 200 });
  } catch (error) {
    console.log(error);
    if (error.code === 'ConditionalCheckFailedException') {
      return callback(null, { statusCode: 200, body: 'Game has ended' });
    }
    return callback(null, { statusCode: 200, body: 'An error occurred while ending the game' });
  }
};
```
Let's deploy the newly modified function with this command.
```
npx serverless deploy --stage prod --function submit_words
```
The `/submit` endpoint can now be set as the redirect url. Click on the workspace event and choose the `message.channels` event. With this being set up, every message posted on a public channel makes a POST request to the endpoint that was set up.

We need two more permissions to get things started. Click on the OAuth & Permissions link and choose `chat.write:bot` (to post the result on the slack channel) and `users:read` (to retrieve the full name of users who participated in a game).

We're almost there, we just need a few more modifications to get things going. In order to automatically invoke the end lambda function 60 seconds after a game is started, the [AWS SQS](https://aws.amazon.com/sqs/) will be used. We need to also give permissions to our lambda functions to use SQS and write/read data from our DynamoDB. Under the resources section in serverless.yml file. Add this snippet. It should be aligned with the `gamesTable` resource yml specification
```
gameQueue:
  Type: AWS::SQS::Queue
  Properties: 
    QueueName: games
    DelaySeconds: 60
    MessageRetentionPeriod: 120
```
Redeploy the app again by using the `yarn production` command. After the deployment, go to the SQS management console on AWS and select the queue. Copy the queue URL and ARN and create a new env variable `QUEUE_URL` with the URL copied as well as the `QUEUE_ARN`. The end game function has to be triggered whenever a message is delivered in the queue. Modify the end_game function declaration in the serverless yml to this snippet.
```yml
 end_game:
   handler: game.end
   name: end_game
   timeout: 20
   events:
     - sqs: ${env:QUEUE_ARN}
```
In the game.js file, the aws-sdk and dotenv package must be required.
```js
const aws = require('aws-sdk');
require('dotenv').config();
```
Once a game is started, a message needs to be added to the queue that will be delivered in exactly 60 seconds due to the queue configuration. Add the snippet below after the `await db.insert(gameItem)` statement.
```js
    await new aws.SQS().sendMessage({
      QueueUrl: process.env.QUEUE_URL,
      MessageBody: JSON.stringify(gameItem),
    }).promise();
```
The end lambda function will receive the event on the queue after 60 seconds. SQS messages are in a different format so the first statement in the function `const game = event` has to be changed to 
```js
  const game = JSON.parse(event.Records[0].body);
```

When users submit words, Slack only sends the user ID and we need a way to retrieve the user’s full name and profile picture also. The `users:read` permission allows us to do this. All we have to do is make a GET request to `https://slack.com/api/users.info` and pass the user ID and workspace token as query parameters. To get the workspace token, go to the auth & permissions link on the Slack dashboard and click on `Install App to Workspace`. Copy the access token and it as a new env variable `TOKEN` in the .env file. Let’s add a function to our app.js file specifically for this. It receives an object of `userid:score` pair and retrieves the user details. Make sure you install the `axios` npm package and require it in the file. Also, configure the dotenv package as we did in the game.js file
```js
getUsers(users) {
    return new Promise(async (resolve) => {
      const slackUrl = `https://slack.com/api/users.info?token=${process.env.TOKEN}&user=`;
      const detailsRequest = Object.keys(users).map(each => axios.get(`${slackUrl}${each}`));
      let finalScore = await Promise.all(detailsRequest);
      finalScore = finalScore.map(({ data: { user }, status }) => {
        if (status === 200) {
          return {
            type: 'section',
            fields: [{
              type: 'plain_text',
              text: 'Name:',
            },
            {
              type: 'plain_text',
              text: user.real_name,
              emoji: true,
            },
            {
              type: 'plain_text',
              text: 'Username:',
            },
            {
              type: 'plain_text',
              text: user.name,
              emoji: true,
            },
            {
              type: 'plain_text',
              text: 'Score:',
            },
            {
              type: 'plain_text',
              text: `${users[user.id].totalScore}`,
            },
            {
              type: 'plain_text',
              text: 'words:',
            },
            {
              type: 'mrkdwn',
              text: users[user.id].words,
            }],
            accessory: {
              type: 'image',
              image_url: user.profile.image_72,
              alt_text: user.real_name,
            },
          };
        }
        return {};
      });
      resolve(finalScore);
    });
  }
```
All we have to do now is call this function from the `computeResult` function by replacing this statement `const results = this.groupByUser(score);` with
```js
const results = await this.getUsers(this.groupByUser(score));
```

Another thing we need to do is post the result of each game to the Slack channel and to achieve that, the end game lambda function has to be modified. Replace the `if(words.length)` block with this snippet. Ensure that axios module has been required in this file (game.js)
```js
if (words.length) {
      const results = await app.computeResults(words, letters);
      axios.post(game.response_url, JSON.stringify({
        response_type: 'in_channel',
        blocks: results,
      }), {
        headers: {
          Authorization: `Bearer ${process.env.TOKEN}`,
        },
      });
    }
    callback(null, {
      statusCode: 200,
    });
```

The last thing we need to do is give permissions to the lambda functions created to use some AWS resources (DynamoDB and SQS). The DynamoDB games table ARN has to be gotten from AWS. Go to the DynamoDB section on AWS, click on tables, select the games table and copy the Amazon Resource Name (ARN). Add it as `DYNAMO_ARN` env variable to the .env file. Now add this snippet to serverless.yml file just under the runtime property
```yml
iamRoleStatements: # permissions for all of your functions can be set here
    - Effect: Allow
      Action: # Gives permission to DynamoDB tables in a specific region
        - dynamodb:GetItem
        - dynamodb:PutItem
        - dynamodb:UpdateItem
        - dynamodb:DeleteItem
      Resource: ${env:DYNAMO_ARN}
    - Effect: Allow
      Action:
        - sqs:SendMessage
        - sqs:ReceiveMessage
      Resource: ${env:QUEUE_ARN}
```
Let's redeploy the app and see if we got everything right. Open your slack channel and use the slash command you created earlier. Submit as many words as you can by replying to the thread and wait for the result to be posted after 60 seconds of starting the game. If anything doesn't work quite right you can check Cloudwatch log groups on AWS to see if any error was logged.

Let’s do a quick recap of what we’ve done over the two posts. We’ve explored what serverless architecture is all about, we were able to set up the app locally and test some functions. We then created a Slack app and got the app set up on AWS so that the game can be played with other teammates on Slack.

P.S
Slack has released a way to give more granular permissions. Also, the conversations API has removed the need to subscribe to events on the Slack workspace. I've released a new version of the app to the Slack app directory with the changes.

If you have any questions or something isn’t working quite right, please leave a comment.
Complete codebase can be found [here](https://github.com/femioladeji/slack-words)
To install the game on your slack workspace, go to [http://slackwords.com/](http://slackwords.com/)