DEV Community

Cover image for How to create a slack command with Deno, Cloud Run & Cloud Build!
Robert Ruiz
Robert Ruiz

Posted on

How to create a slack command with Deno, Cloud Run & Cloud Build!

Introduction:

Do you and your team use slack? You may have seen those slack commands. Those really cool commands that you type out, add some information, and it does some magic.
Alt Text

If you've ever wanted to make your own slack command, this series will teach you how to. This series is also useful if you want to learn what Deno is all about, getting started with Cloud Run, or seeing Cloud Build in action.

With that being, said, I hope you enjoy the series!

What is Shout:

Let me ask you this. Have you ever been in a situation where you're on slack, and you're in a debate, or a conversation and you really want to get your point across? Explaining your point through different ways could help, but don't you want to show how passionately you feel about your point? Why not shout it out? IT WILL GET THE POINT ACROSS!

That's the point of this project, it's a fun little project where you can use a slack command to shout your point across in a fun kind of way.

The plan for this project is to get it running in Deno, deploy it with cloud run, hook it up to slack, and setup a CI/CD deployment with cloud build.

Deno:

You might be wondering, what exactly is Deno? Deno is a runtime environment. Much like Node.js. Actually, it was created by the same person who made Node.js, Ryan Dahl. Ryan wants to correct the mistakes he made with Node.js. He talked about his regrets in a talk in 2018, 10 Things I Regret About Node.js Feel free to take a look at it here.

It can run both Typescript & Javascript. Typescript runs right out of the box! No setup required with that. I'm a huge fan of Typescript so that is great to see.

Getting Started:

First thing is first, we need to install Deno.
After it completes, feel free to open a terminal and test it out.

deno --version
Enter fullscreen mode Exit fullscreen mode

Now, in order for things in this particular project to run smoothly. We will have to install a specific version of Deno. Deno is moving quick, and a lot of times, the latest version of Deno won't work with the latest version of packages. So keeping specific versions ensure everything will work as intended. With that being said, we will want to install version 1.3.2

deno upgrade --version 1.3.2
deno --version
Enter fullscreen mode Exit fullscreen mode

Make sure deno is good to go, and we're ready to move on!

Also, I am using yarn (Node.JS package manager). However I'm only using it for running scripts. So installing it is not required, however it's convenient.

Code:

Let's get started! Let's start with a simple server.
Create a file called: main.ts, and add these bits of code:

import {Application, Router} from 'https://deno.land/x/oak@v6.0.1/mod.ts';
import bodyParser from './body-parser.ts';
import 'https://deno.land/x/dotenv/mod.ts';

const app = new Application();
const PORT = Deno?.env?.get('PORT') ?? 8080;
const router = new Router();

/**
 * Submit text to be shouted
 *
 * @example
 * curl -X POST "https://localhost:8080/" --data '{"text":"TEST"}'
 * https://api.slack.com/interactivity/slash-commands#app_command_handling
 * @param {object} req request object.
 * @param {object} req.body The request payload.
 * @param {string} req.body.text The user's text.
 * @param {object} res response object.
 */
router
    .post('/', async (context) => {
      try {
        // Get the Body:
        const body = await context.request.body();
        if (!context.request.hasBody) {
          context.response.status = 400;
          context.response.body = {
            success: false,
            message: 'No data provided',
          };
          return;
        }
         const json = await context.request.body({type: "json"}).value;
        const result = await FetchShoutCloud(json.text! as string);

        context.response.status = 200;
        context.response.type = 'application/json';
        context.response.body = {
          'text': `${result}`,
        };
        return;
      } catch (error) {
        console.error(error);
        context.response.status = 500;
        return;
      }
    });

async function FetchShoutCloud(command: string): Promise<string> {
  const data = await (await fetch('HTTP://API.SHOUTCLOUD.IO/V1/SHOUT', {
    method: 'POST',
    headers: {
      'Content-Type': 'application/json',
    },
    body: JSON.stringify({'INPUT': command}),
  })).json();
  return data.OUTPUT;
}

app.use(router.routes());
app.use(router.allowedMethods());

app.addEventListener("listen", ({hostname, port, secure}) => {
    console.log(
      `Listening on: ${secure ? "https://" : "http://"}${
        hostname ?? "localhost"
      }:${port}`
    );
  })

await app.listen({port: Number(PORT)});

export default app;
Enter fullscreen mode Exit fullscreen mode

Whoosh that's a lot of code! Let's break this chunk of code down so we can understand what is happening here.

import {Application, Router} from 'https://deno.land/x/oak@v6.0.1/mod.ts';
import bodyParser from './body-parser.ts';
import 'https://deno.land/x/dotenv/mod.ts';

const app = new Application();
const PORT = Deno?.env?.get('PORT') ?? 8080;
const router = new Router();
Enter fullscreen mode Exit fullscreen mode

At the very top of the file we need to import a few packages.

Traditionally with Node.JS or a frontend application like React or Angular, a package.json will be required, with dependencies that'll be downloaded into a node_packages folder. The project will use those files to utilize any packages. However, there is no dependencies within package.json, or node_packages at all. So how does Deno do it? Also, why is the location of the imported packages from a url, rather than a package folder?

Well, simply put, deno has no package manager. It downloads packages (sometimes with specific versions) from that url, and caches the package into a global directory. Reducing the need for a massive node_packages folder for each and every single project you work on. Pretty neat right?

We'll use that, to import three packages:

  • bodyParser from the local repository. Note: This will be used at a future post. This is necessary for receiving slack commands from a POST request.
  • dotenv to fetch environment variables.
  • Oak, the web server we will be using.

Oak is essentially the Deno version of Node JS's Koa framework. So if you've used this before, you're going to be in a familiar place. Regardless, it's a beginner friendly framework.

The next few lines we are going to start up an oak application. After that we will create a router, which creates middleware that allows us to create routes (based on the pathname of the request).

router
    .post('/', async (context) => {
      try {
        // Get the Body:
        const body = await context.request.body();
        if (!context.request.hasBody) {
          context.response.status = 400;
          context.response.body = {
            success: false,
            message: 'No data provided',
          };
          return;
        }
        const json = await context.request.body({type: "json"}).value;
        const result = await FetchShoutCloud(json.text! as string);

        context.response.status = 200;
        context.response.type = 'application/json';
        context.response.body = {
          'text': `${result}`,
        };
        return;
      } catch (error) {
        console.error(error);
        context.response.status = 500;
        return;
      }
    });

async function FetchShoutCloud(command: string): Promise<string> {
  const data = await (await fetch('HTTP://API.SHOUTCLOUD.IO/V1/SHOUT', {
    method: 'POST',
    headers: {
      'Content-Type': 'application/json',
    },
    body: JSON.stringify({'INPUT': command}),
  })).json();
  return data.OUTPUT;
}
Enter fullscreen mode Exit fullscreen mode

The one route we will implement is a POST request to the base url. We will fetch the body of the request. If there is no body, we'll return an error. If there is a body, we'll parse through the contents, and pass the results onto an API called ShoutCloud. They handle the capitalization of the request. Finally, we get the results from that API request, and return back the results in the form of a JSON response.

If anything happens in the middle of this process, it will throw an error, and return back a 500 error.

What does the context mean? The context represents the current request that's going through Oak's middleware. It is very useful when you have multiple middleware functions passing through.

app.use(router.routes());
app.use(router.allowedMethods());

app.addEventListener("listen", ({hostname, port, secure}) => {
    console.log(
      `Listening on: ${secure ? "https://" : "http://"}${
        hostname ?? "localhost"
      }:${port}`
    );
  })

await app.listen({port: Number(PORT)});
export default app;
Enter fullscreen mode Exit fullscreen mode

Lastly, we have to tell the Oak application to use the router middleware. We will return middleware that does all the route processing. The second line will handle methods that are allowed. If none of the routes handle a certain method, then a 405 error will be returned! Meaning if the user attempts to use a GET request rather than a POST request, they'll get a 405 error.

The next block is more for us, it's when the application starts and beginnings listening to request. We will have some feedback on the console knowing whether the application or not.

Finally, the application begins to listen for requests!

Running:

We are almost there. Now it's time to run the file!
Type in the command:

deno run --allow-env --allow-net ./main.ts
Enter fullscreen mode Exit fullscreen mode

By default Deno works in a sandbox environment. That means it has no permissions by default unless you explicitly allow those permissions. In this case we're allowing two permissions:

  • --allow-env: Allows us to access environment variables.
  • --allow-net: Allows us to access the internet

It should see in your terminal, that the application is listening on localhost:8080/

It's up and running!

Testing:

Now that we got it running, let's test it out! How do we test out a POST request though? We could do it through the terminal, but let's make things much easier.

Let's use an API explorer called Insomnia. Specifically Insomnia Core. It's an easy way to explore restful APIs & GraphQL APIs.

Once it's installed, open it up and create a new POST request. You can create a new request with the plus icon near the top left.
Alt Text

Set a name, set the method from GET to POST. And at the right, we'll set the body to JSON.

Once you create the new request add this url in the url field:

http://localhost:8080/
Enter fullscreen mode Exit fullscreen mode

Now within the body tab, we have to input some data.
Paste the following into the text field:

{
"text": "hello!"
}
Enter fullscreen mode Exit fullscreen mode

Alt Text
After that you can click send, and you should see your result in the right!
If you receive a 500 error, ensure your application is on! If you receive a 403 error, ensure your url is correct.

If all went well, you should be able to see your text! Nice work!

Thoughts on Deno:

Deno v1.0.0 was released on May 12, 2020. It is in a stable state after 2 years of development. That being said, there are still a lot of growing pains.

Deno, and a lot of popular packages are growing, and iterating, fast. There is a lot of specific versions of Deno, and specific versions of packages you need to install, or else the application won't compile due to a type errors w/in Deno. This part was very confusing, and took a lot of research to figure out which version was compatible with what.

Overall, Deno is really neat. I don't quite see it surpassing Node.js yet, especially since things are rapidly evolving at a lightning fast pace, and things aren't too stable. However I will definitely use it in the future for production projects. The community is growing, and I wouldn't mind being apart of that. The Deno contributors, and the community was so nice in answering all of my questions I've had throughout this project.

Perhaps Deno will be the next standard. Only time can tell.

Conclusion:

That's all for part one. In the next parts we will:

  • Get it up and running with Cloud Run.
  • Setup a Slack Developer account & Slack Application
  • And finally, automate the deployments with cloud build.

If you have any questions feel free to leave a comment out below or reach out to me.

The full Github repository can be found here:

GitHub logo robeartoe / shout

A slack command, made with Google Cloud Run and the SHOUTCLOUD package.

Top comments (0)