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 include_num_members option to GetConversationInfo #1090

Closed
wants to merge 2 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
13 changes: 7 additions & 6 deletions conversation.go
Expand Up @@ -358,16 +358,17 @@ func (api *Client) CreateConversationContext(ctx context.Context, channelName st
}

// GetConversationInfo retrieves information about a conversation
func (api *Client) GetConversationInfo(channelID string, includeLocale bool) (*Channel, error) {
return api.GetConversationInfoContext(context.Background(), channelID, includeLocale)
func (api *Client) GetConversationInfo(channelID string, includeLocale bool, includeNumMembers bool) (*Channel, error) {
return api.GetConversationInfoContext(context.Background(), channelID, includeLocale, includeNumMembers)
}

// GetConversationInfoContext retrieves information about a conversation with a custom context
func (api *Client) GetConversationInfoContext(ctx context.Context, channelID string, includeLocale bool) (*Channel, error) {
func (api *Client) GetConversationInfoContext(ctx context.Context, channelID string, includeLocale bool, includeNumMembers bool) (*Channel, error) {
values := url.Values{
"token": {api.token},
"channel": {channelID},
"include_locale": {strconv.FormatBool(includeLocale)},
"token": {api.token},
"channel": {channelID},
"include_locale": {strconv.FormatBool(includeLocale)},
"include_num_members": {strconv.FormatBool(includeNumMembers)},
}
response, err := api.channelRequest(ctx, "conversations.info", values)
if err != nil {
Expand Down
2 changes: 1 addition & 1 deletion conversation_test.go
Expand Up @@ -400,7 +400,7 @@ func TestGetConversationInfo(t *testing.T) {
http.HandleFunc("/conversations.info", okChannelJsonHandler)
once.Do(startServer)
api := New("testing-token", OptionAPIURL("http://"+serverAddr+"/"))
channel, err := api.GetConversationInfo("CXXXXXXXX", false)
channel, err := api.GetConversationInfo("CXXXXXXXX", false, false)
if err != nil {
t.Errorf("Unexpected error: %s", err)
return
Expand Down
102 changes: 102 additions & 0 deletions yadmin_calls.go
@@ -0,0 +1,102 @@
package slack

//----------------------------------------------------------------------------------------------------------------
// Admin Teams functions
//----------------------------------------------------------------------------------------------------------------
//-- Support types for AdminTeams list
type AdminTeamPrimaryOwner struct {
PrimaryOwner struct {
UserID string `json:"user_id"`
Email string `json:"email"`
}
}
type AdminTeam struct {
ID string `json:"id"`
Name string `json:"name"`
Discoverability string `json:"discoverability"`
PrimaryOwner AdminTeamPrimaryOwner `json:"primary_owner"`
TeamURL string `json:"team_url"`
}

//----------------------------------------------------------------------------------------------------------------
func (admin *Client) AdminTeamsList() ([]AdminTeam, error) {
// Make the API Call
items, err := admin.
GenericAdminRequest("admin.teams.list").
UrlParamString("limit", "100"). // This is the default limit anyway
Query("teams")

// Return an empty list on error
if err != nil {
return []AdminTeam{}, err
}

// Extract our map[string]interface{} into an actual typed array
arr := make([]AdminTeam, len(items.Responses))
err = items.Extract(&arr)

// Return an empty list on error
if err != nil {
return []AdminTeam{}, err
}

// Return the actual results
return arr, err
}

//--

//----------------------------------------------------------------------------------------------------------------
// Admin Conversations functions
//----------------------------------------------------------------------------------------------------------------
//-- Support types for Admin conversation queries
type AdminConversation struct {
ID string `json:"id"`
Name string `json:"name"`
Purpose string `json:"purpose"`
MemberCount int `json:"member_count,omitempty"`
Created JSONTime `json:"created"`
CreatorID string `json:"creator_id"`
IsPrivate bool `json:"is_private"`
IsArchived bool `json:"is_archived"`
IsGeneral bool `json:"is_general"`
LastActivityTimestamp JSONTime `json:"last_activity_ts"`
IsExtShared bool `json:"is_ext_shared"`
IsGlobalShared bool `json:"is_global_shared"`
IsOrgDefault bool `json:"is_org_default"`
IsOrgMandatory bool `json:"is_org_mandatory"`
IsOrgShared bool `json:"is_org_shared"`
IsFrozen bool `json:"is_frozen"`
ConnectedTeamIDs []string `json:"connected_team_ids"`
InternalTeamIDsCount int `json:"internal_team_ids_count,omitempty"`
InternalTeamIDsSampleTeam string `json:"internal_team_ids_sample_team,omitempty"`
PendingConnectedTeamIDs []string `json:"pending_connected_team_ids"`
IsPendingExtShared bool `json:"is_pending_ext_shared"`
}

//----------------------------------------------------------------------------------------------------------------
func (admin *Client) AdminConversationsSearch(query string) ([]AdminConversation, error) {
// Make the API Call
items, err := admin.
GenericAdminRequest("admin.conversations.search").
UrlParamString("query", "nowak").
UrlParamString("limit", "25"). // Enough to give a reasonable paging, but not so much that th equery times out
Query("conversations")

// Return an empty list on error
if err != nil {
return []AdminConversation{}, err
}

// Extract our map[string]interface{} into an actual typed array
arr := make([]AdminConversation, len(items.Responses))
err = items.Extract(&arr)

// Return an empty list on error
if err != nil {
return []AdminConversation{}, err
}

// Return the actual results
return arr, err
}
184 changes: 184 additions & 0 deletions yadmin_generic.go
@@ -0,0 +1,184 @@
package slack

import (
"context"
"encoding/json"
"fmt"
"net/url"
)

/*----------------------------------------------------------------------------------------------------------------
The original package doesn't have support for most Admin functions, and I just need a few, but I don't want to spend the
effort fully defining everything, so I'm taking a different approach here and going generic.

The idea is that you construct a request, add parameters to it, then execute it (telling it which response field you're
interested in tracking)
It will then page through and gather those items up as an array of interface{} objects.
You can then extract those into either a strongly typed object, or easily unmarshall them into whatever other custom
types you like without having to modify this package.

The original package is restricted to golang 1.18 so no generics, which makes things a little uglier, but we have what
we have
----------------------------------------------------------------------------------------------------------------*/

/*----------------------------------------------------------------------------------------------------------------
Sample usage for https://api.slack.com/methods/admin.conversations.search

items, err := adminClient.
GenericAdminRequest("admin.conversations.search"). //
UrlParamString("query", "something"). //
UrlParamString("limit", "10"). //
Query("conversations")
arr, _ := items.ExtractAdminConversations()
----------------------------------------------------------------------------------------------------------------*/

// This represents our request
type genericAdminRequest struct {
api *Client
cmd string
urlValues url.Values
}

// This is how you start off a request... pass in the urlFragment from the API command, it will be appended to the general Slack WebAPI URL
func (api *Client) GenericAdminRequest(cmd string) *genericAdminRequest {
return &genericAdminRequest{
api: api,
cmd: cmd,
urlValues: url.Values{
"token": {api.token}, // Could use api.appLevelToken instead, but .token works with more stuff without me having to change anything
},
}
}

// These are how you add Url Parameters to the given request
func (req *genericAdminRequest) UrlParamString(name, value string) *genericAdminRequest {
return req.UrlParamStringArr(name, []string{value})
}

func (req *genericAdminRequest) UrlParamStringArr(name string, values []string) *genericAdminRequest {
req.urlValues[name] = values
return req
}

//----------------------------------------------------------------------------------------------------------------
// While we store the map results so we get all fields, we do unmarshall into this type so we get
// structured access to the common Slack status and token fields
// Note: We could union the above types in here and have just a single unmarshall, but without a way to dynamically
// choose the field we want to extract items from, it doesn't really add much value
type SlackCursor struct {
SlackResponse
NextCursor string `json:"next_cursor"`
}

//----------------------------------------------------------------------------------------------------------------
// This is returned by Execute and is used to store the collection of response (as interface{}s)
type GenericExecutionResponse struct {
Responses []interface{}
OutputHint string // Records the fieldName the responses list was extracted from
}

//----------------------------------------------------------------------------------------------------------------
//-- Execute a Slack WebAPI Command, returning only whether there was an error or not
func (req *genericAdminRequest) Execute() error {
_, err := req.QueryContext(context.Background(), "")
return err
}

//----------------------------------------------------------------------------------------------------------------
//-- Execute a Slack WebAPI Command, returning a collated collection of expected objects
func (req *genericAdminRequest) Query(returnFieldName string) (GenericExecutionResponse, error) {
return req.QueryContext(context.Background(), returnFieldName)
}

//----------------------------------------------------------------------------------------------------------------
//-- This will execute the request and return any errors
//-- If a non-empty returnFieldName is provided, that named field will be collated from the SlackResponse objects and returned
func (req *genericAdminRequest) QueryContext(ctx context.Context, returnFieldName string) (GenericExecutionResponse, error) {
cursor := ""
result := GenericExecutionResponse{
OutputHint: returnFieldName,
}

for {
// Make our own copy of the map so we can add/tweak necessary options without affecting the original
curValues := make(url.Values, len(req.urlValues))
for k, v := range req.urlValues {
curValues[k] = make([]string, len(v))
copy(curValues[k], v)
}

// Add cursor if necessary
if cursor != "" {
curValues["cursor"] = []string{cursor}
}

// Do the request
resp := make(map[string]interface{})
endpoint := fmt.Sprintf("%s%s", APIURL, req.cmd)
if err := postForm(ctx, req.api.httpclient, endpoint, curValues, &resp, req.api); err != nil {
return result, err
}

// Coerce our map into the common Slack Response fields so we can check status
sr := SlackCursor{}
err := castFieldToData(resp, &sr)
if err != nil {
return result, err
}

// We errored out
if sr.Err() != nil {
return result, err
}

// Squirrel our field info away
if returnFieldName != "" {
items, ok := resp[returnFieldName].([]interface{})
if !ok {
// If we're not an array, then just treat us as a single and add us anyway
item := resp[returnFieldName].(interface{})
result.Responses = append(result.Responses, item)
} else {
result.Responses = append(result.Responses, items...)
}
}

// Next Cursor could be in one of a couple of places
nextCursor := sr.NextCursor
if nextCursor == "" {
nextCursor = sr.ResponseMetadata.Cursor
}

// If we ran out of results or we don't care about results, get out, otherwise keep looping around
if nextCursor == "" || returnFieldName == "" {
return result, err
} else {
cursor = nextCursor
}

}
}

//----------------------------------------------------------------------------------------------------------------
// Utility function for above...
// src is the source data we intend to remarshall (convert from interface{} to jason then to a struct)
// dst is a pointer to the object we want to unmarshall into and governs the unmarshalling
func castFieldToData(src interface{}, dst interface{}) error {
jsonbody, err := json.Marshal(src)
if err != nil {
return err
}

err = json.Unmarshal(jsonbody, dst)
if err != nil {
return err
}

return nil
}

//----------------------------------------------------------------------------------------------------------------
// Given a pointer to a typed array, this will unmarkshall into that array, it's the simplest generic output handler
func (r GenericExecutionResponse) Extract(arr interface{}) error {
return castFieldToData(r.Responses, arr)
}