Repository: slack-go/slack
Pull request #1119 (2022-10-26 14:51:20)

Add ListTeams and TeamAccess{Granted,Revoked}Events and fix missing enterprise ID
This PR makes a few changes useful for building Slack Enterprise Grid integrations.
- Adds support for [`team_access_granted`](https://api.slack.com/events/team_access_granted) and [`team_access_revoked`](https://api.slack.com/events/team_access_revoked) events
- Adds `ListTeams` to call [`auth.teams.list`](https://api.slack.com/methods/auth.teams.list)
- Fixes an issue where `EnterpriseID` is not filled in the events api for callback events
- Lets callers pass `TeamID` to `CreateConversation`. This is a **breaking change**, but hopefully one that prevents future breaking changes.

stephenwan-opal: @kanata2 thank you for reviewing - i addressed/responded to the PR comments
stephenwan-opal: @kanata2 thanks again for reviewing - could you take a look at the new changes here?
kanata2: Sorry for delay. I'll confirm within few days.

Patch:
diff --git a/auth.go b/auth.go
index f4f7f00..bf6e80d 100644
--- a/auth.go
+++ b/auth.go
@@ -38,3 +38,37 @@ func (api *Client) SendAuthRevokeContext(ctx context.Context, token string) (*Au
 
 	return api.authRequest(ctx, "auth.revoke", values)
 }
+
+type listTeamsResponse struct {
+	Teams []Team `json:"teams"`
+	SlackResponse
+}
+
+type ListTeamsParameters struct {
+	Limit  int
+	Cursor string
+}
+
+// ListTeams returns all workspaces a token can access.
+// More info: https://api.slack.com/methods/admin.teams.list
+func (api *Client) ListTeams(params ListTeamsParameters) ([]Team, string, error) {
+	return api.ListTeamsContext(context.Background(), params)
+}
+
+// ListTeams returns all workspaces a token can access with a custom context.
+func (api *Client) ListTeamsContext(ctx context.Context, params ListTeamsParameters) ([]Team, string, error) {
+	values := url.Values{
+		"token": {api.token},
+	}
+	if params.Cursor != "" {
+		values.Add("cursor", params.Cursor)
+	}
+
+	response := &listTeamsResponse{}
+	err := api.postMethod(ctx, "auth.teams.list", values, response)
+	if err != nil {
+		return nil, "", err
+	}
+
+	return response.Teams, response.ResponseMetadata.Cursor, response.Err()
+}
diff --git a/conversation.go b/conversation.go
index 07864d5..2256086 100644
--- a/conversation.go
+++ b/conversation.go
@@ -342,17 +342,26 @@ func (api *Client) CloseConversationContext(ctx context.Context, channelID strin
 	return response.NoOp, response.AlreadyClosed, response.Err()
 }
 
+type CreateConversationParams struct {
+	ChannelName string
+	IsPrivate   bool
+	TeamID      string
+}
+
 // CreateConversation initiates a public or private channel-based conversation
-func (api *Client) CreateConversation(channelName string, isPrivate bool) (*Channel, error) {
-	return api.CreateConversationContext(context.Background(), channelName, isPrivate)
+func (api *Client) CreateConversation(params CreateConversationParams) (*Channel, error) {
+	return api.CreateConversationContext(context.Background(), params)
 }
 
 // CreateConversationContext initiates a public or private channel-based conversation with a custom context
-func (api *Client) CreateConversationContext(ctx context.Context, channelName string, isPrivate bool) (*Channel, error) {
+func (api *Client) CreateConversationContext(ctx context.Context, params CreateConversationParams) (*Channel, error) {
 	values := url.Values{
 		"token":      {api.token},
-		"name":       {channelName},
-		"is_private": {strconv.FormatBool(isPrivate)},
+		"name":       {params.ChannelName},
+		"is_private": {strconv.FormatBool(params.IsPrivate)},
+	}
+	if params.TeamID != "" {
+		values.Set("team_id", params.TeamID)
 	}
 	response, err := api.channelRequest(ctx, "conversations.create", values)
 	if err != nil {
diff --git a/examples/pins/pins.go b/examples/pins/pins.go
index d13d2d2..5c4f67c 100644
--- a/examples/pins/pins.go
+++ b/examples/pins/pins.go
@@ -43,7 +43,7 @@ func main() {
 	postAsUserID = authTest.UserID
 
 	// Create a temporary channel
-	channel, err := api.CreateConversation(channelName, false)
+	channel, err := api.CreateConversation(slack.CreateConversationParams{ChannelName: channelName})
 
 	if err != nil {
 		// If the channel exists, that means we just need to unarchive it
diff --git a/slackevents/inner_events.go b/slackevents/inner_events.go
index eb1e8df..e88b6ce 100644
--- a/slackevents/inner_events.go
+++ b/slackevents/inner_events.go
@@ -524,6 +524,18 @@ func (e MessageEvent) IsEdited() bool {
 		e.Message.Edited != nil
 }
 
+// TeamAccessGrantedEvent is sent if access to teams was granted for your org-wide app.
+type TeamAccessGrantedEvent struct {
+	Type    string   `json:"type"`
+	TeamIDs []string `json:"team_ids"`
+}
+
+// TeamAccessRevokedEvent is sent if access to teams was revoked for your org-wide app.
+type TeamAccessRevokedEvent struct {
+	Type    string   `json:"type"`
+	TeamIDs []string `json:"team_ids"`
+}
+
 type EventsAPIType string
 
 const (
@@ -599,6 +611,10 @@ const (
 	MessageMetadataUpdated = EventsAPIType("message_metadata_updated")
 	// MessageMetadataPosted A message with metadata was deleted
 	MessageMetadataDeleted = EventsAPIType("message_metadata_deleted")
+	// TeamAccessGranted is sent if access to teams was granted for your org-wide app.
+	TeamAccessGranted = EventsAPIType("team_access_granted")
+	// TeamAccessrevoked is sent if access to teams was revoked for your org-wide app.
+	TeamAccessrevoked = EventsAPIType("team_access_revoked")
 )
 
 // EventsAPIInnerEventMapping maps INNER Event API events to their corresponding struct
@@ -641,4 +657,6 @@ var EventsAPIInnerEventMapping = map[EventsAPIType]interface{}{
 	MessageMetadataPosted:  MessageMetadataPostedEvent{},
 	MessageMetadataUpdated: MessageMetadataUpdatedEvent{},
 	MessageMetadataDeleted: MessageMetadataDeletedEvent{},
+	TeamAccessGranted:      TeamAccessGrantedEvent{},
+	TeamAccessrevoked:      TeamAccessRevokedEvent{},
 }
diff --git a/slackevents/outer_events.go b/slackevents/outer_events.go
index 6277248..bcc85f3 100644
--- a/slackevents/outer_events.go
+++ b/slackevents/outer_events.go
@@ -35,6 +35,7 @@ type EventsAPICallbackEvent struct {
 	Token        string           `json:"token"`
 	TeamID       string           `json:"team_id"`
 	APIAppID     string           `json:"api_app_id"`
+	EnterpriseID string           `json:"enterprise_id"`
 	InnerEvent   *json.RawMessage `json:"event"`
 	AuthedUsers  []string         `json:"authed_users"`
 	AuthedTeams  []string         `json:"authed_teams"`
diff --git a/slackevents/parsers.go b/slackevents/parsers.go
index 23ba2b8..96ba568 100644
--- a/slackevents/parsers.go
+++ b/slackevents/parsers.go
@@ -102,7 +102,7 @@ func parseInnerEvent(e *EventsAPICallbackEvent) (EventsAPIEvent, error) {
 			e.TeamID,
 			"unmarshalling_error",
 			e.APIAppID,
-			"",
+			e.EnterpriseID,
 			&slack.UnmarshallingErrorEvent{ErrorObj: err},
 			EventsAPIInnerEvent{},
 		}, err
@@ -114,7 +114,7 @@ func parseInnerEvent(e *EventsAPICallbackEvent) (EventsAPIEvent, error) {
 			e.TeamID,
 			iE.Type,
 			e.APIAppID,
-			"",
+			e.EnterpriseID,
 			nil,
 			EventsAPIInnerEvent{},
 		}, fmt.Errorf("Inner Event does not exist! %s", iE.Type)
@@ -128,7 +128,7 @@ func parseInnerEvent(e *EventsAPICallbackEvent) (EventsAPIEvent, error) {
 			e.TeamID,
 			"unmarshalling_error",
 			e.APIAppID,
-			"",
+			e.EnterpriseID,
 			&slack.UnmarshallingErrorEvent{ErrorObj: err},
 			EventsAPIInnerEvent{},
 		}, err
@@ -138,7 +138,7 @@ func parseInnerEvent(e *EventsAPICallbackEvent) (EventsAPIEvent, error) {
 		e.TeamID,
 		e.Type,
 		e.APIAppID,
-		"",
+		e.EnterpriseID,
 		e,
 		EventsAPIInnerEvent{iE.Type, recvEvent},
 	}, nil


Test patch:
diff --git a/auth_test.go b/auth_test.go
new file mode 100644
index 0000000..5fe9900
--- /dev/null
+++ b/auth_test.go
@@ -0,0 +1,51 @@
+package slack
+
+import (
+	"net/http"
+	"testing"
+
+	"github.com/stretchr/testify/assert"
+)
+
+func getTeamList(rw http.ResponseWriter, r *http.Request) {
+	rw.Header().Set("Content-Type", "application/json")
+	response := []byte(`{
+    "ok": true,
+    "teams": [
+        {
+            "name": "Shinichi's workspace",
+            "id": "T12345678"
+        },
+        {
+            "name": "Migi's workspace",
+            "id": "T12345679"
+        }
+    ],
+    "response_metadata": {
+        "next_cursor": "dXNlcl9pZDo5MTQyOTI5Mzkz"
+    }
+}`)
+	rw.Write(response)
+}
+
+func TestListTeams(t *testing.T) {
+	http.HandleFunc("/auth.teams.list", getTeamList)
+
+	once.Do(startServer)
+	api := New("testing-token", OptionAPIURL("http://"+serverAddr+"/"))
+
+	teams, cursor, err := api.ListTeams(ListTeamsParameters{})
+	if err != nil {
+		t.Errorf("Unexpected error: %s", err)
+		return
+	}
+
+	assert.Len(t, teams, 2)
+	assert.Equal(t, "T12345678", teams[0].ID)
+	assert.Equal(t, "Shinichi's workspace", teams[0].Name)
+
+	assert.Equal(t, "T12345679", teams[1].ID)
+	assert.Equal(t, "Migi's workspace", teams[1].Name)
+
+	assert.Equal(t, "dXNlcl9pZDo5MTQyOTI5Mzkz", cursor)
+}
diff --git a/conversation_test.go b/conversation_test.go
index 61a4e92..adbad7c 100644
--- a/conversation_test.go
+++ b/conversation_test.go
@@ -385,7 +385,7 @@ func TestCreateConversation(t *testing.T) {
 	http.HandleFunc("/conversations.create", okChannelJsonHandler)
 	once.Do(startServer)
 	api := New("testing-token", OptionAPIURL("http://"+serverAddr+"/"))
-	channel, err := api.CreateConversation("CXXXXXXXX", false)
+	channel, err := api.CreateConversation(CreateConversationParams{ChannelName: "CXXXXXXXX"})
 	if err != nil {
 		t.Errorf("Unexpected error: %s", err)
 		return
diff --git a/slacktest/handlers_test.go b/slacktest/handlers_test.go
index 6678e76..dec4c99 100644
--- a/slacktest/handlers_test.go
+++ b/slacktest/handlers_test.go
@@ -37,7 +37,7 @@ func TestServerCreateConversationHandler(t *testing.T) {
 	go s.Start()
 
 	client := slack.New("ABCDEFG", slack.OptionAPIURL(s.GetAPIURL()))
-	conversation, err := client.CreateConversation("test", false)
+	conversation, err := client.CreateConversation(slack.CreateConversationParams{ChannelName: "test"})
 	assert.NoError(t, err)
 	assert.Equal(t, "C0EAQDV4Z", conversation.ID)
 	assert.Equal(t, "U023BECGF", conversation.Creator)