forked from slackapi/java-slack-sdk
-
Notifications
You must be signed in to change notification settings - Fork 2
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Add the Socket Mode support slackapi#616
- Loading branch information
Showing
51 changed files
with
2,513 additions
and
18 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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> |
113 changes: 113 additions & 0 deletions
113
bolt-socket-mode/src/main/java/com/slack/api/bolt/socket_mode/SocketModeApp.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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(); | ||
} | ||
} |
4 changes: 4 additions & 0 deletions
4
bolt-socket-mode/src/main/java/com/slack/api/bolt/socket_mode/package-info.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; |
17 changes: 17 additions & 0 deletions
17
bolt-socket-mode/src/main/java/com/slack/api/bolt/socket_mode/request/SocketModeRequest.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; | ||
} |
61 changes: 61 additions & 0 deletions
61
...et-mode/src/main/java/com/slack/api/bolt/socket_mode/request/SocketModeRequestParser.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; | ||
} | ||
|
||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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"; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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(); | ||
} | ||
} |
Oops, something went wrong.