From 39325e01eca2f14735d0c8ef186982ed395607d5 Mon Sep 17 00:00:00 2001 From: Kazuhiro Sera Date: Wed, 22 Nov 2023 10:18:50 +0900 Subject: [PATCH] Add functions.* APIs and related property additions to event/interactivity payloads --- .../src/test/java/samples/SimpleApp.java | 110 ++++- .../test_locally/app/RemoteFunctionTest.java | 464 ++++++++++++++++++ bolt/src/test/java/util/MockSlackApi.java | 4 +- .../samples/api/functions.completeError.json | 6 + .../api/functions.completeSuccess.json | 6 + .../BlockActionPayload.json | 16 +- .../app-backend/views/ViewClosedPayload.json | 16 +- .../views/ViewSubmissionPayload.json | 16 +- metadata/web-api/rate_limit_tiers.json | 2 + .../slack/api/methods/AsyncMethodsClient.java | 17 +- .../java/com/slack/api/methods/Methods.java | 7 + .../com/slack/api/methods/MethodsClient.java | 16 + .../slack/api/methods/MethodsRateLimits.java | 3 + .../slack/api/methods/RequestFormBuilder.java | 16 + .../methods/impl/AsyncMethodsClientImpl.java | 24 + .../api/methods/impl/MethodsClientImpl.java | 24 + .../FunctionsCompleteErrorRequest.java | 24 + .../FunctionsCompleteSuccessRequest.java | 24 + .../FunctionsCompleteErrorResponse.java | 19 + .../FunctionsCompleteSuccessResponse.java | 19 + .../com/slack/api/util/json/GsonFactory.java | 3 + .../api/methods/FunctionsTest.java | 45 ++ .../src/test/java/util/MockSlackApi.java | 4 +- .../model/event/FunctionExecutedEvent.java | 80 +++ ...unctionExecutedEventInputValueFactory.java | 75 +++ .../event/FunctionExecutedEventTest.java | 113 +++++ .../java/test_locally/unit/GsonFactory.java | 3 + .../payload/FunctionExecutedPayload.java | 25 + .../payload/BlockActionPayload.java | 29 ++ .../views/payload/ViewClosedPayload.java | 30 ++ .../views/payload/ViewSubmissionPayload.java | 29 ++ .../payload/BlockActionPayloadTest.java | 95 ++++ .../views/ViewSubmissionPayloadTest.java | 105 +++- 33 files changed, 1458 insertions(+), 11 deletions(-) create mode 100644 bolt/src/test/java/test_locally/app/RemoteFunctionTest.java create mode 100644 json-logs/samples/api/functions.completeError.json create mode 100644 json-logs/samples/api/functions.completeSuccess.json create mode 100644 slack-api-client/src/main/java/com/slack/api/methods/request/functions/FunctionsCompleteErrorRequest.java create mode 100644 slack-api-client/src/main/java/com/slack/api/methods/request/functions/FunctionsCompleteSuccessRequest.java create mode 100644 slack-api-client/src/main/java/com/slack/api/methods/response/functions/FunctionsCompleteErrorResponse.java create mode 100644 slack-api-client/src/main/java/com/slack/api/methods/response/functions/FunctionsCompleteSuccessResponse.java create mode 100644 slack-api-client/src/test/java/test_locally/api/methods/FunctionsTest.java create mode 100644 slack-api-model/src/main/java/com/slack/api/model/event/FunctionExecutedEvent.java create mode 100644 slack-api-model/src/main/java/com/slack/api/util/json/GsonFunctionExecutedEventInputValueFactory.java create mode 100644 slack-api-model/src/test/java/test_locally/api/model/event/FunctionExecutedEventTest.java create mode 100644 slack-app-backend/src/main/java/com/slack/api/app_backend/events/payload/FunctionExecutedPayload.java diff --git a/bolt-socket-mode/src/test/java/samples/SimpleApp.java b/bolt-socket-mode/src/test/java/samples/SimpleApp.java index 9475c652f..90fdc6a46 100644 --- a/bolt-socket-mode/src/test/java/samples/SimpleApp.java +++ b/bolt-socket-mode/src/test/java/samples/SimpleApp.java @@ -5,10 +5,7 @@ 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 config.Constants; @@ -153,6 +150,111 @@ public static void main(String[] args) throws Exception { return ctx.ack(); }); + // 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.function("hello", (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( + 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 + // 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!") + ); + return ctx.ack(); + }); + 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!") + ); + return ctx.ack(); + }); + 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") + .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 + // 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!") + ); + 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 + // TODO: remove this token passing by enhancing bolt internals + .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 + // TODO: remove this token passing by enhancing bolt internals + .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/test/java/test_locally/app/RemoteFunctionTest.java b/bolt/src/test/java/test_locally/app/RemoteFunctionTest.java new file mode 100644 index 000000000..c27388496 --- /dev/null +++ b/bolt/src/test/java/test_locally/app/RemoteFunctionTest.java @@ -0,0 +1,464 @@ +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.BlockActionRequest; +import com.slack.api.bolt.request.builtin.EventRequest; +import com.slack.api.bolt.request.builtin.ViewSubmissionRequest; +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.net.URLEncoder; +import java.util.Arrays; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.concurrent.atomic.AtomicBoolean; + +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 eventPayload = "{\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"; + + String actionPayload = "{\n" + + " \"type\": \"block_actions\",\n" + + " \"team\": {\n" + + " \"id\": \"T03E94MJU\",\n" + + " \"domain\": \"seratch\"\n" + + " },\n" + + " \"user\": {\n" + + " \"id\": \"U03E94MK0\",\n" + + " \"name\": \"seratch\",\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\": \"1700615389.639759\",\n" + + " \"app_id\": \"A065ZJM410S\",\n" + + " \"blocks\": [\n" + + " {\n" + + " \"type\": \"actions\",\n" + + " \"block_id\": \"b\",\n" + + " \"elements\": [\n" + + " {\n" + + " \"type\": \"button\",\n" + + " \"action_id\": \"remote-function-button-success\",\n" + + " \"text\": {\n" + + " \"type\": \"plain_text\",\n" + + " \"text\": \"block_actions success\",\n" + + " \"emoji\": true\n" + + " },\n" + + " \"value\": \"clicked\"\n" + + " },\n" + + " {\n" + + " \"type\": \"button\",\n" + + " \"action_id\": \"remote-function-button-error\",\n" + + " \"text\": {\n" + + " \"type\": \"plain_text\",\n" + + " \"text\": \"block_actions error\",\n" + + " \"emoji\": true\n" + + " },\n" + + " \"value\": \"clicked\"\n" + + " },\n" + + " {\n" + + " \"type\": \"button\",\n" + + " \"action_id\": \"remote-function-modal\",\n" + + " \"text\": {\n" + + " \"type\": \"plain_text\",\n" + + " \"text\": \"modal view\",\n" + + " \"emoji\": true\n" + + " },\n" + + " \"value\": \"clicked\"\n" + + " }\n" + + " ]\n" + + " }\n" + + " ],\n" + + " \"team\": \"T03E94MJU\"\n" + + " },\n" + + " \"container\": {\n" + + " \"type\": \"message\",\n" + + " \"message_ts\": \"1700615389.639759\",\n" + + " \"channel_id\": \"D065ZJQQQAE\",\n" + + " \"is_ephemeral\": false\n" + + " },\n" + + " \"actions\": [\n" + + " {\n" + + " \"block_id\": \"b\",\n" + + " \"action_id\": \"remote-function-modal\",\n" + + " \"type\": \"button\",\n" + + " \"text\": {\n" + + " \"type\": \"plain_text\",\n" + + " \"text\": \"modal view\",\n" + + " \"emoji\": true\n" + + " },\n" + + " \"value\": \"clicked\",\n" + + " \"action_ts\": \"1700615393.797628\"\n" + + " }\n" + + " ],\n" + + " \"api_app_id\": \"A065ZJM410S\",\n" + + " \"state\": {\n" + + " \"values\": {}\n" + + " },\n" + + " \"bot_access_token\": \"xwfp-this-is-valid\",\n" + + " \"function_data\": {\n" + + " \"execution_id\": \"Fx066WEAA7BN\",\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" + + "}\n"; + + String viewSubmissionPayload = "{\n" + + " \"type\": \"view_submission\",\n" + + " \"team\": {\n" + + " \"id\": \"T03E94MJU\",\n" + + " \"domain\": \"seratch\"\n" + + " },\n" + + " \"user\": {\n" + + " \"id\": \"U03E94MK0\",\n" + + " \"name\": \"seratch\",\n" + + " \"team_id\": \"T03E94MJU\"\n" + + " },\n" + + " \"view\": {\n" + + " \"id\": \"V066MBN4MTQ\",\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\": \"1700615394.RxtPRJt3\",\n" + + " \"private_metadata\": \"\",\n" + + " \"callback_id\": \"remote-function-view\",\n" + + " \"root_view_id\": \"V066MBN4MTQ\",\n" + + " \"clear_on_close\": false,\n" + + " \"notify_on_close\": true,\n" + + " \"external_id\": \"\"\n" + + " },\n" + + " \"api_app_id\": \"A065ZJM410S\",\n" + + " \"bot_access_token\": \"xwfp-this-is-valid\",\n" + + " \"function_data\": {\n" + + " \"execution_id\": \"Fx066WEAA7BN\",\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" + + "}\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) + && req.getEvent().getBotAccessToken().equals("xwfp-this-is-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("")); + return 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(); + AtomicBoolean called = new AtomicBoolean(false); + app.blockAction("remote-function-modal", (req, ctx) -> { + called.set(req.getPayload().getFunctionData().getFunction().getCallbackId().equals("hello") + && req.getPayload().getFunctionData().getInputs().get("user_id").asString().equals("U03E94MK0") + && req.getPayload().getFunctionData().getInputs().get("amount").asInteger().equals(1) + && 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("")); + return ctx.ack(); + }); + + Response response = app.run(buildActionRequest()); + assertEquals(200L, response.getStatusCode().longValue()); + assertTrue(called.get()); + } + + @Test + public void view_submissions() throws Exception { + App app = buildApp(); + AtomicBoolean called = new AtomicBoolean(false); + app.viewSubmission("remote-function-view", (req, ctx) -> { + called.set(req.getPayload().getFunctionData().getFunction().getCallbackId().equals("hello") + && req.getPayload().getFunctionData().getInputs().get("user_id").asString().equals("U03E94MK0") + && req.getPayload().getFunctionData().getInputs().get("amount").asInteger().equals(1) + && 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("")); + return ctx.ack(); + }); + + Response response = app.run(buildViewSubmissionRequest()); + 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 buildEventRequest() { + Map> rawHeaders = new HashMap<>(); + String timestamp = String.valueOf(System.currentTimeMillis() / 1000); + setRequestHeaders(eventPayload, rawHeaders, timestamp); + return new EventRequest(eventPayload, new RequestHeaders(rawHeaders)); + } + + BlockActionRequest buildActionRequest() { + Map> rawHeaders = new HashMap<>(); + String timestamp = String.valueOf(System.currentTimeMillis() / 1000); + String body = "payload=" + URLEncoder.encode(actionPayload); + setRequestHeaders(body, rawHeaders, timestamp); + return new BlockActionRequest(body, actionPayload, new RequestHeaders(rawHeaders)); + } + + ViewSubmissionRequest buildViewSubmissionRequest() { + Map> rawHeaders = new HashMap<>(); + String timestamp = String.valueOf(System.currentTimeMillis() / 1000); + String body = "payload=" + URLEncoder.encode(viewSubmissionPayload); + setRequestHeaders(body, rawHeaders, timestamp); + return new ViewSubmissionRequest(body, viewSubmissionPayload, 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-client/src/test/java/test_locally/api/methods/FunctionsTest.java b/slack-api-client/src/test/java/test_locally/api/methods/FunctionsTest.java new file mode 100644 index 000000000..dcf14351e --- /dev/null +++ b/slack-api-client/src/test/java/test_locally/api/methods/FunctionsTest.java @@ -0,0 +1,45 @@ +package test_locally.api.methods; + +import com.slack.api.Slack; +import com.slack.api.SlackConfig; +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import util.MockSlackApiServer; + +import java.util.HashMap; + +import static org.hamcrest.CoreMatchers.is; +import static org.hamcrest.MatcherAssert.assertThat; +import static util.MockSlackApi.ValidFunctionToken; + +public class FunctionsTest { + + 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(); + } + + @Test + public void test() throws Exception { + assertThat(slack.methodsAsync(ValidFunctionToken).functionsCompleteSuccess(r -> r + .functionExecutionId("Fn11111111") + .outputs(new HashMap<>()) + ).get().getError(), is("")); + + assertThat(slack.methodsAsync(ValidFunctionToken).functionsCompleteError(r -> r + .functionExecutionId("Fn11111111") + .error("something wrong") + ).get().getError(), is("")); + } +} diff --git a/slack-api-client/src/test/java/util/MockSlackApi.java b/slack-api-client/src/test/java/util/MockSlackApi.java index e5363fa09..f510143c1 100644 --- a/slack-api-client/src/test/java/util/MockSlackApi.java +++ b/slack-api-client/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 ExpiredToken = "xoxb-this-is-expired"; public static final String InvalidToken = "xoxb-this-is-INVALID"; @@ -43,7 +44,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.startsWith("Bearer " + ValidToken)) { + } else if (!authorizationHeader.startsWith("Bearer " + ValidToken) + && !authorizationHeader.startsWith("Bearer " + ValidFunctionToken)) { resp.setStatus(200); if (authorizationHeader.equals("Bearer " + ExpiredToken)) { resp.getWriter().write("{\"ok\":false,\"error\":\"token_expired\"}"); 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..7af8b0ee4 --- /dev/null +++ b/slack-api-model/src/test/java/test_locally/api/model/event/FunctionExecutedEventTest.java @@ -0,0 +1,113 @@ +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("amount").asDouble(), is(1.0D)); + assertThat(event.getInputs().get("amount").asFloat(), is(1.0F)); + 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())); + } }