From faae06df8b30e64f97c534081a424aae2b237679 Mon Sep 17 00:00:00 2001 From: Kazuhiro Sera Date: Mon, 20 Nov 2023 15:04:11 +0900 Subject: [PATCH 1/3] 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 1615a4bb8..3f9cc4eac 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)); } + } From 006bdbb6ee48bb7051c5568110974f5ead8a4f5f Mon Sep 17 00:00:00 2001 From: Kazuhiro Sera Date: Wed, 8 May 2024 11:53:17 +0900 Subject: [PATCH 2/3] Add complete/fail to context --- .../com/slack/api/bolt/context/Context.java | 5 + .../api/bolt/context/FunctionUtility.java | 35 ++++ .../bolt/context/builtin/ActionContext.java | 3 +- .../bolt/context/builtin/EventContext.java | 10 +- .../builtin/ViewSubmissionContext.java | 3 +- .../request/builtin/BlockActionRequest.java | 3 + .../bolt/request/builtin/EventRequest.java | 14 +- .../request/builtin/ViewClosedRequest.java | 3 + .../builtin/ViewSubmissionRequest.java | 3 + .../test_locally/app/RemoteFunctionTest.java | 159 ++++++++++++++---- bolt/src/test/java/util/MockSlackApi.java | 4 +- .../FunctionsCompleteSuccessRequest.java | 2 +- 12 files changed, 199 insertions(+), 45 deletions(-) create mode 100644 bolt/src/main/java/com/slack/api/bolt/context/FunctionUtility.java 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 c4013b452..831990b3e 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 @@ -69,6 +69,11 @@ public abstract class Context { */ protected String functionBotAccessToken; + /** + * The ID of function_executed event delivery. + */ + protected String functionExecutionId; + /** * The scopes associated to the botToken */ diff --git a/bolt/src/main/java/com/slack/api/bolt/context/FunctionUtility.java b/bolt/src/main/java/com/slack/api/bolt/context/FunctionUtility.java new file mode 100644 index 000000000..618f9f0d1 --- /dev/null +++ b/bolt/src/main/java/com/slack/api/bolt/context/FunctionUtility.java @@ -0,0 +1,35 @@ +package com.slack.api.bolt.context; + +import com.slack.api.methods.MethodsClient; +import com.slack.api.methods.SlackApiException; +import com.slack.api.methods.request.chat.ChatPostMessageRequest; +import com.slack.api.methods.response.chat.ChatPostMessageResponse; +import com.slack.api.methods.response.functions.FunctionsCompleteErrorResponse; +import com.slack.api.methods.response.functions.FunctionsCompleteSuccessResponse; +import com.slack.api.model.block.LayoutBlock; + +import java.io.IOException; +import java.util.List; +import java.util.Map; + +public interface FunctionUtility { + + String getFunctionExecutionId(); + + MethodsClient client(); + + default FunctionsCompleteSuccessResponse complete(Map outputs) throws IOException, SlackApiException { + return this.client().functionsCompleteSuccess(r -> r + .functionExecutionId(this.getFunctionExecutionId()) + .outputs(outputs) + ); + } + + default FunctionsCompleteErrorResponse fail(String error) throws IOException, SlackApiException { + return this.client().functionsCompleteError(r -> r + .functionExecutionId(this.getFunctionExecutionId()) + .error(error) + ); + } + +} diff --git a/bolt/src/main/java/com/slack/api/bolt/context/builtin/ActionContext.java b/bolt/src/main/java/com/slack/api/bolt/context/builtin/ActionContext.java index 3a8d27784..d45d17126 100644 --- a/bolt/src/main/java/com/slack/api/bolt/context/builtin/ActionContext.java +++ b/bolt/src/main/java/com/slack/api/bolt/context/builtin/ActionContext.java @@ -2,6 +2,7 @@ import com.slack.api.bolt.context.ActionRespondUtility; import com.slack.api.bolt.context.Context; +import com.slack.api.bolt.context.FunctionUtility; import com.slack.api.bolt.util.Responder; import lombok.*; @@ -15,7 +16,7 @@ @AllArgsConstructor @ToString(callSuper = true) @EqualsAndHashCode(callSuper = false) -public class ActionContext extends Context implements ActionRespondUtility { +public class ActionContext extends Context implements ActionRespondUtility, FunctionUtility { private String triggerId; private String responseUrl; diff --git a/bolt/src/main/java/com/slack/api/bolt/context/builtin/EventContext.java b/bolt/src/main/java/com/slack/api/bolt/context/builtin/EventContext.java index 510f38419..56ae20072 100644 --- a/bolt/src/main/java/com/slack/api/bolt/context/builtin/EventContext.java +++ b/bolt/src/main/java/com/slack/api/bolt/context/builtin/EventContext.java @@ -1,9 +1,16 @@ package com.slack.api.bolt.context.builtin; import com.slack.api.bolt.context.Context; +import com.slack.api.bolt.context.FunctionUtility; import com.slack.api.bolt.context.SayUtility; +import com.slack.api.methods.SlackApiException; +import com.slack.api.methods.response.functions.FunctionsCompleteErrorResponse; +import com.slack.api.methods.response.functions.FunctionsCompleteSuccessResponse; import lombok.*; +import java.io.IOException; +import java.util.Map; + @Getter @Setter @Builder @@ -11,7 +18,7 @@ @EqualsAndHashCode(callSuper = false) @NoArgsConstructor @AllArgsConstructor -public class EventContext extends Context implements SayUtility { +public class EventContext extends Context implements SayUtility, FunctionUtility { private String channelId; @@ -21,5 +28,4 @@ public class EventContext extends Context implements SayUtility { // X-Slack-Retry-Reason: http_error in HTTP Mode // "retry_reason": "timeout", in Socket Mode private String retryReason; - } diff --git a/bolt/src/main/java/com/slack/api/bolt/context/builtin/ViewSubmissionContext.java b/bolt/src/main/java/com/slack/api/bolt/context/builtin/ViewSubmissionContext.java index cf4095277..969d74990 100644 --- a/bolt/src/main/java/com/slack/api/bolt/context/builtin/ViewSubmissionContext.java +++ b/bolt/src/main/java/com/slack/api/bolt/context/builtin/ViewSubmissionContext.java @@ -4,6 +4,7 @@ import com.slack.api.app_backend.views.payload.ViewSubmissionPayload; import com.slack.api.app_backend.views.response.ViewSubmissionResponse; import com.slack.api.bolt.context.Context; +import com.slack.api.bolt.context.FunctionUtility; import com.slack.api.bolt.context.InputBlockRespondUtility; import com.slack.api.bolt.util.Responder; import com.slack.api.bolt.response.Response; @@ -20,7 +21,7 @@ @AllArgsConstructor @ToString(callSuper = true) @EqualsAndHashCode(callSuper = false) -public class ViewSubmissionContext extends Context implements InputBlockRespondUtility { +public class ViewSubmissionContext extends Context implements InputBlockRespondUtility, FunctionUtility { private List responseUrls; private Responder responder; 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 3f4b5c19c..d3d071271 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 @@ -42,6 +42,9 @@ public BlockActionRequest( getContext().setTeamId(payload.getUser().getTeamId()); } getContext().setRequestUserId(payload.getUser().getId()); + if (payload.getFunctionData() != null) { + getContext().setFunctionExecutionId(payload.getFunctionData().getExecutionId()); + } } } 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 725eb6a9c..98fee2a7e 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 @@ -107,11 +107,15 @@ public EventRequest( 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); + if (this.eventType != null && this.eventType.equals(FunctionExecutedEvent.TYPE_NAME)) { + if (event.get("bot_access_token") != null) { + String functionBotAccessToken = event.get("bot_access_token").getAsString(); + this.getContext().setFunctionBotAccessToken(functionBotAccessToken); + } + if (event.get("function_execution_id") != null) { + String functionExecutionId = event.get("function_execution_id").getAsString(); + this.getContext().setFunctionExecutionId(functionExecutionId); + } } } 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 42abcd448..69fe0c8ee 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 @@ -43,6 +43,9 @@ public ViewClosedRequest( } getContext().setRequestUserId(payload.getUser().getId()); getContext().setFunctionBotAccessToken(payload.getBotAccessToken()); + if (payload.getFunctionData() != null) { + getContext().setFunctionExecutionId(payload.getFunctionData().getExecutionId()); + } } 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 65b382e92..a621a4901 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 @@ -44,6 +44,9 @@ public ViewSubmissionRequest( getContext().setRequestUserId(payload.getUser().getId()); getContext().setResponseUrls(payload.getResponseUrls()); getContext().setFunctionBotAccessToken(payload.getBotAccessToken()); + if (payload.getFunctionData() != null) { + getContext().setFunctionExecutionId(payload.getFunctionData().getExecutionId()); + } } 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 22ec7e974..206823d89 100644 --- a/bolt/src/test/java/test_locally/app/RemoteFunctionTest.java +++ b/bolt/src/test/java/test_locally/app/RemoteFunctionTest.java @@ -1,6 +1,7 @@ package test_locally.app; import com.google.gson.Gson; +import com.google.gson.JsonObject; import com.slack.api.Slack; import com.slack.api.SlackConfig; import com.slack.api.app_backend.SlackSignature; @@ -10,19 +11,22 @@ 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; import lombok.extern.slf4j.Slf4j; +import okhttp3.OkHttpClient; +import okhttp3.Request; +import okhttp3.RequestBody; import org.junit.After; import org.junit.Before; import org.junit.Test; import util.AuthTestMockServer; import util.MockSlackApiServer; +import java.io.UnsupportedEncodingException; import java.net.URLEncoder; -import java.util.Arrays; +import java.nio.charset.StandardCharsets; +import java.util.Collections; import java.util.HashMap; import java.util.List; import java.util.Map; @@ -50,7 +54,6 @@ public void tearDown() throws Exception { server.stop(); } - final Gson gson = GsonFactory.createSnakeCase(); final String secret = "foo-bar-baz"; final SlackSignature.Generator generator = new SlackSignature.Generator(secret); @@ -360,14 +363,8 @@ public void all_function_events() throws Exception { && 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("")); - called.set(ctx.client().functionsCompleteError(r -> r - .functionExecutionId(req.getEvent().getFunctionExecutionId()) - .error("something wrong") - ).getError().equals("")); + called.set(ctx.complete(new HashMap<>()).getError().isEmpty()); + called.set(ctx.fail("something wrong").getError().isEmpty()); return ctx.ack(); }); @@ -386,10 +383,7 @@ public void static_callback_id() throws Exception { && 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("")); + called.set(ctx.complete(new HashMap<>()).getError().isEmpty()); return ctx.ack(); }); app.function("something-else", (req, ctx) -> ctx.ack()); @@ -409,10 +403,7 @@ public void regexp_callback_id() throws Exception { && 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("")); + called.set(ctx.complete(new HashMap<>()).getError().isEmpty()); return ctx.ack(); }); app.function("something-else", (req, ctx) -> ctx.ack()); @@ -432,10 +423,7 @@ public void button_clicks() throws Exception { && req.getPayload().getFunctionData().getInputs().get("amount").asInteger().equals(1) && req.getPayload().getBotAccessToken().equals("xwfp-this-is-valid") ); - called.set(ctx.client().functionsCompleteSuccess(r -> r - .functionExecutionId(req.getPayload().getFunctionData().getExecutionId()) - .outputs(new HashMap<>()) - ).getError().equals("")); + called.set(ctx.complete(new HashMap<>()).getError().isEmpty()); return ctx.ack(); }); @@ -454,10 +442,10 @@ public void view_submissions() throws Exception { && req.getPayload().getFunctionData().getInputs().get("amount").asInteger().equals(1) && req.getPayload().getBotAccessToken().equals("xwfp-this-is-valid") ); - called.set(ctx.client().functionsCompleteSuccess(r -> r - .functionExecutionId(req.getPayload().getFunctionData().getExecutionId()) - .outputs(new HashMap<>()) - ).getError().equals("")); + Map outputs = new HashMap<>(); + outputs.put("message", "Completed!"); + outputs.put("number", 123); + called.set(ctx.complete(outputs).getError().isEmpty()); return ctx.ack(); }); @@ -475,8 +463,8 @@ App buildApp() { } void setRequestHeaders(String requestBody, Map> rawHeaders, String timestamp) { - rawHeaders.put(SlackSignature.HeaderNames.X_SLACK_REQUEST_TIMESTAMP, Arrays.asList(timestamp)); - rawHeaders.put(SlackSignature.HeaderNames.X_SLACK_SIGNATURE, Arrays.asList(generator.generate(timestamp, requestBody))); + rawHeaders.put(SlackSignature.HeaderNames.X_SLACK_REQUEST_TIMESTAMP, Collections.singletonList(timestamp)); + rawHeaders.put(SlackSignature.HeaderNames.X_SLACK_SIGNATURE, Collections.singletonList(generator.generate(timestamp, requestBody))); } EventRequest buildEventRequest() { @@ -486,20 +474,123 @@ EventRequest buildEventRequest() { return new EventRequest(eventPayload, new RequestHeaders(rawHeaders)); } - BlockActionRequest buildActionRequest() { + BlockActionRequest buildActionRequest() throws UnsupportedEncodingException { Map> rawHeaders = new HashMap<>(); String timestamp = String.valueOf(System.currentTimeMillis() / 1000); - String body = "payload=" + URLEncoder.encode(actionPayload); + String body = "payload=" + URLEncoder.encode(actionPayload, "UTF-8"); setRequestHeaders(body, rawHeaders, timestamp); return new BlockActionRequest(body, actionPayload, new RequestHeaders(rawHeaders)); } - ViewSubmissionRequest buildViewSubmissionRequest() { + ViewSubmissionRequest buildViewSubmissionRequest() throws UnsupportedEncodingException { Map> rawHeaders = new HashMap<>(); String timestamp = String.valueOf(System.currentTimeMillis() / 1000); - String body = "payload=" + URLEncoder.encode(viewSubmissionPayload); + String body = "payload=" + URLEncoder.encode(viewSubmissionPayload, "UTF-8"); setRequestHeaders(body, rawHeaders, timestamp); return new ViewSubmissionRequest(body, viewSubmissionPayload, new RequestHeaders(rawHeaders)); } + @Test + public void compileDocumentExamples() { + // https://api.slack.com/automation/functions/custom-bolt + App app = buildApp(); + + app.function("sample_function", (req, ctx) -> { + String userId = req.getEvent().getInputs().get("user_id").asString(); + app.executorService().submit(() -> { + try { + ctx.client().chatPostMessage(r -> r.channel(userId).text("Greetings <@" + userId + ">!")); + ctx.complete(new HashMap<>()); + } catch (Exception e) { + String error = "Failed to handle a function request: " + e.getMessage(); + ctx.logger.error(error, e); + try { + ctx.fail(error); + } catch (Exception ee) { + ctx.logger.error("Failed to perform a functions.completeError call: " + ee.getMessage(), ee); + } + } + }); + return ctx.ack(); + }); + + app.function("create_issue", (req, ctx) -> { + app.executorService().submit(() -> { + try { + Map inputs = req.getEvent().getInputs(); + Map requestBody = new HashMap<>(); + + Map p = new HashMap<>(); + String project = inputs.get("project").asString(); + if (project.matches("^\\d+$")) p.put("id", project); + else p.put("key", project); + requestBody.put("project", p); + + Map i = new HashMap<>(); + String issuetype = inputs.get("issuetype").asString(); + if (project.matches("^\\d+$")) i.put("id", issuetype); + else i.put("name", issuetype); + requestBody.put("issuetype", i); + + requestBody.put("description", inputs.get("description").asString()); + requestBody.put("summary", inputs.get("summary").asString()); + + Gson gson = new Gson(); + String jiraBaseURL = System.getenv("JIRA_BASE_URL"); + String issueEndpoint = "https://" + jiraBaseURL + "/rest/api/latest/issue"; + OkHttpClient client = new OkHttpClient(); + okhttp3.Response issueResponse = client.newCall(new Request.Builder() + .post(RequestBody.create(gson.toJson(requestBody).getBytes(StandardCharsets.UTF_8))) + .url(issueEndpoint) + .addHeader("Accept", "application/json") + .addHeader("Authorization", "Bearer " + System.getenv("JIRA_SERVICE_TOKEN")) + .addHeader("Content-Type", "application/json") + .build() + ).execute(); + + if (issueResponse.isSuccessful()) { + String issueResponseBody = issueResponse.body().string(); + JsonObject issue = gson.fromJson(issueResponseBody, JsonObject.class); + Map outputs = new HashMap<>(); + outputs.put("issue_id", issue.get("id").getAsString()); + outputs.put("issue_key", issue.get("key").getAsString()); + outputs.put("issue_url", "https://" + jiraBaseURL + "/browse/" + issue.get("key").getAsString()); + ctx.complete(outputs); + } else { + try { + ctx.fail("Failed to create a JIRA issue (status: " + issueResponse.code() + ")"); + } catch (Exception ee) { + ctx.logger.error("Failed to perform a functions.completeError call: " + ee.getMessage(), ee); + } + } + + } catch (Exception e) { + try { + ctx.fail("Failed to create a JIRA issue: " + e.getMessage()); + } catch (Exception ee) { + ctx.logger.error("Failed to perform a functions.completeError call: " + ee.getMessage(), ee); + } + } + }); + return ctx.ack(); + }); + + app.blockAction("approve_button", (req, ctx) -> { + app.executorService().submit(() -> { + try { + Map outputs = new HashMap<>(); + outputs.put("message", "Request approved 👍"); + ctx.complete(outputs); + } catch (Exception e) { + try { + ctx.fail("Failed to handle a function request: " + e.getMessage()); + } catch (Exception ee) { + ctx.logger.error("Failed to perform a functions.completeError call: " + ee.getMessage(), ee); + } + } + }); + return ctx.ack(); + }); + } + } diff --git a/bolt/src/test/java/util/MockSlackApi.java b/bolt/src/test/java/util/MockSlackApi.java index 576879e8c..b853bd653 100644 --- a/bolt/src/test/java/util/MockSlackApi.java +++ b/bolt/src/test/java/util/MockSlackApi.java @@ -43,7 +43,7 @@ protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws I resp.setContentType("application/json"); return; } else if (!authorizationHeader.equals("Bearer " + ValidToken) - && !authorizationHeader.equals("Bearer " + ValidFunctionToken)) { + && !authorizationHeader.equals("Bearer " + ValidFunctionToken)) { resp.setStatus(200); resp.getWriter().write("{\"ok\":false,\"error\":\"invalid_auth\"}"); resp.setContentType("application/json"); @@ -61,6 +61,8 @@ protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws I } else { body = body.replaceFirst("\"error\": \"\"", "\"error\": \"something-wrong\""); } + } else if (methodName.startsWith("functions.") && !requestBody.contains("function_execution_id=Fx")) { + body = body.replaceFirst("\"error\": \"\"", "\"error\": \"invalid function_execution_id\""); } else { body = body.replaceFirst("\"ok\": false,", "\"ok\": true,"); } diff --git a/slack-api-client/src/main/java/com/slack/api/methods/request/functions/FunctionsCompleteSuccessRequest.java b/slack-api-client/src/main/java/com/slack/api/methods/request/functions/FunctionsCompleteSuccessRequest.java index 705e569ca..58c78ec45 100644 --- a/slack-api-client/src/main/java/com/slack/api/methods/request/functions/FunctionsCompleteSuccessRequest.java +++ b/slack-api-client/src/main/java/com/slack/api/methods/request/functions/FunctionsCompleteSuccessRequest.java @@ -20,5 +20,5 @@ public class FunctionsCompleteSuccessRequest implements SlackApiRequest { private String token; private String functionExecutionId; - private Map outputs; + private Map outputs; } \ No newline at end of file From 9dd41181ff0acfa01543b85c49ce7c66d75e7c8a Mon Sep 17 00:00:00 2001 From: Kazuhiro Sera Date: Tue, 20 Aug 2024 16:06:38 +0900 Subject: [PATCH 3/3] Update example apps --- .../src/test/java/samples/SimpleApp.java | 106 +++++++++++++++++- 1 file changed, 105 insertions(+), 1 deletion(-) diff --git a/bolt-socket-mode/src/test/java/samples/SimpleApp.java b/bolt-socket-mode/src/test/java/samples/SimpleApp.java index 67a740de4..7128f4dd5 100644 --- a/bolt-socket-mode/src/test/java/samples/SimpleApp.java +++ b/bolt-socket-mode/src/test/java/samples/SimpleApp.java @@ -151,7 +151,111 @@ public static void main(String[] args) throws Exception { return ctx.ack(); }); - // Note that this is still in beta as of Nov 2023 + /* Example App Manifest +{ + "display_information": { + "name": "manifest-test-app-2" + }, + "features": { + "bot_user": { + "display_name": "test-bot", + "always_online": true + } + }, + "oauth_config": { + "scopes": { + "bot": [ + "commands", + "chat:write", + "app_mentions:read" + ] + } + }, + "settings": { + "event_subscriptions": { + "bot_events": [ + "app_mention", + "function_executed" + ] + }, + "interactivity": { + "is_enabled": true + }, + "org_deploy_enabled": true, + "socket_mode_enabled": true, + "token_rotation_enabled": false, + "hermes_app_type": "remote", + "function_runtime": "remote" + }, + "functions": { + "hello": { + "title": "Hello", + "description": "Hello world!", + "input_parameters": { + "amount": { + "type": "number", + "title": "Amount", + "description": "How many do you need?", + "is_required": false, + "hint": "How many do you need?", + "name": "amount", + "maximum": 10, + "minimum": 1 + }, + "user_id": { + "type": "slack#/types/user_id", + "title": "User", + "description": "Who to send it", + "is_required": true, + "hint": "Select a user in the workspace", + "name": "user_id" + }, + "message": { + "type": "string", + "title": "Message", + "description": "Whatever you want to tell", + "is_required": false, + "hint": "up to 100 characters", + "name": "message", + "maxLength": 100, + "minLength": 1 + } + }, + "output_parameters": { + "amount": { + "type": "number", + "title": "Amount", + "description": "How many do you need?", + "is_required": false, + "hint": "How many do you need?", + "name": "amount", + "maximum": 10, + "minimum": 1 + }, + "user_id": { + "type": "slack#/types/user_id", + "title": "User", + "description": "Who to send it", + "is_required": true, + "hint": "Select a user in the workspace", + "name": "user_id" + }, + "message": { + "type": "string", + "title": "Message", + "description": "Whatever you want to tell", + "is_required": false, + "hint": "up to 100 characters", + "name": "message", + "maxLength": 100, + "minLength": 1 + } + } + } + } +} + */ + // app.event(FunctionExecutedEvent.class, (req, ctx) -> { // app.function("hello", (req, ctx) -> { app.function(Pattern.compile("^he.+$"), (req, ctx) -> {