Skip to content

Commit

Permalink
Add app.function listener support
Browse files Browse the repository at this point in the history
  • Loading branch information
seratch committed May 7, 2024
1 parent ede1541 commit 4b144ec
Show file tree
Hide file tree
Showing 9 changed files with 129 additions and 35 deletions.
22 changes: 3 additions & 19 deletions bolt-socket-mode/src/test/java/samples/SimpleApp.java
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
import java.util.Arrays;
import java.util.HashMap;
import java.util.Map;
import java.util.regex.Pattern;

import static com.slack.api.model.block.Blocks.*;
import static com.slack.api.model.block.composition.BlockCompositions.dispatchActionConfig;
Expand Down Expand Up @@ -151,14 +152,11 @@ public static void main(String[] args) throws Exception {
});

// Note that this is still in beta as of Nov 2023
app.event(FunctionExecutedEvent.class, (req, ctx) -> {
// TODO: future updates enable passing callback_id as below
// app.event(FunctionExecutedEvent.class, (req, ctx) -> {
// app.function("hello", (req, ctx) -> {
// app.function(Pattern.compile("^he.+$"), (req, ctx) -> {
app.function(Pattern.compile("^he.+$"), (req, ctx) -> {
ctx.logger.info("req: {}", req);
ctx.client().chatPostMessage(r -> r
// TODO: remove this token passing by enhancing bolt internals
.token(req.getEvent().getBotAccessToken())
.channel(req.getEvent().getInputs().get("user_id").asString())
.text("hey!")
.blocks(asBlocks(actions(a -> a.blockId("b").elements(asElements(
Expand All @@ -174,14 +172,10 @@ public static void main(String[] args) throws Exception {
Map<String, Object> outputs = new HashMap<>();
outputs.put("user_id", req.getPayload().getFunctionData().getInputs().get("user_id").asString());
ctx.client().functionsCompleteSuccess(r -> r
// TODO: remove this token passing by enhancing bolt internals
.token(req.getPayload().getBotAccessToken())
.functionExecutionId(req.getPayload().getFunctionData().getExecutionId())
.outputs(outputs)
);
ctx.client().chatUpdate(r -> r
// TODO: remove this token passing by enhancing bolt internals
.token(req.getPayload().getBotAccessToken())
.channel(req.getPayload().getContainer().getChannelId())
.ts(req.getPayload().getContainer().getMessageTs())
.text("Thank you!")
Expand All @@ -190,14 +184,10 @@ public static void main(String[] args) throws Exception {
});
app.blockAction("remote-function-button-error", (req, ctx) -> {
ctx.client().functionsCompleteError(r -> r
// TODO: remove this token passing by enhancing bolt internals
.token(req.getPayload().getBotAccessToken())
.functionExecutionId(req.getPayload().getFunctionData().getExecutionId())
.error("test error!")
);
ctx.client().chatUpdate(r -> r
// TODO: remove this token passing by enhancing bolt internals
.token(req.getPayload().getBotAccessToken())
.channel(req.getPayload().getContainer().getChannelId())
.ts(req.getPayload().getContainer().getMessageTs())
.text("Thank you!")
Expand All @@ -206,8 +196,6 @@ public static void main(String[] args) throws Exception {
});
app.blockAction("remote-function-modal", (req, ctx) -> {
ctx.client().viewsOpen(r -> r
// TODO: remove this token passing by enhancing bolt internals
.token(req.getPayload().getBotAccessToken())
.triggerId(req.getPayload().getInteractivity().getInteractivityPointer())
.view(view(v -> v
.type("modal")
Expand All @@ -223,8 +211,6 @@ public static void main(String[] args) throws Exception {
)))
)));
ctx.client().chatUpdate(r -> r
// TODO: remove this token passing by enhancing bolt internals
.token(req.getPayload().getBotAccessToken())
.channel(req.getPayload().getContainer().getChannelId())
.ts(req.getPayload().getContainer().getMessageTs())
.text("Thank you!")
Expand All @@ -236,7 +222,6 @@ public static void main(String[] args) throws Exception {
Map<String, Object> outputs = new HashMap<>();
outputs.put("user_id", ctx.getRequestUserId());
ctx.client().functionsCompleteSuccess(r -> r
// TODO: remove this token passing by enhancing bolt internals
.token(req.getPayload().getBotAccessToken())
.functionExecutionId(req.getPayload().getFunctionData().getExecutionId())
.outputs(outputs)
Expand All @@ -247,7 +232,6 @@ public static void main(String[] args) throws Exception {
Map<String, Object> outputs = new HashMap<>();
outputs.put("user_id", ctx.getRequestUserId());
ctx.client().functionsCompleteSuccess(r -> r
// TODO: remove this token passing by enhancing bolt internals
.token(req.getPayload().getBotAccessToken())
.functionExecutionId(req.getPayload().getFunctionData().getExecutionId())
.outputs(outputs)
Expand Down
33 changes: 29 additions & 4 deletions bolt/src/main/java/com/slack/api/bolt/App.java
Original file line number Diff line number Diff line change
Expand Up @@ -28,10 +28,7 @@
import com.slack.api.methods.MethodsClient;
import com.slack.api.methods.SlackApiException;
import com.slack.api.methods.response.auth.AuthTestResponse;
import com.slack.api.model.event.AppUninstalledEvent;
import com.slack.api.model.event.Event;
import com.slack.api.model.event.MessageEvent;
import com.slack.api.model.event.TokensRevokedEvent;
import com.slack.api.model.event.*;
import com.slack.api.util.json.GsonFactory;
import lombok.AllArgsConstructor;
import lombok.Builder;
Expand Down Expand Up @@ -582,6 +579,7 @@ public Response run(Request request) throws Exception {
if (request == null || request.getContext() == null) {
return Response.builder().statusCode(400).body("Invalid Request").build();
}
request.getContext().setAttachingFunctionTokenEnabled(this.config().isAttachingFunctionTokenEnabled());
request.getContext().setSlack(slack()); // use the properly configured API client

if (neverStarted.get()) {
Expand Down Expand Up @@ -648,6 +646,33 @@ public App event(EventHandler<?> handler) {
return this;
}

public App function(String callbackId, BoltEventHandler<FunctionExecutedEvent> handler) {
return event(FunctionExecutedEvent.class, true, (req, ctx) -> {
if (log.isDebugEnabled()) {
log.debug("Run a function_executed event handler (callback_id: {})", callbackId);
}
if (callbackId.equals(req.getEvent().getFunction().getCallbackId())) {
return handler.apply(req, ctx);
} else {
return null;

Check warning on line 657 in bolt/src/main/java/com/slack/api/bolt/App.java

View check run for this annotation

Codecov / codecov/patch

bolt/src/main/java/com/slack/api/bolt/App.java#L657

Added line #L657 was not covered by tests
}
});
}

public App function(Pattern callbackId, BoltEventHandler<FunctionExecutedEvent> handler) {
return event(FunctionExecutedEvent.class, true, (req, ctx) -> {
if (log.isDebugEnabled()) {
log.debug("Run a function_executed event handler (callback_id: {})", callbackId);
}
String sentCallbackId = req.getEvent().getFunction().getCallbackId();
if (callbackId.matcher(sentCallbackId).matches()) {
return handler.apply(req, ctx);
} else {
return null;

Check warning on line 671 in bolt/src/main/java/com/slack/api/bolt/App.java

View check run for this annotation

Codecov / codecov/patch

bolt/src/main/java/com/slack/api/bolt/App.java#L671

Added line #L671 was not covered by tests
}
});
}

public App message(String pattern, BoltEventHandler<MessageEvent> messageHandler) {
return message(Pattern.compile("^.*" + Pattern.quote(pattern) + ".*$"), messageHandler);
}
Expand Down
9 changes: 9 additions & 0 deletions bolt/src/main/java/com/slack/api/bolt/AppConfig.java
Original file line number Diff line number Diff line change
Expand Up @@ -380,6 +380,15 @@ public void setOauthRedirectUriPath(String oauthRedirectUriPath) {
@Builder.Default
private boolean allEventsApiAutoAckEnabled = false;

/**
* When true, the framework automatically attaches context#functionBotAccessToken
* to context#client instead of context#botToken.
* Enabling this behavior only affects function_executed event handlers
* and app.action/app.view handlers associated with the function token.
*/
@Builder.Default
private boolean attachingFunctionTokenEnabled = true;

// ---------------------------------
// Default middleware configuration
// ---------------------------------
Expand Down
23 changes: 21 additions & 2 deletions bolt/src/main/java/com/slack/api/bolt/context/Context.java
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,21 @@ public abstract class Context {
* A bot token associated with this request. The format must be starting with `xoxb-`.
*/
protected String botToken;

/**
* When true, the framework automatically attaches context#functionBotAccessToken
* to context#client instead of context#botToken.
* Enabling this behavior only affects function_executed event handlers
* and app.action/app.view handlers associated with the function token.
*/
private boolean attachingFunctionTokenEnabled;

/**
* The bot token associated with this "function_executed"-type event and its interactions.
* The format must be starting with `xoxb-`.
*/
protected String functionBotAccessToken;

/**
* The scopes associated to the botToken
*/
Expand Down Expand Up @@ -88,17 +103,21 @@ public abstract class Context {
protected final Map<String, String> additionalValues = new HashMap<>();

public MethodsClient client() {
String primaryToken = (isAttachingFunctionTokenEnabled() && functionBotAccessToken != null)
? functionBotAccessToken : botToken;
// We used to pass teamId only for org-wide installations, but we changed this behavior since version 1.10.
// The reasons are 1) having teamId in the MethodsClient can reduce TeamIdCache's auth.test API calls
// 2) OpenID Connect + token rotation allows only refresh token to perform auth.test API calls.
return getSlack().methods(botToken, teamId);
return getSlack().methods(primaryToken, teamId);
}

public AsyncMethodsClient asyncClient() {
String primaryToken = (isAttachingFunctionTokenEnabled() && functionBotAccessToken != null)
? functionBotAccessToken : botToken;
// We used to pass teamId only for org-wide installations, but we changed this behavior since version 1.10.
// The reasons are 1) having teamId in the MethodsClient can reduce TeamIdCache's auth.test API calls
// 2) OpenID Connect + token rotation allows only refresh token to perform auth.test API calls.
return getSlack().methodsAsync(botToken, teamId);
return getSlack().methodsAsync(primaryToken, teamId);
}

public ChatPostMessageResponse say(BuilderConfigurator<ChatPostMessageRequest.ChatPostMessageRequestBuilder> request) throws IOException, SlackApiException {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,8 +23,14 @@ public BlockActionRequest(
this.headers = headers;
this.payload = GsonFactory.createSnakeCase().fromJson(payloadBody, BlockActionPayload.class);
if (this.payload != null) {
getContext().setFunctionBotAccessToken(payload.getBotAccessToken());
getContext().setResponseUrl(payload.getResponseUrl());
getContext().setTriggerId(payload.getTriggerId());
if (payload.getTriggerId() == null
&& payload.getInteractivity() != null
&& payload.getInteractivity().getInteractivityPointer() != null) {
getContext().setTriggerId(payload.getInteractivity().getInteractivityPointer());
}
if (payload.getEnterprise() != null) {
getContext().setEnterpriseId(payload.getEnterprise().getId());
} else if (payload.getTeam() != null) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
import com.slack.api.bolt.request.RequestHeaders;
import com.slack.api.bolt.request.RequestType;
import com.slack.api.model.event.MessageEvent;
import com.slack.api.model.event.FunctionExecutedEvent;
import com.slack.api.util.json.GsonFactory;
import lombok.ToString;

Expand Down Expand Up @@ -105,6 +106,13 @@ public EventRequest(
} else if (event.get("channel_id") != null) {
this.getContext().setChannelId(event.get("channel_id").getAsString());
}

if (this.eventType != null
&& this.eventType.equals(FunctionExecutedEvent.TYPE_NAME)
&& event.get("bot_access_token") != null) {
String functionBotAccessToken = event.get("bot_access_token").getAsString();
this.getContext().setFunctionBotAccessToken(functionBotAccessToken);
}
}

private EventContext context = new EventContext();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ public ViewClosedRequest(
getContext().setTeamId(payload.getUser().getTeamId());
}
getContext().setRequestUserId(payload.getUser().getId());
getContext().setFunctionBotAccessToken(payload.getBotAccessToken());
}

private DefaultContext context = new DefaultContext();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ public ViewSubmissionRequest(
}
getContext().setRequestUserId(payload.getUser().getId());
getContext().setResponseUrls(payload.getResponseUrls());
getContext().setFunctionBotAccessToken(payload.getBotAccessToken());
}

private ViewSubmissionContext context = new ViewSubmissionContext();
Expand Down
61 changes: 51 additions & 10 deletions bolt/src/test/java/test_locally/app/RemoteFunctionTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
import com.slack.api.bolt.request.builtin.BlockActionRequest;
import com.slack.api.bolt.request.builtin.EventRequest;
import com.slack.api.bolt.request.builtin.ViewSubmissionRequest;
import com.slack.api.bolt.request.builtin.EventRequest;
import com.slack.api.bolt.response.Response;
import com.slack.api.model.event.FunctionExecutedEvent;
import com.slack.api.util.json.GsonFactory;
Expand All @@ -26,6 +27,7 @@
import java.util.List;
import java.util.Map;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.regex.Pattern;

import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertTrue;
Expand Down Expand Up @@ -356,17 +358,13 @@ public void all_function_events() throws Exception {
called.set(req.getEvent().getFunction().getCallbackId().equals("hello")
&& req.getEvent().getInputs().get("user_id").asString().equals("U03E94MK0")
&& req.getEvent().getInputs().get("amount").asInteger().equals(1)
&& req.getEvent().getBotAccessToken().equals("xwfp-this-is-valid")
);
&& ctx.isAttachingFunctionTokenEnabled()
&& ctx.getFunctionBotAccessToken().equals("xwfp-valid"));
called.set(ctx.client().functionsCompleteSuccess(r -> r
// TODO: remove this token passing by enhancing bolt internals
.token(req.getEvent().getBotAccessToken())
.functionExecutionId(req.getEvent().getFunctionExecutionId())
.outputs(new HashMap<>())
).getError().equals(""));
called.set(ctx.client().functionsCompleteError(r -> r
// TODO: remove this token passing by enhancing bolt internals
.token(req.getEvent().getBotAccessToken())
.functionExecutionId(req.getEvent().getFunctionExecutionId())
.error("something wrong")
).getError().equals(""));
Expand All @@ -378,6 +376,52 @@ public void all_function_events() throws Exception {
assertTrue(called.get());
}

@Test
public void static_callback_id() throws Exception {
App app = buildApp();
AtomicBoolean called = new AtomicBoolean(false);
app.function("hello", (req, ctx) -> {
called.set(req.getEvent().getFunction().getCallbackId().equals("hello")
&& req.getEvent().getInputs().get("user_id").asString().equals("U03E94MK0")
&& req.getEvent().getInputs().get("amount").asInteger().equals(1)
&& ctx.isAttachingFunctionTokenEnabled()
&& ctx.getFunctionBotAccessToken().equals("xwfp-valid"));
called.set(ctx.client().functionsCompleteSuccess(r -> r
.functionExecutionId(req.getEvent().getFunctionExecutionId())
.outputs(new HashMap<>())
).getError().equals(""));
return ctx.ack();
});
app.function("something-else", (req, ctx) -> ctx.ack());

Response response = app.run(buildEventRequest());
assertEquals(200L, response.getStatusCode().longValue());
assertTrue(called.get());
}

@Test
public void regexp_callback_id() throws Exception {
App app = buildApp();
AtomicBoolean called = new AtomicBoolean(false);
app.function(Pattern.compile("^he.+"), (req, ctx) -> {
called.set(req.getEvent().getFunction().getCallbackId().equals("hello")
&& req.getEvent().getInputs().get("user_id").asString().equals("U03E94MK0")
&& req.getEvent().getInputs().get("amount").asInteger().equals(1)
&& ctx.isAttachingFunctionTokenEnabled()
&& ctx.getFunctionBotAccessToken().equals("xwfp-valid"));
called.set(ctx.client().functionsCompleteSuccess(r -> r
.functionExecutionId(req.getEvent().getFunctionExecutionId())
.outputs(new HashMap<>())
).getError().equals(""));
return ctx.ack();
});
app.function("something-else", (req, ctx) -> ctx.ack());

Response response = app.run(buildEventRequest());
assertEquals(200L, response.getStatusCode().longValue());
assertTrue(called.get());
}

@Test
public void button_clicks() throws Exception {
App app = buildApp();
Expand All @@ -389,8 +433,6 @@ public void button_clicks() throws Exception {
&& req.getPayload().getBotAccessToken().equals("xwfp-this-is-valid")
);
called.set(ctx.client().functionsCompleteSuccess(r -> r
// TODO: remove this token passing by enhancing bolt internals
.token(req.getPayload().getBotAccessToken())
.functionExecutionId(req.getPayload().getFunctionData().getExecutionId())
.outputs(new HashMap<>())
).getError().equals(""));
Expand All @@ -413,8 +455,6 @@ public void view_submissions() throws Exception {
&& req.getPayload().getBotAccessToken().equals("xwfp-this-is-valid")
);
called.set(ctx.client().functionsCompleteSuccess(r -> r
// TODO: remove this token passing by enhancing bolt internals
.token(req.getPayload().getBotAccessToken())
.functionExecutionId(req.getPayload().getFunctionData().getExecutionId())
.outputs(new HashMap<>())
).getError().equals(""));
Expand Down Expand Up @@ -461,4 +501,5 @@ ViewSubmissionRequest buildViewSubmissionRequest() {
setRequestHeaders(body, rawHeaders, timestamp);
return new ViewSubmissionRequest(body, viewSubmissionPayload, new RequestHeaders(rawHeaders));
}

}

0 comments on commit 4b144ec

Please sign in to comment.