Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add Jakarta EE compatible Socket Mode client ref: #919 #1352

Merged
merged 3 commits into from
Aug 23, 2024
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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;

Check warning on line 88 in bolt-jakarta-socket-mode/src/main/java/com/slack/api/bolt/jakarta_socket_mode/SocketModeApp.java

View check run for this annotation

Codecov / codecov/patch

bolt-jakarta-socket-mode/src/main/java/com/slack/api/bolt/jakarta_socket_mode/SocketModeApp.java#L86-L88

Added lines #L86 - L88 were not covered by tests
}
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);

Check warning on line 95 in bolt-jakarta-socket-mode/src/main/java/com/slack/api/bolt/jakarta_socket_mode/SocketModeApp.java

View check run for this annotation

Codecov / codecov/patch

bolt-jakarta-socket-mode/src/main/java/com/slack/api/bolt/jakarta_socket_mode/SocketModeApp.java#L95

Added line #L95 was not covered by tests
}
} 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;

Check warning on line 106 in bolt-jakarta-socket-mode/src/main/java/com/slack/api/bolt/jakarta_socket_mode/SocketModeApp.java

View check run for this annotation

Codecov / codecov/patch

bolt-jakarta-socket-mode/src/main/java/com/slack/api/bolt/jakarta_socket_mode/SocketModeApp.java#L104-L106

Added lines #L104 - L106 were not covered by tests
}
};
}

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

Check warning on line 113 in bolt-jakarta-socket-mode/src/main/java/com/slack/api/bolt/jakarta_socket_mode/SocketModeApp.java

View check run for this annotation

Codecov / codecov/patch

bolt-jakarta-socket-mode/src/main/java/com/slack/api/bolt/jakarta_socket_mode/SocketModeApp.java#L112-L113

Added lines #L112 - L113 were not covered by tests


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);
}

Check warning on line 134 in bolt-jakarta-socket-mode/src/main/java/com/slack/api/bolt/jakarta_socket_mode/SocketModeApp.java

View check run for this annotation

Codecov / codecov/patch

bolt-jakarta-socket-mode/src/main/java/com/slack/api/bolt/jakarta_socket_mode/SocketModeApp.java#L133-L134

Added lines #L133 - L134 were not covered by tests

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;
}

Check warning on line 155 in bolt-jakarta-socket-mode/src/main/java/com/slack/api/bolt/jakarta_socket_mode/SocketModeApp.java

View check run for this annotation

Codecov / codecov/patch

bolt-jakarta-socket-mode/src/main/java/com/slack/api/bolt/jakarta_socket_mode/SocketModeApp.java#L151-L155

Added lines #L151 - L155 were not covered by tests

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

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

Check warning on line 161 in bolt-jakarta-socket-mode/src/main/java/com/slack/api/bolt/jakarta_socket_mode/SocketModeApp.java

View check run for this annotation

Codecov / codecov/patch

bolt-jakarta-socket-mode/src/main/java/com/slack/api/bolt/jakarta_socket_mode/SocketModeApp.java#L160-L161

Added lines #L160 - L161 were not covered by tests

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();

Check warning on line 175 in bolt-jakarta-socket-mode/src/main/java/com/slack/api/bolt/jakarta_socket_mode/SocketModeApp.java

View check run for this annotation

Codecov / codecov/patch

bolt-jakarta-socket-mode/src/main/java/com/slack/api/bolt/jakarta_socket_mode/SocketModeApp.java#L175

Added line #L175 was not covered by tests
}
this.client.setAutoReconnectEnabled(true);
this.clientStopped = false;
if (blockCurrentThread) {
Thread.sleep(Long.MAX_VALUE);

Check warning on line 180 in bolt-jakarta-socket-mode/src/main/java/com/slack/api/bolt/jakarta_socket_mode/SocketModeApp.java

View check run for this annotation

Codecov / codecov/patch

bolt-jakarta-socket-mode/src/main/java/com/slack/api/bolt/jakarta_socket_mode/SocketModeApp.java#L180

Added line #L180 was not covered by tests
}
}

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;

Check warning on line 210 in bolt-jakarta-socket-mode/src/main/java/com/slack/api/bolt/jakarta_socket_mode/SocketModeApp.java

View check run for this annotation

Codecov / codecov/patch

bolt-jakarta-socket-mode/src/main/java/com/slack/api/bolt/jakarta_socket_mode/SocketModeApp.java#L210

Added line #L210 was not covered by tests
}
}
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