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
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.
And add Bot User to the created app
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.
Open IAM from the AWS console and create a new role.
Since it will be used in lambda, select "lambda" and proceed first.
This time I want to be able to output the log to CloudWatch Log, so select ʻAWS LambdaBasicExecutionRolewith write permission
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
.
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.
First, select "Create API" from the API Gateway page of the aws console, and create it with the API name (sampleBotAPI
this time).
From the created screen, select Action-> Create Method
to add the POST
method.
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.
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!
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"}
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
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);
}
}
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);
}
}
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.
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.
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.
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();
}
}
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
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.
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.
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.
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
Looking at CloudWatch, you can see the contents of the inputStream output as a log.
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 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!
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
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.
Then install with "Install App to Workspace".
ʻOAuth Access Tokenand
Bot User OAuth Access Token` are displayed, so make a note of them as well.
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.
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
I wrote it down as an environment variable on the AWS lambda page,
SLACK_BOT_USER_ACCESS_TOKEN
: token starting with xoxp-SLACK_APP_AUTH_TOKEN
: token starting with xoxb-Register the three. You can now retrieve it from the System.getenv
method.
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!
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!
It works as expected without timing out.
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.
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