Repository: slack-go/slack
Pull request #1097 (2022-08-15 13:45:34)

Support team.profile.get method
This PR is a duplicate of #474 that was closed due to no activity on my part.  Reopening to have it merged as I believe others could benefit from it.

This PR adds support to retrieve custom fields defined on the Team, which can be populated via the `users.profile.set` endpoint.  Profile field variables are custom to the team, so being able to retrieve them is important when setting new values.  These values can also be found within the profile section of the Admin Console as noted in the comment of the function.

This PR also adds support for updating the custom fields on a user using these variables.  The new function accepts a map of the existing struct `UserProfileCustomField`.  The key of the map is the custom Team Variable to define the custom field on the user.   

Note that any existing field set, but not passed to function to be updated will be cleared based on Slack's native functionality.  

##### Pull Request Guidelines

These are recommendations for pull requests.
They are strictly guidelines to help manage expectations.

##### PR preparation
Run `make pr-prep` from the root of the repository to run formatting, linting and tests.

##### Should this be an issue instead
- [ ] is it a convenience method? (no new functionality, streamlines some use case)
- [ ] exposes a previously private type, const, method, etc.
- [ ] is it application specific (caching, retry logic, rate limiting, etc)
- [ ] is it performance related.

##### API changes

Since API changes have to be maintained they undergo a more detailed review and are more likely to require changes.

- no tests, if you're adding to the API include at least a single test of the happy case.
- If you can accomplish your goal without changing the API, then do so.
- dependency changes. updates are okay. adding/removing need justification.

###### Examples of API changes that do not meet guidelines:
- in library cache for users. caches are use case specific.
- Convenience methods for Sending Messages, update, post, ephemeral, etc. consider opening an issue instead.



Patch:
diff --git a/team.go b/team.go
index 029e2b5..cc1d700 100644
--- a/team.go
+++ b/team.go
@@ -24,6 +24,26 @@ type TeamInfo struct {
 	Icon        map[string]interface{} `json:"icon"`
 }
 
+type TeamProfileResponse struct {
+	Profile TeamProfile `json:"profile"`
+	SlackResponse
+}
+
+type TeamProfile struct {
+	Fields []TeamProfileField `json:"fields"`
+}
+
+type TeamProfileField struct {
+	ID             string          `json:"id"`
+	Ordering       int             `json:"ordering"`
+	Label          string          `json:"label"`
+	Hint           string          `json:"hint"`
+	Type           string          `json:"type"`
+	PossibleValues []string        `json:"possible_values"`
+	IsHidden       bool            `json:"is_hidden"`
+	Options        map[string]bool `json:"options"`
+}
+
 type LoginResponse struct {
 	Logins []Login `json:"logins"`
 	Paging `json:"paging"`
@@ -95,6 +115,15 @@ func (api *Client) accessLogsRequest(ctx context.Context, path string, values ur
 	return response, response.Err()
 }
 
+func (api *Client) teamProfileRequest(ctx context.Context, client httpClient, path string, values url.Values) (*TeamProfileResponse, error) {
+	response := &TeamProfileResponse{}
+	err := api.postMethod(ctx, path, values, response)
+	if err != nil {
+		return nil, err
+	}
+	return response, response.Err()
+}
+
 // GetTeamInfo gets the Team Information of the user
 func (api *Client) GetTeamInfo() (*TeamInfo, error) {
 	return api.GetTeamInfoContext(context.Background())
@@ -113,6 +142,25 @@ func (api *Client) GetTeamInfoContext(ctx context.Context) (*TeamInfo, error) {
 	return &response.Team, nil
 }
 
+// GetTeamProfile gets the Team Profile settings of the user
+func (api *Client) GetTeamProfile() (*TeamProfile, error) {
+	return api.GetTeamProfileContext(context.Background())
+}
+
+// GetTeamProfileContext gets the Team Profile settings of the user with a custom context
+func (api *Client) GetTeamProfileContext(ctx context.Context) (*TeamProfile, error) {
+	values := url.Values{
+		"token": {api.token},
+	}
+
+	response, err := api.teamProfileRequest(ctx, api.httpclient, "team.profile.get", values)
+	if err != nil {
+		return nil, err
+	}
+	return &response.Profile, nil
+
+}
+
 // GetAccessLogs retrieves a page of logins according to the parameters given
 func (api *Client) GetAccessLogs(params AccessLogParameters) ([]Login, *Paging, error) {
 	return api.GetAccessLogsContext(context.Background(), params)
diff --git a/users.go b/users.go
index d22b44f..f11ca68 100644
--- a/users.go
+++ b/users.go
@@ -556,6 +556,55 @@ func (api *Client) SetUserRealNameContextWithUser(ctx context.Context, user, rea
 	return response.Err()
 }
 
+// SetUserCustomFields sets Custom Profile fields on the provided users account.  Due to the non-repeating elements
+// within the request, a map fields is required.  The key in the map signifies the field that will be updated.
+//
+// Note: You may need to change the way the custom field is populated within the Profile section of the Admin Console from
+// SCIM or User Entered to API.
+//
+// See GetTeamProfile for information to retrieve possible fields for your account.
+func (api *Client) SetUserCustomFields(userID string, customFields map[string]UserProfileCustomField) error {
+	return api.SetUserCustomFieldsContext(context.Background(), userID, customFields)
+}
+
+// SetUserCustomFieldsContext will set a users custom profile field with context.
+//
+// For more information see SetUserCustomFields
+func (api *Client) SetUserCustomFieldsContext(ctx context.Context, userID string, customFields map[string]UserProfileCustomField) error {
+
+	// Convert data to data type with custom marshall / unmarshall
+	// For more information, see UserProfileCustomFields definition.
+	updateFields := UserProfileCustomFields{}
+	updateFields.SetMap(customFields)
+
+	// This anonymous struct is needed to set the fields level of the request data.  The base struct for
+	// UserProfileCustomFields has an unexported variable named fields that does not contain a struct tag,
+	// which has resulted in this configuration.
+	profile, err := json.Marshal(&struct {
+		Fields UserProfileCustomFields `json:"fields"`
+	}{
+		Fields: updateFields,
+	})
+
+	if err != nil {
+		return err
+	}
+
+	values := url.Values{
+		"token":   {api.token},
+		"user":    {userID},
+		"profile": {string(profile)},
+	}
+
+	response := &userResponseFull{}
+	if err := postForm(ctx, api.httpclient, APIURL+"users.profile.set", values, response, api); err != nil {
+		return err
+	}
+
+	return response.Err()
+
+}
+
 // SetUserCustomStatus will set a custom status and emoji for the currently
 // authenticated user. If statusEmoji is "" and statusText is not, the Slack API
 // will automatically set it to ":speech_balloon:". Otherwise, if both are ""


Test patch:
diff --git a/team_test.go b/team_test.go
index ecc3769..d0055d1 100644
--- a/team_test.go
+++ b/team_test.go
@@ -54,6 +54,76 @@ func TestGetTeamInfo(t *testing.T) {
 	}
 }
 
+func getTeamProfile(rw http.ResponseWriter, r *http.Request) {
+	rw.Header().Set("Content-Type", "application/json")
+	response := []byte(`{
+		"ok":true,
+		"profile":{
+			 "fields":[
+					{
+						"id":"XXXD7KN555",
+						"ordering":2,
+						"field_name":"",
+						"label":"Skype",
+						"hint":"This will be displayed on your profile.",
+						"type":"text",
+						"possible_values":null,
+						"options":null,
+						"is_hidden":true
+				},
+				{
+					"id":"XXXGGE5AAN7",
+					"ordering":4,
+					"field_name":"title",
+					"label":"Title",
+					"hint":"",
+					"type":"text",
+					"possible_values":null,
+					"options":{
+						"is_protected":true,
+						"is_scim":true
+					},
+					"is_hidden":false
+					}
+				]
+			}
+		}`)
+
+	rw.Write(response)
+}
+
+func TestGetTeamProfile(t *testing.T) {
+	http.HandleFunc("/team.profile.get", getTeamProfile)
+
+	once.Do(startServer)
+	api := New("testing-token", OptionAPIURL("http://"+serverAddr+"/"))
+
+	teamProfile, err := api.GetTeamProfile()
+	if err != nil {
+		t.Errorf("Unexpected error: %s", err)
+		return
+	}
+
+	// t.Fatal refers to -> t.Errorf & return
+	if teamProfile.Fields[0].ID != "XXXD7KN555" {
+		t.Fatal(ErrIncorrectResponse)
+	}
+	if teamProfile.Fields[0].Label != "Skype" {
+		t.Fatal(ErrIncorrectResponse)
+	}
+
+	if teamProfile.Fields[1].ID != "XXXGGE5AAN7" {
+		t.Fatal(ErrIncorrectResponse)
+	}
+	if teamProfile.Fields[1].Label != "Title" {
+		t.Fatal(ErrIncorrectResponse)
+	}
+	if !teamProfile.Fields[1].Options["is_protected"] {
+		t.Fatal(ErrIncorrectResponse)
+	}
+
+}
+
 func getTeamAccessLogs(rw http.ResponseWriter, r *http.Request) {
 	rw.Header().Set("Content-Type", "application/json")
 	response := []byte(`{"ok": true, "logins": [{