From 4b144ecd7726888b3544c9331297904bcba023cf Mon Sep 17 00:00:00 2001 From: Kazuhiro Sera Date: Mon, 20 Nov 2023 15:04:11 +0900 Subject: [PATCH] Add app.function listener support --- .../src/test/java/samples/SimpleApp.java | 22 +------ .../src/main/java/com/slack/api/bolt/App.java | 33 ++++++++-- .../java/com/slack/api/bolt/AppConfig.java | 9 +++ .../com/slack/api/bolt/context/Context.java | 23 ++++++- .../request/builtin/BlockActionRequest.java | 6 ++ .../bolt/request/builtin/EventRequest.java | 8 +++ .../request/builtin/ViewClosedRequest.java | 1 + .../builtin/ViewSubmissionRequest.java | 1 + .../test_locally/app/RemoteFunctionTest.java | 61 ++++++++++++++++--- 9 files changed, 129 insertions(+), 35 deletions(-) diff --git a/bolt-socket-mode/src/test/java/samples/SimpleApp.java b/bolt-socket-mode/src/test/java/samples/SimpleApp.java index 90fdc6a46..67a740de4 100644 --- a/bolt-socket-mode/src/test/java/samples/SimpleApp.java +++ b/bolt-socket-mode/src/test/java/samples/SimpleApp.java @@ -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; @@ -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( @@ -174,14 +172,10 @@ public static void main(String[] args) throws Exception { Map 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!") @@ -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!") @@ -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") @@ -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!") @@ -236,7 +222,6 @@ public static void main(String[] args) throws Exception { Map 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) @@ -247,7 +232,6 @@ public static void main(String[] args) throws Exception { Map 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) diff --git a/bolt/src/main/java/com/slack/api/bolt/App.java b/bolt/src/main/java/com/slack/api/bolt/App.java index 7df6263a7..ca98076ee 100644 --- a/bolt/src/main/java/com/slack/api/bolt/App.java +++ b/bolt/src/main/java/com/slack/api/bolt/App.java @@ -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; @@ -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()) { @@ -648,6 +646,33 @@ public App event(EventHandler handler) { return this; } + public App function(String callbackId, BoltEventHandler 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; + } + }); + } + + public App function(Pattern callbackId, BoltEventHandler 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; + } + }); + } + public App message(String pattern, BoltEventHandler messageHandler) { return message(Pattern.compile("^.*" + Pattern.quote(pattern) + ".*$"), messageHandler); } diff --git a/bolt/src/main/java/com/slack/api/bolt/AppConfig.java b/bolt/src/main/java/com/slack/api/bolt/AppConfig.java index ce3874bdf..d43241539 100644 --- a/bolt/src/main/java/com/slack/api/bolt/AppConfig.java +++ b/bolt/src/main/java/com/slack/api/bolt/AppConfig.java @@ -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 // --------------------------------- diff --git a/bolt/src/main/java/com/slack/api/bolt/context/Context.java b/bolt/src/main/java/com/slack/api/bolt/context/Context.java index 26cc64352..c4013b452 100644 --- a/bolt/src/main/java/com/slack/api/bolt/context/Context.java +++ b/bolt/src/main/java/com/slack/api/bolt/context/Context.java @@ -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 */ @@ -88,17 +103,21 @@ public abstract class Context { protected final Map 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 request) throws IOException, SlackApiException { diff --git a/bolt/src/main/java/com/slack/api/bolt/request/builtin/BlockActionRequest.java b/bolt/src/main/java/com/slack/api/bolt/request/builtin/BlockActionRequest.java index c73eaaa54..3f4b5c19c 100644 --- a/bolt/src/main/java/com/slack/api/bolt/request/builtin/BlockActionRequest.java +++ b/bolt/src/main/java/com/slack/api/bolt/request/builtin/BlockActionRequest.java @@ -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) { diff --git a/bolt/src/main/java/com/slack/api/bolt/request/builtin/EventRequest.java b/bolt/src/main/java/com/slack/api/bolt/request/builtin/EventRequest.java index a366a9dc7..725eb6a9c 100644 --- a/bolt/src/main/java/com/slack/api/bolt/request/builtin/EventRequest.java +++ b/bolt/src/main/java/com/slack/api/bolt/request/builtin/EventRequest.java @@ -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; @@ -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(); diff --git a/bolt/src/main/java/com/slack/api/bolt/request/builtin/ViewClosedRequest.java b/bolt/src/main/java/com/slack/api/bolt/request/builtin/ViewClosedRequest.java index 02ad288e2..42abcd448 100644 --- a/bolt/src/main/java/com/slack/api/bolt/request/builtin/ViewClosedRequest.java +++ b/bolt/src/main/java/com/slack/api/bolt/request/builtin/ViewClosedRequest.java @@ -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(); diff --git a/bolt/src/main/java/com/slack/api/bolt/request/builtin/ViewSubmissionRequest.java b/bolt/src/main/java/com/slack/api/bolt/request/builtin/ViewSubmissionRequest.java index 7255dec6f..65b382e92 100644 --- a/bolt/src/main/java/com/slack/api/bolt/request/builtin/ViewSubmissionRequest.java +++ b/bolt/src/main/java/com/slack/api/bolt/request/builtin/ViewSubmissionRequest.java @@ -43,6 +43,7 @@ public ViewSubmissionRequest( } getContext().setRequestUserId(payload.getUser().getId()); getContext().setResponseUrls(payload.getResponseUrls()); + getContext().setFunctionBotAccessToken(payload.getBotAccessToken()); } private ViewSubmissionContext context = new ViewSubmissionContext(); diff --git a/bolt/src/test/java/test_locally/app/RemoteFunctionTest.java b/bolt/src/test/java/test_locally/app/RemoteFunctionTest.java index c27388496..22ec7e974 100644 --- a/bolt/src/test/java/test_locally/app/RemoteFunctionTest.java +++ b/bolt/src/test/java/test_locally/app/RemoteFunctionTest.java @@ -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; @@ -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; @@ -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("")); @@ -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(); @@ -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("")); @@ -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("")); @@ -461,4 +501,5 @@ ViewSubmissionRequest buildViewSubmissionRequest() { setRequestHeaders(body, rawHeaders, timestamp); return new ViewSubmissionRequest(body, viewSubmissionPayload, new RequestHeaders(rawHeaders)); } + }