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