1package slack
2
3import (
4	"context"
5	"encoding/json"
6	"net/url"
7	"strconv"
8	"strings"
9	"time"
10)
11
12const (
13	DEFAULT_USER_PHOTO_CROP_X = -1
14	DEFAULT_USER_PHOTO_CROP_Y = -1
15	DEFAULT_USER_PHOTO_CROP_W = -1
16)
17
18// UserProfile contains all the information details of a given user
19type UserProfile struct {
20	FirstName             string                  `json:"first_name"`
21	LastName              string                  `json:"last_name"`
22	RealName              string                  `json:"real_name"`
23	RealNameNormalized    string                  `json:"real_name_normalized"`
24	DisplayName           string                  `json:"display_name"`
25	DisplayNameNormalized string                  `json:"display_name_normalized"`
26	Email                 string                  `json:"email"`
27	Skype                 string                  `json:"skype"`
28	Phone                 string                  `json:"phone"`
29	Image24               string                  `json:"image_24"`
30	Image32               string                  `json:"image_32"`
31	Image48               string                  `json:"image_48"`
32	Image72               string                  `json:"image_72"`
33	Image192              string                  `json:"image_192"`
34	Image512              string                  `json:"image_512"`
35	ImageOriginal         string                  `json:"image_original"`
36	Title                 string                  `json:"title"`
37	BotID                 string                  `json:"bot_id,omitempty"`
38	ApiAppID              string                  `json:"api_app_id,omitempty"`
39	StatusText            string                  `json:"status_text,omitempty"`
40	StatusEmoji           string                  `json:"status_emoji,omitempty"`
41	StatusExpiration      int                     `json:"status_expiration"`
42	Team                  string                  `json:"team"`
43	Fields                UserProfileCustomFields `json:"fields"`
44}
45
46// UserProfileCustomFields represents user profile's custom fields.
47// Slack API's response data type is inconsistent so we use the struct.
48// For detail, please see below.
49// https://github.com/slack-go/slack/pull/298#discussion_r185159233
50type UserProfileCustomFields struct {
51	fields map[string]UserProfileCustomField
52}
53
54// UnmarshalJSON is the implementation of the json.Unmarshaler interface.
55func (fields *UserProfileCustomFields) UnmarshalJSON(b []byte) error {
56	// https://github.com/slack-go/slack/pull/298#discussion_r185159233
57	if string(b) == "[]" {
58		return nil
59	}
60	return json.Unmarshal(b, &fields.fields)
61}
62
63// MarshalJSON is the implementation of the json.Marshaler interface.
64func (fields UserProfileCustomFields) MarshalJSON() ([]byte, error) {
65	if len(fields.fields) == 0 {
66		return []byte("[]"), nil
67	}
68	return json.Marshal(fields.fields)
69}
70
71// ToMap returns a map of custom fields.
72func (fields *UserProfileCustomFields) ToMap() map[string]UserProfileCustomField {
73	return fields.fields
74}
75
76// Len returns the number of custom fields.
77func (fields *UserProfileCustomFields) Len() int {
78	return len(fields.fields)
79}
80
81// SetMap sets a map of custom fields.
82func (fields *UserProfileCustomFields) SetMap(m map[string]UserProfileCustomField) {
83	fields.fields = m
84}
85
86// FieldsMap returns a map of custom fields.
87func (profile *UserProfile) FieldsMap() map[string]UserProfileCustomField {
88	return profile.Fields.ToMap()
89}
90
91// SetFieldsMap sets a map of custom fields.
92func (profile *UserProfile) SetFieldsMap(m map[string]UserProfileCustomField) {
93	profile.Fields.SetMap(m)
94}
95
96// UserProfileCustomField represents a custom user profile field
97type UserProfileCustomField struct {
98	Value string `json:"value"`
99	Alt   string `json:"alt"`
100	Label string `json:"label"`
101}
102
103// User contains all the information of a user
104type User struct {
105	ID                string         `json:"id"`
106	TeamID            string         `json:"team_id"`
107	Name              string         `json:"name"`
108	Deleted           bool           `json:"deleted"`
109	Color             string         `json:"color"`
110	RealName          string         `json:"real_name"`
111	TZ                string         `json:"tz,omitempty"`
112	TZLabel           string         `json:"tz_label"`
113	TZOffset          int            `json:"tz_offset"`
114	Profile           UserProfile    `json:"profile"`
115	IsBot             bool           `json:"is_bot"`
116	IsAdmin           bool           `json:"is_admin"`
117	IsOwner           bool           `json:"is_owner"`
118	IsPrimaryOwner    bool           `json:"is_primary_owner"`
119	IsRestricted      bool           `json:"is_restricted"`
120	IsUltraRestricted bool           `json:"is_ultra_restricted"`
121	IsStranger        bool           `json:"is_stranger"`
122	IsAppUser         bool           `json:"is_app_user"`
123	IsInvitedUser     bool           `json:"is_invited_user"`
124	Has2FA            bool           `json:"has_2fa"`
125	HasFiles          bool           `json:"has_files"`
126	Presence          string         `json:"presence"`
127	Locale            string         `json:"locale"`
128	Updated           JSONTime       `json:"updated"`
129	Enterprise        EnterpriseUser `json:"enterprise_user,omitempty"`
130}
131
132// UserPresence contains details about a user online status
133type UserPresence struct {
134	Presence        string   `json:"presence,omitempty"`
135	Online          bool     `json:"online,omitempty"`
136	AutoAway        bool     `json:"auto_away,omitempty"`
137	ManualAway      bool     `json:"manual_away,omitempty"`
138	ConnectionCount int      `json:"connection_count,omitempty"`
139	LastActivity    JSONTime `json:"last_activity,omitempty"`
140}
141
142type UserIdentityResponse struct {
143	User UserIdentity `json:"user"`
144	Team TeamIdentity `json:"team"`
145	SlackResponse
146}
147
148type UserIdentity struct {
149	ID       string `json:"id"`
150	Name     string `json:"name"`
151	Email    string `json:"email"`
152	Image24  string `json:"image_24"`
153	Image32  string `json:"image_32"`
154	Image48  string `json:"image_48"`
155	Image72  string `json:"image_72"`
156	Image192 string `json:"image_192"`
157	Image512 string `json:"image_512"`
158}
159
160// EnterpriseUser is present when a user is part of Slack Enterprise Grid
161// https://api.slack.com/types/user#enterprise_grid_user_objects
162type EnterpriseUser struct {
163	ID             string   `json:"id"`
164	EnterpriseID   string   `json:"enterprise_id"`
165	EnterpriseName string   `json:"enterprise_name"`
166	IsAdmin        bool     `json:"is_admin"`
167	IsOwner        bool     `json:"is_owner"`
168	Teams          []string `json:"teams"`
169}
170
171type TeamIdentity struct {
172	ID            string `json:"id"`
173	Name          string `json:"name"`
174	Domain        string `json:"domain"`
175	Image34       string `json:"image_34"`
176	Image44       string `json:"image_44"`
177	Image68       string `json:"image_68"`
178	Image88       string `json:"image_88"`
179	Image102      string `json:"image_102"`
180	Image132      string `json:"image_132"`
181	Image230      string `json:"image_230"`
182	ImageDefault  bool   `json:"image_default"`
183	ImageOriginal string `json:"image_original"`
184}
185
186type userResponseFull struct {
187	Members []User `json:"members,omitempty"`
188	User    `json:"user,omitempty"`
189	Users   []User `json:"users,omitempty"`
190	UserPresence
191	SlackResponse
192	Metadata ResponseMetadata `json:"response_metadata"`
193}
194
195type UserSetPhotoParams struct {
196	CropX int
197	CropY int
198	CropW int
199}
200
201func NewUserSetPhotoParams() UserSetPhotoParams {
202	return UserSetPhotoParams{
203		CropX: DEFAULT_USER_PHOTO_CROP_X,
204		CropY: DEFAULT_USER_PHOTO_CROP_Y,
205		CropW: DEFAULT_USER_PHOTO_CROP_W,
206	}
207}
208
209func (api *Client) userRequest(ctx context.Context, path string, values url.Values) (*userResponseFull, error) {
210	response := &userResponseFull{}
211	err := api.postMethod(ctx, path, values, response)
212	if err != nil {
213		return nil, err
214	}
215
216	return response, response.Err()
217}
218
219// GetUserPresence will retrieve the current presence status of given user.
220func (api *Client) GetUserPresence(user string) (*UserPresence, error) {
221	return api.GetUserPresenceContext(context.Background(), user)
222}
223
224// GetUserPresenceContext will retrieve the current presence status of given user with a custom context.
225func (api *Client) GetUserPresenceContext(ctx context.Context, user string) (*UserPresence, error) {
226	values := url.Values{
227		"token": {api.token},
228		"user":  {user},
229	}
230
231	response, err := api.userRequest(ctx, "users.getPresence", values)
232	if err != nil {
233		return nil, err
234	}
235	return &response.UserPresence, nil
236}
237
238// GetUserInfo will retrieve the complete user information
239func (api *Client) GetUserInfo(user string) (*User, error) {
240	return api.GetUserInfoContext(context.Background(), user)
241}
242
243// GetUserInfoContext will retrieve the complete user information with a custom context
244func (api *Client) GetUserInfoContext(ctx context.Context, user string) (*User, error) {
245	values := url.Values{
246		"token":          {api.token},
247		"user":           {user},
248		"include_locale": {strconv.FormatBool(true)},
249	}
250
251	response, err := api.userRequest(ctx, "users.info", values)
252	if err != nil {
253		return nil, err
254	}
255	return &response.User, nil
256}
257
258// GetUsersInfo will retrieve the complete multi-users information
259func (api *Client) GetUsersInfo(users ...string) (*[]User, error) {
260	return api.GetUsersInfoContext(context.Background(), users...)
261}
262
263// GetUsersInfoContext will retrieve the complete multi-users information with a custom context
264func (api *Client) GetUsersInfoContext(ctx context.Context, users ...string) (*[]User, error) {
265	values := url.Values{
266		"token":          {api.token},
267		"users":          {strings.Join(users, ",")},
268		"include_locale": {strconv.FormatBool(true)},
269	}
270
271	response, err := api.userRequest(ctx, "users.info", values)
272	if err != nil {
273		return nil, err
274	}
275	return &response.Users, nil
276}
277
278// GetUsersOption options for the GetUsers method call.
279type GetUsersOption func(*UserPagination)
280
281// GetUsersOptionLimit limit the number of users returned
282func GetUsersOptionLimit(n int) GetUsersOption {
283	return func(p *UserPagination) {
284		p.limit = n
285	}
286}
287
288// GetUsersOptionPresence include user presence
289func GetUsersOptionPresence(n bool) GetUsersOption {
290	return func(p *UserPagination) {
291		p.presence = n
292	}
293}
294
295func newUserPagination(c *Client, options ...GetUsersOption) (up UserPagination) {
296	up = UserPagination{
297		c:     c,
298		limit: 200, // per slack api documentation.
299	}
300
301	for _, opt := range options {
302		opt(&up)
303	}
304
305	return up
306}
307
308// UserPagination allows for paginating over the users
309type UserPagination struct {
310	Users        []User
311	limit        int
312	presence     bool
313	previousResp *ResponseMetadata
314	c            *Client
315}
316
317// Done checks if the pagination has completed
318func (UserPagination) Done(err error) bool {
319	return err == errPaginationComplete
320}
321
322// Failure checks if pagination failed.
323func (t UserPagination) Failure(err error) error {
324	if t.Done(err) {
325		return nil
326	}
327
328	return err
329}
330
331func (t UserPagination) Next(ctx context.Context) (_ UserPagination, err error) {
332	var (
333		resp *userResponseFull
334	)
335
336	if t.c == nil || (t.previousResp != nil && t.previousResp.Cursor == "") {
337		return t, errPaginationComplete
338	}
339
340	t.previousResp = t.previousResp.initialize()
341
342	values := url.Values{
343		"limit":          {strconv.Itoa(t.limit)},
344		"presence":       {strconv.FormatBool(t.presence)},
345		"token":          {t.c.token},
346		"cursor":         {t.previousResp.Cursor},
347		"include_locale": {strconv.FormatBool(true)},
348	}
349
350	if resp, err = t.c.userRequest(ctx, "users.list", values); err != nil {
351		return t, err
352	}
353
354	t.c.Debugf("GetUsersContext: got %d users; metadata %v", len(resp.Members), resp.Metadata)
355	t.Users = resp.Members
356	t.previousResp = &resp.Metadata
357
358	return t, nil
359}
360
361// GetUsersPaginated fetches users in a paginated fashion, see GetUsersContext for usage.
362func (api *Client) GetUsersPaginated(options ...GetUsersOption) UserPagination {
363	return newUserPagination(api, options...)
364}
365
366// GetUsers returns the list of users (with their detailed information)
367func (api *Client) GetUsers() ([]User, error) {
368	return api.GetUsersContext(context.Background())
369}
370
371// GetUsersContext returns the list of users (with their detailed information) with a custom context
372func (api *Client) GetUsersContext(ctx context.Context) (results []User, err error) {
373	p := api.GetUsersPaginated()
374	for err == nil {
375		p, err = p.Next(ctx)
376		if err == nil {
377			results = append(results, p.Users...)
378		} else if rateLimitedError, ok := err.(*RateLimitedError); ok {
379			select {
380			case <-ctx.Done():
381				err = ctx.Err()
382			case <-time.After(rateLimitedError.RetryAfter):
383				err = nil
384			}
385		}
386	}
387
388	return results, p.Failure(err)
389}
390
391// GetUserByEmail will retrieve the complete user information by email
392func (api *Client) GetUserByEmail(email string) (*User, error) {
393	return api.GetUserByEmailContext(context.Background(), email)
394}
395
396// GetUserByEmailContext will retrieve the complete user information by email with a custom context
397func (api *Client) GetUserByEmailContext(ctx context.Context, email string) (*User, error) {
398	values := url.Values{
399		"token": {api.token},
400		"email": {email},
401	}
402	response, err := api.userRequest(ctx, "users.lookupByEmail", values)
403	if err != nil {
404		return nil, err
405	}
406	return &response.User, nil
407}
408
409// SetUserAsActive marks the currently authenticated user as active
410func (api *Client) SetUserAsActive() error {
411	return api.SetUserAsActiveContext(context.Background())
412}
413
414// SetUserAsActiveContext marks the currently authenticated user as active with a custom context
415func (api *Client) SetUserAsActiveContext(ctx context.Context) (err error) {
416	values := url.Values{
417		"token": {api.token},
418	}
419
420	_, err = api.userRequest(ctx, "users.setActive", values)
421	return err
422}
423
424// SetUserPresence changes the currently authenticated user presence
425func (api *Client) SetUserPresence(presence string) error {
426	return api.SetUserPresenceContext(context.Background(), presence)
427}
428
429// SetUserPresenceContext changes the currently authenticated user presence with a custom context
430func (api *Client) SetUserPresenceContext(ctx context.Context, presence string) error {
431	values := url.Values{
432		"token":    {api.token},
433		"presence": {presence},
434	}
435
436	_, err := api.userRequest(ctx, "users.setPresence", values)
437	return err
438}
439
440// GetUserIdentity will retrieve user info available per identity scopes
441func (api *Client) GetUserIdentity() (*UserIdentityResponse, error) {
442	return api.GetUserIdentityContext(context.Background())
443}
444
445// GetUserIdentityContext will retrieve user info available per identity scopes with a custom context
446func (api *Client) GetUserIdentityContext(ctx context.Context) (response *UserIdentityResponse, err error) {
447	values := url.Values{
448		"token": {api.token},
449	}
450	response = &UserIdentityResponse{}
451
452	err = api.postMethod(ctx, "users.identity", values, response)
453	if err != nil {
454		return nil, err
455	}
456
457	if err := response.Err(); err != nil {
458		return nil, err
459	}
460
461	return response, nil
462}
463
464// SetUserPhoto changes the currently authenticated user's profile image
465func (api *Client) SetUserPhoto(image string, params UserSetPhotoParams) error {
466	return api.SetUserPhotoContext(context.Background(), image, params)
467}
468
469// SetUserPhotoContext changes the currently authenticated user's profile image using a custom context
470func (api *Client) SetUserPhotoContext(ctx context.Context, image string, params UserSetPhotoParams) (err error) {
471	response := &SlackResponse{}
472	values := url.Values{
473		"token": {api.token},
474	}
475	if params.CropX != DEFAULT_USER_PHOTO_CROP_X {
476		values.Add("crop_x", strconv.Itoa(params.CropX))
477	}
478	if params.CropY != DEFAULT_USER_PHOTO_CROP_Y {
479		values.Add("crop_y", strconv.Itoa(params.CropX))
480	}
481	if params.CropW != DEFAULT_USER_PHOTO_CROP_W {
482		values.Add("crop_w", strconv.Itoa(params.CropW))
483	}
484
485	err = postLocalWithMultipartResponse(ctx, api.httpclient, api.endpoint+"users.setPhoto", image, "image", api.token, values, response, api)
486	if err != nil {
487		return err
488	}
489
490	return response.Err()
491}
492
493// DeleteUserPhoto deletes the current authenticated user's profile image
494func (api *Client) DeleteUserPhoto() error {
495	return api.DeleteUserPhotoContext(context.Background())
496}
497
498// DeleteUserPhotoContext deletes the current authenticated user's profile image with a custom context
499func (api *Client) DeleteUserPhotoContext(ctx context.Context) (err error) {
500	response := &SlackResponse{}
501	values := url.Values{
502		"token": {api.token},
503	}
504
505	err = api.postMethod(ctx, "users.deletePhoto", values, response)
506	if err != nil {
507		return err
508	}
509
510	return response.Err()
511}
512
513// SetUserRealName changes the currently authenticated user's realName
514//
515// For more information see SetUserRealNameContextWithUser
516func (api *Client) SetUserRealName(realName string) error {
517	return api.SetUserRealNameContextWithUser(context.Background(), "", realName)
518}
519
520// SetUserRealNameContextWithUser will set a real name for the provided user with a custom context
521func (api *Client) SetUserRealNameContextWithUser(ctx context.Context, user, realName string) error {
522	profile, err := json.Marshal(
523		&struct {
524			RealName string `json:"real_name"`
525		}{
526			RealName: realName,
527		},
528	)
529
530	if err != nil {
531		return err
532	}
533
534	values := url.Values{
535		"token":   {api.token},
536		"profile": {string(profile)},
537	}
538
539	// optional field. It should not be set if empty
540	if user != "" {
541		values["user"] = []string{user}
542	}
543
544	response := &userResponseFull{}
545	if err = api.postMethod(ctx, "users.profile.set", values, response); err != nil {
546		return err
547	}
548
549	return response.Err()
550}
551
552// SetUserCustomStatus will set a custom status and emoji for the currently
553// authenticated user. If statusEmoji is "" and statusText is not, the Slack API
554// will automatically set it to ":speech_balloon:". Otherwise, if both are ""
555// the Slack API will unset the custom status/emoji. If statusExpiration is set to 0
556// the status will not expire.
557func (api *Client) SetUserCustomStatus(statusText, statusEmoji string, statusExpiration int64) error {
558	return api.SetUserCustomStatusContextWithUser(context.Background(), "", statusText, statusEmoji, statusExpiration)
559}
560
561// SetUserCustomStatusContext will set a custom status and emoji for the currently authenticated user with a custom context
562//
563// For more information see SetUserCustomStatus
564func (api *Client) SetUserCustomStatusContext(ctx context.Context, statusText, statusEmoji string, statusExpiration int64) error {
565	return api.SetUserCustomStatusContextWithUser(ctx, "", statusText, statusEmoji, statusExpiration)
566}
567
568// SetUserCustomStatusWithUser will set a custom status and emoji for the provided user.
569//
570// For more information see SetUserCustomStatus
571func (api *Client) SetUserCustomStatusWithUser(user, statusText, statusEmoji string, statusExpiration int64) error {
572	return api.SetUserCustomStatusContextWithUser(context.Background(), user, statusText, statusEmoji, statusExpiration)
573}
574
575// SetUserCustomStatusContextWithUser will set a custom status and emoji for the provided user with a custom context
576//
577// For more information see SetUserCustomStatus
578func (api *Client) SetUserCustomStatusContextWithUser(ctx context.Context, user, statusText, statusEmoji string, statusExpiration int64) error {
579	// XXX(theckman): this anonymous struct is for making requests to the Slack
580	// API for setting and unsetting a User's Custom Status/Emoji. To change
581	// these values we must provide a JSON document as the profile POST field.
582	//
583	// We use an anonymous struct over UserProfile because to unset the values
584	// on the User's profile we cannot use the `json:"omitempty"` tag. This is
585	// because an empty string ("") is what's used to unset the values. Check
586	// out the API docs for more details:
587	//
588	// - https://api.slack.com/docs/presence-and-status#custom_status
589	profile, err := json.Marshal(
590		&struct {
591			StatusText       string `json:"status_text"`
592			StatusEmoji      string `json:"status_emoji"`
593			StatusExpiration int64  `json:"status_expiration"`
594		}{
595			StatusText:       statusText,
596			StatusEmoji:      statusEmoji,
597			StatusExpiration: statusExpiration,
598		},
599	)
600
601	if err != nil {
602		return err
603	}
604
605	values := url.Values{
606		"token":   {api.token},
607		"profile": {string(profile)},
608	}
609
610	// optional field. It should not be set if empty
611	if user != "" {
612		values["user"] = []string{user}
613	}
614
615	response := &userResponseFull{}
616	if err = api.postMethod(ctx, "users.profile.set", values, response); err != nil {
617		return err
618	}
619
620	return response.Err()
621}
622
623// UnsetUserCustomStatus removes the custom status message for the currently
624// authenticated user. This is a convenience method that wraps (*Client).SetUserCustomStatus().
625func (api *Client) UnsetUserCustomStatus() error {
626	return api.UnsetUserCustomStatusContext(context.Background())
627}
628
629// UnsetUserCustomStatusContext removes the custom status message for the currently authenticated user
630// with a custom context. This is a convenience method that wraps (*Client).SetUserCustomStatus().
631func (api *Client) UnsetUserCustomStatusContext(ctx context.Context) error {
632	return api.SetUserCustomStatusContext(ctx, "", "", 0)
633}
634
635// GetUserProfileParameters are the parameters required to get user profile
636type GetUserProfileParameters struct {
637	UserID        string
638	IncludeLabels bool
639}
640
641// GetUserProfile retrieves a user's profile information.
642func (api *Client) GetUserProfile(params *GetUserProfileParameters) (*UserProfile, error) {
643	return api.GetUserProfileContext(context.Background(), params)
644}
645
646type getUserProfileResponse struct {
647	SlackResponse
648	Profile *UserProfile `json:"profile"`
649}
650
651// GetUserProfileContext retrieves a user's profile information with a context.
652func (api *Client) GetUserProfileContext(ctx context.Context, params *GetUserProfileParameters) (*UserProfile, error) {
653	values := url.Values{"token": {api.token}}
654
655	if params.UserID != "" {
656		values.Add("user", params.UserID)
657	}
658	if params.IncludeLabels {
659		values.Add("include_labels", "true")
660	}
661	resp := &getUserProfileResponse{}
662
663	err := api.postMethod(ctx, "users.profile.get", values, &resp)
664	if err != nil {
665		return nil, err
666	}
667
668	if err := resp.Err(); err != nil {
669		return nil, err
670	}
671
672	return resp.Profile, nil
673}
674