Create a SlackBot with AWS lambda & API Gateway in Java

Introduction

When making a slack bot, I think that I will use Python or Node.js in most cases, but this time I will dare to do it in Java. I couldn't find an article about making a serverless slack bot via AWS in Java in Japanese or English, so I'll write the implementation procedure here. I'm an amateur when it comes to Java, so it's better to do this! I would appreciate it if you could point out any points. The whole code is on github https://github.com/kamata1729/sampleSlackBotWithJava

Creating a slack bot

Create an app for creating bots

First of all, let's make a Slack Bot! https://api.slack.com/apps From here, press Create New App to create a new app. image.png

And add Bot User to the created app image.png

Create an endpoint to receive slack events

From here, we will create an endpoint to receive events from slack on AWS. Specifically, API Gateway receives the event and passes it to the lambda function.

Creating an IAM role

Open IAM from the AWS console and create a new role. image.png

Since it will be used in lambda, select "lambda" and proceed first. image.png

This time I want to be able to output the log to CloudWatch Log, so select ʻAWS LambdaBasicExecutionRolewith write permission image.png

I will not use it this time for creating the next tag, so you can ignore it. On the next confirmation screen, give a role name and you're done! This time, I named it sampleBotRole. image.png

Creating a lambda function

Now go to the lambda console and choose to create a function. Select "Create from scratch", this time select sampleBot for the function name, and java 8 for the runtime. Also, for the role, I selected the sampleBotRole created earlier. image.png

Creating an API Gateway

First, select "Create API" from the API Gateway page of the aws console, and create it with the API name (sampleBotAPI this time). image.png From the created screen, select Action-> Create Method to add the POST method. image.png Press the check mark to create a POST method, set it to match the lambda function you created earlier on the setup screen, and save it. image.png On the subsequent screen, select Action-> Deploy API and enter the stage name to deploy. At this time, the URL to call the endpoint will be displayed at the top of the screen, so make a note of it. This completes the endpoint creation! image.png

About authentication of slack Event API

Before implementing the lambda function, let's talk about authenticating the slack Event API. With the slack Event API, you first need to send back a specific string to see if your program is for the Event API.

For authentication, the following json is sent first.

{
    "token": "Jhj5dZrVaK7ZwHHjRyZWjbDl",
    "challenge": "3eZbrw1aBm2rZgRNFdxV2595E9CY3gmdALWMmHkvFXO7tYXAYM8P",
    "type": "url_verification"
}

On the other hand, if you can send back the contents of challenge within 3 seconds, authentication is completed. There are three formats for sending back. For details, please refer to https://api.slack.com/events/url_verification.

HTTP 200 OK
Content-type: text/plain
3eZbrw1aBm2rZgRNFdxV2595E9CY3gmdALWMmHkvFXO7tYXAYM8P
HTTP 200 OK
Content-type: application/x-www-form-urlencoded
challenge=3eZbrw1aBm2rZgRNFdxV2595E9CY3gmdALWMmHkvFXO7tYXAYM8P
HTTP 200 OK
Content-type: application/json
{"challenge":"3eZbrw1aBm2rZgRNFdxV2595E9CY3gmdALWMmHkvFXO7tYXAYM8P"}

About AWS lambda input / output

When defining AWS lambda I / O in Java, you need to implement a handler implementation, which can be done in three ways. I will write it briefly here, but see below for details https://docs.aws.amazon.com/ja_jp/lambda/latest/dg/java-programming-model-handler-types.html https://docs.aws.amazon.com/ja_jp/lambda/latest/dg/java-handler-using-predefined-interfaces.html

1. Load the handler method directly

It uses the default implemented types without defining any special input / outout types. Implement as follows


outputType handler-name(inputType input, Context context) {
   ...
}

It seems that ʻinputType, ʻoutputType of this supports string type, integer type, Boolean type, map type, and list type by default. (See below for details https://docs.aws.amazon.com/ja_jp/lambda/latest/dg/java-programming-model-req-resp.html)

For example, the following implementation is possible

package example;

import com.amazonaws.services.lambda.runtime.Context; 

public class Hello {
    public String myHandler(String name, Context context) {
        return String.format("Hello %s.", name);
    }
}

2. Use POJO type

If you want to specify your own type for ʻinputType and ʻoutputType, you can also define and use the type that matches the input / output format.

This can be implemented as follows:

package example;

import com.amazonaws.services.lambda.runtime.Context; 

public class HelloPojo {

    // Define two classes/POJOs for use with Lambda function.
    public static class RequestClass {
      ...
    }

    public static class ResponseClass {
      ...
    }

    public static ResponseClass myHandler(RequestClass request, Context context) {
        String greetingString = String.format("Hello %s, %s.", request.getFirstName(), request.getLastName());
        return new ResponseClass(greetingString);
    }
}

3. Use RequestStreamHandler

There is also a way to enable any I / O using ʻInputStream and ʻOutputStream. You can reply by writing a byte string to ʻOutputStream`. An example is a program that returns a given string in uppercase.

package example;

import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;

import com.amazonaws.services.lambda.runtime.RequestStreamHandler;
import com.amazonaws.services.lambda.runtime.Context; 

public class Hello implements RequestStreamHandler {
    // if input is "test", then return "TEST"
    public void handleRequest(InputStream inputStream, OutputStream outputStream, Context context)
            throws IOException {
        int letter;
        while((letter = inputStream.read()) != -1)
        {
            outputStream.write(Character.toUpperCase(letter));
        }
    }
}

This time, it's hard to receive nested json as POJO type, so I'll use RequestStreamHandler.

Make lambda function compatible with slack authentication

Based on the above, we will code the lambda function, but unlike python etc., in the case of Java, the code cannot be edited inline. Therefore, this time, I will create a java project with local maven and upload it in a zip using gradle.

Creating a project

First, create a project with maven. (Maven is used only for creating projects, so it doesn't matter if you create it in any other way.)

$ mvn archetype:generate

You may be asked a lot on the way, but except for groupId and ʻartifactId, you can enter. This time, groupId is jp.com.hoge, and ʻartifactId is SampleBot. When executed, a folder with the project name will be created and src / main / java / jp / com / hoge / App.java will be generated.

Editing App.java

Edit App.java as follows. Even if you fail to get the contents of challenge, if you do not send back some character string, it will be considered as a timeout, and the event will be sent again after 1 minute and 5 minutes, so for the time being" OK I will reply with ".

src/main/java/jp/com/hoge/App.java


package jp.com.hoge;

import java.io.*;
import java.net.URLEncoder;
import java.net.HttpURLConnection;
import java.net.URL;
import java.nio.charset.Charset;
import java.lang.StringBuilder;
import java.util.HashMap;
import java.util.Map;
import java.util.Random;
import org.json.JSONException;
import org.json.JSONObject;
import org.json.JSONArray;

import com.amazonaws.services.lambda.runtime.*;

public class App implements RequestStreamHandler
{
    @Override
    public void handleRequest(InputStream inputStream, OutputStream outputStream, Context context) throws IOException {
        //Default response If you don't reply anything, the same event will be sent several times
        String response = "HTTP 200 OK\nContent-type: text/plain\nOK";
        try{
            BufferedReader rd = new BufferedReader(new InputStreamReader(inputStream, Charset.forName("UTF-8"))); 
            String jsonText = readAll(rd);
            System.out.println(jsonText); //System.The output of out is written to CloudWatch Logs
            JSONObject json = new JSONObject(jsonText);
            if (json.has("type")) {
                String eventType = json.get("type").toString();
                if (eventType.equals("url_verification")) {
                    //Set the content of challenge to response
                    response = "HTTP 200 OK\nContent-type: text/plain\n" + json.get("challenge").toString();
                }
            }
        } catch(IOException e) {
            e.printStackTrace();
        } finally {
            //Write the contents of response to outputStream
            outputStream.write(response.getBytes());
            outputStream.flush();
            outputStream.close();
        }
        return;
    }

    /* get String from BufferedReader */
    private static String readAll(Reader rd) throws IOException {
        StringBuilder sb = new StringBuilder();
        int cp;
        while ((cp = rd.read()) != -1) {
            sb.append((char) cp);
        }
        return sb.toString();
    }
}

Edit build.gradle

Also, place build.gradle directly under the project folder. If you have other libraries you want to use, write them in dependencies or create a lib folder and put the .jar file in it.

build.gradle


apply plugin: 'java'

repositories {
    mavenCentral()
}

dependencies {
    compile (
        'com.amazonaws:aws-lambda-java-core:1.1.0',
        'com.amazonaws:aws-lambda-java-events:1.1.0',
        'org.json:json:20180813'
    )
    testCompile 'junit:junit:4.+'
}

task buildZip(type: Zip) {
    from compileJava
    from processResources              
    into('lib') {
        from configurations.compileClasspath
    }           
}

build.dependsOn buildZip

Build with gradle

When you reach this point, execute the following directly under the project folder to build.

$ gradle build

If the build is successful, build / distribution / SampleBot.zip should be generated.

Test execution of lambda function

Open the AWS lambda console and upload the SampleBot.zip you just created. Enter jp.com.hoge.App in the handler and don't forget to save it. image.png

Execution of test event

Let's test the lambda function first. Select "Create test event" from the down arrow in the box that says "Select test event ..." at the top right of the screen, and create it as shown in the figure below.

image.png

Click the "Test" button at the top right of the screen to run the test. If successful, the execution result will be displayed, and if you press "Details", it will appear as shown below. You can see that the output is done properly image.png

Looking at CloudWatch, you can see the contents of the inputStream output as a log. image.png

Subscribe to Slack Event API

At this point, you can finally subscribe to the Slack Event API! Select "Event Subscriptions" on the left side of the page when you created the bot image.png Turn on Enable Events and enter the URL you wrote down when deploying the API in the Request URL field. Once the response is confirmed, it will look like the one below and you will be able to subscribe to events! image.png

This time, let's receive an event called ʻapp_mention`. This is an event that reacts when a bot is mentioned with an @. Press "Forget Save changes" once added image.png

bot installation

Then install the bot in your workspace Select "OAuth & Permissions" and scroll down to the Scopes menu. Select "Send messages as SampleBot`" and "Save Changes". You can now send a message as a SampleBot. image.png

Then install with "Install App to Workspace". ʻOAuth Access TokenandBot User OAuth Access Token` are displayed, so make a note of them as well. image.png

Make a parrot return bot

From here, I will create an example of a bot that responds to a statement addressed to me by returning a parrot. The whole source code is posted on github https://github.com/kamata1729/sampleSlackBotWithJava

The ʻapp_mention` event is sent as follows.

{
    "token": "ZZZZZZWSxiZZZ2yIvs3peJ",
    "team_id": "T061EG9R6",
    "api_app_id": "A0MDYCDME",
    "event": {
        "type": "app_mention",
        "user": "U061F7AUR",
        "text": "What is the hour of the pearl, <@U0LAN0Z89>?",
        "ts": "1515449522.000016",
        "channel": "C0LAN2Q65",
        "event_ts": "1515449522000016"
    },
    "type": "event_callback",
    "event_id": "Ev0LAN670R",
    "event_time": 1515449522000016,
    "authed_users": [
        "U0LAN0Z89"
    ]
}

On the other hand, create a bot on the same channel that posts " What is the hour of the pearl, <@ U061F7AUR>? ", Changing the mention to the other person's username.

Get bot user id

We need to replace the user id in the text, so get the user id of the bot. First, get the token of waork space from the following. https://api.slack.com/custom-integrations/legacy-tokens Use that token to access the following url, get the id of samplebot, and make a note of it. A string starting with an uppercase U. https://slack.com/api/users.list?token=取得したtoken

Registration of environment variables

I wrote it down as an environment variable on the AWS lambda page,

Register the three. You can now retrieve it from the System.getenv method. image.png

Post a message using the chat.postMessage API

You can post a message by throwing JSON into the chat.postMessage API. The json at that time is sent in this way.

{
    "token": SLACK_APP_AUTH_TOKEN,
    "channel": channel,
    "text": message,
    "username": "sampleBot"
}

At that time, as Request Property, " Content-Type ":" application / json; charset = UTF-8 ", "Authorization": "Bearer " + SLACK_BOT_USER_ACCESS_TOKEN Must be set.

Based on the above, I edited App.java as follows.

App.java


package jp.com.hoge;

import java.io.*;
import java.net.URLEncoder;
import java.net.HttpURLConnection;
import java.net.ProtocolException;
import java.net.URL;
import java.nio.charset.Charset;
import java.lang.StringBuilder;
import java.util.HashMap;
import java.util.Map;
import java.util.Random;
import org.json.JSONException;
import org.json.JSONObject;
import org.json.JSONArray;

import com.amazonaws.services.lambda.runtime.*;

public class App implements RequestStreamHandler
{
    public static String SLACK_BOT_USER_ACCESS_TOKEN = "";
    public static String SLACK_APP_AUTH_TOKEN = "";
    public static String USER_ID = "";

    @Override
    public void handleRequest(InputStream inputStream, OutputStream outputStream, Context context) throws IOException {
        //Reading environment variables
        App.SLACK_BOT_USER_ACCESS_TOKEN = System.getenv("SLACK_BOT_USER_ACCESS_TOKEN");
        App.SLACK_APP_AUTH_TOKEN = System.getenv("SLACK_APP_AUTH_TOKEN");
        App.USER_ID = System.getenv("USER_ID");

        //Default response If you don't reply anything, the same event will be sent several times
        String response = "HTTP 200 OK\nContent-type: text/plain\nOK";
        try{
            BufferedReader rd = new BufferedReader(new InputStreamReader(inputStream, Charset.forName("UTF-8"))); 
            String jsonText = readAll(rd);
            System.out.println(jsonText); //System.The output of out is written to CloudWatch Logs
            JSONObject json = new JSONObject(jsonText);

            //When testing the Event API
            if (json.has("type")) {
                String eventType = json.get("type").toString();
                if (eventType.equals("url_verification")) {
                    //Set the content of challenge to response
                    response = "HTTP 200 OK\nContent-type: text/plain\n" + json.get("challenge").toString();
                }
            }
            
            // app_At the time of the mention event
            if (json.has("event")) {
                JSONObject eventObject = json.getJSONObject("event");
                if(eventObject.has("type")) {
                    String eventType = eventObject.get("type").toString();
                    if (eventType.equals("app_mention")){
                        String user = eventObject.get("user").toString();
                        if (user.equals(App.USER_ID)) { return; } //Ignore if the statement is the bot user himself
                        String channel = eventObject.get("channel").toString();
                        String text = eventObject.get("text").toString();
                        String responseText = text.replace(App.USER_ID, user);
                        System.out.println(responseText);
                        System.out.println(postMessage(responseText, channel));
                    }
                }
            }       
        } catch(IOException e) {
            e.printStackTrace();
        } finally {
            //Write the contents of response to outputStream
            outputStream.write(response.getBytes());
            outputStream.flush();
            outputStream.close();
        }
        return;
    }

    /* get String from BufferedReader */
    private static String readAll(Reader rd) throws IOException {
        StringBuilder sb = new StringBuilder();
        int cp;
        while ((cp = rd.read()) != -1) {
            sb.append((char) cp);
        }
        return sb.toString();
    }

    /* post message to selected channel */
    public static String postMessage(String message, String channel) {
        String strUrl = "https://slack.com/api/chat.postMessage";
        String ret = "";
        URL url;

        HttpURLConnection urlConnection = null;
        try {
            url = new URL(strUrl);
            urlConnection = (HttpURLConnection) url.openConnection();
        } catch(IOException e) {
            e.printStackTrace();
            return "IOException";
        }
        
        urlConnection.setDoOutput(true);
        urlConnection.setConnectTimeout(100000);
        urlConnection.setReadTimeout(100000);
        urlConnection.setRequestProperty("Content-Type", "application/json; charset=UTF-8");
        String auth = "Bearer " + App.SLACK_BOT_USER_ACCESS_TOKEN;
        urlConnection.setRequestProperty("Authorization", auth);

        try {
            urlConnection.setRequestMethod("POST");
        } catch(ProtocolException e) {
            e.printStackTrace();
            return "ProtocolException";
        }

        try {
            urlConnection.connect();
        } catch(IOException e) {
            e.printStackTrace();
            return "IOException";
        }

        HashMap<String, Object> jsonMap = new HashMap<>();
        jsonMap.put("token", App.SLACK_APP_AUTH_TOKEN);
        jsonMap.put("channel", channel);
        jsonMap.put("text", message);
        jsonMap.put("username", " sampleBot");

        OutputStream outputStream = null;
        try {
            outputStream = urlConnection.getOutputStream();
        } catch(IOException e) {
            e.printStackTrace();
            return "IOException";
        }

        if (jsonMap.size() > 0) {
            JSONObject responseJsonObject = new JSONObject(jsonMap);
            String jsonText = responseJsonObject.toString();
            PrintStream ps = new PrintStream(outputStream);
            ps.print(jsonText);
            ps.close();
        }

        try {
            if (outputStream != null) {
                outputStream.close();
            }
            int responseCode = urlConnection.getResponseCode();
            BufferedReader rd = new BufferedReader(new InputStreamReader(urlConnection.getInputStream(), "UTF-8"));
            ret = readAll(rd);
        } catch(IOException e) {
            e.printStackTrace();
            return "IOException";
        
        } finally {
            if (urlConnection != null) {
                urlConnection.disconnect();
            }         
        }
        return ret;
    }
}

Just upload it with gradle build as before and you're done!

State of operation

So far, we have something that actually works. But there are still pitfalls. That's because slack has a timeout of only 3 seconds, so if you include the time to start the lambda function, it will time out unexpectedly quickly!

Success story

It works as expected without timing out. samplebot.gif

Failure example

This is the result of moving it after a while. The timeout time has passed, probably because I need to start the lambda function again. Since ** slack judged it as an error and resent the event several times **, the lambda function was called multiple times and posted multiple times. samplebot2.gif

Solution

It's been a long time so far, so I'll explain how to solve this in the next article. Please see that as well [AWS lambda] Prevent Slack Bot from responding multiple times with timeouts

Recommended Posts

Create a SlackBot with AWS lambda & API Gateway in Java
Implement API Gateway Lambda Authorizer in Java Lambda
Create a CSR with extended information in Java
I can't create a Java class with a specific name in IntelliJ
Create a periodical program with Ruby x AWS Lambda x CloudWatch Events
AWS Lambda with Java starting now Part 1
Create a TODO app in Java 7 Create Header
Split a string with ". (Dot)" in Java
Working with huge JSON in Java Lambda
With [AWS] CodeStar, you can build a Spring (Java) project running on Lambda in just 3 minutes! !!
I wrote a Lambda function in Java and deployed it with SAM
I want to ForEach an array with a Lambda expression in Java
Interact with LINE Message API using Lambda (Java)
Read a string in a PDF file with Java
Create a simple bulletin board with Java + MySQL
[Windows] [IntelliJ] [Java] [Tomcat] Create a Tomcat9 environment with IntelliJ
Let's create a timed process with Java Timer! !!
Create a Lambda Container Image based on Java 15
[Java] Create something like a product search API
Try to create a bulletin board in Java
How to use Java framework with AWS Lambda! ??
[Java] Create a collection with only one element
Let's create a super-simple web framework in Java
How to use Java API with lambda expression
Create a web api server with spring boot
Let's make a calculator application with Java ~ Create a display area in the window
Validate the identity token of a user authenticated with AWS Cognito in Java
API Gateway ↔ Lambda ↔ Dynamo
Zabbix API in Java
Create JSON in Java
Submit a job to AWS Batch with Java (Eclipse)
How to deploy Java to AWS Lambda with Serverless Framework
[Beginner] Create a competitive game with basic Java knowledge
I tried to create a Clova skill in Java
How to create a data URI (base64) in Java
Quickly implement a singleton with an enum in Java
[Note] Create a java environment from scratch with docker
Output true with if (a == 1 && a == 2 && a == 3) in Java (Invisible Identifier)
Let's write a proxy integrated Lambda function of Amazon API Gateway with Spring Cloud Function
Let's create a TODO application in Java 11 Exception handling when accessing TODO with a non-existent ID
Use Lambda Layers with Java
I tried to create a java8 development environment with Chocolatey
Create XML-RPC API with Wicket
Regularly post imaged tweets on Twitter with AWS Lambda + Java
Log aggregation and analysis (working with AWS Athena in Java)
Create hyperlinks in Java PowerPoint
Handle exceptions coolly with Java 8 lambda expressions and Stream API
Java Stream API in 5 minutes
[Java] Create a temporary file
Create a playground with Xcode 12
Create Azure Functions in Java
Create a method to return the tax rate in Java
Even in Java, I want to output true with a == 1 && a == 2 && a == 3
Cognito ↔ API Gateway ↔ Lambda ↔ DynamoDB
Build AWS Lambda with Quarkus
Generate AWS Signature V4 in Java and request an API
Create a simple DRUD application with Java + SpringBoot + Gradle + thymeleaf (1)
I tried to create an API to get data from a spreadsheet in Ruby (with service account)
Create and integrate Slack App and AWS Lambda (for ruby) in 30 minutes
Create a simple web server with the Java standard library com.sun.net.httpserver
Let's create a TODO application in Java 4 Implementation of posting function