diff --git a/examples/function/function.go b/examples/function/function.go new file mode 100644 index 000000000..d654890fe --- /dev/null +++ b/examples/function/function.go @@ -0,0 +1,60 @@ +package main + +import ( + "fmt" + "github.com/slack-go/slack" + "github.com/slack-go/slack/slackevents" + "github.com/slack-go/slack/socketmode" + "os" +) + +func main() { + api := slack.New( + os.Getenv("SLACK_BOT_TOKEN"), + slack.OptionDebug(true), + slack.OptionAppLevelToken(os.Getenv("SLACK_APP_TOKEN")), + ) + client := socketmode.New(api, socketmode.OptionDebug(true)) + + go func() { + for evt := range client.Events { + switch evt.Type { + case socketmode.EventTypeEventsAPI: + eventsAPIEvent, ok := evt.Data.(slackevents.EventsAPIEvent) + if !ok { + fmt.Printf("Ignored %+v\n", evt) + continue + } + + fmt.Printf("Event received: %+v\n", eventsAPIEvent) + client.Ack(*evt.Request) + + switch eventsAPIEvent.Type { + case slackevents.CallbackEvent: + innerEvent := eventsAPIEvent.InnerEvent + switch ev := innerEvent.Data.(type) { + case *slackevents.FunctionExecutedEvent: + callbackID := ev.Function.CallbackID + if callbackID == "sample_function" { + userId := ev.Inputs["user_id"] + payload := map[string]string{ + "user_id": userId, + } + + err := api.FunctionCompleteSuccess(ev.FunctionExecutionID, slack.FunctionCompleteSuccessRequestOptionOutput(payload)) + if err != nil { + fmt.Printf("failed posting message: %v \n", err) + } + } + } + default: + client.Debugf("unsupported Events API event received\n") + } + + default: + fmt.Fprintf(os.Stderr, "Unexpected event type received: %s\n", evt.Type) + } + } + }() + client.Run() +} diff --git a/examples/function/manifest.json b/examples/function/manifest.json new file mode 100644 index 000000000..5f673f96d --- /dev/null +++ b/examples/function/manifest.json @@ -0,0 +1,56 @@ +{ + "display_information": { + "name": "Function Example" + }, + "features": { + "app_home": { + "home_tab_enabled": false, + "messages_tab_enabled": true, + "messages_tab_read_only_enabled": true + }, + "bot_user": { + "display_name": "Function Example", + "always_online": true + } + }, + "oauth_config": { + "scopes": { + "bot": [ + "chat:write" + ] + } + }, + "settings": { + "interactivity": { + "is_enabled": true + }, + "org_deploy_enabled": true, + "socket_mode_enabled": true, + "token_rotation_enabled": false + }, + "functions": { + "sample_function": { + "title": "Sample function", + "description": "Runs sample function", + "input_parameters": { + "user_id": { + "type": "slack#/types/user_id", + "title": "User", + "description": "Message recipient", + "is_required": true, + "hint": "Select a user in the workspace", + "name": "user_id" + } + }, + "output_parameters": { + "user_id": { + "type": "slack#/types/user_id", + "title": "User", + "description": "User that completed the function", + "is_required": true, + "name": "user_id" + } + } + } + } +} diff --git a/function_execute.go b/function_execute.go new file mode 100644 index 000000000..4ec8f9f4c --- /dev/null +++ b/function_execute.go @@ -0,0 +1,93 @@ +package slack + +import ( + "context" + "encoding/json" +) + +type ( + FunctionCompleteSuccessRequest struct { + FunctionExecutionID string `json:"function_execution_id"` + Outputs map[string]string `json:"outputs"` + } + + FunctionCompleteErrorRequest struct { + FunctionExecutionID string `json:"function_execution_id"` + Error string `json:"error"` + } +) + +type FunctionCompleteSuccessRequestOption func(opt *FunctionCompleteSuccessRequest) error + +func FunctionCompleteSuccessRequestOptionOutput(outputs map[string]string) FunctionCompleteSuccessRequestOption { + return func(opt *FunctionCompleteSuccessRequest) error { + if len(outputs) > 0 { + opt.Outputs = outputs + } + return nil + } +} + +// FunctionCompleteSuccess indicates function is completed +func (api *Client) FunctionCompleteSuccess(functionExecutionId string, options ...FunctionCompleteSuccessRequestOption) error { + return api.FunctionCompleteSuccessContext(context.Background(), functionExecutionId, options...) +} + +// FunctionCompleteSuccess indicates function is completed +func (api *Client) FunctionCompleteSuccessContext(ctx context.Context, functionExecutionId string, options ...FunctionCompleteSuccessRequestOption) error { + // More information: https://api.slack.com/methods/functions.completeSuccess + r := &FunctionCompleteSuccessRequest{ + FunctionExecutionID: functionExecutionId, + } + for _, option := range options { + option(r) + } + + endpoint := api.endpoint + "functions.completeSuccess" + jsonData, err := json.Marshal(r) + if err != nil { + return err + } + + response := &SlackResponse{} + if err := postJSON(ctx, api.httpclient, endpoint, api.token, jsonData, response, api); err != nil { + return err + } + + if !response.Ok { + return response.Err() + } + + return nil +} + +// FunctionCompleteError indicates function is completed with error +func (api *Client) FunctionCompleteError(functionExecutionID string, errorMessage string) error { + return api.FunctionCompleteErrorContext(context.Background(), functionExecutionID, errorMessage) +} + +// FunctionCompleteErrorContext indicates function is completed with error +func (api *Client) FunctionCompleteErrorContext(ctx context.Context, functionExecutionID string, errorMessage string) error { + // More information: https://api.slack.com/methods/functions.completeError + r := FunctionCompleteErrorRequest{ + FunctionExecutionID: functionExecutionID, + } + r.Error = errorMessage + + endpoint := api.endpoint + "functions.completeError" + jsonData, err := json.Marshal(r) + if err != nil { + return err + } + + response := &SlackResponse{} + if err := postJSON(ctx, api.httpclient, endpoint, api.token, jsonData, response, api); err != nil { + return err + } + + if !response.Ok { + return response.Err() + } + + return nil +} diff --git a/function_execute_test.go b/function_execute_test.go new file mode 100644 index 000000000..356e22328 --- /dev/null +++ b/function_execute_test.go @@ -0,0 +1,80 @@ +package slack + +import ( + "context" + "encoding/json" + "io" + "net/http" + "testing" +) + +func postHandler(t *testing.T) func(rw http.ResponseWriter, r *http.Request) { + return func(rw http.ResponseWriter, r *http.Request) { + body, err := io.ReadAll(r.Body) + defer r.Body.Close() + if err != nil { + t.Error(err) + return + } + + var req FunctionCompleteSuccessRequest + err = json.Unmarshal(body, &req) + if err != nil { + t.Error(err) + return + } + + switch req.FunctionExecutionID { + case "function-success": + postSuccess(rw, r) + case "function-failure": + postFailure(rw, r) + } + } +} + +func postSuccess(rw http.ResponseWriter, r *http.Request) { + rw.Header().Set("Content-Type", "application/json") + response := []byte(`{ + "ok": true + }`) + rw.Write(response) +} + +func postFailure(rw http.ResponseWriter, r *http.Request) { + rw.Header().Set("Content-Type", "application/json") + response := []byte(`{ + "ok": false, + "error": "function_execution_not_found" + }`) + rw.Write(response) + rw.WriteHeader(500) +} + +func TestFunctionComplete(t *testing.T) { + http.HandleFunc("/functions.completeSuccess", postHandler(t)) + + once.Do(startServer) + + api := New("testing-token", OptionAPIURL("http://"+serverAddr+"/")) + + err := api.FunctionCompleteSuccess("function-success") + if err != nil { + t.Error(err) + } + + err = api.FunctionCompleteSuccess("function-failure") + if err == nil { + t.Fail() + } + + err = api.FunctionCompleteSuccessContext(context.Background(), "function-success") + if err != nil { + t.Error(err) + } + + err = api.FunctionCompleteSuccessContext(context.Background(), "function-failure") + if err == nil { + t.Fail() + } +}