diff --git a/bolt-socket-mode/src/test/java/samples/SimpleApp.java b/bolt-socket-mode/src/test/java/samples/SimpleApp.java index 9475c652f..924e89b5e 100644 --- a/bolt-socket-mode/src/test/java/samples/SimpleApp.java +++ b/bolt-socket-mode/src/test/java/samples/SimpleApp.java @@ -1,20 +1,20 @@ package samples; +import com.google.gson.Gson; import com.slack.api.bolt.App; import com.slack.api.bolt.AppConfig; import com.slack.api.bolt.socket_mode.SocketModeApp; import com.slack.api.model.Message; import com.slack.api.model.block.element.RichTextSectionElement; -import com.slack.api.model.event.AppMentionEvent; -import com.slack.api.model.event.MessageChangedEvent; -import com.slack.api.model.event.MessageDeletedEvent; -import com.slack.api.model.event.MessageEvent; +import com.slack.api.model.event.*; import com.slack.api.model.view.ViewState; +import com.slack.api.util.json.GsonFactory; import config.Constants; 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; @@ -153,6 +153,93 @@ public static void main(String[] args) throws Exception { return ctx.ack(); }); + // app.event(FunctionExecutedEvent.class, (req, ctx) -> { + // app.function("hello", (req, ctx) -> { + app.function(Pattern.compile("^he.+$"), (req, ctx) -> { + ctx.logger.info("req: {}", req); + ctx.client().chatPostMessage(r -> r + .channel(req.getEvent().getInputs().get("user_id").asString()) + .text("hey!") + .blocks(asBlocks(actions(a -> a.blockId("b").elements(asElements( + button(b -> b.actionId("remote-function-button-success").value("clicked").text(plainText("block_actions success"))), + button(b -> b.actionId("remote-function-button-error").value("clicked").text(plainText("block_actions error"))), + button(b -> b.actionId("remote-function-modal").value("clicked").text(plainText("modal view"))) + ))))) + ); + return ctx.ack(); + }); + + app.blockAction("remote-function-button-success", (req, ctx) -> { + Map outputs = new HashMap<>(); + outputs.put("user_id", req.getPayload().getFunctionData().getInputs().get("user_id").asString()); + ctx.client().functionsCompleteSuccess(r -> r + .functionExecutionId(req.getPayload().getFunctionData().getExecutionId()) + .outputs(outputs) + ); + ctx.client().chatUpdate(r -> r + .channel(req.getPayload().getContainer().getChannelId()) + .ts(req.getPayload().getContainer().getMessageTs()) + .text("Thank you!") + ); + return ctx.ack(); + }); + app.blockAction("remote-function-button-error", (req, ctx) -> { + ctx.client().functionsCompleteError(r -> r + .functionExecutionId(req.getPayload().getFunctionData().getExecutionId()) + .error("test error!") + ); + ctx.client().chatUpdate(r -> r + .channel(req.getPayload().getContainer().getChannelId()) + .ts(req.getPayload().getContainer().getMessageTs()) + .text("Thank you!") + ); + return ctx.ack(); + }); + app.blockAction("remote-function-modal", (req, ctx) -> { + ctx.client().viewsOpen(r -> r + .triggerId(req.getPayload().getInteractivity().getInteractivityPointer()) + .view(view(v -> v + .type("modal") + .callbackId("remote-function-view") + .title(viewTitle(vt -> vt.type("plain_text").text("Remote Function test"))) + .close(viewClose(vc -> vc.type("plain_text").text("Close"))) + .submit(viewSubmit(vs -> vs.type("plain_text").text("Submit"))) + .notifyOnClose(true) + .blocks(asBlocks(input(input -> input + .blockId("text-block") + .element(plainTextInput(pti -> pti.actionId("text-action").multiline(true))) + .label(plainText(pt -> pt.text("Text").emoji(true))) + ))) + ))); + ctx.client().chatUpdate(r -> r + .channel(req.getPayload().getContainer().getChannelId()) + .ts(req.getPayload().getContainer().getMessageTs()) + .text("Thank you!") + ); + return ctx.ack(); + }); + + app.viewSubmission("remote-function-view", (req, ctx) -> { + Map outputs = new HashMap<>(); + outputs.put("user_id", ctx.getRequestUserId()); + ctx.client().functionsCompleteSuccess(r -> r + .token(req.getPayload().getBotAccessToken()) + .functionExecutionId(req.getPayload().getFunctionData().getExecutionId()) + .outputs(outputs) + ); + return ctx.ack(); + }); + app.viewClosed("remote-function-view", (req, ctx) -> { + Map outputs = new HashMap<>(); + outputs.put("user_id", ctx.getRequestUserId()); + ctx.client().functionsCompleteSuccess(r -> r + .token(req.getPayload().getBotAccessToken()) + .functionExecutionId(req.getPayload().getFunctionData().getExecutionId()) + .outputs(outputs) + ); + return ctx.ack(); + }); + String appToken = System.getenv(Constants.SLACK_SDK_TEST_SOCKET_MODE_APP_TOKEN); SocketModeApp socketModeApp = new SocketModeApp(appToken, app); socketModeApp.start(); 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..d0bd99a78 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, (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, (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 83323b5bd..2342bdffa 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 @@ -7,6 +7,7 @@ import com.slack.api.bolt.request.Request; import com.slack.api.bolt.request.RequestHeaders; import com.slack.api.bolt.request.RequestType; +import com.slack.api.model.event.FunctionExecutedEvent; import com.slack.api.util.json.GsonFactory; import lombok.ToString; @@ -104,6 +105,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 new file mode 100644 index 000000000..c73589bfd --- /dev/null +++ b/bolt/src/test/java/test_locally/app/RemoteFunctionTest.java @@ -0,0 +1,235 @@ +package test_locally.app; + +import com.google.gson.Gson; +import com.slack.api.Slack; +import com.slack.api.SlackConfig; +import com.slack.api.app_backend.SlackSignature; +import com.slack.api.bolt.App; +import com.slack.api.bolt.AppConfig; +import com.slack.api.bolt.request.RequestHeaders; +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 org.junit.After; +import org.junit.Before; +import org.junit.Test; +import util.AuthTestMockServer; +import util.MockSlackApiServer; + +import java.util.Arrays; +import java.util.HashMap; +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; + +@Slf4j +public class RemoteFunctionTest { + + MockSlackApiServer server = new MockSlackApiServer(); + SlackConfig config = new SlackConfig(); + Slack slack = Slack.getInstance(config); + + @Before + public void setup() throws Exception { + server.start(); + config.setMethodsEndpointUrlPrefix(server.getMethodsEndpointPrefix()); + } + + @After + 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); + + String payload = "{\n" + + " \"token\": \"xxx\",\n" + + " \"team_id\": \"T03E94MJU\",\n" + + " \"api_app_id\": \"A065ZJM410S\",\n" + + " \"event\": {\n" + + " \"type\": \"function_executed\",\n" + + " \"function\": {\n" + + " \"id\": \"Fn066C7U22JD\",\n" + + " \"callback_id\": \"hello\",\n" + + " \"title\": \"Hello\",\n" + + " \"description\": \"Hello world!\",\n" + + " \"type\": \"app\",\n" + + " \"input_parameters\": [\n" + + " {\n" + + " \"type\": \"number\",\n" + + " \"name\": \"amount\",\n" + + " \"description\": \"How many do you need?\",\n" + + " \"title\": \"Amount\",\n" + + " \"is_required\": false,\n" + + " \"hint\": \"How many do you need?\",\n" + + " \"maximum\": 10,\n" + + " \"minimum\": 1\n" + + " },\n" + + " {\n" + + " \"type\": \"slack#/types/user_id\",\n" + + " \"name\": \"user_id\",\n" + + " \"description\": \"Who to send it\",\n" + + " \"title\": \"User\",\n" + + " \"is_required\": true,\n" + + " \"hint\": \"Select a user in the workspace\"\n" + + " },\n" + + " {\n" + + " \"type\": \"string\",\n" + + " \"name\": \"message\",\n" + + " \"description\": \"Whatever you want to tell\",\n" + + " \"title\": \"Message\",\n" + + " \"is_required\": false,\n" + + " \"hint\": \"up to 100 characters\",\n" + + " \"maxLength\": 100,\n" + + " \"minLength\": 1\n" + + " }\n" + + " ],\n" + + " \"output_parameters\": [\n" + + " {\n" + + " \"type\": \"number\",\n" + + " \"name\": \"amount\",\n" + + " \"description\": \"How many do you need?\",\n" + + " \"title\": \"Amount\",\n" + + " \"is_required\": false,\n" + + " \"hint\": \"How many do you need?\",\n" + + " \"maximum\": 10,\n" + + " \"minimum\": 1\n" + + " },\n" + + " {\n" + + " \"type\": \"slack#/types/user_id\",\n" + + " \"name\": \"user_id\",\n" + + " \"description\": \"Who to send it\",\n" + + " \"title\": \"User\",\n" + + " \"is_required\": true,\n" + + " \"hint\": \"Select a user in the workspace\"\n" + + " },\n" + + " {\n" + + " \"type\": \"string\",\n" + + " \"name\": \"message\",\n" + + " \"description\": \"Whatever you want to tell\",\n" + + " \"title\": \"Message\",\n" + + " \"is_required\": false,\n" + + " \"hint\": \"up to 100 characters\",\n" + + " \"maxLength\": 100,\n" + + " \"minLength\": 1\n" + + " }\n" + + " ],\n" + + " \"app_id\": \"A065ZJM410S\",\n" + + " \"date_created\": 1700110468,\n" + + " \"date_updated\": 1700110470,\n" + + " \"date_deleted\": 0,\n" + + " \"form_enabled\": false\n" + + " },\n" + + " \"inputs\": {\n" + + " \"amount\": 1,\n" + + " \"message\": \"hey\",\n" + + " \"user_id\": \"U03E94MK0\"\n" + + " },\n" + + " \"function_execution_id\": \"Fx066G2XBP0E\",\n" + + " \"workflow_execution_id\": \"Wx066862SLRM\",\n" + + " \"event_ts\": \"1700554202.283041\",\n" + + " \"bot_access_token\": \"xwfp-this-is-valid\"\n" + + " },\n" + + " \"type\": \"event_callback\",\n" + + " \"event_id\": \"Ev067BMBHK16\",\n" + + " \"event_time\": 1700554202\n" + + "}\n"; + + @Test + public void all_function_events() throws Exception { + App app = buildApp(); + AtomicBoolean called = new AtomicBoolean(false); + app.event(FunctionExecutedEvent.class, (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("")); + called.set(ctx.client().functionsCompleteError(r -> r + .functionExecutionId(req.getEvent().getFunctionExecutionId()) + .error("something wrong") + ).getError().equals("")); + return ctx.ack(); + }); + + Response response = app.run(buildRequest()); + assertEquals(200L, response.getStatusCode().longValue()); + 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(); + }); + + Response response = app.run(buildRequest()); + 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(); + }); + + Response response = app.run(buildRequest()); + assertEquals(200L, response.getStatusCode().longValue()); + assertTrue(called.get()); + } + + App buildApp() { + return new App(AppConfig.builder() + .signingSecret(secret) + .singleTeamBotToken(AuthTestMockServer.ValidToken) + .slack(slack) + .build()); + } + + 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))); + } + + EventRequest buildRequest() { + Map> rawHeaders = new HashMap<>(); + String timestamp = String.valueOf(System.currentTimeMillis() / 1000); + setRequestHeaders(payload, rawHeaders, timestamp); + return new EventRequest(payload, new RequestHeaders(rawHeaders)); + } +} diff --git a/bolt/src/test/java/util/MockSlackApi.java b/bolt/src/test/java/util/MockSlackApi.java index 5c7ded30a..576879e8c 100644 --- a/bolt/src/test/java/util/MockSlackApi.java +++ b/bolt/src/test/java/util/MockSlackApi.java @@ -20,6 +20,7 @@ public class MockSlackApi extends HttpServlet { public static final String ValidToken = "xoxb-this-is-valid"; + public static final String ValidFunctionToken = "xwfp-this-is-valid"; public static final String InvalidToken = "xoxb-this-is-INVALID"; private final FileReader reader = new FileReader("../json-logs/samples/api/"); @@ -41,7 +42,8 @@ protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws I resp.getWriter().write("{\"ok\":false,\"error\":\"not_authed\"}"); resp.setContentType("application/json"); return; - } else if (!authorizationHeader.equals("Bearer " + ValidToken)) { + } else if (!authorizationHeader.equals("Bearer " + ValidToken) + && !authorizationHeader.equals("Bearer " + ValidFunctionToken)) { resp.setStatus(200); resp.getWriter().write("{\"ok\":false,\"error\":\"invalid_auth\"}"); resp.setContentType("application/json"); diff --git a/json-logs/samples/api/functions.completeError.json b/json-logs/samples/api/functions.completeError.json new file mode 100644 index 000000000..1b3fc766f --- /dev/null +++ b/json-logs/samples/api/functions.completeError.json @@ -0,0 +1,6 @@ +{ + "ok": false, + "error": "", + "needed": "", + "provided": "" +} \ No newline at end of file diff --git a/json-logs/samples/api/functions.completeSuccess.json b/json-logs/samples/api/functions.completeSuccess.json new file mode 100644 index 000000000..1b3fc766f --- /dev/null +++ b/json-logs/samples/api/functions.completeSuccess.json @@ -0,0 +1,6 @@ +{ + "ok": false, + "error": "", + "needed": "", + "provided": "" +} \ No newline at end of file diff --git a/json-logs/samples/app-backend/interactive-components/BlockActionPayload.json b/json-logs/samples/app-backend/interactive-components/BlockActionPayload.json index 112a3a24b..521b9961b 100644 --- a/json-logs/samples/app-backend/interactive-components/BlockActionPayload.json +++ b/json-logs/samples/app-backend/interactive-components/BlockActionPayload.json @@ -1558,5 +1558,19 @@ } } ], - "is_enterprise_install": false + "is_enterprise_install": false, + "bot_access_token": "", + "function_data": { + "execution_id": "", + "function": { + "callback_id": "" + } + }, + "interactivity": { + "interactivity_pointer": "", + "interactor": { + "id": "", + "secret": "" + } + } } \ No newline at end of file diff --git a/json-logs/samples/app-backend/views/ViewClosedPayload.json b/json-logs/samples/app-backend/views/ViewClosedPayload.json index 389ca0f7a..bdfa5a33f 100644 --- a/json-logs/samples/app-backend/views/ViewClosedPayload.json +++ b/json-logs/samples/app-backend/views/ViewClosedPayload.json @@ -52,5 +52,19 @@ "bot_id": "" }, "is_enterprise_install": false, - "is_cleared": false + "is_cleared": false, + "bot_access_token": "", + "function_data": { + "execution_id": "", + "function": { + "callback_id": "" + } + }, + "interactivity": { + "interactivity_pointer": "", + "interactor": { + "id": "", + "secret": "" + } + } } \ No newline at end of file diff --git a/json-logs/samples/app-backend/views/ViewSubmissionPayload.json b/json-logs/samples/app-backend/views/ViewSubmissionPayload.json index beae3b04f..47aeed8d8 100644 --- a/json-logs/samples/app-backend/views/ViewSubmissionPayload.json +++ b/json-logs/samples/app-backend/views/ViewSubmissionPayload.json @@ -53,5 +53,19 @@ "bot_id": "" }, "is_enterprise_install": false, - "is_cleared": false + "is_cleared": false, + "bot_access_token": "", + "function_data": { + "execution_id": "", + "function": { + "callback_id": "" + } + }, + "interactivity": { + "interactivity_pointer": "", + "interactor": { + "id": "", + "secret": "" + } + } } \ No newline at end of file diff --git a/metadata/web-api/rate_limit_tiers.json b/metadata/web-api/rate_limit_tiers.json index 0c3a24b4b..2ceb55620 100644 --- a/metadata/web-api/rate_limit_tiers.json +++ b/metadata/web-api/rate_limit_tiers.json @@ -196,6 +196,8 @@ "files.revokePublicURL": "Tier3", "files.sharedPublicURL": "Tier3", "files.upload": "Tier2", + "functions.completeError": "Tier3", + "functions.completeSuccess": "Tier3", "groups.archive": "Tier2", "groups.create": "Tier2", "groups.createChild": "Tier2", diff --git a/slack-api-client/src/main/java/com/slack/api/methods/AsyncMethodsClient.java b/slack-api-client/src/main/java/com/slack/api/methods/AsyncMethodsClient.java index ed35ff06f..724ee50a8 100644 --- a/slack-api-client/src/main/java/com/slack/api/methods/AsyncMethodsClient.java +++ b/slack-api-client/src/main/java/com/slack/api/methods/AsyncMethodsClient.java @@ -65,6 +65,8 @@ import com.slack.api.methods.request.emoji.EmojiListRequest; import com.slack.api.methods.request.files.*; import com.slack.api.methods.request.files.remote.*; +import com.slack.api.methods.request.functions.FunctionsCompleteErrorRequest; +import com.slack.api.methods.request.functions.FunctionsCompleteSuccessRequest; import com.slack.api.methods.request.migration.MigrationExchangeRequest; import com.slack.api.methods.request.oauth.OAuthAccessRequest; import com.slack.api.methods.request.oauth.OAuthTokenRequest; @@ -168,6 +170,8 @@ import com.slack.api.methods.response.emoji.EmojiListResponse; import com.slack.api.methods.response.files.*; import com.slack.api.methods.response.files.remote.*; +import com.slack.api.methods.response.functions.FunctionsCompleteErrorResponse; +import com.slack.api.methods.response.functions.FunctionsCompleteSuccessResponse; import com.slack.api.methods.response.migration.MigrationExchangeResponse; import com.slack.api.methods.response.oauth.OAuthAccessResponse; import com.slack.api.methods.response.oauth.OAuthTokenResponse; @@ -208,7 +212,6 @@ import com.slack.api.methods.response.workflows.WorkflowsStepFailedResponse; import com.slack.api.methods.response.workflows.WorkflowsUpdateStepResponse; -import java.io.IOException; import java.util.concurrent.CompletableFuture; /** @@ -1111,6 +1114,18 @@ CompletableFuture CompletableFuture filesRemoteUpdate(RequestConfigurator req); + // ------------------------------ + // functions + // ------------------------------ + + CompletableFuture functionsCompleteSuccess(FunctionsCompleteSuccessRequest req); + + CompletableFuture functionsCompleteSuccess(RequestConfigurator req); + + CompletableFuture functionsCompleteError(FunctionsCompleteErrorRequest req); + + CompletableFuture functionsCompleteError(RequestConfigurator req); + // ------------------------------ // migration // ------------------------------ diff --git a/slack-api-client/src/main/java/com/slack/api/methods/Methods.java b/slack-api-client/src/main/java/com/slack/api/methods/Methods.java index d621318c7..581b1c64a 100644 --- a/slack-api-client/src/main/java/com/slack/api/methods/Methods.java +++ b/slack-api-client/src/main/java/com/slack/api/methods/Methods.java @@ -465,6 +465,13 @@ private Methods() { public static final String FILES_REMOTE_SHARE = "files.remote.share"; public static final String FILES_REMOTE_UPDATE = "files.remote.update"; + // ------------------------------ + // functions + // ------------------------------ + + public static final String FUNCTIONS_COMPLETE_SUCCESS = "functions.completeSuccess"; + public static final String FUNCTIONS_COMPLETE_ERROR = "functions.completeError"; + // ------------------------------ // groups // ------------------------------ diff --git a/slack-api-client/src/main/java/com/slack/api/methods/MethodsClient.java b/slack-api-client/src/main/java/com/slack/api/methods/MethodsClient.java index 57ce2a89d..22106a2ce 100644 --- a/slack-api-client/src/main/java/com/slack/api/methods/MethodsClient.java +++ b/slack-api-client/src/main/java/com/slack/api/methods/MethodsClient.java @@ -75,6 +75,8 @@ import com.slack.api.methods.request.files.comments.FilesCommentsDeleteRequest; import com.slack.api.methods.request.files.comments.FilesCommentsEditRequest; import com.slack.api.methods.request.files.remote.*; +import com.slack.api.methods.request.functions.FunctionsCompleteErrorRequest; +import com.slack.api.methods.request.functions.FunctionsCompleteSuccessRequest; import com.slack.api.methods.request.groups.*; import com.slack.api.methods.request.im.*; import com.slack.api.methods.request.migration.MigrationExchangeRequest; @@ -191,6 +193,8 @@ import com.slack.api.methods.response.files.comments.FilesCommentsDeleteResponse; import com.slack.api.methods.response.files.comments.FilesCommentsEditResponse; import com.slack.api.methods.response.files.remote.*; +import com.slack.api.methods.response.functions.FunctionsCompleteErrorResponse; +import com.slack.api.methods.response.functions.FunctionsCompleteSuccessResponse; import com.slack.api.methods.response.groups.*; import com.slack.api.methods.response.im.*; import com.slack.api.methods.response.migration.MigrationExchangeResponse; @@ -1412,6 +1416,18 @@ AdminUsergroupsRemoveChannelsResponse adminUsergroupsRemoveChannels( FilesRemoteUpdateResponse filesRemoteUpdate(RequestConfigurator req) throws IOException, SlackApiException; + // ------------------------------ + // functions + // ------------------------------ + + FunctionsCompleteSuccessResponse functionsCompleteSuccess(FunctionsCompleteSuccessRequest req) throws IOException, SlackApiException; + + FunctionsCompleteSuccessResponse functionsCompleteSuccess(RequestConfigurator req) throws IOException, SlackApiException; + + FunctionsCompleteErrorResponse functionsCompleteError(FunctionsCompleteErrorRequest req) throws IOException, SlackApiException; + + FunctionsCompleteErrorResponse functionsCompleteError(RequestConfigurator req) throws IOException, SlackApiException; + // ------------------------------ // groups // ------------------------------ diff --git a/slack-api-client/src/main/java/com/slack/api/methods/MethodsRateLimits.java b/slack-api-client/src/main/java/com/slack/api/methods/MethodsRateLimits.java index 9aa6b8647..752c09cdd 100644 --- a/slack-api-client/src/main/java/com/slack/api/methods/MethodsRateLimits.java +++ b/slack-api-client/src/main/java/com/slack/api/methods/MethodsRateLimits.java @@ -369,6 +369,9 @@ public static void setRateLimitTier(String methodName, MethodsRateLimitTier tier setRateLimitTier(FILES_REMOTE_SHARE, Tier2); setRateLimitTier(FILES_REMOTE_UPDATE, Tier2); + setRateLimitTier(FUNCTIONS_COMPLETE_SUCCESS, Tier3); + setRateLimitTier(FUNCTIONS_COMPLETE_ERROR, Tier3); + setRateLimitTier(MIGRATION_EXCHANGE, Tier2); setRateLimitTier(OAUTH_ACCESS, Tier4); diff --git a/slack-api-client/src/main/java/com/slack/api/methods/RequestFormBuilder.java b/slack-api-client/src/main/java/com/slack/api/methods/RequestFormBuilder.java index 9e1accb61..4a894a55e 100644 --- a/slack-api-client/src/main/java/com/slack/api/methods/RequestFormBuilder.java +++ b/slack-api-client/src/main/java/com/slack/api/methods/RequestFormBuilder.java @@ -73,6 +73,8 @@ import com.slack.api.methods.request.files.comments.FilesCommentsDeleteRequest; import com.slack.api.methods.request.files.comments.FilesCommentsEditRequest; import com.slack.api.methods.request.files.remote.*; +import com.slack.api.methods.request.functions.FunctionsCompleteErrorRequest; +import com.slack.api.methods.request.functions.FunctionsCompleteSuccessRequest; import com.slack.api.methods.request.groups.*; import com.slack.api.methods.request.im.*; import com.slack.api.methods.request.migration.MigrationExchangeRequest; @@ -2016,6 +2018,20 @@ public static MultipartBody.Builder toMultipartBody(FilesRemoteUpdateRequest req return form; } + public static FormBody.Builder toForm(FunctionsCompleteSuccessRequest req) { + FormBody.Builder form = new FormBody.Builder(); + setIfNotNull("function_execution_id", req.getFunctionExecutionId(), form); + setIfNotNull("outputs", GSON.toJson(req.getOutputs()), form); + return form; + } + + public static FormBody.Builder toForm(FunctionsCompleteErrorRequest req) { + FormBody.Builder form = new FormBody.Builder(); + setIfNotNull("function_execution_id", req.getFunctionExecutionId(), form); + setIfNotNull("error", req.getError(), form); + return form; + } + public static FormBody.Builder toForm(GroupsArchiveRequest req) { FormBody.Builder form = new FormBody.Builder(); setIfNotNull("channel", req.getChannel(), form); diff --git a/slack-api-client/src/main/java/com/slack/api/methods/impl/AsyncMethodsClientImpl.java b/slack-api-client/src/main/java/com/slack/api/methods/impl/AsyncMethodsClientImpl.java index 2b881af00..0199d10a4 100644 --- a/slack-api-client/src/main/java/com/slack/api/methods/impl/AsyncMethodsClientImpl.java +++ b/slack-api-client/src/main/java/com/slack/api/methods/impl/AsyncMethodsClientImpl.java @@ -69,6 +69,8 @@ import com.slack.api.methods.request.emoji.EmojiListRequest; import com.slack.api.methods.request.files.*; import com.slack.api.methods.request.files.remote.*; +import com.slack.api.methods.request.functions.FunctionsCompleteErrorRequest; +import com.slack.api.methods.request.functions.FunctionsCompleteSuccessRequest; import com.slack.api.methods.request.migration.MigrationExchangeRequest; import com.slack.api.methods.request.oauth.OAuthAccessRequest; import com.slack.api.methods.request.oauth.OAuthTokenRequest; @@ -172,6 +174,8 @@ import com.slack.api.methods.response.emoji.EmojiListResponse; import com.slack.api.methods.response.files.*; import com.slack.api.methods.response.files.remote.*; +import com.slack.api.methods.response.functions.FunctionsCompleteErrorResponse; +import com.slack.api.methods.response.functions.FunctionsCompleteSuccessResponse; import com.slack.api.methods.response.migration.MigrationExchangeResponse; import com.slack.api.methods.response.oauth.OAuthAccessResponse; import com.slack.api.methods.response.oauth.OAuthTokenResponse; @@ -2022,6 +2026,26 @@ public CompletableFuture filesRemoteUpdate(RequestCon return filesRemoteUpdate(req.configure(FilesRemoteUpdateRequest.builder()).build()); } + @Override + public CompletableFuture functionsCompleteSuccess(FunctionsCompleteSuccessRequest req) { + return executor.execute(FUNCTIONS_COMPLETE_SUCCESS, toMap(req), () -> methods.functionsCompleteSuccess(req)); + } + + @Override + public CompletableFuture functionsCompleteSuccess(RequestConfigurator req) { + return functionsCompleteSuccess(req.configure(FunctionsCompleteSuccessRequest.builder()).build()); + } + + @Override + public CompletableFuture functionsCompleteError(FunctionsCompleteErrorRequest req) { + return executor.execute(FUNCTIONS_COMPLETE_ERROR, toMap(req), () -> methods.functionsCompleteError(req)); + } + + @Override + public CompletableFuture functionsCompleteError(RequestConfigurator req) { + return functionsCompleteError(req.configure(FunctionsCompleteErrorRequest.builder()).build()); + } + @Override public CompletableFuture migrationExchange(MigrationExchangeRequest req) { return executor.execute(MIGRATION_EXCHANGE, toMap(req), () -> methods.migrationExchange(req)); diff --git a/slack-api-client/src/main/java/com/slack/api/methods/impl/MethodsClientImpl.java b/slack-api-client/src/main/java/com/slack/api/methods/impl/MethodsClientImpl.java index b22d35b2f..db60fba68 100644 --- a/slack-api-client/src/main/java/com/slack/api/methods/impl/MethodsClientImpl.java +++ b/slack-api-client/src/main/java/com/slack/api/methods/impl/MethodsClientImpl.java @@ -77,6 +77,8 @@ import com.slack.api.methods.request.files.comments.FilesCommentsDeleteRequest; import com.slack.api.methods.request.files.comments.FilesCommentsEditRequest; import com.slack.api.methods.request.files.remote.*; +import com.slack.api.methods.request.functions.FunctionsCompleteErrorRequest; +import com.slack.api.methods.request.functions.FunctionsCompleteSuccessRequest; import com.slack.api.methods.request.groups.*; import com.slack.api.methods.request.im.*; import com.slack.api.methods.request.migration.MigrationExchangeRequest; @@ -193,6 +195,8 @@ import com.slack.api.methods.response.files.comments.FilesCommentsDeleteResponse; import com.slack.api.methods.response.files.comments.FilesCommentsEditResponse; import com.slack.api.methods.response.files.remote.*; +import com.slack.api.methods.response.functions.FunctionsCompleteErrorResponse; +import com.slack.api.methods.response.functions.FunctionsCompleteSuccessResponse; import com.slack.api.methods.response.groups.*; import com.slack.api.methods.response.im.*; import com.slack.api.methods.response.migration.MigrationExchangeResponse; @@ -2359,6 +2363,26 @@ public FilesRemoteUpdateResponse filesRemoteUpdate(RequestConfigurator req) throws IOException, SlackApiException { + return functionsCompleteSuccess(req.configure(FunctionsCompleteSuccessRequest.builder()).build()); + } + + @Override + public FunctionsCompleteErrorResponse functionsCompleteError(FunctionsCompleteErrorRequest req) throws IOException, SlackApiException { + return postFormWithTokenAndParseResponse(toForm(req), Methods.FUNCTIONS_COMPLETE_ERROR, getToken(req), FunctionsCompleteErrorResponse.class); + } + + @Override + public FunctionsCompleteErrorResponse functionsCompleteError(RequestConfigurator req) throws IOException, SlackApiException { + return functionsCompleteError(req.configure(FunctionsCompleteErrorRequest.builder()).build()); + } + @Override public GroupsArchiveResponse groupsArchive(GroupsArchiveRequest req) throws IOException, SlackApiException { return postFormWithTokenAndParseResponse(toForm(req), Methods.GROUPS_ARCHIVE, getToken(req), GroupsArchiveResponse.class); diff --git a/slack-api-client/src/main/java/com/slack/api/methods/request/functions/FunctionsCompleteErrorRequest.java b/slack-api-client/src/main/java/com/slack/api/methods/request/functions/FunctionsCompleteErrorRequest.java new file mode 100644 index 000000000..6ad5f6ea1 --- /dev/null +++ b/slack-api-client/src/main/java/com/slack/api/methods/request/functions/FunctionsCompleteErrorRequest.java @@ -0,0 +1,24 @@ +package com.slack.api.methods.request.functions; + +import com.slack.api.methods.SlackApiRequest; +import lombok.Builder; +import lombok.Data; + +import java.util.Map; + +/** + * https://api.slack.com/methods/functions.completeError + */ +@Data +@Builder +public class FunctionsCompleteErrorRequest implements SlackApiRequest { + + /** + * Authentication token bearing required scopes. + * Tokens should be passed as an HTTP Authorization header or alternatively, as a POST parameter. + */ + private String token; + + private String functionExecutionId; + private String error; +} \ No newline at end of file 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 new file mode 100644 index 000000000..705e569ca --- /dev/null +++ b/slack-api-client/src/main/java/com/slack/api/methods/request/functions/FunctionsCompleteSuccessRequest.java @@ -0,0 +1,24 @@ +package com.slack.api.methods.request.functions; + +import com.slack.api.methods.SlackApiRequest; +import lombok.Builder; +import lombok.Data; + +import java.util.Map; + +/** + * https://api.slack.com/methods/functions.completeSuccess + */ +@Data +@Builder +public class FunctionsCompleteSuccessRequest implements SlackApiRequest { + + /** + * Authentication token bearing required scopes. + * Tokens should be passed as an HTTP Authorization header or alternatively, as a POST parameter. + */ + private String token; + + private String functionExecutionId; + private Map outputs; +} \ No newline at end of file diff --git a/slack-api-client/src/main/java/com/slack/api/methods/response/functions/FunctionsCompleteErrorResponse.java b/slack-api-client/src/main/java/com/slack/api/methods/response/functions/FunctionsCompleteErrorResponse.java new file mode 100644 index 000000000..dfa935ab9 --- /dev/null +++ b/slack-api-client/src/main/java/com/slack/api/methods/response/functions/FunctionsCompleteErrorResponse.java @@ -0,0 +1,19 @@ +package com.slack.api.methods.response.functions; + +import com.slack.api.methods.SlackApiTextResponse; +import lombok.Data; + +import java.util.List; +import java.util.Map; + +@Data +public class FunctionsCompleteErrorResponse implements SlackApiTextResponse { + + private boolean ok; + private String warning; + private String error; + private String needed; + private String provided; + private transient Map> httpResponseHeaders; + +} \ No newline at end of file diff --git a/slack-api-client/src/main/java/com/slack/api/methods/response/functions/FunctionsCompleteSuccessResponse.java b/slack-api-client/src/main/java/com/slack/api/methods/response/functions/FunctionsCompleteSuccessResponse.java new file mode 100644 index 000000000..018e1651f --- /dev/null +++ b/slack-api-client/src/main/java/com/slack/api/methods/response/functions/FunctionsCompleteSuccessResponse.java @@ -0,0 +1,19 @@ +package com.slack.api.methods.response.functions; + +import com.slack.api.methods.SlackApiTextResponse; +import lombok.Data; + +import java.util.List; +import java.util.Map; + +@Data +public class FunctionsCompleteSuccessResponse implements SlackApiTextResponse { + + private boolean ok; + private String warning; + private String error; + private String needed; + private String provided; + private transient Map> httpResponseHeaders; + +} \ No newline at end of file diff --git a/slack-api-client/src/main/java/com/slack/api/util/json/GsonFactory.java b/slack-api-client/src/main/java/com/slack/api/util/json/GsonFactory.java index 5f8c49ba1..f5e39b439 100644 --- a/slack-api-client/src/main/java/com/slack/api/util/json/GsonFactory.java +++ b/slack-api-client/src/main/java/com/slack/api/util/json/GsonFactory.java @@ -12,6 +12,7 @@ import com.slack.api.model.block.composition.TextObject; import com.slack.api.model.block.element.BlockElement; import com.slack.api.model.block.element.RichTextElement; +import com.slack.api.model.event.FunctionExecutedEvent; import com.slack.api.model.event.MessageChangedEvent; /** @@ -32,6 +33,7 @@ public static Gson createSnakeCase() { .registerTypeAdapter(ContextBlockElement.class, new GsonContextBlockElementFactory()) .registerTypeAdapter(BlockElement.class, new GsonBlockElementFactory()) .registerTypeAdapter(RichTextElement.class, new GsonRichTextElementFactory()) + .registerTypeAdapter(FunctionExecutedEvent.InputValue.class, new GsonFunctionExecutedEventInputValueFactory()) .registerTypeAdapter(Attachment.VideoHtml.class, new GsonMessageAttachmentVideoHtmlFactory()) .registerTypeAdapter(MessageChangedEvent.PreviousMessage.class, new GsonMessageChangedEventPreviousMessageFactory()) .registerTypeAdapter(AppWorkflow.StepInputValue.class, new GsonAppWorkflowStepInputValueFactory()) @@ -53,6 +55,7 @@ public static Gson createSnakeCase(SlackConfig config) { .registerTypeAdapter(ContextBlockElement.class, new GsonContextBlockElementFactory(failOnUnknownProps)) .registerTypeAdapter(BlockElement.class, new GsonBlockElementFactory(failOnUnknownProps)) .registerTypeAdapter(RichTextElement.class, new GsonRichTextElementFactory(failOnUnknownProps)) + .registerTypeAdapter(FunctionExecutedEvent.InputValue.class, new GsonFunctionExecutedEventInputValueFactory()) .registerTypeAdapter(Attachment.VideoHtml.class, new GsonMessageAttachmentVideoHtmlFactory(failOnUnknownProps)) .registerTypeAdapter(MessageChangedEvent.PreviousMessage.class, new GsonMessageChangedEventPreviousMessageFactory(failOnUnknownProps)) .registerTypeAdapter(AppWorkflow.StepInputValue.class, new GsonAppWorkflowStepInputValueFactory(failOnUnknownProps)) diff --git a/slack-api-model/src/main/java/com/slack/api/model/event/FunctionExecutedEvent.java b/slack-api-model/src/main/java/com/slack/api/model/event/FunctionExecutedEvent.java new file mode 100644 index 000000000..7559415fb --- /dev/null +++ b/slack-api-model/src/main/java/com/slack/api/model/event/FunctionExecutedEvent.java @@ -0,0 +1,80 @@ +package com.slack.api.model.event; + +import com.google.gson.annotations.SerializedName; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.util.List; +import java.util.Map; + +@Data +public class FunctionExecutedEvent implements Event { + + public static final String TYPE_NAME = "function_executed"; + + private final String type = TYPE_NAME; + private Function function; + private Map inputs; + private String functionExecutionId; + private String workflowExecutionId; + private String eventTs; + private String botAccessToken; + + + @Data + public static class FunctionParameter { + private String type; // "string", "number", "slack#/types/user_id" + private String name; + private String description; + private String title; + @SerializedName("is_required") + private boolean required; + private String hint; + private Integer maximum; + private Integer minimum; + @SerializedName("maxLength") + private Integer maxLength; + @SerializedName("minLength") + private Integer minLength; + } + + @Data + public static class Function { + private String id; // "Fn066C7U22JD" + private String callbackId; + private String title; + private String description; + private String type; // "app" + private List inputParameters; + private List outputParameters; + private String appId; + private Integer dateCreated; + private Integer dateUpdated; + private Integer dateDeleted; + private boolean formEnabled; + // TODO: other data patterns + } + + @Data + @AllArgsConstructor + @NoArgsConstructor + public static class InputValue { + private String stringValue; + private List stringValues; + + public String asString() { + return this.stringValue; + } + public Integer asInteger() { + return this.stringValue != null ? Integer.valueOf(this.stringValue) : null; + } + public Double asDouble() { + return this.stringValue != null ? Double.valueOf(this.stringValue) : null; + } + public Float asFloat() { + return this.stringValue != null ? Float.valueOf(this.stringValue) : null; + } + // TODO: other data patterns + } +} \ No newline at end of file diff --git a/slack-api-model/src/main/java/com/slack/api/util/json/GsonFunctionExecutedEventInputValueFactory.java b/slack-api-model/src/main/java/com/slack/api/util/json/GsonFunctionExecutedEventInputValueFactory.java new file mode 100644 index 000000000..1fd5757b3 --- /dev/null +++ b/slack-api-model/src/main/java/com/slack/api/util/json/GsonFunctionExecutedEventInputValueFactory.java @@ -0,0 +1,75 @@ +package com.slack.api.util.json; + +import com.google.gson.*; +import com.slack.api.model.event.FunctionExecutedEvent; + +import java.lang.reflect.Type; +import java.util.ArrayList; +import java.util.List; + +public class GsonFunctionExecutedEventInputValueFactory + implements JsonDeserializer, + JsonSerializer { + + private static final String REPORT_THIS = "Please report this issue at https://github.com/slackapi/java-slack-sdk/issues"; + + private final boolean failOnUnknownProperties; + + public GsonFunctionExecutedEventInputValueFactory() { + this(false); + } + + public GsonFunctionExecutedEventInputValueFactory(boolean failOnUnknownProperties) { + this.failOnUnknownProperties = failOnUnknownProperties; + } + + @Override + public FunctionExecutedEvent.InputValue deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context) + throws JsonParseException { + FunctionExecutedEvent.InputValue result = new FunctionExecutedEvent.InputValue(); + if (json.isJsonPrimitive()) { + result.setStringValue(json.getAsString()); + return result; + } else if (json.isJsonArray()) { + result.setStringValues(parseStringArray(json)); + return result; + // TODO: } else if (json.isJsonObject()) { + } else { + if (failOnUnknownProperties) { + String message = "The whole value (" + json + ") is unsupported. " + REPORT_THIS; + throw new JsonParseException(message); + } + } + return result; + } + + private List parseStringArray(JsonElement json) throws JsonParseException { + List values = new ArrayList<>(); + for (JsonElement elem : json.getAsJsonArray()) { + if (elem.isJsonPrimitive()) { + values.add(elem.getAsString()); + } else { + if (failOnUnknownProperties) { + String message = "An unexpected element (" + elem + ") in an array is detected. " + REPORT_THIS; + throw new JsonParseException(message); + } + } + } + return values; + } + + @Override + public JsonElement serialize(FunctionExecutedEvent.InputValue src, Type typeOfSrc, JsonSerializationContext context) { + if (src.getStringValue() != null) { + return new JsonPrimitive(src.getStringValue()); + } else if (src.getStringValues() != null) { + JsonArray array = new JsonArray(); + for (String value : src.getStringValues()) { + array.add(value); + } + return array; + } else { + return JsonNull.INSTANCE; + } + } +} diff --git a/slack-api-model/src/test/java/test_locally/api/model/event/FunctionExecutedEventTest.java b/slack-api-model/src/test/java/test_locally/api/model/event/FunctionExecutedEventTest.java new file mode 100644 index 000000000..a3c1d9b8d --- /dev/null +++ b/slack-api-model/src/test/java/test_locally/api/model/event/FunctionExecutedEventTest.java @@ -0,0 +1,111 @@ +package test_locally.api.model.event; + +import com.google.gson.Gson; +import com.slack.api.model.event.FunctionExecutedEvent; +import org.junit.Test; +import test_locally.unit.GsonFactory; + +import static org.hamcrest.CoreMatchers.is; +import static org.hamcrest.CoreMatchers.notNullValue; +import static org.hamcrest.MatcherAssert.assertThat; + +public class FunctionExecutedEventTest { + + String JSON = "{\n" + + " \"type\": \"function_executed\",\n" + + " \"function\": {\n" + + " \"id\": \"Fn066C7U22JD\",\n" + + " \"callback_id\": \"hello\",\n" + + " \"title\": \"Hello\",\n" + + " \"description\": \"Hello world!\",\n" + + " \"type\": \"app\",\n" + + " \"input_parameters\": [\n" + + " {\n" + + " \"type\": \"number\",\n" + + " \"name\": \"amount\",\n" + + " \"description\": \"How many do you need?\",\n" + + " \"title\": \"Amount\",\n" + + " \"is_required\": false,\n" + + " \"hint\": \"How many do you need?\",\n" + + " \"maximum\": 10,\n" + + " \"minimum\": 1\n" + + " },\n" + + " {\n" + + " \"type\": \"slack#/types/user_id\",\n" + + " \"name\": \"user_id\",\n" + + " \"description\": \"Who to send it\",\n" + + " \"title\": \"User\",\n" + + " \"is_required\": true,\n" + + " \"hint\": \"Select a user in the workspace\"\n" + + " },\n" + + " {\n" + + " \"type\": \"string\",\n" + + " \"name\": \"message\",\n" + + " \"description\": \"Whatever you want to tell\",\n" + + " \"title\": \"Message\",\n" + + " \"is_required\": false,\n" + + " \"hint\": \"up to 100 characters\",\n" + + " \"maxLength\": 100,\n" + + " \"minLength\": 1\n" + + " }\n" + + " ],\n" + + " \"output_parameters\": [\n" + + " {\n" + + " \"type\": \"number\",\n" + + " \"name\": \"amount\",\n" + + " \"description\": \"How many do you need?\",\n" + + " \"title\": \"Amount\",\n" + + " \"is_required\": false,\n" + + " \"hint\": \"How many do you need?\",\n" + + " \"maximum\": 10,\n" + + " \"minimum\": 1\n" + + " },\n" + + " {\n" + + " \"type\": \"slack#/types/user_id\",\n" + + " \"name\": \"user_id\",\n" + + " \"description\": \"Who to send it\",\n" + + " \"title\": \"User\",\n" + + " \"is_required\": true,\n" + + " \"hint\": \"Select a user in the workspace\"\n" + + " },\n" + + " {\n" + + " \"type\": \"string\",\n" + + " \"name\": \"message\",\n" + + " \"description\": \"Whatever you want to tell\",\n" + + " \"title\": \"Message\",\n" + + " \"is_required\": false,\n" + + " \"hint\": \"up to 100 characters\",\n" + + " \"maxLength\": 100,\n" + + " \"minLength\": 1\n" + + " }\n" + + " ],\n" + + " \"app_id\": \"A065ZJM410S\",\n" + + " \"date_created\": 1700110468,\n" + + " \"date_updated\": 1700110470,\n" + + " \"date_deleted\": 0,\n" + + " \"form_enabled\": false\n" + + " },\n" + + " \"inputs\": {\n" + + " \"amount\": 1,\n" + + " \"message\": \"hey\",\n" + + " \"user_id\": \"U03E94MK0\"\n" + + " },\n" + + " \"function_execution_id\": \"Fx065S3T3W2K\",\n" + + " \"workflow_execution_id\": \"Wx0666KGEUQ2\",\n" + + " \"event_ts\": \"1700201360.208558\",\n" + + " \"bot_access_token\": \"xwfp-...\"\n" + + "}"; + + @Test + public void deserialize() { + Gson gson = GsonFactory.createSnakeCase(); + FunctionExecutedEvent event = gson.fromJson(JSON, FunctionExecutedEvent.class); + assertThat(event, is(notNullValue())); + assertThat(event.getFunction().getInputParameters().size(), is(3)); + assertThat(event.getInputs().get("amount").asInteger(), is(1)); + assertThat(event.getInputs().get("message").asString(), is("hey")); + assertThat(event.getInputs().get("user_id").asString(), is("U03E94MK0")); + assertThat(event.getFunctionExecutionId(), is("Fx065S3T3W2K")); + assertThat(event.getBotAccessToken(), is("xwfp-...")); + } +} diff --git a/slack-api-model/src/test/java/test_locally/unit/GsonFactory.java b/slack-api-model/src/test/java/test_locally/unit/GsonFactory.java index 591f0a2a0..9bc17794b 100644 --- a/slack-api-model/src/test/java/test_locally/unit/GsonFactory.java +++ b/slack-api-model/src/test/java/test_locally/unit/GsonFactory.java @@ -9,6 +9,7 @@ import com.slack.api.model.block.composition.TextObject; import com.slack.api.model.block.element.BlockElement; import com.slack.api.model.block.element.RichTextElement; +import com.slack.api.model.event.FunctionExecutedEvent; import com.slack.api.model.event.MessageChangedEvent; import com.slack.api.util.json.*; @@ -32,6 +33,8 @@ public static Gson createSnakeCase(boolean failOnUnknownProperties, boolean unkn .registerTypeAdapter(ContextBlockElement.class, new GsonContextBlockElementFactory(failOnUnknownProperties)) .registerTypeAdapter(TextObject.class, new GsonTextObjectFactory(failOnUnknownProperties)) .registerTypeAdapter(RichTextElement.class, new GsonRichTextElementFactory(failOnUnknownProperties)) + .registerTypeAdapter(FunctionExecutedEvent.InputValue.class, + new GsonFunctionExecutedEventInputValueFactory(failOnUnknownProperties)) .registerTypeAdapter(Attachment.VideoHtml.class, new GsonMessageAttachmentVideoHtmlFactory(failOnUnknownProperties)) .registerTypeAdapter(MessageChangedEvent.PreviousMessage.class, diff --git a/slack-app-backend/src/main/java/com/slack/api/app_backend/events/payload/FunctionExecutedPayload.java b/slack-app-backend/src/main/java/com/slack/api/app_backend/events/payload/FunctionExecutedPayload.java new file mode 100644 index 000000000..d888c1929 --- /dev/null +++ b/slack-app-backend/src/main/java/com/slack/api/app_backend/events/payload/FunctionExecutedPayload.java @@ -0,0 +1,25 @@ +package com.slack.api.app_backend.events.payload; + +import com.slack.api.model.event.FunctionExecutedEvent; +import lombok.Data; + +import java.util.List; + +@Data +public class FunctionExecutedPayload implements EventsApiPayload { + + private String token; + private String enterpriseId; + private String teamId; + private String apiAppId; + private String type; + private List authedUsers; + private List authedTeams; + private List authorizations; + private boolean isExtSharedChannel; + private String eventId; + private Integer eventTime; + private String eventContext; + + private FunctionExecutedEvent event; +} diff --git a/slack-app-backend/src/main/java/com/slack/api/app_backend/interactive_components/payload/BlockActionPayload.java b/slack-app-backend/src/main/java/com/slack/api/app_backend/interactive_components/payload/BlockActionPayload.java index 7bdd32b1e..3340926da 100644 --- a/slack-app-backend/src/main/java/com/slack/api/app_backend/interactive_components/payload/BlockActionPayload.java +++ b/slack-app-backend/src/main/java/com/slack/api/app_backend/interactive_components/payload/BlockActionPayload.java @@ -7,6 +7,7 @@ import com.slack.api.model.block.composition.ConfirmationDialogObject; import com.slack.api.model.block.composition.OptionObject; import com.slack.api.model.block.composition.PlainTextObject; +import com.slack.api.model.event.FunctionExecutedEvent; import com.slack.api.model.view.View; import com.slack.api.model.view.ViewState; import lombok.AllArgsConstructor; @@ -15,6 +16,7 @@ import lombok.NoArgsConstructor; import java.util.List; +import java.util.Map; /** * https://api.slack.com/messaging/interactivity/enabling @@ -44,6 +46,11 @@ public class BlockActionPayload { private List actions; private boolean isEnterpriseInstall; + private String botAccessToken; // for remote function's interactivity + private FunctionData functionData; // for remote function's interactivity + private Interactivity interactivity; // for remote function's interactivity + + @Data public static class Enterprise { private String id; @@ -177,4 +184,26 @@ public static class SelectedOption { private RichTextBlock richTextValue; } + @Data + public static class FunctionData { + private String executionId; + private Function function; + private Map inputs; + } + + @Data + public static class Function { + private String callbackId; + } + + @Data + public static class Interactivity { + private String interactivityPointer; // you can use this in the same way with trigger_id + private Interactor interactor; + } + @Data + public static class Interactor { + private String id; + private String secret; + } } diff --git a/slack-app-backend/src/main/java/com/slack/api/app_backend/views/payload/ViewClosedPayload.java b/slack-app-backend/src/main/java/com/slack/api/app_backend/views/payload/ViewClosedPayload.java index 0f23f8a4e..2dac3a884 100644 --- a/slack-app-backend/src/main/java/com/slack/api/app_backend/views/payload/ViewClosedPayload.java +++ b/slack-app-backend/src/main/java/com/slack/api/app_backend/views/payload/ViewClosedPayload.java @@ -1,11 +1,14 @@ package com.slack.api.app_backend.views.payload; +import com.slack.api.model.event.FunctionExecutedEvent; import com.slack.api.model.view.View; import lombok.AllArgsConstructor; import lombok.Builder; import lombok.Data; import lombok.NoArgsConstructor; +import java.util.Map; + /** * @see Modals */ @@ -46,4 +49,31 @@ public static class User { private String name; private String teamId; } + + private String botAccessToken; // for remote function's interactivity + private FunctionData functionData; // for remote function's interactivity + private Interactivity interactivity; // for remote function's interactivity + + @Data + public static class FunctionData { + private String executionId; + private Function function; + private Map inputs; + } + + @Data + public static class Function { + private String callbackId; + } + + @Data + public static class Interactivity { + private String interactivityPointer; // you can use this in the same way with trigger_id + private Interactor interactor; + } + @Data + public static class Interactor { + private String id; + private String secret; + } } \ No newline at end of file diff --git a/slack-app-backend/src/main/java/com/slack/api/app_backend/views/payload/ViewSubmissionPayload.java b/slack-app-backend/src/main/java/com/slack/api/app_backend/views/payload/ViewSubmissionPayload.java index f7ba46992..892e1d2f1 100644 --- a/slack-app-backend/src/main/java/com/slack/api/app_backend/views/payload/ViewSubmissionPayload.java +++ b/slack-app-backend/src/main/java/com/slack/api/app_backend/views/payload/ViewSubmissionPayload.java @@ -1,5 +1,6 @@ package com.slack.api.app_backend.views.payload; +import com.slack.api.model.event.FunctionExecutedEvent; import com.slack.api.model.view.View; import lombok.AllArgsConstructor; import lombok.Builder; @@ -7,6 +8,7 @@ import lombok.NoArgsConstructor; import java.util.List; +import java.util.Map; /** * @see Modals @@ -64,4 +66,31 @@ public static class ResponseUrl { private String responseUrl; } + private String botAccessToken; // for remote function's interactivity + private FunctionData functionData; // for remote function's interactivity + private Interactivity interactivity; // for remote function's interactivity + + @Data + public static class FunctionData { + private String executionId; + private Function function; + private Map inputs; + } + + @Data + public static class Function { + private String callbackId; + } + + @Data + public static class Interactivity { + private String interactivityPointer; // you can use this in the same way with trigger_id + private Interactor interactor; + } + @Data + public static class Interactor { + private String id; + private String secret; + } + } diff --git a/slack-app-backend/src/test/java/test_locally/app_backend/interactive_components/payload/BlockActionPayloadTest.java b/slack-app-backend/src/test/java/test_locally/app_backend/interactive_components/payload/BlockActionPayloadTest.java index 8dc28f2e6..453396c66 100644 --- a/slack-app-backend/src/test/java/test_locally/app_backend/interactive_components/payload/BlockActionPayloadTest.java +++ b/slack-app-backend/src/test/java/test_locally/app_backend/interactive_components/payload/BlockActionPayloadTest.java @@ -397,4 +397,99 @@ public void threaded_message() { .getSelectedOption().getValue(), is("schedule")); } + + String jsonInteractionsFromRemoteFunction = "{\n" + + " \"type\": \"block_actions\",\n" + + " \"team\": {\n" + + " \"id\": \"T03E94MJU\",\n" + + " \"domain\": \"test\"\n" + + " },\n" + + " \"user\": {\n" + + " \"id\": \"U03E94MK0\",\n" + + " \"name\": \"kaz\",\n" + + " \"team_id\": \"T03E94MJU\"\n" + + " },\n" + + " \"channel\": {\n" + + " \"id\": \"D065ZJQQQAE\",\n" + + " \"name\": \"directmessage\"\n" + + " },\n" + + " \"message\": {\n" + + " \"bot_id\": \"B065SV9Q70W\",\n" + + " \"type\": \"message\",\n" + + " \"text\": \"hey!\",\n" + + " \"user\": \"U066C7XNE6M\",\n" + + " \"ts\": \"1700455285.968429\",\n" + + " \"app_id\": \"A065ZJM410S\",\n" + + " \"blocks\": [\n" + + " {\n" + + " \"type\": \"actions\",\n" + + " \"block_id\": \"b\",\n" + + " \"elements\": [\n" + + " {\n" + + " \"type\": \"button\",\n" + + " \"action_id\": \"a\",\n" + + " \"text\": {\n" + + " \"type\": \"plain_text\",\n" + + " \"text\": \"Click this!\",\n" + + " \"emoji\": true\n" + + " },\n" + + " \"value\": \"clicked\"\n" + + " }\n" + + " ]\n" + + " }\n" + + " ],\n" + + " \"team\": \"T03E94MJU\"\n" + + " },\n" + + " \"container\": {\n" + + " \"type\": \"message\",\n" + + " \"message_ts\": \"1700455285.968429\",\n" + + " \"channel_id\": \"D065ZJQQQAE\",\n" + + " \"is_ephemeral\": false\n" + + " },\n" + + " \"actions\": [\n" + + " {\n" + + " \"block_id\": \"b\",\n" + + " \"action_id\": \"a\",\n" + + " \"type\": \"button\",\n" + + " \"text\": {\n" + + " \"type\": \"plain_text\",\n" + + " \"text\": \"Click this!\",\n" + + " \"emoji\": true\n" + + " },\n" + + " \"value\": \"clicked\",\n" + + " \"action_ts\": \"1700455293.945608\"\n" + + " }\n" + + " ],\n" + + " \"api_app_id\": \"A065ZJM410S\",\n" + + " \"state\": {\n" + + " \"values\": {}\n" + + " },\n" + + " \"bot_access_token\": \"xwfp-valid\",\n" + + " \"function_data\": {\n" + + " \"execution_id\": \"Fx066J3N9ME0\",\n" + + " \"function\": {\n" + + " \"callback_id\": \"hello\"\n" + + " },\n" + + " \"inputs\": {\n" + + " \"amount\": 1,\n" + + " \"message\": \"hey\",\n" + + " \"user_id\": \"U03E94MK0\"\n" + + " }\n" + + " },\n" + + " \"interactivity\": {\n" + + " \"interactor\": {\n" + + " \"secret\": \"interactor-secret\",\n" + + " \"id\": \"U03E94MK0\"\n" + + " },\n" + + " \"interactivity_pointer\": \"111.222.333\"\n" + + " }\n" + + "}\n"; + + @Test + public void interactionsFromRemoteFunction() { + BlockActionPayload payload = GSON.fromJson(jsonInteractionsFromRemoteFunction, BlockActionPayload.class); + assertThat(payload.getType(), is("block_actions")); + assertThat(payload.getActions().size(), is(1)); + } + } diff --git a/slack-app-backend/src/test/java/test_locally/app_backend/views/ViewSubmissionPayloadTest.java b/slack-app-backend/src/test/java/test_locally/app_backend/views/ViewSubmissionPayloadTest.java index 33368f4ef..edfb3b917 100644 --- a/slack-app-backend/src/test/java/test_locally/app_backend/views/ViewSubmissionPayloadTest.java +++ b/slack-app-backend/src/test/java/test_locally/app_backend/views/ViewSubmissionPayloadTest.java @@ -1,12 +1,14 @@ package test_locally.app_backend.views; import com.google.gson.Gson; +import com.slack.api.SlackConfig; import com.slack.api.app_backend.views.payload.ViewSubmissionPayload; import com.slack.api.model.block.InputBlock; import com.slack.api.model.block.LayoutBlock; import com.slack.api.model.block.element.TimePickerElement; import com.slack.api.model.view.ViewState; import com.slack.api.util.json.GsonFactory; +import config.SlackTestConfig; import org.junit.Test; import java.util.stream.Collectors; @@ -16,7 +18,8 @@ public class ViewSubmissionPayloadTest { - private Gson gson = GsonFactory.createSnakeCase(); + + private Gson gson = GsonFactory.createSnakeCase(SlackTestConfig.get()); private String json = "{\n" + " \"type\": \"view_submission\",\n" + @@ -400,4 +403,104 @@ public void inputElementsAddedInOctober2022() { is(1666869900)); } + + @Test + public void remoteFunctions() { + String json = "{\n" + + " \"type\": \"view_submission\",\n" + + " \"team\": {\n" + + " \"id\": \"T03E94MJU\",\n" + + " \"domain\": \"test\"\n" + + " },\n" + + " \"user\": {\n" + + " \"id\": \"U03E94MK0\",\n" + + " \"name\": \"kaz\",\n" + + " \"team_id\": \"T03E94MJU\"\n" + + " },\n" + + " \"view\": {\n" + + " \"id\": \"V066196HSPR\",\n" + + " \"team_id\": \"T03E94MJU\",\n" + + " \"app_id\": \"A065ZJM410S\",\n" + + " \"app_installed_team_id\": \"T03E94MJU\",\n" + + " \"bot_id\": \"B065SV9Q70W\",\n" + + " \"title\": {\n" + + " \"type\": \"plain_text\",\n" + + " \"text\": \"Remote Function test\",\n" + + " \"emoji\": false\n" + + " },\n" + + " \"type\": \"modal\",\n" + + " \"blocks\": [\n" + + " {\n" + + " \"type\": \"input\",\n" + + " \"block_id\": \"text-block\",\n" + + " \"label\": {\n" + + " \"type\": \"plain_text\",\n" + + " \"text\": \"Text\",\n" + + " \"emoji\": true\n" + + " },\n" + + " \"optional\": false,\n" + + " \"dispatch_action\": false,\n" + + " \"element\": {\n" + + " \"type\": \"plain_text_input\",\n" + + " \"action_id\": \"text-action\",\n" + + " \"multiline\": true,\n" + + " \"dispatch_action_config\": {\n" + + " \"trigger_actions_on\": [\n" + + " \"on_enter_pressed\"\n" + + " ]\n" + + " }\n" + + " }\n" + + " }\n" + + " ],\n" + + " \"close\": {\n" + + " \"type\": \"plain_text\",\n" + + " \"text\": \"Close\",\n" + + " \"emoji\": false\n" + + " },\n" + + " \"submit\": {\n" + + " \"type\": \"plain_text\",\n" + + " \"text\": \"Submit\",\n" + + " \"emoji\": false\n" + + " },\n" + + " \"state\": {\n" + + " \"values\": {\n" + + " \"text-block\": {\n" + + " \"text-action\": {\n" + + " \"type\": \"plain_text_input\",\n" + + " \"value\": \"test\"\n" + + " }\n" + + " }\n" + + " }\n" + + " },\n" + + " \"hash\": \"1700459058.dyRTuN2P\",\n" + + " \"callback_id\": \"remote-function-view\",\n" + + " \"root_view_id\": \"V066196HSPR\",\n" + + " \"clear_on_close\": false,\n" + + " \"notify_on_close\": false,\n" + + " \"external_id\": \"\"\n" + + " },\n" + + " \"api_app_id\": \"A065ZJM410S\",\n" + + " \"bot_access_token\": \"xwfp-valid\",\n" + + " \"function_data\": {\n" + + " \"execution_id\": \"Fx0674QF1X08\",\n" + + " \"function\": {\n" + + " \"callback_id\": \"hello\"\n" + + " },\n" + + " \"inputs\": {\n" + + " \"amount\": 1,\n" + + " \"message\": \"hey\",\n" + + " \"user_id\": \"U03E94MK0\"\n" + + " }\n" + + " },\n" + + " \"interactivity\": {\n" + + " \"interactor\": {\n" + + " \"secret\": \"secret\",\n" + + " \"id\": \"U03E94MK0\"\n" + + " },\n" + + " \"interactivity_pointer\": \"111.222.333\"\n" + + " }\n" + + "}"; + ViewSubmissionPayload payload = gson.fromJson(json, ViewSubmissionPayload.class); + assertThat(payload, is(notNullValue())); + } }