Skip to content

Commit

Permalink
Add the Socket Mode support slackapi#616
Browse files Browse the repository at this point in the history
  • Loading branch information
seratch committed Jan 6, 2021
1 parent f086a93 commit 1414021
Show file tree
Hide file tree
Showing 51 changed files with 2,513 additions and 18 deletions.
78 changes: 78 additions & 0 deletions bolt-socket-mode/pom.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>

<parent>
<groupId>com.slack.api</groupId>
<artifactId>slack-sdk-parent</artifactId>
<version>1.5.0-SNAPSHOT</version>
</parent>

<properties>
<javax.websocket-api.version>1.1</javax.websocket-api.version>
<tyrus-standalone-client.version>1.17</tyrus-standalone-client.version>
<java-websocket.version>1.5.1</java-websocket.version>
</properties>

<groupId>com.slack.api</groupId>
<artifactId>bolt-socket-mode</artifactId>
<version>1.5.0-SNAPSHOT</version>
<packaging>jar</packaging>

<dependencies>
<dependency>
<groupId>com.slack.api</groupId>
<artifactId>slack-api-model</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>com.slack.api</groupId>
<artifactId>slack-api-client</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>com.slack.api</groupId>
<artifactId>slack-app-backend</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>com.slack.api</groupId>
<artifactId>bolt</artifactId>
<version>${project.version}</version>
</dependency>

<dependency>
<groupId>javax.websocket</groupId>
<artifactId>javax.websocket-api</artifactId>
<version>${javax.websocket-api.version}</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.glassfish.tyrus.bundles</groupId>
<artifactId>tyrus-standalone-client</artifactId>
<version>${tyrus-standalone-client.version}</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.java-websocket</groupId>
<artifactId>Java-WebSocket</artifactId>
<version>${java-websocket.version}</version>
<scope>provided</scope>
</dependency>

<dependency>
<groupId>org.glassfish.tyrus.bundles</groupId>
<artifactId>tyrus-standalone-client</artifactId>
<version>${tyrus-standalone-client.version}</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.java-websocket</groupId>
<artifactId>Java-WebSocket</artifactId>
<version>${java-websocket.version}</version>
<scope>test</scope>
</dependency>
</dependencies>

</project>
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
package com.slack.api.bolt.socket_mode;

import com.google.gson.Gson;
import com.google.gson.JsonElement;
import com.slack.api.bolt.App;
import com.slack.api.bolt.response.Response;
import com.slack.api.bolt.socket_mode.request.SocketModeRequest;
import com.slack.api.bolt.socket_mode.request.SocketModeRequestParser;
import com.slack.api.socket_mode.SocketModeClient;
import com.slack.api.socket_mode.response.AckResponse;
import com.slack.api.util.json.GsonFactory;
import lombok.extern.slf4j.Slf4j;

import java.io.IOException;
import java.util.HashMap;
import java.util.Map;

@Slf4j
public class SocketModeApp {
private boolean clientStopped = false;
private final SocketModeClient client;
private final App app;

private static SocketModeClient buildSocketModeClient(
App app,
String appToken,
SocketModeClient.Backend backend
) throws IOException {
final SocketModeClient client = app.slack().socketMode(appToken, backend);
final SocketModeRequestParser requestParser = new SocketModeRequestParser(app.config());
final Gson gson = GsonFactory.createSnakeCase(app.slack().getConfig());
client.addWebSocketMessageListener(message -> {
long startMillis = System.currentTimeMillis();
SocketModeRequest req = requestParser.parse(message);
if (req != null) {
try {
Response boltResponse = app.run(req.getBoltRequest());
if (boltResponse.getStatusCode() != 200) {
log.warn("Unsuccessful Bolt app execution (status: {}, body: {})",
boltResponse.getStatusCode(), boltResponse.getBody());
return;
}
if (boltResponse.getBody() != null) {
Map<String, Object> response = new HashMap<>();
if (boltResponse.getContentType().startsWith("application/json")) {
response.put("envelope_id", req.getEnvelope().getEnvelopeId());
response.put("payload", gson.fromJson(boltResponse.getBody(), JsonElement.class));
} else {
response.put("envelope_id", req.getEnvelope().getEnvelopeId());
Map<String, Object> payload = new HashMap<>();
payload.put("text", boltResponse.getBody());
response.put("payload", payload);
}
client.sendSocketModeResponse(gson.toJson(response));
} else {
client.sendSocketModeResponse(new AckResponse(req.getEnvelope().getEnvelopeId()));
}
long spentMillis = System.currentTimeMillis() - startMillis;
log.debug("Response time: {} milliseconds", spentMillis);
return;
} catch (Exception e) {
log.error("Failed to handle a request: {}", e.getMessage(), e);
return;
}
}
});
return client;
}

public SocketModeApp(String appToken, App app) throws IOException {
this(appToken, SocketModeClient.Backend.Tyrus, app);
}

public SocketModeApp(
String appToken,
SocketModeClient.Backend backend,
App app
) throws IOException {
this(buildSocketModeClient(app, appToken, backend), app);
}

public SocketModeApp(SocketModeClient socketModeClient, App app) {
this.client = socketModeClient;
this.app = app;
}

public void start() throws Exception {
run(true);
}

public void startAsync() throws Exception {
run(false);
}

public void run(boolean blockCurrentThread) throws Exception {
app.start();
if (clientStopped) {
client.connectToNewEndpoint();
} else {
client.connect();
}
client.setAutoReconnectEnabled(true);
if (blockCurrentThread) {
Thread.sleep(Long.MAX_VALUE);
}
}

public void stop() throws Exception {
client.disconnect();
clientStopped = true;
app.stop();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
/**
* Built-in Socket Mode adapter supports.
*/
package com.slack.api.bolt.socket_mode;
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
package com.slack.api.bolt.socket_mode.request;

import com.slack.api.bolt.request.Request;
import com.slack.api.socket_mode.request.SocketModeEnvelope;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;

@Data
@AllArgsConstructor
@NoArgsConstructor
@Builder
public class SocketModeRequest {
private SocketModeEnvelope envelope;
private Request<?> boltRequest;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
package com.slack.api.bolt.socket_mode.request;

import com.google.gson.Gson;
import com.google.gson.JsonElement;
import com.slack.api.bolt.AppConfig;
import com.slack.api.bolt.request.RequestHeaders;
import com.slack.api.bolt.util.SlackRequestParser;
import com.slack.api.socket_mode.request.EventsApiEnvelope;
import com.slack.api.socket_mode.request.InteractiveEnvelope;
import com.slack.api.socket_mode.request.SlashCommandsEnvelope;
import com.slack.api.socket_mode.request.SocketModeEnvelope;
import com.slack.api.util.json.GsonFactory;
import lombok.Data;

import java.util.Arrays;
import java.util.Collections;
import java.util.List;

public class SocketModeRequestParser {
private static final Gson GSON = GsonFactory.createSnakeCase();
private final SlackRequestParser slackRequestParser;

public SocketModeRequestParser(AppConfig appConfig) {
this.slackRequestParser = new SlackRequestParser(appConfig);
}

@Data
public static class GenericSocketModeEnvelope implements SocketModeEnvelope {
private String type;
private String envelopeId;
private Boolean acceptsResponsePayload;
private JsonElement payload;
private Integer retryAttempt;
private String retryReason;
}

private static final List<String> ENVELOPE_TYPES = Arrays.asList(
EventsApiEnvelope.TYPE,
InteractiveEnvelope.TYPE,
SlashCommandsEnvelope.TYPE
);

public SocketModeRequest parse(String message) {
GenericSocketModeEnvelope envelope = GSON.fromJson(message, GenericSocketModeEnvelope.class);
if (ENVELOPE_TYPES.contains(envelope.getType())) {
return SocketModeRequest.builder()
.envelope(envelope)
.boltRequest(slackRequestParser.parse(SlackRequestParser.HttpRequest.builder()
.socketMode(true)
.requestUri("")
.remoteAddress("")
.queryString(Collections.emptyMap())
.requestBody(GSON.toJson(envelope.getPayload()))
.headers(new RequestHeaders(Collections.emptyMap())) // TODO: retries in Events API
.build()))
.build();
}
return null;
}

}
10 changes: 10 additions & 0 deletions bolt-socket-mode/src/test/java/config/Constants.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
package config;

public class Constants {
private Constants() {
}

// Socket Mode
public static final String SLACK_SDK_TEST_SOCKET_MODE_APP_TOKEN = "SLACK_SDK_TEST_SOCKET_MODE_APP_TOKEN";
public static final String SLACK_SDK_TEST_SOCKET_MODE_BOT_TOKEN = "SLACK_SDK_TEST_SOCKET_MODE_BOT_TOKEN";
}
74 changes: 74 additions & 0 deletions bolt-socket-mode/src/test/java/samples/Examples.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
package samples;

import com.slack.api.bolt.App;
import com.slack.api.bolt.AppConfig;
import com.slack.api.bolt.socket_mode.SocketModeApp;
import com.slack.api.model.event.AppMentionEvent;
import com.slack.api.model.event.MessageEvent;
import config.Constants;

import static com.slack.api.model.block.Blocks.asBlocks;
import static com.slack.api.model.block.Blocks.input;
import static com.slack.api.model.block.composition.BlockCompositions.plainText;
import static com.slack.api.model.block.element.BlockElements.plainTextInput;
import static com.slack.api.model.view.Views.*;

public class Examples {

public static void main(String[] args) throws Exception {
App app = new App(AppConfig.builder()
.singleTeamBotToken(System.getenv(Constants.SLACK_SDK_TEST_SOCKET_MODE_BOT_TOKEN))
.build());
app.use((req, resp, chain) -> {
req.getContext().logger.info(req.getRequestBodyAsString());
return chain.next(req);
});

app.command("/hi-socket-mode", (req, ctx) -> {
return ctx.ack("Yes, I'm running in the Socket Mode!");
});

app.event(AppMentionEvent.class, (req, ctx) -> {
ctx.say("Hi there!");
return ctx.ack();
});

app.event(MessageEvent.class, (req, ctx) -> {
ctx.asyncClient().reactionsAdd(r -> r
.channel(req.getEvent().getChannel())
.name("eyes")
.timestamp(req.getEvent().getTs())
);
return ctx.ack();
});

app.globalShortcut("socket-mode-global-shortcut", (req, ctx) -> {
ctx.asyncClient().viewsOpen(r -> r
.triggerId(req.getContext().getTriggerId())
.view(view(v -> v
.type("modal")
.callbackId("test-view")
.title(viewTitle(vt -> vt.type("plain_text").text("Modal by Global Shortcut")))
.close(viewClose(vc -> vc.type("plain_text").text("Close")))
.submit(viewSubmit(vs -> vs.type("plain_text").text("Submit")))
.blocks(asBlocks(input(input -> input
.blockId("agenda-block")
.element(plainTextInput(pti -> pti.actionId("agenda-action").multiline(true)))
.label(plainText(pt -> pt.text("Detailed Agenda").emoji(true)))
)))
)));
return ctx.ack();
});

app.viewSubmission("test-view", (req, ctx) -> ctx.ack());

app.messageShortcut("socket-mode-message-shortcut", (req, ctx) -> {
ctx.respond("It works!");
return ctx.ack();
});

String appToken = System.getenv(Constants.SLACK_SDK_TEST_SOCKET_MODE_APP_TOKEN);
SocketModeApp socketModeApp = new SocketModeApp(appToken, app);
socketModeApp.start();
}
}
Loading

0 comments on commit 1414021

Please sign in to comment.