Skip to content

Commit

Permalink
Add Jakarta EE compatible Socket Mode client ref: #919 (#1352)
Browse files Browse the repository at this point in the history
Co-authored-by: William Bergamin <[email protected]>
  • Loading branch information
seratch and WilliamBergamin authored Aug 23, 2024
1 parent e542f29 commit 9cb6946
Show file tree
Hide file tree
Showing 51 changed files with 4,944 additions and 24 deletions.
97 changes: 97 additions & 0 deletions bolt-jakarta-socket-mode/pom.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
<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.41.1-SNAPSHOT</version>
</parent>

<properties>
<tyrus-standalone-client.version>2.2.0</tyrus-standalone-client.version>
<jakarta.websocket-api.version>2.2.0</jakarta.websocket-api.version>
</properties>

<artifactId>bolt-jakarta-socket-mode</artifactId>
<version>1.41.1-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>
<exclusions>
<exclusion>
<groupId>javax.websocket</groupId>
<artifactId>javax.websocket-api</artifactId>
</exclusion>
<exclusion>
<groupId>org.glassfish.tyrus.bundles</groupId>
<artifactId>tyrus-standalone-client</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>com.slack.api</groupId>
<artifactId>slack-jakarta-socket-mode-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>jakarta.websocket</groupId>
<artifactId>jakarta.websocket-client-api</artifactId>
<version>${jakarta.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.eclipse.jetty</groupId>
<artifactId>jetty-servlet</artifactId>
<version>${jetty-for-tests.version}</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.eclipse.jetty</groupId>
<artifactId>jetty-server</artifactId>
<version>${jetty-for-tests.version}</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.eclipse.jetty</groupId>
<artifactId>jetty-webapp</artifactId>
<version>${jetty-for-tests.version}</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.eclipse.jetty.websocket</groupId>
<artifactId>websocket-server</artifactId>
<version>${jetty-for-tests.version}</version>
<scope>test</scope>
</dependency>
</dependencies>

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

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

import java.io.IOException;
import java.util.HashMap;
import java.util.Map;
import java.util.function.Function;
import java.util.function.Supplier;

@Slf4j
public class SocketModeApp {
private boolean clientStopped = true;
private final App app;
private final Supplier<SocketModeClient> clientFactory;
private SocketModeClient client;

private static final Function<ErrorContext, Response> DEFAULT_ERROR_HANDLER = (context) -> {
Exception e = context.getException();
log.error("Failed to handle a request: {}", e.getMessage(), e);
return null;
};

@Data
@Builder
public static class ErrorContext {
private Request<?> request;
private Exception exception;
}

// -------------------------------------------

private static void sendSocketModeResponse(
SocketModeClient client,
Gson gson,
SocketModeRequest req,
Response boltResponse
) {
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()));
}
}

private static Supplier<SocketModeClient> buildSocketModeClientFactory(
App app,
String appToken,
Function<ErrorContext, Response> errorHandler
) {
return () -> {
try {
final SocketModeClient client = JakartaSocketModeClientFactory.create(app.slack(), appToken);
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;
}
sendSocketModeResponse(client, gson, req, boltResponse);
} catch (Exception e) {
ErrorContext context = ErrorContext.builder().request(req.getBoltRequest()).exception(e).build();
Response errorResponse = errorHandler.apply(context);
if (errorResponse != null) {
sendSocketModeResponse(client, gson, req, errorResponse);
}
} finally {
long spentMillis = System.currentTimeMillis() - startMillis;
log.debug("Response time: {} milliseconds", spentMillis);
}
}
});
return client;
} catch (IOException e) {
log.error("Failed to start a new Socket Mode client (error: {})", e.getMessage(), e);
return null;
}
};
}

public SocketModeApp(App app) throws IOException {
this(System.getenv("SLACK_APP_TOKEN"), app);
}


public SocketModeApp(String appToken, App app) throws IOException {
this(appToken, DEFAULT_ERROR_HANDLER, app);
}

public SocketModeApp(
String appToken,
Function<ErrorContext, Response> errorHandler,
App app
) throws IOException {
this(buildSocketModeClientFactory(app, appToken, errorHandler), app);
}

public SocketModeApp(
String appToken,
App app,
Function<ErrorContext, Response> errorHandler
) throws IOException {
this(buildSocketModeClientFactory(app, appToken, errorHandler), app);
}

public SocketModeApp(Supplier<SocketModeClient> clientFactory, App app) {
this.clientFactory = clientFactory;
this.app = app;
}

/**
* If you would like to synchronously detect the connection error as an exception when bootstrapping,
* use this constructor. The first line can throw an exception
* in the case where either the token or network settings are valid.
*
* <code>
* SocketModeClient client = JakartaSocketModeClientFactory.create(appToken);
* SocketModeApp socketModeApp = new SocketModeApp(client, app);
* </code>
*/
public SocketModeApp(SocketModeClient socketModeClient, App app) {
this.client = socketModeClient;
this.clientFactory = () -> 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 {
this.app.start();
if (this.client == null) {
this.client = clientFactory.get();
}
if (this.isClientStopped()) {
this.client.connectToNewEndpoint();
} else {
this.client.connect();
}
this.client.setAutoReconnectEnabled(true);
this.clientStopped = false;
if (blockCurrentThread) {
Thread.sleep(Long.MAX_VALUE);
}
}

public void stop() throws Exception {
if (this.client != null && this.client.verifyConnection()) {
this.client.disconnect();
}
this.clientStopped = true;
this.app.stop();
}

public void close() throws Exception {
this.stop();
this.client = null;
}

// -------------------------------------------
// Accessors
// -------------------------------------------

public boolean isClientStopped() {
return clientStopped;
}

public SocketModeClient getClient() {
return client;
}

public App getApp() {
return app;
}
}
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.jakarta_socket_mode;
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
package com.slack.api.bolt.jakarta_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;
}
Loading

0 comments on commit 9cb6946

Please sign in to comment.