Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add custom function support #1241

Merged
merged 3 commits into from
Aug 20, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
128 changes: 108 additions & 20 deletions bolt-socket-mode/src/test/java/samples/SimpleApp.java
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
import java.util.Arrays;
import java.util.HashMap;
import java.util.Map;
import java.util.regex.Pattern;

import static com.slack.api.model.block.Blocks.*;
import static com.slack.api.model.block.composition.BlockCompositions.dispatchActionConfig;
Expand Down Expand Up @@ -150,15 +151,116 @@ 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
/* Example App Manifest
{
"display_information": {
"name": "manifest-test-app-2"
},
"features": {
"bot_user": {
"display_name": "test-bot",
"always_online": true
}
},
"oauth_config": {
"scopes": {
"bot": [
"commands",
"chat:write",
"app_mentions:read"
]
}
},
"settings": {
"event_subscriptions": {
"bot_events": [
"app_mention",
"function_executed"
]
},
"interactivity": {
"is_enabled": true
},
"org_deploy_enabled": true,
"socket_mode_enabled": true,
"token_rotation_enabled": false,
"hermes_app_type": "remote",
"function_runtime": "remote"
},
"functions": {
"hello": {
"title": "Hello",
"description": "Hello world!",
"input_parameters": {
"amount": {
"type": "number",
"title": "Amount",
"description": "How many do you need?",
"is_required": false,
"hint": "How many do you need?",
"name": "amount",
"maximum": 10,
"minimum": 1
},
"user_id": {
"type": "slack#/types/user_id",
"title": "User",
"description": "Who to send it",
"is_required": true,
"hint": "Select a user in the workspace",
"name": "user_id"
},
"message": {
"type": "string",
"title": "Message",
"description": "Whatever you want to tell",
"is_required": false,
"hint": "up to 100 characters",
"name": "message",
"maxLength": 100,
"minLength": 1
}
},
"output_parameters": {
"amount": {
"type": "number",
"title": "Amount",
"description": "How many do you need?",
"is_required": false,
"hint": "How many do you need?",
"name": "amount",
"maximum": 10,
"minimum": 1
},
"user_id": {
"type": "slack#/types/user_id",
"title": "User",
"description": "Who to send it",
"is_required": true,
"hint": "Select a user in the workspace",
"name": "user_id"
},
"message": {
"type": "string",
"title": "Message",
"description": "Whatever you want to tell",
"is_required": false,
"hint": "up to 100 characters",
"name": "message",
"maxLength": 100,
"minLength": 1
}
}
}
}
}
*/

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

if (neverStarted.get()) {
Expand Down Expand Up @@ -648,6 +646,33 @@
return this;
}

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

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

View check run for this annotation

Codecov / codecov/patch

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

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

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

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

View check run for this annotation

Codecov / codecov/patch

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

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

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

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

// ---------------------------------
// Default middleware configuration
// ---------------------------------
Expand Down
28 changes: 26 additions & 2 deletions bolt/src/main/java/com/slack/api/bolt/context/Context.java
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,26 @@ 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 ID of function_executed event delivery.
*/
protected String functionExecutionId;

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

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

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

public ChatPostMessageResponse say(BuilderConfigurator<ChatPostMessageRequest.ChatPostMessageRequestBuilder> request) throws IOException, SlackApiException {
Expand Down
35 changes: 35 additions & 0 deletions bolt/src/main/java/com/slack/api/bolt/context/FunctionUtility.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
package com.slack.api.bolt.context;

import com.slack.api.methods.MethodsClient;
import com.slack.api.methods.SlackApiException;
import com.slack.api.methods.request.chat.ChatPostMessageRequest;
import com.slack.api.methods.response.chat.ChatPostMessageResponse;
import com.slack.api.methods.response.functions.FunctionsCompleteErrorResponse;
import com.slack.api.methods.response.functions.FunctionsCompleteSuccessResponse;
import com.slack.api.model.block.LayoutBlock;

import java.io.IOException;
import java.util.List;
import java.util.Map;

public interface FunctionUtility {

String getFunctionExecutionId();

MethodsClient client();

default FunctionsCompleteSuccessResponse complete(Map<String, ?> outputs) throws IOException, SlackApiException {
return this.client().functionsCompleteSuccess(r -> r
.functionExecutionId(this.getFunctionExecutionId())
.outputs(outputs)
);
}

default FunctionsCompleteErrorResponse fail(String error) throws IOException, SlackApiException {
return this.client().functionsCompleteError(r -> r
.functionExecutionId(this.getFunctionExecutionId())
.error(error)
);
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

import com.slack.api.bolt.context.ActionRespondUtility;
import com.slack.api.bolt.context.Context;
import com.slack.api.bolt.context.FunctionUtility;
import com.slack.api.bolt.util.Responder;
import lombok.*;

Expand All @@ -15,7 +16,7 @@
@AllArgsConstructor
@ToString(callSuper = true)
@EqualsAndHashCode(callSuper = false)
public class ActionContext extends Context implements ActionRespondUtility {
public class ActionContext extends Context implements ActionRespondUtility, FunctionUtility {

private String triggerId;
private String responseUrl;
Expand Down
Loading