DEV Community

Oladeji Femi
Oladeji Femi

Posted on

Creating a slack game using Serverless architecture & AWS - part 2

In the previous article, 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.

"scripts": {
   "dev": "serverless offline start",
   "production": "serverless deploy --stage prod"
 }
Enter fullscreen mode Exit fullscreen mode

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.

let option = {};

if (process.env.NODE_ENV === 'development') {
 option = {
   region: 'localhost',
   endpoint: 'http://localhost:8000'
 };
}
Enter fullscreen mode Exit fullscreen mode

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 and click on the Create New App.

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.

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

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' });
  }
};
Enter fullscreen mode Exit fullscreen mode

Let's deploy the newly modified function with this command.

npx serverless deploy --stage prod --function submit_words
Enter fullscreen mode Exit fullscreen mode

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 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
Enter fullscreen mode Exit fullscreen mode

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.

 end_game:
   handler: game.end
   name: end_game
   timeout: 20
   events:
     - sqs: ${env:QUEUE_ARN}
Enter fullscreen mode Exit fullscreen mode

In the game.js file, the aws-sdk and dotenv package must be required.

const aws = require('aws-sdk');
require('dotenv').config();
Enter fullscreen mode Exit fullscreen mode

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.

    await new aws.SQS().sendMessage({
      QueueUrl: process.env.QUEUE_URL,
      MessageBody: JSON.stringify(gameItem),
    }).promise();
Enter fullscreen mode Exit fullscreen mode

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

  const game = JSON.parse(event.Records[0].body);
Enter fullscreen mode Exit fullscreen mode

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

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);
    });
  }
Enter fullscreen mode Exit fullscreen mode

All we have to do now is call this function from the computeResult function by replacing this statement const results = this.groupByUser(score); with

const results = await this.getUsers(this.groupByUser(score));
Enter fullscreen mode Exit fullscreen mode

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)

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,
    });
Enter fullscreen mode Exit fullscreen mode

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

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}
Enter fullscreen mode Exit fullscreen mode

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
To install the game on your slack workspace, go to http://slackwords.com/

Top comments (0)