Skip to content

Commit

Permalink
feat: Add functions.completeError and functions.completeSuccess (#1328)
Browse files Browse the repository at this point in the history
Completion of #1301

- Adds the new complete functions for the Function Execution Event
- Adds the context version of those methods

---
> this PR to handle event
[function_executed](https://api.slack.com/events/function_executed) and
response the function with
[functions.completeSuccess](https://api.slack.com/methods/functions.completeSuccess)
and
[functions.completeError](https://api.slack.com/methods/functions.completeError)

---------

Co-authored-by: Yoga Setiawan <[email protected]>
  • Loading branch information
gideonw and yogasw authored Oct 14, 2024
1 parent 132e0d1 commit 21e61c5
Show file tree
Hide file tree
Showing 4 changed files with 289 additions and 0 deletions.
60 changes: 60 additions & 0 deletions examples/function/function.go
Original file line number Diff line number Diff line change
@@ -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()
}
56 changes: 56 additions & 0 deletions examples/function/manifest.json
Original file line number Diff line number Diff line change
@@ -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"
}
}
}
}
}
93 changes: 93 additions & 0 deletions function_execute.go
Original file line number Diff line number Diff line change
@@ -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
}
80 changes: 80 additions & 0 deletions function_execute_test.go
Original file line number Diff line number Diff line change
@@ -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()
}
}

0 comments on commit 21e61c5

Please sign in to comment.