Codebase list golang-github-nlopes-slack / 5da40ea
New upstream version 0.6.0+dfsg Nobuhiro Iwamatsu 2 years ago
129 changed file(s) with 11694 addition(s) and 1592 deletion(s). Raw diff Collapse all Expand all
0 ##### Pull Request Guidelines
1
2 These are recommendations for pull requests.
3 They are strictly guidelines to help manage expectations.
4
5 ##### should this be an issue instead
6 - [ ] is it a convenience method? (no new functionality, streamlines some use case)
7 - [ ] exposes a previously private type, const, method, etc.
8 - [ ] is it application specific (caching, retry logic, rate limiting, etc)
9 - [ ] is it performance related.
10
11 ##### API changes
12
13 Since API changes have to be maintained they undergo a more detailed review and are more likely to require changes.
14
15 - no tests, if you're adding to the API include at least a single test of the happy case.
16 - If you can accomplish your goal without changing the API, then do so.
17 - dependency changes. updates are okay. adding/removing need justification.
18
19 ###### examples of API changes that do not meet guidelines:
20 - in library cache for users. caches are use case specific.
21 - Convenience methods for Sending Messages, update, post, ephemeral, etc. consider opening an issue instead.
00 *.test
11 *~
2 .idea/
0 {
1 "DisableAll": true,
2 "Enable": [
3 "structcheck",
4 "vet",
5 "misspell",
6 "unconvert",
7 "interfacer",
8 "goimports"
9 ],
10 "Vendor": true,
11 "Exclude": ["vendor"],
12 "Deadline": "300s"
13 }
00 language: go
11
2 go:
3 - 1.4
4 - 1.5
5 - 1.6
6 - 1.7
7 - 1.8
8 - 1.x
9 - tip
2 env:
3 - GO111MODULE=on
4
5 install: true
106
117 before_install:
128 - export PATH=$HOME/gopath/bin:$PATH
9 # install gometalinter
10 - curl -L https://git.io/vp6lP | sh
1311
1412 script:
15 - go test -race ./...
16 - go test -cover ./...
13 - PATH=$PWD/bin:$PATH gometalinter ./...
14 - go test -race -cover ./...
1715
1816 matrix:
19 allow_failures:
20 - go: tip
17 allow_failures:
18 - go: tip
19 include:
20 - go: "1.7.x"
21 script: go test -v ./...
22 - go: "1.8.x"
23 script: go test -v ./...
24 - go: "1.9.x"
25 script: go test -v ./...
26 - go: "1.10.x"
27 script: go test -v ./...
28 - go: "1.11.x"
29 script: go test -v -mod=vendor ./...
30 - go: "tip"
31 script: go test -v -mod=vendor ./...
2132
2233 git:
2334 depth: 10
0 ### v0.6.0 - August 31, 2019
1 full differences can be viewed using `git log --oneline --decorate --color v0.5.0..v0.6.0`
2 thanks to everyone who has contributed since January!
3
4
5 #### Breaking Changes:
6 - Info struct has had fields removed related to deprecated functionality by slack.
7 - minor adjustments to some structs.
8 - some internal default values have changed, usually to be more inline with slack defaults or to correct inability to set a particular value. (Message Parse for example.)
9
10 ##### Highlights:
11 - new slacktest package easy mocking for slack client. use, enjoy, please submit PRs for improvements and default behaviours! shamelessly taken from the [slack-test repo](https://github.com/lusis/slack-test) thank you lusis for letting us use it and bring it into the slack repo.
12 - blocks, blocks, blocks.
13 - RTM ManagedConnection has undergone a significant cleanup.
14 in particular handles backoffs gracefully, removed many deadlocks,
15 and Disconnect is now much more responsive.
16
17 ### v0.5.0 - January 20, 2019
18 full differences can be viewed using `git log --oneline --decorate --color v0.4.0..v0.5.0`
19 - Breaking changes: various old struct fields have been removed or updated to match slack's api.
20 - deadlock fix in RTM disconnect.
21
22 ### v0.4.0 - October 06, 2018
23 full differences can be viewed using `git log --oneline --decorate --color v0.3.0..v0.4.0`
24 - Breaking Change: renamed ApplyMessageOption, to mark it as unsafe,
25 this means it may break without warning in the future.
26 - Breaking: Msg structure files field changed to an array.
27 - General: implementation for new security headers.
28 - RTM: deadlock fix between connect/disconnect.
29 - Events: various new fields added.
30 - Web: various fixes, new fields exposed, new methods added.
31 - Interactions: minor additions expect breaking changes in next release for dialogs/button clicks.
32 - Utils: new methods added.
33
34 ### v0.3.0 - July 30, 2018
35 full differences can be viewed using `git log --oneline --decorate --color v0.2.0..v0.3.0`
36 - slack events initial support added. (still considered experimental and undergoing changes, stability not promised)
37 - vendored depedencies using dep, ensure using up to date tooling before filing issues.
38 - RTM has improved its ability to identify dead connections and reconnect automatically (worth calling out in case it has unintended side effects).
39 - bug fixes (various timestamp handling, error handling, RTM locking, etc).
40
41 ### v0.2.0 - Feb 10, 2018
42
43 Release adds a bunch of functionality and improvements, mainly to give people a recent version to vendor against.
44
45 Please check [0.2.0](https://github.com/nlopes/slack/releases/tag/v0.2.0)
46
047 ### v0.1.0 - May 28, 2017
148
249 This is released before adding context support.
00 Slack API in Go [![GoDoc](https://godoc.org/github.com/nlopes/slack?status.svg)](https://godoc.org/github.com/nlopes/slack) [![Build Status](https://travis-ci.org/nlopes/slack.svg)](https://travis-ci.org/nlopes/slack)
11 ===============
2
3 [![Join the chat at https://gitter.im/go-slack/Lobby](https://badges.gitter.im/go-slack/Lobby.svg)](https://gitter.im/go-slack/Lobby?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge)
24
35 This library supports most if not all of the `api.slack.com` REST
46 calls, as well as the Real-Time Messaging protocol over websocket, in
57 a fully managed way.
68
7 ## Change log
89
9 ### v0.1.0 - May 28, 2017
1010
11 This is released before adding context support.
12 As the used context package is the one from Go 1.7 this will be the last
13 compatible with Go < 1.7.
1411
15 Please check [0.1.0](https://github.com/nlopes/slack/releases/tag/v0.1.0)
12 ## Changelog
1613
17 ### CHANGELOG.md
18
19 As of this version a [CHANGELOG.md](https://github.com/nlopes/slack/blob/master/README.md) is available. Please visit it for updates.
14 [CHANGELOG.md](https://github.com/nlopes/slack/blob/master/CHANGELOG.md) is available. Please visit it for updates.
2015
2116 ## Installing
2217
3934 api := slack.New("YOUR_TOKEN_HERE")
4035 // If you set debugging, it will log all requests to the console
4136 // Useful when encountering issues
42 // api.SetDebug(true)
37 // slack.New("YOUR_TOKEN_HERE", slack.OptionDebug(true))
4338 groups, err := api.GetGroups(false)
4439 if err != nil {
4540 fmt.Printf("%s\n", err)
7671 See https://github.com/nlopes/slack/blob/master/examples/websocket/websocket.go
7772
7873
74 ## Minimal EventsAPI usage:
75
76 See https://github.com/nlopes/slack/blob/master/examples/eventsapi/events.go
77
78
7979 ## Contributing
8080
8181 You are more than welcome to contribute to this project. Fork and
00 package slack
11
22 import (
3 "errors"
3 "context"
44 "fmt"
55 "net/url"
6 "strings"
67 )
78
8 type adminResponse struct {
9 OK bool `json:"ok"`
10 Error string `json:"error"`
11 }
12
13 func adminRequest(method string, teamName string, values url.Values, debug bool) (*adminResponse, error) {
14 adminResponse := &adminResponse{}
15 err := parseAdminResponse(method, teamName, values, adminResponse, debug)
16 if err != nil {
17 return nil, err
18 }
19
20 if !adminResponse.OK {
21 return nil, errors.New(adminResponse.Error)
22 }
23
24 return adminResponse, nil
9 func (api *Client) adminRequest(ctx context.Context, method string, teamName string, values url.Values) error {
10 resp := &SlackResponse{}
11 err := parseAdminResponse(ctx, api.httpclient, method, teamName, values, resp, api)
12 if err != nil {
13 return err
14 }
15
16 return resp.Err()
2517 }
2618
2719 // DisableUser disabled a user account, given a user ID
2820 func (api *Client) DisableUser(teamName string, uid string) error {
21 return api.DisableUserContext(context.Background(), teamName, uid)
22 }
23
24 // DisableUserContext disabled a user account, given a user ID with a custom context
25 func (api *Client) DisableUserContext(ctx context.Context, teamName string, uid string) error {
2926 values := url.Values{
3027 "user": {uid},
31 "token": {api.config.token},
32 "set_active": {"true"},
33 "_attempts": {"1"},
34 }
35
36 _, err := adminRequest("setInactive", teamName, values, api.debug)
37 if err != nil {
38 return fmt.Errorf("Failed to disable user with id '%s': %s", uid, err)
28 "token": {api.token},
29 "set_active": {"true"},
30 "_attempts": {"1"},
31 }
32
33 if err := api.adminRequest(ctx, "setInactive", teamName, values); err != nil {
34 return fmt.Errorf("failed to disable user with id '%s': %s", uid, err)
3935 }
4036
4137 return nil
4238 }
4339
4440 // InviteGuest invites a user to Slack as a single-channel guest
45 func (api *Client) InviteGuest(
46 teamName string,
47 channel string,
48 firstName string,
49 lastName string,
50 emailAddress string,
51 ) error {
41 func (api *Client) InviteGuest(teamName, channel, firstName, lastName, emailAddress string) error {
42 return api.InviteGuestContext(context.Background(), teamName, channel, firstName, lastName, emailAddress)
43 }
44
45 // InviteGuestContext invites a user to Slack as a single-channel guest with a custom context
46 func (api *Client) InviteGuestContext(ctx context.Context, teamName, channel, firstName, lastName, emailAddress string) error {
5247 values := url.Values{
5348 "email": {emailAddress},
5449 "channels": {channel},
5550 "first_name": {firstName},
5651 "last_name": {lastName},
5752 "ultra_restricted": {"1"},
58 "token": {api.config.token},
53 "token": {api.token},
54 "resend": {"true"},
5955 "set_active": {"true"},
6056 "_attempts": {"1"},
6157 }
6258
63 _, err := adminRequest("invite", teamName, values, api.debug)
59 err := api.adminRequest(ctx, "invite", teamName, values)
6460 if err != nil {
6561 return fmt.Errorf("Failed to invite single-channel guest: %s", err)
6662 }
6965 }
7066
7167 // InviteRestricted invites a user to Slack as a restricted account
72 func (api *Client) InviteRestricted(
73 teamName string,
74 channel string,
75 firstName string,
76 lastName string,
77 emailAddress string,
78 ) error {
68 func (api *Client) InviteRestricted(teamName, channel, firstName, lastName, emailAddress string) error {
69 return api.InviteRestrictedContext(context.Background(), teamName, channel, firstName, lastName, emailAddress)
70 }
71
72 // InviteRestrictedContext invites a user to Slack as a restricted account with a custom context
73 func (api *Client) InviteRestrictedContext(ctx context.Context, teamName, channel, firstName, lastName, emailAddress string) error {
7974 values := url.Values{
8075 "email": {emailAddress},
8176 "channels": {channel},
8277 "first_name": {firstName},
8378 "last_name": {lastName},
8479 "restricted": {"1"},
85 "token": {api.config.token},
86 "set_active": {"true"},
87 "_attempts": {"1"},
88 }
89
90 _, err := adminRequest("invite", teamName, values, api.debug)
80 "token": {api.token},
81 "resend": {"true"},
82 "set_active": {"true"},
83 "_attempts": {"1"},
84 }
85
86 err := api.adminRequest(ctx, "invite", teamName, values)
9187 if err != nil {
9288 return fmt.Errorf("Failed to restricted account: %s", err)
9389 }
9692 }
9793
9894 // InviteToTeam invites a user to a Slack team
99 func (api *Client) InviteToTeam(
100 teamName string,
101 firstName string,
102 lastName string,
103 emailAddress string,
104 ) error {
95 func (api *Client) InviteToTeam(teamName, firstName, lastName, emailAddress string) error {
96 return api.InviteToTeamContext(context.Background(), teamName, firstName, lastName, emailAddress)
97 }
98
99 // InviteToTeamContext invites a user to a Slack team with a custom context
100 func (api *Client) InviteToTeamContext(ctx context.Context, teamName, firstName, lastName, emailAddress string) error {
105101 values := url.Values{
106102 "email": {emailAddress},
107103 "first_name": {firstName},
108104 "last_name": {lastName},
109 "token": {api.config.token},
110 "set_active": {"true"},
111 "_attempts": {"1"},
112 }
113
114 _, err := adminRequest("invite", teamName, values, api.debug)
105 "token": {api.token},
106 "set_active": {"true"},
107 "_attempts": {"1"},
108 }
109
110 err := api.adminRequest(ctx, "invite", teamName, values)
115111 if err != nil {
116112 return fmt.Errorf("Failed to invite to team: %s", err)
117113 }
120116 }
121117
122118 // SetRegular enables the specified user
123 func (api *Client) SetRegular(teamName string, user string) error {
119 func (api *Client) SetRegular(teamName, user string) error {
120 return api.SetRegularContext(context.Background(), teamName, user)
121 }
122
123 // SetRegularContext enables the specified user with a custom context
124 func (api *Client) SetRegularContext(ctx context.Context, teamName, user string) error {
124125 values := url.Values{
125126 "user": {user},
126 "token": {api.config.token},
127 "set_active": {"true"},
128 "_attempts": {"1"},
129 }
130
131 _, err := adminRequest("setRegular", teamName, values, api.debug)
127 "token": {api.token},
128 "set_active": {"true"},
129 "_attempts": {"1"},
130 }
131
132 err := api.adminRequest(ctx, "setRegular", teamName, values)
132133 if err != nil {
133134 return fmt.Errorf("Failed to change the user (%s) to a regular user: %s", user, err)
134135 }
137138 }
138139
139140 // SendSSOBindingEmail sends an SSO binding email to the specified user
140 func (api *Client) SendSSOBindingEmail(teamName string, user string) error {
141 func (api *Client) SendSSOBindingEmail(teamName, user string) error {
142 return api.SendSSOBindingEmailContext(context.Background(), teamName, user)
143 }
144
145 // SendSSOBindingEmailContext sends an SSO binding email to the specified user with a custom context
146 func (api *Client) SendSSOBindingEmailContext(ctx context.Context, teamName, user string) error {
141147 values := url.Values{
142148 "user": {user},
143 "token": {api.config.token},
144 "set_active": {"true"},
145 "_attempts": {"1"},
146 }
147
148 _, err := adminRequest("sendSSOBind", teamName, values, api.debug)
149 "token": {api.token},
150 "set_active": {"true"},
151 "_attempts": {"1"},
152 }
153
154 err := api.adminRequest(ctx, "sendSSOBind", teamName, values)
149155 if err != nil {
150156 return fmt.Errorf("Failed to send SSO binding email for user (%s): %s", user, err)
151157 }
155161
156162 // SetUltraRestricted converts a user into a single-channel guest
157163 func (api *Client) SetUltraRestricted(teamName, uid, channel string) error {
164 return api.SetUltraRestrictedContext(context.Background(), teamName, uid, channel)
165 }
166
167 // SetUltraRestrictedContext converts a user into a single-channel guest with a custom context
168 func (api *Client) SetUltraRestrictedContext(ctx context.Context, teamName, uid, channel string) error {
158169 values := url.Values{
159170 "user": {uid},
160171 "channel": {channel},
161 "token": {api.config.token},
162 "set_active": {"true"},
163 "_attempts": {"1"},
164 }
165
166 _, err := adminRequest("setUltraRestricted", teamName, values, api.debug)
172 "token": {api.token},
173 "set_active": {"true"},
174 "_attempts": {"1"},
175 }
176
177 err := api.adminRequest(ctx, "setUltraRestricted", teamName, values)
167178 if err != nil {
168179 return fmt.Errorf("Failed to ultra-restrict account: %s", err)
169180 }
172183 }
173184
174185 // SetRestricted converts a user into a restricted account
175 func (api *Client) SetRestricted(teamName, uid string) error {
186 func (api *Client) SetRestricted(teamName, uid string, channelIds ...string) error {
187 return api.SetRestrictedContext(context.Background(), teamName, uid, channelIds...)
188 }
189
190 // SetRestrictedContext converts a user into a restricted account with a custom context
191 func (api *Client) SetRestrictedContext(ctx context.Context, teamName, uid string, channelIds ...string) error {
176192 values := url.Values{
177193 "user": {uid},
178 "token": {api.config.token},
179 "set_active": {"true"},
180 "_attempts": {"1"},
181 }
182
183 _, err := adminRequest("setRestricted", teamName, values, api.debug)
184 if err != nil {
185 return fmt.Errorf("Failed to restrict account: %s", err)
186 }
187
188 return nil
189 }
194 "token": {api.token},
195 "set_active": {"true"},
196 "_attempts": {"1"},
197 "channels": {strings.Join(channelIds, ",")},
198 }
199
200 err := api.adminRequest(ctx, "setRestricted", teamName, values)
201 if err != nil {
202 return fmt.Errorf("failed to restrict account: %s", err)
203 }
204
205 return nil
206 }
1616 Name string `json:"name"` // Required.
1717 Text string `json:"text"` // Required.
1818 Style string `json:"style,omitempty"` // Optional. Allowed values: "default", "primary", "danger".
19 Type string `json:"type"` // Required. Must be set to "button" or "select".
19 Type actionType `json:"type"` // Required. Must be set to "button" or "select".
2020 Value string `json:"value,omitempty"` // Optional.
2121 DataSource string `json:"data_source,omitempty"` // Optional.
2222 MinQueryLength int `json:"min_query_length,omitempty"` // Optional. Default value is 1.
2424 SelectedOptions []AttachmentActionOption `json:"selected_options,omitempty"` // Optional. The first element of this array will be set as the pre-selected option for this menu.
2525 OptionGroups []AttachmentActionOptionGroup `json:"option_groups,omitempty"` // Optional.
2626 Confirm *ConfirmationField `json:"confirm,omitempty"` // Optional.
27 URL string `json:"url,omitempty"` // Optional.
28 }
29
30 // actionType returns the type of the action
31 func (a AttachmentAction) actionType() actionType {
32 return a.Type
2733 }
2834
2935 // AttachmentActionOption the individual option to appear in action menu.
4046 }
4147
4248 // AttachmentActionCallback is sent from Slack when a user clicks a button in an interactive message (aka AttachmentAction)
43 type AttachmentActionCallback struct {
44 Actions []AttachmentAction `json:"actions"`
45 CallbackID string `json:"callback_id"`
46 Team Team `json:"team"`
47 Channel Channel `json:"channel"`
48 User User `json:"user"`
49
50 OriginalMessage Message `json:"original_message"`
51
52 ActionTs string `json:"action_ts"`
53 MessageTs string `json:"message_ts"`
54 AttachmentID string `json:"attachment_id"`
55 Token string `json:"token"`
56 ResponseURL string `json:"response_url"`
57 }
49 // DEPRECATED: use InteractionCallback
50 type AttachmentActionCallback InteractionCallback
5851
5952 // ConfirmationField are used to ask users to confirm actions
6053 type ConfirmationField struct {
7063 Fallback string `json:"fallback"`
7164
7265 CallbackID string `json:"callback_id,omitempty"`
66 ID int `json:"id,omitempty"`
7367
68 AuthorID string `json:"author_id,omitempty"`
7469 AuthorName string `json:"author_name,omitempty"`
7570 AuthorSubname string `json:"author_subname,omitempty"`
7671 AuthorLink string `json:"author_link,omitempty"`
0 package slack
1
2 import (
3 "context"
4 "net/url"
5 )
6
7 // AuthRevokeResponse contains our Auth response from the auth.revoke endpoint
8 type AuthRevokeResponse struct {
9 SlackResponse // Contains the "ok", and "Error", if any
10 Revoked bool `json:"revoked,omitempty"`
11 }
12
13 // authRequest sends the actual request, and unmarshals the response
14 func (api *Client) authRequest(ctx context.Context, path string, values url.Values) (*AuthRevokeResponse, error) {
15 response := &AuthRevokeResponse{}
16 err := api.postMethod(ctx, path, values, response)
17 if err != nil {
18 return nil, err
19 }
20
21 return response, response.Err()
22 }
23
24 // SendAuthRevoke will send a revocation for our token
25 func (api *Client) SendAuthRevoke(token string) (*AuthRevokeResponse, error) {
26 return api.SendAuthRevokeContext(context.Background(), token)
27 }
28
29 // SendAuthRevokeContext will retrieve the satus from api.test
30 func (api *Client) SendAuthRevokeContext(ctx context.Context, token string) (*AuthRevokeResponse, error) {
31 if token == "" {
32 token = api.token
33 }
34 values := url.Values{
35 "token": {token},
36 }
37
38 return api.authRequest(ctx, "auth.revoke", values)
39 }
00 package slack
11
22 import (
3 "math"
43 "math/rand"
54 "time"
65 )
1312 // conjunction with the time package.
1413 type backoff struct {
1514 attempts int
16 //Factor is the multiplying factor for each increment step
17 Factor float64
18 //Jitter eases contention by randomizing backoff steps
19 Jitter bool
20 //Min and Max are the minimum and maximum values of the counter
21 Min, Max time.Duration
15 // Initial value to scale out
16 Initial time.Duration
17 // Jitter value randomizes an additional delay between 0 and Jitter
18 Jitter time.Duration
19 // Max maximum values of the backoff
20 Max time.Duration
2221 }
2322
2423 // Returns the current value of the counter and then multiplies it
2524 // Factor
26 func (b *backoff) Duration() time.Duration {
27 //Zero-values are nonsensical, so we use
28 //them to apply defaults
29 if b.Min == 0 {
30 b.Min = 100 * time.Millisecond
31 }
25 func (b *backoff) Duration() (dur time.Duration) {
26 // Zero-values are nonsensical, so we use
27 // them to apply defaults
3228 if b.Max == 0 {
3329 b.Max = 10 * time.Second
3430 }
35 if b.Factor == 0 {
36 b.Factor = 2
31
32 if b.Initial == 0 {
33 b.Initial = 100 * time.Millisecond
3734 }
38 //calculate this duration
39 dur := float64(b.Min) * math.Pow(b.Factor, float64(b.attempts))
40 if b.Jitter == true {
41 dur = rand.Float64()*(dur-float64(b.Min)) + float64(b.Min)
35
36 // calculate this duration
37 if dur = time.Duration(1 << uint(b.attempts)); dur > 0 {
38 dur = dur * b.Initial
39 } else {
40 dur = b.Max
4241 }
43 //cap!
44 if dur > float64(b.Max) {
45 return b.Max
42
43 if b.Jitter > 0 {
44 dur = dur + time.Duration(rand.Intn(int(b.Jitter)))
4645 }
47 //bump attempts count
46
47 // bump attempts count
4848 b.attempts++
49 //return as a time.Duration
50 return time.Duration(dur)
49
50 return dur
5151 }
5252
5353 //Resets the current value of the counter back to Min
0 package slack
1
2 // @NOTE: Blocks are in beta and subject to change.
3
4 // More Information: https://api.slack.com/block-kit
5
6 // MessageBlockType defines a named string type to define each block type
7 // as a constant for use within the package.
8 type MessageBlockType string
9
10 const (
11 MBTSection MessageBlockType = "section"
12 MBTDivider MessageBlockType = "divider"
13 MBTImage MessageBlockType = "image"
14 MBTAction MessageBlockType = "actions"
15 MBTContext MessageBlockType = "context"
16 )
17
18 // Block defines an interface all block types should implement
19 // to ensure consistency between blocks.
20 type Block interface {
21 BlockType() MessageBlockType
22 }
23
24 // Blocks is a convenience struct defined to allow dynamic unmarshalling of
25 // the "blocks" value in Slack's JSON response, which varies depending on block type
26 type Blocks struct {
27 BlockSet []Block `json:"blocks,omitempty"`
28 }
29
30 // BlockAction is the action callback sent when a block is interacted with
31 type BlockAction struct {
32 ActionID string `json:"action_id"`
33 BlockID string `json:"block_id"`
34 Type actionType `json:"type"`
35 Text TextBlockObject `json:"text"`
36 Value string `json:"value"`
37 ActionTs string `json:"action_ts"`
38 SelectedOption OptionBlockObject `json:"selected_option"`
39 SelectedUser string `json:"selected_user"`
40 SelectedChannel string `json:"selected_channel"`
41 SelectedConversation string `json:"selected_conversation"`
42 SelectedDate string `json:"selected_date"`
43 InitialOption OptionBlockObject `json:"initial_option"`
44 InitialUser string `json:"initial_user"`
45 InitialChannel string `json:"initial_channel"`
46 InitialConversation string `json:"initial_conversation"`
47 InitialDate string `json:"initial_date"`
48 }
49
50 // actionType returns the type of the action
51 func (b BlockAction) actionType() actionType {
52 return b.Type
53 }
54
55 // NewBlockMessage creates a new Message that contains one or more blocks to be displayed
56 func NewBlockMessage(blocks ...Block) Message {
57 return Message{
58 Msg: Msg{
59 Blocks: Blocks{
60 BlockSet: blocks,
61 },
62 },
63 }
64 }
65
66 // AddBlockMessage appends a block to the end of the existing list of blocks
67 func AddBlockMessage(message Message, newBlk Block) Message {
68 message.Msg.Blocks.BlockSet = append(message.Msg.Blocks.BlockSet, newBlk)
69 return message
70 }
0 package slack
1
2 // ActionBlock defines data that is used to hold interactive elements.
3 //
4 // More Information: https://api.slack.com/reference/messaging/blocks#actions
5 type ActionBlock struct {
6 Type MessageBlockType `json:"type"`
7 BlockID string `json:"block_id,omitempty"`
8 Elements BlockElements `json:"elements"`
9 }
10
11 // BlockType returns the type of the block
12 func (s ActionBlock) BlockType() MessageBlockType {
13 return s.Type
14 }
15
16 // NewActionBlock returns a new instance of an Action Block
17 func NewActionBlock(blockID string, elements ...BlockElement) *ActionBlock {
18 return &ActionBlock{
19 Type: MBTAction,
20 BlockID: blockID,
21 Elements: BlockElements{
22 ElementSet: elements,
23 },
24 }
25 }
0 package slack
1
2 import (
3 "testing"
4
5 "github.com/stretchr/testify/assert"
6 )
7
8 func TestNewActionBlock(t *testing.T) {
9
10 approveBtnTxt := NewTextBlockObject("plain_text", "Approve", false, false)
11 approveBtn := NewButtonBlockElement("", "click_me_123", approveBtnTxt)
12
13 actionBlock := NewActionBlock("test", approveBtn)
14 assert.Equal(t, string(actionBlock.Type), "actions")
15 assert.Equal(t, actionBlock.BlockID, "test")
16 assert.Equal(t, len(actionBlock.Elements.ElementSet), 1)
17
18 }
0 package slack
1
2 // ContextBlock defines data that is used to display message context, which can
3 // include both images and text.
4 //
5 // More Information: https://api.slack.com/reference/messaging/blocks#actions
6 type ContextBlock struct {
7 Type MessageBlockType `json:"type"`
8 BlockID string `json:"block_id,omitempty"`
9 ContextElements ContextElements `json:"elements"`
10 }
11
12 // BlockType returns the type of the block
13 func (s ContextBlock) BlockType() MessageBlockType {
14 return s.Type
15 }
16
17 type ContextElements struct {
18 Elements []MixedElement
19 }
20
21 // NewContextBlock returns a new instance of a context block
22 func NewContextBlock(blockID string, mixedElements ...MixedElement) *ContextBlock {
23 elements := ContextElements{
24 Elements: mixedElements,
25 }
26 return &ContextBlock{
27 Type: MBTContext,
28 BlockID: blockID,
29 ContextElements: elements,
30 }
31 }
0 package slack
1
2 import (
3 "testing"
4
5 "github.com/stretchr/testify/assert"
6 )
7
8 func TestNewContextBlock(t *testing.T) {
9
10 locationPinImage := NewImageBlockElement("https://api.slack.com/img/blocks/bkb_template_images/tripAgentLocationMarker.png", "Location Pin Icon")
11 textExample := NewTextBlockObject("plain_text", "Location: Central Business District", true, false)
12
13 elements := []MixedElement{locationPinImage, textExample}
14
15 contextBlock := NewContextBlock("test", elements...)
16 assert.Equal(t, string(contextBlock.Type), "context")
17 assert.Equal(t, contextBlock.BlockID, "test")
18 assert.Equal(t, len(contextBlock.ContextElements.Elements), 2)
19
20 }
0 package slack
1
2 import (
3 "encoding/json"
4
5 "github.com/pkg/errors"
6 )
7
8 type sumtype struct {
9 TypeVal string `json:"type"`
10 }
11
12 // MarshalJSON implements the Marshaller interface for Blocks so that any JSON
13 // marshalling is delegated and proper type determination can be made before marshal
14 func (b Blocks) MarshalJSON() ([]byte, error) {
15 bytes, err := json.Marshal(b.BlockSet)
16 if err != nil {
17 return nil, err
18 }
19
20 return bytes, nil
21 }
22
23 // UnmarshalJSON implements the Unmarshaller interface for Blocks, so that any JSON
24 // unmarshalling is delegated and proper type determination can be made before unmarshal
25 func (b *Blocks) UnmarshalJSON(data []byte) error {
26 var raw []json.RawMessage
27
28 if string(data) == "{}" {
29 return nil
30 }
31
32 err := json.Unmarshal(data, &raw)
33 if err != nil {
34 return err
35 }
36
37 var blocks Blocks
38 for _, r := range raw {
39 s := sumtype{}
40 err := json.Unmarshal(r, &s)
41 if err != nil {
42 return err
43 }
44
45 var blockType string
46 if s.TypeVal != "" {
47 blockType = s.TypeVal
48 }
49
50 var block Block
51 switch blockType {
52 case "actions":
53 block = &ActionBlock{}
54 case "context":
55 block = &ContextBlock{}
56 case "divider":
57 block = &DividerBlock{}
58 case "image":
59 block = &ImageBlock{}
60 case "section":
61 block = &SectionBlock{}
62 default:
63 return errors.New("unsupported block type")
64 }
65
66 err = json.Unmarshal(r, block)
67 if err != nil {
68 return err
69 }
70
71 blocks.BlockSet = append(blocks.BlockSet, block)
72 }
73
74 *b = blocks
75 return nil
76 }
77
78 // MarshalJSON implements the Marshaller interface for BlockElements so that any JSON
79 // marshalling is delegated and proper type determination can be made before marshal
80 func (b *BlockElements) MarshalJSON() ([]byte, error) {
81 bytes, err := json.Marshal(b.ElementSet)
82 if err != nil {
83 return nil, err
84 }
85
86 return bytes, nil
87 }
88
89 // UnmarshalJSON implements the Unmarshaller interface for BlockElements, so that any JSON
90 // unmarshalling is delegated and proper type determination can be made before unmarshal
91 func (b *BlockElements) UnmarshalJSON(data []byte) error {
92 var raw []json.RawMessage
93
94 if string(data) == "{}" {
95 return nil
96 }
97
98 err := json.Unmarshal(data, &raw)
99 if err != nil {
100 return err
101 }
102
103 var blockElements BlockElements
104 for _, r := range raw {
105 s := sumtype{}
106 err := json.Unmarshal(r, &s)
107 if err != nil {
108 return err
109 }
110
111 var blockElementType string
112 if s.TypeVal != "" {
113 blockElementType = s.TypeVal
114 }
115
116 var blockElement BlockElement
117 switch blockElementType {
118 case "image":
119 blockElement = &ImageBlockElement{}
120 case "button":
121 blockElement = &ButtonBlockElement{}
122 case "overflow":
123 blockElement = &OverflowBlockElement{}
124 case "datepicker":
125 blockElement = &DatePickerBlockElement{}
126 case "static_select", "external_select", "users_select", "conversations_select", "channels_select":
127 blockElement = &SelectBlockElement{}
128 default:
129 return errors.New("unsupported block element type")
130 }
131
132 err = json.Unmarshal(r, blockElement)
133 if err != nil {
134 return err
135 }
136
137 blockElements.ElementSet = append(blockElements.ElementSet, blockElement)
138 }
139
140 *b = blockElements
141 return nil
142 }
143
144 // MarshalJSON implements the Marshaller interface for Accessory so that any JSON
145 // marshalling is delegated and proper type determination can be made before marshal
146 func (a *Accessory) MarshalJSON() ([]byte, error) {
147 bytes, err := json.Marshal(toBlockElement(a))
148 if err != nil {
149 return nil, err
150 }
151
152 return bytes, nil
153 }
154
155 // UnmarshalJSON implements the Unmarshaller interface for Accessory, so that any JSON
156 // unmarshalling is delegated and proper type determination can be made before unmarshal
157 func (a *Accessory) UnmarshalJSON(data []byte) error {
158 var r json.RawMessage
159
160 if string(data) == "{\"accessory\":null}" {
161 return nil
162 }
163
164 err := json.Unmarshal(data, &r)
165 if err != nil {
166 return err
167 }
168
169 s := sumtype{}
170 err = json.Unmarshal(r, &s)
171 if err != nil {
172 return err
173 }
174
175 var blockElementType string
176 if s.TypeVal != "" {
177 blockElementType = s.TypeVal
178 }
179
180 switch blockElementType {
181 case "image":
182 element, err := unmarshalBlockElement(r, &ImageBlockElement{})
183 if err != nil {
184 return err
185 }
186 a.ImageElement = element.(*ImageBlockElement)
187 case "button":
188 element, err := unmarshalBlockElement(r, &ButtonBlockElement{})
189 if err != nil {
190 return err
191 }
192 a.ButtonElement = element.(*ButtonBlockElement)
193 case "overflow":
194 element, err := unmarshalBlockElement(r, &OverflowBlockElement{})
195 if err != nil {
196 return err
197 }
198 a.OverflowElement = element.(*OverflowBlockElement)
199 case "datepicker":
200 element, err := unmarshalBlockElement(r, &DatePickerBlockElement{})
201 if err != nil {
202 return err
203 }
204 a.DatePickerElement = element.(*DatePickerBlockElement)
205 case "static_select":
206 element, err := unmarshalBlockElement(r, &SelectBlockElement{})
207 if err != nil {
208 return err
209 }
210 a.SelectElement = element.(*SelectBlockElement)
211 }
212
213 return nil
214 }
215
216 func unmarshalBlockElement(r json.RawMessage, element BlockElement) (BlockElement, error) {
217 err := json.Unmarshal(r, element)
218 if err != nil {
219 return nil, err
220 }
221 return element, nil
222 }
223
224 func toBlockElement(element *Accessory) BlockElement {
225 if element.ImageElement != nil {
226 return element.ImageElement
227 }
228 if element.ButtonElement != nil {
229 return element.ButtonElement
230 }
231 if element.OverflowElement != nil {
232 return element.OverflowElement
233 }
234 if element.DatePickerElement != nil {
235 return element.DatePickerElement
236 }
237 if element.SelectElement != nil {
238 return element.SelectElement
239 }
240
241 return nil
242 }
243
244 // MarshalJSON implements the Marshaller interface for ContextElements so that any JSON
245 // marshalling is delegated and proper type determination can be made before marshal
246 func (e *ContextElements) MarshalJSON() ([]byte, error) {
247 bytes, err := json.Marshal(e.Elements)
248 if err != nil {
249 return nil, err
250 }
251
252 return bytes, nil
253 }
254
255 // UnmarshalJSON implements the Unmarshaller interface for ContextElements, so that any JSON
256 // unmarshalling is delegated and proper type determination can be made before unmarshal
257 func (e *ContextElements) UnmarshalJSON(data []byte) error {
258 var raw []json.RawMessage
259
260 if string(data) == "{\"elements\":null}" {
261 return nil
262 }
263
264 err := json.Unmarshal(data, &raw)
265 if err != nil {
266 return err
267 }
268
269 for _, r := range raw {
270 s := sumtype{}
271 err := json.Unmarshal(r, &s)
272 if err != nil {
273 return err
274 }
275
276 var contextElementType string
277 if s.TypeVal != "" {
278 contextElementType = s.TypeVal
279 }
280
281 switch contextElementType {
282 case PlainTextType, MarkdownType:
283 elem, err := unmarshalBlockObject(r, &TextBlockObject{})
284 if err != nil {
285 return err
286 }
287
288 e.Elements = append(e.Elements, elem.(*TextBlockObject))
289 case "image":
290 elem, err := unmarshalBlockElement(r, &ImageBlockElement{})
291 if err != nil {
292 return err
293 }
294
295 e.Elements = append(e.Elements, elem.(*ImageBlockElement))
296 default:
297 return errors.New("unsupported context element type")
298 }
299 }
300
301 return nil
302 }
0 package slack
1
2 // DividerBlock for displaying a divider line between blocks (similar to <hr> tag in html)
3 //
4 // More Information: https://api.slack.com/reference/messaging/blocks#divider
5 type DividerBlock struct {
6 Type MessageBlockType `json:"type"`
7 BlockID string `json:"block_id,omitempty"`
8 }
9
10 // BlockType returns the type of the block
11 func (s DividerBlock) BlockType() MessageBlockType {
12 return s.Type
13 }
14
15 // NewDividerBlock returns a new instance of a divider block
16 func NewDividerBlock() *DividerBlock {
17 return &DividerBlock{
18 Type: MBTDivider,
19 }
20
21 }
0 package slack
1
2 import (
3 "testing"
4
5 "github.com/stretchr/testify/assert"
6 )
7
8 func TestNewDividerBlock(t *testing.T) {
9
10 dividerBlock := NewDividerBlock()
11 assert.Equal(t, string(dividerBlock.Type), "divider")
12
13 }
0 package slack
1
2 // https://api.slack.com/reference/messaging/block-elements
3
4 const (
5 METImage MessageElementType = "image"
6 METButton MessageElementType = "button"
7 METOverflow MessageElementType = "overflow"
8 METDatepicker MessageElementType = "datepicker"
9
10 MixedElementImage MixedElementType = "mixed_image"
11 MixedElementText MixedElementType = "mixed_text"
12
13 OptTypeStatic string = "static_select"
14 OptTypeExternal string = "external_select"
15 OptTypeUser string = "users_select"
16 OptTypeConversations string = "conversations_select"
17 OptTypeChannels string = "channels_select"
18 )
19
20 type MessageElementType string
21 type MixedElementType string
22
23 // BlockElement defines an interface that all block element types should implement.
24 type BlockElement interface {
25 ElementType() MessageElementType
26 }
27
28 type MixedElement interface {
29 MixedElementType() MixedElementType
30 }
31
32 type Accessory struct {
33 ImageElement *ImageBlockElement
34 ButtonElement *ButtonBlockElement
35 OverflowElement *OverflowBlockElement
36 DatePickerElement *DatePickerBlockElement
37 SelectElement *SelectBlockElement
38 }
39
40 // NewAccessory returns a new Accessory for a given block element
41 func NewAccessory(element BlockElement) *Accessory {
42 switch element.(type) {
43 case *ImageBlockElement:
44 return &Accessory{ImageElement: element.(*ImageBlockElement)}
45 case *ButtonBlockElement:
46 return &Accessory{ButtonElement: element.(*ButtonBlockElement)}
47 case *OverflowBlockElement:
48 return &Accessory{OverflowElement: element.(*OverflowBlockElement)}
49 case *DatePickerBlockElement:
50 return &Accessory{DatePickerElement: element.(*DatePickerBlockElement)}
51 case *SelectBlockElement:
52 return &Accessory{SelectElement: element.(*SelectBlockElement)}
53 }
54
55 return nil
56 }
57
58 // BlockElements is a convenience struct defined to allow dynamic unmarshalling of
59 // the "elements" value in Slack's JSON response, which varies depending on BlockElement type
60 type BlockElements struct {
61 ElementSet []BlockElement `json:"elements,omitempty"`
62 }
63
64 // ImageBlockElement An element to insert an image - this element can be used
65 // in section and context blocks only. If you want a block with only an image
66 // in it, you're looking for the image block.
67 //
68 // More Information: https://api.slack.com/reference/messaging/block-elements#image
69 type ImageBlockElement struct {
70 Type MessageElementType `json:"type"`
71 ImageURL string `json:"image_url"`
72 AltText string `json:"alt_text"`
73 }
74
75 // ElementType returns the type of the Element
76 func (s ImageBlockElement) ElementType() MessageElementType {
77 return s.Type
78 }
79
80 func (s ImageBlockElement) MixedElementType() MixedElementType {
81 return MixedElementImage
82 }
83
84 // NewImageBlockElement returns a new instance of an image block element
85 func NewImageBlockElement(imageURL, altText string) *ImageBlockElement {
86 return &ImageBlockElement{
87 Type: METImage,
88 ImageURL: imageURL,
89 AltText: altText,
90 }
91 }
92
93 type Style string
94
95 const (
96 StyleDefault Style = "default"
97 StylePrimary Style = "primary"
98 StyleDanger Style = "danger"
99 )
100
101 // ButtonBlockElement defines an interactive element that inserts a button. The
102 // button can be a trigger for anything from opening a simple link to starting
103 // a complex workflow.
104 //
105 // More Information: https://api.slack.com/reference/messaging/block-elements#button
106 type ButtonBlockElement struct {
107 Type MessageElementType `json:"type,omitempty"`
108 Text *TextBlockObject `json:"text"`
109 ActionID string `json:"action_id,omitempty"`
110 URL string `json:"url,omitempty"`
111 Value string `json:"value,omitempty"`
112 Confirm *ConfirmationBlockObject `json:"confirm,omitempty"`
113 Style Style `json:"style,omitempty"`
114 }
115
116 // ElementType returns the type of the element
117 func (s ButtonBlockElement) ElementType() MessageElementType {
118 return s.Type
119 }
120
121 // add styling to button object
122 func (s *ButtonBlockElement) WithStyle(style Style) {
123 s.Style = style
124 }
125
126 // NewButtonBlockElement returns an instance of a new button element to be used within a block
127 func NewButtonBlockElement(actionID, value string, text *TextBlockObject) *ButtonBlockElement {
128 return &ButtonBlockElement{
129 Type: METButton,
130 ActionID: actionID,
131 Text: text,
132 Value: value,
133 }
134 }
135
136 // SelectBlockElement defines the simplest form of select menu, with a static list
137 // of options passed in when defining the element.
138 //
139 // More Information: https://api.slack.com/reference/messaging/block-elements#select
140 type SelectBlockElement struct {
141 Type string `json:"type,omitempty"`
142 Placeholder *TextBlockObject `json:"placeholder,omitempty"`
143 ActionID string `json:"action_id,omitempty"`
144 Options []*OptionBlockObject `json:"options,omitempty"`
145 OptionGroups []*OptionGroupBlockObject `json:"option_groups,omitempty"`
146 InitialOption *OptionBlockObject `json:"initial_option,omitempty"`
147 InitialUser string `json:"initial_user,omitempty"`
148 InitialConversation string `json:"initial_conversation,omitempty"`
149 InitialChannel string `json:"initial_channel,omitempty"`
150 MinQueryLength int `json:"min_query_length,omitempty"`
151 Confirm *ConfirmationBlockObject `json:"confirm,omitempty"`
152 }
153
154 // ElementType returns the type of the Element
155 func (s SelectBlockElement) ElementType() MessageElementType {
156 return MessageElementType(s.Type)
157 }
158
159 // NewOptionsSelectBlockElement returns a new instance of SelectBlockElement for use with
160 // the Options object only.
161 func NewOptionsSelectBlockElement(optType string, placeholder *TextBlockObject, actionID string, options ...*OptionBlockObject) *SelectBlockElement {
162 return &SelectBlockElement{
163 Type: optType,
164 Placeholder: placeholder,
165 ActionID: actionID,
166 Options: options,
167 }
168 }
169
170 // NewOptionsGroupSelectBlockElement returns a new instance of SelectBlockElement for use with
171 // the Options object only.
172 func NewOptionsGroupSelectBlockElement(
173 optType string,
174 placeholder *TextBlockObject,
175 actionID string,
176 optGroups ...*OptionGroupBlockObject,
177 ) *SelectBlockElement {
178 return &SelectBlockElement{
179 Type: optType,
180 Placeholder: placeholder,
181 ActionID: actionID,
182 OptionGroups: optGroups,
183 }
184 }
185
186 // OverflowBlockElement defines the fields needed to use an overflow element.
187 // And Overflow Element is like a cross between a button and a select menu -
188 // when a user clicks on this overflow button, they will be presented with a
189 // list of options to choose from.
190 //
191 // More Information: https://api.slack.com/reference/messaging/block-elements#overflow
192 type OverflowBlockElement struct {
193 Type MessageElementType `json:"type"`
194 ActionID string `json:"action_id,omitempty"`
195 Options []*OptionBlockObject `json:"options"`
196 Confirm *ConfirmationBlockObject `json:"confirm,omitempty"`
197 }
198
199 // ElementType returns the type of the Element
200 func (s OverflowBlockElement) ElementType() MessageElementType {
201 return s.Type
202 }
203
204 // NewOverflowBlockElement returns an instance of a new Overflow Block Element
205 func NewOverflowBlockElement(actionID string, options ...*OptionBlockObject) *OverflowBlockElement {
206 return &OverflowBlockElement{
207 Type: METOverflow,
208 ActionID: actionID,
209 Options: options,
210 }
211 }
212
213 // DatePickerBlockElement defines an element which lets users easily select a
214 // date from a calendar style UI. Date picker elements can be used inside of
215 // section and actions blocks.
216 //
217 // More Information: https://api.slack.com/reference/messaging/block-elements#datepicker
218 type DatePickerBlockElement struct {
219 Type MessageElementType `json:"type"`
220 ActionID string `json:"action_id"`
221 Placeholder *TextBlockObject `json:"placeholder,omitempty"`
222 InitialDate string `json:"initial_date,omitempty"`
223 Confirm *ConfirmationBlockObject `json:"confirm,omitempty"`
224 }
225
226 // ElementType returns the type of the Element
227 func (s DatePickerBlockElement) ElementType() MessageElementType {
228 return s.Type
229 }
230
231 // NewDatePickerBlockElement returns an instance of a date picker element
232 func NewDatePickerBlockElement(actionID string) *DatePickerBlockElement {
233 return &DatePickerBlockElement{
234 Type: METDatepicker,
235 ActionID: actionID,
236 }
237 }
0 package slack
1
2 import (
3 "testing"
4
5 "github.com/stretchr/testify/assert"
6 )
7
8 func TestNewImageBlockElement(t *testing.T) {
9
10 imageElement := NewImageBlockElement("https://api.slack.com/img/blocks/bkb_template_images/tripAgentLocationMarker.png", "Location Pin Icon")
11
12 assert.Equal(t, string(imageElement.Type), "image")
13 assert.Contains(t, imageElement.ImageURL, "tripAgentLocationMarker")
14 assert.Equal(t, imageElement.AltText, "Location Pin Icon")
15
16 }
17
18 func TestNewButtonBlockElement(t *testing.T) {
19
20 btnTxt := NewTextBlockObject("plain_text", "Next 2 Results", false, false)
21 btnElement := NewButtonBlockElement("test", "click_me_123", btnTxt)
22
23 assert.Equal(t, string(btnElement.Type), "button")
24 assert.Equal(t, btnElement.ActionID, "test")
25 assert.Equal(t, btnElement.Value, "click_me_123")
26 assert.Equal(t, btnElement.Text.Text, "Next 2 Results")
27
28 }
29
30 func TestNewOptionsSelectBlockElement(t *testing.T) {
31
32 testOptionText := NewTextBlockObject("plain_text", "Option One", false, false)
33 testOption := NewOptionBlockObject("test", testOptionText)
34
35 option := NewOptionsSelectBlockElement("static_select", nil, "test", testOption)
36 assert.Equal(t, option.Type, "static_select")
37 assert.Equal(t, len(option.Options), 1)
38 assert.Nil(t, option.OptionGroups)
39
40 }
41
42 func TestNewOptionsGroupSelectBlockElement(t *testing.T) {
43
44 testOptionText := NewTextBlockObject("plain_text", "Option One", false, false)
45 testOption := NewOptionBlockObject("test", testOptionText)
46 testLabel := NewTextBlockObject("plain_text", "Test Label", false, false)
47 testGroupOption := NewOptionGroupBlockElement(testLabel, testOption)
48
49 optGroup := NewOptionsGroupSelectBlockElement("static_select", nil, "test", testGroupOption)
50
51 assert.Equal(t, string(optGroup.Type), "static_select")
52 assert.Equal(t, optGroup.ActionID, "test")
53 assert.Equal(t, len(optGroup.OptionGroups), 1)
54
55 }
56
57 func TestNewOverflowBlockElement(t *testing.T) {
58
59 // Build Text Objects associated with each option
60 overflowOptionTextOne := NewTextBlockObject("plain_text", "Option One", false, false)
61 overflowOptionTextTwo := NewTextBlockObject("plain_text", "Option Two", false, false)
62 overflowOptionTextThree := NewTextBlockObject("plain_text", "Option Three", false, false)
63
64 // Build each option, providing a value for the option
65 overflowOptionOne := NewOptionBlockObject("value-0", overflowOptionTextOne)
66 overflowOptionTwo := NewOptionBlockObject("value-1", overflowOptionTextTwo)
67 overflowOptionThree := NewOptionBlockObject("value-2", overflowOptionTextThree)
68
69 // Build overflow section
70 overflowElement := NewOverflowBlockElement("test", overflowOptionOne, overflowOptionTwo, overflowOptionThree)
71
72 assert.Equal(t, string(overflowElement.Type), "overflow")
73 assert.Equal(t, overflowElement.ActionID, "test")
74 assert.Equal(t, len(overflowElement.Options), 3)
75
76 }
77
78 func TestNewDatePickerBlockElement(t *testing.T) {
79
80 datepickerElement := NewDatePickerBlockElement("test")
81
82 assert.Equal(t, string(datepickerElement.Type), "datepicker")
83 assert.Equal(t, datepickerElement.ActionID, "test")
84
85 }
0 package slack
1
2 // ImageBlock defines data required to display an image as a block element
3 //
4 // More Information: https://api.slack.com/reference/messaging/blocks#image
5 type ImageBlock struct {
6 Type MessageBlockType `json:"type"`
7 ImageURL string `json:"image_url"`
8 AltText string `json:"alt_text"`
9 BlockID string `json:"block_id,omitempty"`
10 Title *TextBlockObject `json:"title"`
11 }
12
13 // BlockType returns the type of the block
14 func (s ImageBlock) BlockType() MessageBlockType {
15 return s.Type
16 }
17
18 // NewImageBlock returns an instance of a new Image Block type
19 func NewImageBlock(imageURL, altText, blockID string, title *TextBlockObject) *ImageBlock {
20 return &ImageBlock{
21 Type: MBTImage,
22 ImageURL: imageURL,
23 AltText: altText,
24 BlockID: blockID,
25 Title: title,
26 }
27 }
0 package slack
1
2 import (
3 "testing"
4
5 "github.com/stretchr/testify/assert"
6 )
7
8 func TestNewImageBlock(t *testing.T) {
9
10 imageText := NewTextBlockObject("plain_text", "Location", false, false)
11 imageBlock := NewImageBlock("https://api.slack.com/img/blocks/bkb_template_images/tripAgentLocationMarker.png", "Marker", "test", imageText)
12
13 assert.Equal(t, string(imageBlock.Type), "image")
14 assert.Equal(t, imageBlock.Title.Type, "plain_text")
15 assert.Equal(t, imageBlock.BlockID, "test")
16 assert.Contains(t, imageBlock.Title.Text, "Location")
17 assert.Contains(t, imageBlock.ImageURL, "tripAgentLocationMarker.png")
18
19 }
0 package slack
1
2 import (
3 "encoding/json"
4 )
5
6 // Block Objects are also known as Composition Objects
7 //
8 // For more information: https://api.slack.com/reference/messaging/composition-objects
9
10 // BlockObject defines an interface that all block object types should
11 // implement.
12 // @TODO: Is this interface needed?
13
14 // blockObject object types
15 const (
16 MarkdownType = "mrkdwn"
17 PlainTextType = "plain_text"
18 // The following objects don't actually have types and their corresponding
19 // const values are just for internal use
20 motConfirmation = "confirm"
21 motOption = "option"
22 motOptionGroup = "option_group"
23 )
24
25 type MessageObjectType string
26
27 type blockObject interface {
28 validateType() MessageObjectType
29 }
30
31 type BlockObjects struct {
32 TextObjects []*TextBlockObject
33 ConfirmationObjects []*ConfirmationBlockObject
34 OptionObjects []*OptionBlockObject
35 OptionGroupObjects []*OptionGroupBlockObject
36 }
37
38 // UnmarshalJSON implements the Unmarshaller interface for BlockObjects, so that any JSON
39 // unmarshalling is delegated and proper type determination can be made before unmarshal
40 func (b *BlockObjects) UnmarshalJSON(data []byte) error {
41 var raw []json.RawMessage
42 err := json.Unmarshal(data, &raw)
43 if err != nil {
44 return err
45 }
46
47 for _, r := range raw {
48 var obj map[string]interface{}
49 err := json.Unmarshal(r, &obj)
50 if err != nil {
51 return err
52 }
53
54 blockObjectType := getBlockObjectType(obj)
55
56 switch blockObjectType {
57 case PlainTextType, MarkdownType:
58 object, err := unmarshalBlockObject(r, &TextBlockObject{})
59 if err != nil {
60 return err
61 }
62 b.TextObjects = append(b.TextObjects, object.(*TextBlockObject))
63 case motConfirmation:
64 object, err := unmarshalBlockObject(r, &ConfirmationBlockObject{})
65 if err != nil {
66 return err
67 }
68 b.ConfirmationObjects = append(b.ConfirmationObjects, object.(*ConfirmationBlockObject))
69 case motOption:
70 object, err := unmarshalBlockObject(r, &OptionBlockObject{})
71 if err != nil {
72 return err
73 }
74 b.OptionObjects = append(b.OptionObjects, object.(*OptionBlockObject))
75 case motOptionGroup:
76 object, err := unmarshalBlockObject(r, &OptionGroupBlockObject{})
77 if err != nil {
78 return err
79 }
80 b.OptionGroupObjects = append(b.OptionGroupObjects, object.(*OptionGroupBlockObject))
81
82 }
83 }
84
85 return nil
86 }
87
88 // Ideally would have a better way to identify the block objects for
89 // type casting at time of unmarshalling, should be adapted if possible
90 // to accomplish in a more reliable manner.
91 func getBlockObjectType(obj map[string]interface{}) string {
92 if t, ok := obj["type"].(string); ok {
93 return t
94 }
95 if _, ok := obj["confirm"].(string); ok {
96 return "confirm"
97 }
98 if _, ok := obj["options"].(string); ok {
99 return "option_group"
100 }
101 if _, ok := obj["text"].(string); ok {
102 if _, ok := obj["value"].(string); ok {
103 return "option"
104 }
105 }
106 return ""
107 }
108
109 func unmarshalBlockObject(r json.RawMessage, object blockObject) (blockObject, error) {
110 err := json.Unmarshal(r, object)
111 if err != nil {
112 return nil, err
113 }
114 return object, nil
115 }
116
117 // TextBlockObject defines a text element object to be used with blocks
118 //
119 // More Information: https://api.slack.com/reference/messaging/composition-objects#text
120 type TextBlockObject struct {
121 Type string `json:"type"`
122 Text string `json:"text"`
123 Emoji bool `json:"emoji,omitempty"`
124 Verbatim bool `json:"verbatim,omitempty"`
125 }
126
127 // validateType enforces block objects for element and block parameters
128 func (s TextBlockObject) validateType() MessageObjectType {
129 return MessageObjectType(s.Type)
130 }
131
132 // validateType enforces block objects for element and block parameters
133 func (s TextBlockObject) MixedElementType() MixedElementType {
134 return MixedElementText
135 }
136
137 // NewTextBlockObject returns an instance of a new Text Block Object
138 func NewTextBlockObject(elementType, text string, emoji, verbatim bool) *TextBlockObject {
139 return &TextBlockObject{
140 Type: elementType,
141 Text: text,
142 Emoji: emoji,
143 Verbatim: verbatim,
144 }
145 }
146
147 // ConfirmationBlockObject defines a dialog that provides a confirmation step to
148 // any interactive element. This dialog will ask the user to confirm their action by
149 // offering a confirm and deny buttons.
150 //
151 // More Information: https://api.slack.com/reference/messaging/composition-objects#confirm
152 type ConfirmationBlockObject struct {
153 Title *TextBlockObject `json:"title"`
154 Text *TextBlockObject `json:"text"`
155 Confirm *TextBlockObject `json:"confirm"`
156 Deny *TextBlockObject `json:"deny"`
157 }
158
159 // validateType enforces block objects for element and block parameters
160 func (s ConfirmationBlockObject) validateType() MessageObjectType {
161 return motConfirmation
162 }
163
164 // NewConfirmationBlockObject returns an instance of a new Confirmation Block Object
165 func NewConfirmationBlockObject(title, text, confirm, deny *TextBlockObject) *ConfirmationBlockObject {
166 return &ConfirmationBlockObject{
167 Title: title,
168 Text: text,
169 Confirm: confirm,
170 Deny: deny,
171 }
172 }
173
174 // OptionBlockObject represents a single selectable item in a select menu
175 //
176 // More Information: https://api.slack.com/reference/messaging/composition-objects#option
177 type OptionBlockObject struct {
178 Text *TextBlockObject `json:"text"`
179 Value string `json:"value"`
180 URL string `json:"url"`
181 }
182
183 // NewOptionBlockObject returns an instance of a new Option Block Element
184 func NewOptionBlockObject(value string, text *TextBlockObject) *OptionBlockObject {
185 return &OptionBlockObject{
186 Text: text,
187 Value: value,
188 }
189 }
190
191 // validateType enforces block objects for element and block parameters
192 func (s OptionBlockObject) validateType() MessageObjectType {
193 return motOption
194 }
195
196 // OptionGroupBlockObject Provides a way to group options in a select menu.
197 //
198 // More Information: https://api.slack.com/reference/messaging/composition-objects#option-group
199 type OptionGroupBlockObject struct {
200 Label *TextBlockObject `json:"label,omitempty"`
201 Options []*OptionBlockObject `json:"options"`
202 }
203
204 // validateType enforces block objects for element and block parameters
205 func (s OptionGroupBlockObject) validateType() MessageObjectType {
206 return motOptionGroup
207 }
208
209 // NewOptionGroupBlockElement returns an instance of a new option group block element
210 func NewOptionGroupBlockElement(label *TextBlockObject, options ...*OptionBlockObject) *OptionGroupBlockObject {
211 return &OptionGroupBlockObject{
212 Label: label,
213 Options: options,
214 }
215 }
0 package slack
1
2 import (
3 "testing"
4
5 "github.com/stretchr/testify/assert"
6 )
7
8 func TestNewImageBlockObject(t *testing.T) {
9
10 imageObject := NewImageBlockElement("https://api.slack.com/img/blocks/bkb_template_images/beagle.png", "Beagle")
11
12 assert.Equal(t, string(imageObject.Type), "image")
13 assert.Equal(t, imageObject.AltText, "Beagle")
14 assert.Contains(t, imageObject.ImageURL, "beagle.png")
15
16 }
17
18 func TestNewTextBlockObject(t *testing.T) {
19
20 textObject := NewTextBlockObject("plain_text", "test", true, false)
21
22 assert.Equal(t, textObject.Type, "plain_text")
23 assert.Equal(t, textObject.Text, "test")
24 assert.True(t, textObject.Emoji, "Emoji property should be true")
25 assert.False(t, textObject.Verbatim, "Verbatim should be false")
26
27 }
28
29 func TestNewConfirmationBlockObject(t *testing.T) {
30
31 titleObj := NewTextBlockObject("plain_text", "testTitle", false, false)
32 textObj := NewTextBlockObject("plain_text", "testText", false, false)
33 confirmObj := NewTextBlockObject("plain_text", "testConfirm", false, false)
34
35 confirmation := NewConfirmationBlockObject(titleObj, textObj, confirmObj, nil)
36
37 assert.Equal(t, confirmation.Title.Text, "testTitle")
38 assert.Equal(t, confirmation.Text.Text, "testText")
39 assert.Equal(t, confirmation.Confirm.Text, "testConfirm")
40 assert.Nil(t, confirmation.Deny, "Deny should be nil")
41
42 }
43
44 func TestNewOptionBlockObject(t *testing.T) {
45
46 valTextObj := NewTextBlockObject("plain_text", "testText", false, false)
47 optObj := NewOptionBlockObject("testOpt", valTextObj)
48
49 assert.Equal(t, optObj.Text.Text, "testText")
50 assert.Equal(t, optObj.Value, "testOpt")
51
52 }
53
54 func TestNewOptionGroupBlockElement(t *testing.T) {
55
56 labelObj := NewTextBlockObject("plain_text", "testLabel", false, false)
57 valTextObj := NewTextBlockObject("plain_text", "testText", false, false)
58 optObj := NewOptionBlockObject("testOpt", valTextObj)
59
60 optGroup := NewOptionGroupBlockElement(labelObj, optObj)
61
62 assert.Equal(t, optGroup.Label.Text, "testLabel")
63 assert.Len(t, optGroup.Options, 1, "Options should contain one element")
64
65 }
0 package slack
1
2 // SectionBlock defines a new block of type section
3 //
4 // More Information: https://api.slack.com/reference/messaging/blocks#section
5 type SectionBlock struct {
6 Type MessageBlockType `json:"type"`
7 Text *TextBlockObject `json:"text,omitempty"`
8 BlockID string `json:"block_id,omitempty"`
9 Fields []*TextBlockObject `json:"fields,omitempty"`
10 Accessory *Accessory `json:"accessory,omitempty"`
11 }
12
13 // BlockType returns the type of the block
14 func (s SectionBlock) BlockType() MessageBlockType {
15 return s.Type
16 }
17
18 // SectionBlockOption allows configuration of options for a new section block
19 type SectionBlockOption func(*SectionBlock)
20
21 func SectionBlockOptionBlockID(blockID string) SectionBlockOption {
22 return func(block *SectionBlock) {
23 block.BlockID = blockID
24 }
25 }
26
27 // NewSectionBlock returns a new instance of a section block to be rendered
28 func NewSectionBlock(textObj *TextBlockObject, fields []*TextBlockObject, accessory *Accessory, options ...SectionBlockOption) *SectionBlock {
29 block := SectionBlock{
30 Type: MBTSection,
31 Text: textObj,
32 Fields: fields,
33 Accessory: accessory,
34 }
35
36 for _, option := range options {
37 option(&block)
38 }
39
40 return &block
41 }
0 package slack
1
2 import (
3 "testing"
4
5 "github.com/stretchr/testify/assert"
6 )
7
8 func TestNewSectionBlock(t *testing.T) {
9
10 textInfo := NewTextBlockObject("mrkdwn", "*<fakeLink.toHotelPage.com|The Ritz-Carlton New Orleans>*\n★★★★★\n$340 per night\nRated: 9.1 - Excellent", false, false)
11
12 sectionBlock := NewSectionBlock(textInfo, nil, nil, SectionBlockOptionBlockID("test_block"))
13 assert.Equal(t, string(sectionBlock.Type), "section")
14 assert.Equal(t, string(sectionBlock.BlockID), "test_block")
15 assert.Equal(t, len(sectionBlock.Fields), 0)
16 assert.Nil(t, sectionBlock.Accessory)
17 assert.Equal(t, sectionBlock.Text.Type, "mrkdwn")
18 assert.Contains(t, sectionBlock.Text.Text, "New Orleans")
19
20 }
21
22 func TestNewBlockSectionContainsAddedTextBlockAndAccessory(t *testing.T) {
23 textBlockObject := NewTextBlockObject("mrkdwn", "You have a new test: *Hi there* :wave:", true, false)
24 conflictImage := NewImageBlockElement("https://api.slack.com/img/blocks/bkb_template_images/notificationsWarningIcon.png", "notifications warning icon")
25 sectionBlock := NewSectionBlock(textBlockObject, nil, NewAccessory(conflictImage))
26
27 assert.Equal(t, sectionBlock.BlockType(), MBTSection)
28 assert.Equal(t, len(sectionBlock.BlockID), 0)
29 textBlockInSection := sectionBlock.Text
30 assert.Equal(t, textBlockInSection.Text, textBlockObject.Text)
31 assert.Equal(t, textBlockInSection.Type, textBlockObject.Type)
32 assert.True(t, textBlockInSection.Emoji)
33 assert.False(t, textBlockInSection.Verbatim)
34 assert.Equal(t, sectionBlock.Accessory.ImageElement, conflictImage)
35 }
0 package slack
1
2 import (
3 "testing"
4
5 "github.com/stretchr/testify/assert"
6 )
7
8 func TestNewBlockMessage(t *testing.T) {
9
10 dividerBlock := NewDividerBlock()
11 blockMessage := NewBlockMessage(dividerBlock)
12
13 assert.Equal(t, len(blockMessage.Msg.Blocks.BlockSet), 1)
14
15 }
00 package slack
11
22 import (
3 "errors"
3 "context"
44 "net/url"
55 )
66
1717 SlackResponse
1818 }
1919
20 func botRequest(path string, values url.Values, debug bool) (*botResponseFull, error) {
20 func (api *Client) botRequest(ctx context.Context, path string, values url.Values) (*botResponseFull, error) {
2121 response := &botResponseFull{}
22 err := post(path, values, response, debug)
22 err := api.postMethod(ctx, path, values, response)
2323 if err != nil {
2424 return nil, err
2525 }
26 if !response.Ok {
27 return nil, errors.New(response.Error)
26
27 if err := response.Err(); err != nil {
28 return nil, err
2829 }
30
2931 return response, nil
3032 }
3133
3234 // GetBotInfo will retrieve the complete bot information
3335 func (api *Client) GetBotInfo(bot string) (*Bot, error) {
36 return api.GetBotInfoContext(context.Background(), bot)
37 }
38
39 // GetBotInfoContext will retrieve the complete bot information using a custom context
40 func (api *Client) GetBotInfoContext(ctx context.Context, bot string) (*Bot, error) {
3441 values := url.Values{
35 "token": {api.config.token},
36 "bot": {bot},
42 "token": {api.token},
3743 }
38 response, err := botRequest("bots.info", values, api.debug)
44
45 if bot != "" {
46 values.Add("bot", bot)
47 }
48
49 response, err := api.botRequest(ctx, "bots.info", values)
3950 if err != nil {
4051 return nil, err
4152 }
2323 http.HandleFunc("/bots.info", getBotInfo)
2424
2525 once.Do(startServer)
26 SLACK_API = "http://" + serverAddr + "/"
27 api := New("testing-token")
26 api := New("testing-token", OptionAPIURL("http://"+serverAddr+"/"))
2827
2928 bot, err := api.GetBotInfo("B02875YLA")
3029 if err != nil {
00 package slack
11
22 import (
3 "errors"
3 "context"
44 "net/url"
55 "strconv"
66 )
1717
1818 // Channel contains information about the channel
1919 type Channel struct {
20 groupConversation
21 IsChannel bool `json:"is_channel"`
22 IsGeneral bool `json:"is_general"`
23 IsMember bool `json:"is_member"`
24 }
25
26 func channelRequest(path string, values url.Values, debug bool) (*channelResponseFull, error) {
20 GroupConversation
21 IsChannel bool `json:"is_channel"`
22 IsGeneral bool `json:"is_general"`
23 IsMember bool `json:"is_member"`
24 Locale string `json:"locale"`
25 }
26
27 func (api *Client) channelRequest(ctx context.Context, path string, values url.Values) (*channelResponseFull, error) {
2728 response := &channelResponseFull{}
28 err := post(path, values, response, debug)
29 if err != nil {
30 return nil, err
31 }
32 if !response.Ok {
33 return nil, errors.New(response.Error)
34 }
35 return response, nil
29 err := postForm(ctx, api.httpclient, api.endpoint+path, values, response, api)
30 if err != nil {
31 return nil, err
32 }
33
34 return response, response.Err()
35 }
36
37 type channelsConfig struct {
38 values url.Values
39 }
40
41 // GetChannelsOption option provided when getting channels.
42 type GetChannelsOption func(*channelsConfig) error
43
44 // GetChannelsOptionExcludeMembers excludes the members collection from each channel.
45 func GetChannelsOptionExcludeMembers() GetChannelsOption {
46 return func(config *channelsConfig) error {
47 config.values.Add("exclude_members", "true")
48 return nil
49 }
50 }
51
52 // GetChannelsOptionExcludeArchived excludes archived channels from results.
53 func GetChannelsOptionExcludeArchived() GetChannelsOption {
54 return func(config *channelsConfig) error {
55 config.values.Add("exclude_archived", "true")
56 return nil
57 }
3658 }
3759
3860 // ArchiveChannel archives the given channel
39 func (api *Client) ArchiveChannel(channel string) error {
40 values := url.Values{
41 "token": {api.config.token},
42 "channel": {channel},
43 }
44 _, err := channelRequest("channels.archive", values, api.debug)
45 if err != nil {
46 return err
47 }
48 return nil
61 // see https://api.slack.com/methods/channels.archive
62 func (api *Client) ArchiveChannel(channelID string) error {
63 return api.ArchiveChannelContext(context.Background(), channelID)
64 }
65
66 // ArchiveChannelContext archives the given channel with a custom context
67 // see https://api.slack.com/methods/channels.archive
68 func (api *Client) ArchiveChannelContext(ctx context.Context, channelID string) (err error) {
69 values := url.Values{
70 "token": {api.token},
71 "channel": {channelID},
72 }
73
74 _, err = api.channelRequest(ctx, "channels.archive", values)
75 return err
4976 }
5077
5178 // UnarchiveChannel unarchives the given channel
52 func (api *Client) UnarchiveChannel(channel string) error {
53 values := url.Values{
54 "token": {api.config.token},
55 "channel": {channel},
56 }
57 _, err := channelRequest("channels.unarchive", values, api.debug)
58 if err != nil {
59 return err
60 }
61 return nil
79 // see https://api.slack.com/methods/channels.unarchive
80 func (api *Client) UnarchiveChannel(channelID string) error {
81 return api.UnarchiveChannelContext(context.Background(), channelID)
82 }
83
84 // UnarchiveChannelContext unarchives the given channel with a custom context
85 // see https://api.slack.com/methods/channels.unarchive
86 func (api *Client) UnarchiveChannelContext(ctx context.Context, channelID string) (err error) {
87 values := url.Values{
88 "token": {api.token},
89 "channel": {channelID},
90 }
91
92 _, err = api.channelRequest(ctx, "channels.unarchive", values)
93 return err
6294 }
6395
6496 // CreateChannel creates a channel with the given name and returns a *Channel
65 func (api *Client) CreateChannel(channel string) (*Channel, error) {
66 values := url.Values{
67 "token": {api.config.token},
68 "name": {channel},
69 }
70 response, err := channelRequest("channels.create", values, api.debug)
97 // see https://api.slack.com/methods/channels.create
98 func (api *Client) CreateChannel(channelName string) (*Channel, error) {
99 return api.CreateChannelContext(context.Background(), channelName)
100 }
101
102 // CreateChannelContext creates a channel with the given name and returns a *Channel with a custom context
103 // see https://api.slack.com/methods/channels.create
104 func (api *Client) CreateChannelContext(ctx context.Context, channelName string) (*Channel, error) {
105 values := url.Values{
106 "token": {api.token},
107 "name": {channelName},
108 }
109
110 response, err := api.channelRequest(ctx, "channels.create", values)
71111 if err != nil {
72112 return nil, err
73113 }
75115 }
76116
77117 // GetChannelHistory retrieves the channel history
78 func (api *Client) GetChannelHistory(channel string, params HistoryParameters) (*History, error) {
79 values := url.Values{
80 "token": {api.config.token},
81 "channel": {channel},
118 // see https://api.slack.com/methods/channels.history
119 func (api *Client) GetChannelHistory(channelID string, params HistoryParameters) (*History, error) {
120 return api.GetChannelHistoryContext(context.Background(), channelID, params)
121 }
122
123 // GetChannelHistoryContext retrieves the channel history with a custom context
124 // see https://api.slack.com/methods/channels.history
125 func (api *Client) GetChannelHistoryContext(ctx context.Context, channelID string, params HistoryParameters) (*History, error) {
126 values := url.Values{
127 "token": {api.token},
128 "channel": {channelID},
82129 }
83130 if params.Latest != DEFAULT_HISTORY_LATEST {
84131 values.Add("latest", params.Latest)
96143 values.Add("inclusive", "0")
97144 }
98145 }
146
99147 if params.Unreads != DEFAULT_HISTORY_UNREADS {
100148 if params.Unreads {
101149 values.Add("unreads", "1")
103151 values.Add("unreads", "0")
104152 }
105153 }
106 response, err := channelRequest("channels.history", values, api.debug)
154
155 response, err := api.channelRequest(ctx, "channels.history", values)
107156 if err != nil {
108157 return nil, err
109158 }
111160 }
112161
113162 // GetChannelInfo retrieves the given channel
114 func (api *Client) GetChannelInfo(channel string) (*Channel, error) {
115 values := url.Values{
116 "token": {api.config.token},
117 "channel": {channel},
118 }
119 response, err := channelRequest("channels.info", values, api.debug)
163 // see https://api.slack.com/methods/channels.info
164 func (api *Client) GetChannelInfo(channelID string) (*Channel, error) {
165 return api.GetChannelInfoContext(context.Background(), channelID)
166 }
167
168 // GetChannelInfoContext retrieves the given channel with a custom context
169 // see https://api.slack.com/methods/channels.info
170 func (api *Client) GetChannelInfoContext(ctx context.Context, channelID string) (*Channel, error) {
171 values := url.Values{
172 "token": {api.token},
173 "channel": {channelID},
174 "include_locale": {strconv.FormatBool(true)},
175 }
176
177 response, err := api.channelRequest(ctx, "channels.info", values)
120178 if err != nil {
121179 return nil, err
122180 }
124182 }
125183
126184 // InviteUserToChannel invites a user to a given channel and returns a *Channel
127 func (api *Client) InviteUserToChannel(channel, user string) (*Channel, error) {
128 values := url.Values{
129 "token": {api.config.token},
130 "channel": {channel},
185 // see https://api.slack.com/methods/channels.invite
186 func (api *Client) InviteUserToChannel(channelID, user string) (*Channel, error) {
187 return api.InviteUserToChannelContext(context.Background(), channelID, user)
188 }
189
190 // InviteUserToChannelContext invites a user to a given channel and returns a *Channel with a custom context
191 // see https://api.slack.com/methods/channels.invite
192 func (api *Client) InviteUserToChannelContext(ctx context.Context, channelID, user string) (*Channel, error) {
193 values := url.Values{
194 "token": {api.token},
195 "channel": {channelID},
131196 "user": {user},
132197 }
133 response, err := channelRequest("channels.invite", values, api.debug)
198
199 response, err := api.channelRequest(ctx, "channels.invite", values)
134200 if err != nil {
135201 return nil, err
136202 }
138204 }
139205
140206 // JoinChannel joins the currently authenticated user to a channel
141 func (api *Client) JoinChannel(channel string) (*Channel, error) {
142 values := url.Values{
143 "token": {api.config.token},
144 "name": {channel},
145 }
146 response, err := channelRequest("channels.join", values, api.debug)
207 // see https://api.slack.com/methods/channels.join
208 func (api *Client) JoinChannel(channelName string) (*Channel, error) {
209 return api.JoinChannelContext(context.Background(), channelName)
210 }
211
212 // JoinChannelContext joins the currently authenticated user to a channel with a custom context
213 // see https://api.slack.com/methods/channels.join
214 func (api *Client) JoinChannelContext(ctx context.Context, channelName string) (*Channel, error) {
215 values := url.Values{
216 "token": {api.token},
217 "name": {channelName},
218 }
219
220 response, err := api.channelRequest(ctx, "channels.join", values)
147221 if err != nil {
148222 return nil, err
149223 }
151225 }
152226
153227 // LeaveChannel makes the authenticated user leave the given channel
154 func (api *Client) LeaveChannel(channel string) (bool, error) {
155 values := url.Values{
156 "token": {api.config.token},
157 "channel": {channel},
158 }
159 response, err := channelRequest("channels.leave", values, api.debug)
228 // see https://api.slack.com/methods/channels.leave
229 func (api *Client) LeaveChannel(channelID string) (bool, error) {
230 return api.LeaveChannelContext(context.Background(), channelID)
231 }
232
233 // LeaveChannelContext makes the authenticated user leave the given channel with a custom context
234 // see https://api.slack.com/methods/channels.leave
235 func (api *Client) LeaveChannelContext(ctx context.Context, channelID string) (bool, error) {
236 values := url.Values{
237 "token": {api.token},
238 "channel": {channelID},
239 }
240
241 response, err := api.channelRequest(ctx, "channels.leave", values)
160242 if err != nil {
161243 return false, err
162244 }
163 if response.NotInChannel {
164 return response.NotInChannel, nil
165 }
166 return false, nil
245
246 return response.NotInChannel, nil
167247 }
168248
169249 // KickUserFromChannel kicks a user from a given channel
170 func (api *Client) KickUserFromChannel(channel, user string) error {
171 values := url.Values{
172 "token": {api.config.token},
173 "channel": {channel},
250 // see https://api.slack.com/methods/channels.kick
251 func (api *Client) KickUserFromChannel(channelID, user string) error {
252 return api.KickUserFromChannelContext(context.Background(), channelID, user)
253 }
254
255 // KickUserFromChannelContext kicks a user from a given channel with a custom context
256 // see https://api.slack.com/methods/channels.kick
257 func (api *Client) KickUserFromChannelContext(ctx context.Context, channelID, user string) (err error) {
258 values := url.Values{
259 "token": {api.token},
260 "channel": {channelID},
174261 "user": {user},
175262 }
176 _, err := channelRequest("channels.kick", values, api.debug)
177 if err != nil {
178 return err
179 }
180 return nil
263
264 _, err = api.channelRequest(ctx, "channels.kick", values)
265 return err
181266 }
182267
183268 // GetChannels retrieves all the channels
184 func (api *Client) GetChannels(excludeArchived bool) ([]Channel, error) {
185 values := url.Values{
186 "token": {api.config.token},
187 }
269 // see https://api.slack.com/methods/channels.list
270 func (api *Client) GetChannels(excludeArchived bool, options ...GetChannelsOption) ([]Channel, error) {
271 return api.GetChannelsContext(context.Background(), excludeArchived, options...)
272 }
273
274 // GetChannelsContext retrieves all the channels with a custom context
275 // see https://api.slack.com/methods/channels.list
276 func (api *Client) GetChannelsContext(ctx context.Context, excludeArchived bool, options ...GetChannelsOption) ([]Channel, error) {
277 config := channelsConfig{
278 values: url.Values{
279 "token": {api.token},
280 },
281 }
282
188283 if excludeArchived {
189 values.Add("exclude_archived", "1")
190 }
191 response, err := channelRequest("channels.list", values, api.debug)
284 options = append(options, GetChannelsOptionExcludeArchived())
285 }
286
287 for _, opt := range options {
288 if err := opt(&config); err != nil {
289 return nil, err
290 }
291 }
292
293 response, err := api.channelRequest(ctx, "channels.list", config.values)
192294 if err != nil {
193295 return nil, err
194296 }
200302 // timer before making the call. In this way, any further updates needed during the timeout will not generate extra calls
201303 // (just one per channel). This is useful for when reading scroll-back history, or following a busy live channel. A
202304 // timeout of 5 seconds is a good starting point. Be sure to flush these calls on shutdown/logout.
203 func (api *Client) SetChannelReadMark(channel, ts string) error {
204 values := url.Values{
205 "token": {api.config.token},
206 "channel": {channel},
305 // see https://api.slack.com/methods/channels.mark
306 func (api *Client) SetChannelReadMark(channelID, ts string) error {
307 return api.SetChannelReadMarkContext(context.Background(), channelID, ts)
308 }
309
310 // SetChannelReadMarkContext sets the read mark of a given channel to a specific point with a custom context
311 // For more details see SetChannelReadMark documentation
312 // see https://api.slack.com/methods/channels.mark
313 func (api *Client) SetChannelReadMarkContext(ctx context.Context, channelID, ts string) (err error) {
314 values := url.Values{
315 "token": {api.token},
316 "channel": {channelID},
207317 "ts": {ts},
208318 }
209 _, err := channelRequest("channels.mark", values, api.debug)
210 if err != nil {
211 return err
212 }
213 return nil
319
320 _, err = api.channelRequest(ctx, "channels.mark", values)
321 return err
214322 }
215323
216324 // RenameChannel renames a given channel
217 func (api *Client) RenameChannel(channel, name string) (*Channel, error) {
218 values := url.Values{
219 "token": {api.config.token},
220 "channel": {channel},
325 // see https://api.slack.com/methods/channels.rename
326 func (api *Client) RenameChannel(channelID, name string) (*Channel, error) {
327 return api.RenameChannelContext(context.Background(), channelID, name)
328 }
329
330 // RenameChannelContext renames a given channel with a custom context
331 // see https://api.slack.com/methods/channels.rename
332 func (api *Client) RenameChannelContext(ctx context.Context, channelID, name string) (*Channel, error) {
333 values := url.Values{
334 "token": {api.token},
335 "channel": {channelID},
221336 "name": {name},
222337 }
338
223339 // XXX: the created entry in this call returns a string instead of a number
224340 // so I may have to do some workaround to solve it.
225 response, err := channelRequest("channels.rename", values, api.debug)
341 response, err := api.channelRequest(ctx, "channels.rename", values)
226342 if err != nil {
227343 return nil, err
228344 }
229345 return &response.Channel, nil
230
231 }
232
233 // SetChannelPurpose sets the channel purpose and returns the purpose that was
234 // successfully set
235 func (api *Client) SetChannelPurpose(channel, purpose string) (string, error) {
236 values := url.Values{
237 "token": {api.config.token},
238 "channel": {channel},
346 }
347
348 // SetChannelPurpose sets the channel purpose and returns the purpose that was successfully set
349 // see https://api.slack.com/methods/channels.setPurpose
350 func (api *Client) SetChannelPurpose(channelID, purpose string) (string, error) {
351 return api.SetChannelPurposeContext(context.Background(), channelID, purpose)
352 }
353
354 // SetChannelPurposeContext sets the channel purpose and returns the purpose that was successfully set with a custom context
355 // see https://api.slack.com/methods/channels.setPurpose
356 func (api *Client) SetChannelPurposeContext(ctx context.Context, channelID, purpose string) (string, error) {
357 values := url.Values{
358 "token": {api.token},
359 "channel": {channelID},
239360 "purpose": {purpose},
240361 }
241 response, err := channelRequest("channels.setPurpose", values, api.debug)
362
363 response, err := api.channelRequest(ctx, "channels.setPurpose", values)
242364 if err != nil {
243365 return "", err
244366 }
246368 }
247369
248370 // SetChannelTopic sets the channel topic and returns the topic that was successfully set
249 func (api *Client) SetChannelTopic(channel, topic string) (string, error) {
250 values := url.Values{
251 "token": {api.config.token},
252 "channel": {channel},
371 // see https://api.slack.com/methods/channels.setTopic
372 func (api *Client) SetChannelTopic(channelID, topic string) (string, error) {
373 return api.SetChannelTopicContext(context.Background(), channelID, topic)
374 }
375
376 // SetChannelTopicContext sets the channel topic and returns the topic that was successfully set with a custom context
377 // see https://api.slack.com/methods/channels.setTopic
378 func (api *Client) SetChannelTopicContext(ctx context.Context, channelID, topic string) (string, error) {
379 values := url.Values{
380 "token": {api.token},
381 "channel": {channelID},
253382 "topic": {topic},
254383 }
255 response, err := channelRequest("channels.setTopic", values, api.debug)
384
385 response, err := api.channelRequest(ctx, "channels.setTopic", values)
256386 if err != nil {
257387 return "", err
258388 }
260390 }
261391
262392 // GetChannelReplies gets an entire thread (a message plus all the messages in reply to it).
263 func (api *Client) GetChannelReplies(channel, thread_ts string) ([]Message, error) {
264 values := url.Values{
265 "token": {api.config.token},
266 "channel": {channel},
393 // see https://api.slack.com/methods/channels.replies
394 func (api *Client) GetChannelReplies(channelID, thread_ts string) ([]Message, error) {
395 return api.GetChannelRepliesContext(context.Background(), channelID, thread_ts)
396 }
397
398 // GetChannelRepliesContext gets an entire thread (a message plus all the messages in reply to it) with a custom context
399 // see https://api.slack.com/methods/channels.replies
400 func (api *Client) GetChannelRepliesContext(ctx context.Context, channelID, thread_ts string) ([]Message, error) {
401 values := url.Values{
402 "token": {api.token},
403 "channel": {channelID},
267404 "thread_ts": {thread_ts},
268405 }
269 response, err := channelRequest("channels.replies", values, api.debug)
406 response, err := api.channelRequest(ctx, "channels.replies", values)
270407 if err != nil {
271408 return nil, err
272409 }
00 package slack
11
22 import (
3 "context"
34 "encoding/json"
4 "errors"
5 "net/http"
56 "net/url"
6 "strings"
7
8 "github.com/nlopes/slack/slackutilsx"
79 )
810
911 const (
1012 DEFAULT_MESSAGE_USERNAME = ""
11 DEFAULT_MESSAGE_THREAD_TIMESTAMP = ""
13 DEFAULT_MESSAGE_REPLY_BROADCAST = false
1214 DEFAULT_MESSAGE_ASUSER = false
1315 DEFAULT_MESSAGE_PARSE = ""
16 DEFAULT_MESSAGE_THREAD_TIMESTAMP = ""
1417 DEFAULT_MESSAGE_LINK_NAMES = 0
1518 DEFAULT_MESSAGE_UNFURL_LINKS = false
1619 DEFAULT_MESSAGE_UNFURL_MEDIA = true
2124 )
2225
2326 type chatResponseFull struct {
24 Channel string `json:"channel"`
25 Timestamp string `json:"ts"`
26 Text string `json:"text"`
27 Channel string `json:"channel"`
28 Timestamp string `json:"ts"` //Regular message timestamp
29 MessageTimeStamp string `json:"message_ts"` //Ephemeral message timestamp
30 Text string `json:"text"`
2731 SlackResponse
32 }
33
34 // getMessageTimestamp will inspect the `chatResponseFull` to ruturn a timestamp value
35 // in `chat.postMessage` its under `ts`
36 // in `chat.postEphemeral` its under `message_ts`
37 func (c chatResponseFull) getMessageTimestamp() string {
38 if len(c.Timestamp) > 0 {
39 return c.Timestamp
40 }
41 return c.MessageTimeStamp
2842 }
2943
3044 // PostMessageParameters contains all the parameters necessary (including the optional ones) for a PostMessage() request
3145 type PostMessageParameters struct {
32 Text string `json:"text"`
33 Username string `json:"user_name"`
34 AsUser bool `json:"as_user"`
35 Parse string `json:"parse"`
36 ThreadTimestamp string `json:"thread_ts"`
37 LinkNames int `json:"link_names"`
38 Attachments []Attachment `json:"attachments"`
39 UnfurlLinks bool `json:"unfurl_links"`
40 UnfurlMedia bool `json:"unfurl_media"`
41 IconURL string `json:"icon_url"`
42 IconEmoji string `json:"icon_emoji"`
43 Markdown bool `json:"mrkdwn,omitempty"`
44 EscapeText bool `json:"escape_text"`
46 Username string `json:"username"`
47 AsUser bool `json:"as_user"`
48 Parse string `json:"parse"`
49 ThreadTimestamp string `json:"thread_ts"`
50 ReplyBroadcast bool `json:"reply_broadcast"`
51 LinkNames int `json:"link_names"`
52 UnfurlLinks bool `json:"unfurl_links"`
53 UnfurlMedia bool `json:"unfurl_media"`
54 IconURL string `json:"icon_url"`
55 IconEmoji string `json:"icon_emoji"`
56 Markdown bool `json:"mrkdwn,omitempty"`
57 EscapeText bool `json:"escape_text"`
58
59 // chat.postEphemeral support
60 Channel string `json:"channel"`
61 User string `json:"user"`
4562 }
4663
4764 // NewPostMessageParameters provides an instance of PostMessageParameters with all the sane default values set
4865 func NewPostMessageParameters() PostMessageParameters {
4966 return PostMessageParameters{
50 Username: DEFAULT_MESSAGE_USERNAME,
51 AsUser: DEFAULT_MESSAGE_ASUSER,
52 Parse: DEFAULT_MESSAGE_PARSE,
53 LinkNames: DEFAULT_MESSAGE_LINK_NAMES,
54 Attachments: nil,
55 UnfurlLinks: DEFAULT_MESSAGE_UNFURL_LINKS,
56 UnfurlMedia: DEFAULT_MESSAGE_UNFURL_MEDIA,
57 IconURL: DEFAULT_MESSAGE_ICON_URL,
58 IconEmoji: DEFAULT_MESSAGE_ICON_EMOJI,
59 Markdown: DEFAULT_MESSAGE_MARKDOWN,
60 EscapeText: DEFAULT_MESSAGE_ESCAPE_TEXT,
67 Username: DEFAULT_MESSAGE_USERNAME,
68 User: DEFAULT_MESSAGE_USERNAME,
69 AsUser: DEFAULT_MESSAGE_ASUSER,
70 Parse: DEFAULT_MESSAGE_PARSE,
71 ThreadTimestamp: DEFAULT_MESSAGE_THREAD_TIMESTAMP,
72 LinkNames: DEFAULT_MESSAGE_LINK_NAMES,
73 UnfurlLinks: DEFAULT_MESSAGE_UNFURL_LINKS,
74 UnfurlMedia: DEFAULT_MESSAGE_UNFURL_MEDIA,
75 IconURL: DEFAULT_MESSAGE_ICON_URL,
76 IconEmoji: DEFAULT_MESSAGE_ICON_EMOJI,
77 Markdown: DEFAULT_MESSAGE_MARKDOWN,
78 EscapeText: DEFAULT_MESSAGE_ESCAPE_TEXT,
6179 }
6280 }
6381
6482 // DeleteMessage deletes a message in a channel
6583 func (api *Client) DeleteMessage(channel, messageTimestamp string) (string, string, error) {
66 respChannel, respTimestamp, _, err := api.SendMessage(channel, MsgOptionDelete(messageTimestamp))
84 respChannel, respTimestamp, _, err := api.SendMessageContext(context.Background(), channel, MsgOptionDelete(messageTimestamp))
85 return respChannel, respTimestamp, err
86 }
87
88 // DeleteMessageContext deletes a message in a channel with a custom context
89 func (api *Client) DeleteMessageContext(ctx context.Context, channel, messageTimestamp string) (string, string, error) {
90 respChannel, respTimestamp, _, err := api.SendMessageContext(ctx, channel, MsgOptionDelete(messageTimestamp))
6791 return respChannel, respTimestamp, err
6892 }
6993
7094 // PostMessage sends a message to a channel.
7195 // Message is escaped by default according to https://api.slack.com/docs/formatting
7296 // Use http://davestevens.github.io/slack-message-builder/ to help crafting your message.
73 func (api *Client) PostMessage(channel, text string, params PostMessageParameters) (string, string, error) {
74 respChannel, respTimestamp, _, err := api.SendMessage(
75 channel,
76 MsgOptionText(text, params.EscapeText),
77 MsgOptionAttachments(params.Attachments...),
78 MsgOptionPostMessageParameters(params),
97 func (api *Client) PostMessage(channelID string, options ...MsgOption) (string, string, error) {
98 respChannel, respTimestamp, _, err := api.SendMessageContext(
99 context.Background(),
100 channelID,
101 MsgOptionPost(),
102 MsgOptionCompose(options...),
79103 )
80104 return respChannel, respTimestamp, err
81105 }
82106
107 // PostMessageContext sends a message to a channel with a custom context
108 // For more details, see PostMessage documentation.
109 func (api *Client) PostMessageContext(ctx context.Context, channelID string, options ...MsgOption) (string, string, error) {
110 respChannel, respTimestamp, _, err := api.SendMessageContext(
111 ctx,
112 channelID,
113 MsgOptionPost(),
114 MsgOptionCompose(options...),
115 )
116 return respChannel, respTimestamp, err
117 }
118
119 // PostEphemeral sends an ephemeral message to a user in a channel.
120 // Message is escaped by default according to https://api.slack.com/docs/formatting
121 // Use http://davestevens.github.io/slack-message-builder/ to help crafting your message.
122 func (api *Client) PostEphemeral(channelID, userID string, options ...MsgOption) (string, error) {
123 return api.PostEphemeralContext(
124 context.Background(),
125 channelID,
126 userID,
127 options...,
128 )
129 }
130
131 // PostEphemeralContext sends an ephemeal message to a user in a channel with a custom context
132 // For more details, see PostEphemeral documentation
133 func (api *Client) PostEphemeralContext(ctx context.Context, channelID, userID string, options ...MsgOption) (timestamp string, err error) {
134 _, timestamp, _, err = api.SendMessageContext(ctx, channelID, MsgOptionPostEphemeral(userID), MsgOptionCompose(options...))
135 return timestamp, err
136 }
137
83138 // UpdateMessage updates a message in a channel
84 func (api *Client) UpdateMessage(channel, timestamp, text string) (string, string, string, error) {
85 return api.SendMessage(channel, MsgOptionUpdate(timestamp), MsgOptionText(text, true))
139 func (api *Client) UpdateMessage(channelID, timestamp string, options ...MsgOption) (string, string, string, error) {
140 return api.SendMessageContext(context.Background(), channelID, MsgOptionUpdate(timestamp), MsgOptionCompose(options...))
141 }
142
143 // UpdateMessageContext updates a message in a channel
144 func (api *Client) UpdateMessageContext(ctx context.Context, channelID, timestamp string, options ...MsgOption) (string, string, string, error) {
145 return api.SendMessageContext(ctx, channelID, MsgOptionUpdate(timestamp), MsgOptionCompose(options...))
146 }
147
148 // UnfurlMessage unfurls a message in a channel
149 func (api *Client) UnfurlMessage(channelID, timestamp string, unfurls map[string]Attachment, options ...MsgOption) (string, string, string, error) {
150 return api.SendMessageContext(context.Background(), channelID, MsgOptionUnfurl(timestamp, unfurls), MsgOptionCompose(options...))
86151 }
87152
88153 // SendMessage more flexible method for configuring messages.
89154 func (api *Client) SendMessage(channel string, options ...MsgOption) (string, string, string, error) {
90 channel, values, err := ApplyMsgOptions(api.config.token, channel, options...)
91 if err != nil {
155 return api.SendMessageContext(context.Background(), channel, options...)
156 }
157
158 // SendMessageContext more flexible method for configuring messages with a custom context.
159 func (api *Client) SendMessageContext(ctx context.Context, channelID string, options ...MsgOption) (_channel string, _timestamp string, _text string, err error) {
160 var (
161 req *http.Request
162 parser func(*chatResponseFull) responseParser
163 response chatResponseFull
164 )
165
166 if req, parser, err = buildSender(api.endpoint, options...).BuildRequest(api.token, channelID); err != nil {
92167 return "", "", "", err
93168 }
94169
95 response, err := chatRequest(channel, values, api.debug)
96 if err != nil {
170 if err = doPost(ctx, api.httpclient, req, parser(&response), api); err != nil {
97171 return "", "", "", err
98172 }
99173
100 return response.Channel, response.Timestamp, response.Text, nil
101 }
102
103 // ApplyMsgOptions utility function for debugging/testing chat requests.
104 func ApplyMsgOptions(token, channel string, options ...MsgOption) (string, url.Values, error) {
174 return response.Channel, response.getMessageTimestamp(), response.Text, response.Err()
175 }
176
177 // UnsafeApplyMsgOptions utility function for debugging/testing chat requests.
178 // NOTE: USE AT YOUR OWN RISK: No issues relating to the use of this function
179 // will be supported by the library.
180 func UnsafeApplyMsgOptions(token, channel, apiurl string, options ...MsgOption) (string, url.Values, error) {
181 config, err := applyMsgOptions(token, channel, apiurl, options...)
182 return config.endpoint, config.values, err
183 }
184
185 func applyMsgOptions(token, channel, apiurl string, options ...MsgOption) (sendConfig, error) {
105186 config := sendConfig{
106 mode: chatPostMessage,
187 apiurl: apiurl,
188 endpoint: apiurl + string(chatPostMessage),
107189 values: url.Values{
108190 "token": {token},
109191 "channel": {channel},
112194
113195 for _, opt := range options {
114196 if err := opt(&config); err != nil {
115 return string(config.mode), config.values, err
116 }
117 }
118
119 return string(config.mode), config.values, nil
120 }
121
122 func escapeMessage(message string) string {
123 replacer := strings.NewReplacer("&", "&amp;", "<", "&lt;", ">", "&gt;")
124 return replacer.Replace(message)
125 }
126
127 func chatRequest(path string, values url.Values, debug bool) (*chatResponseFull, error) {
128 response := &chatResponseFull{}
129 err := post(path, values, response, debug)
130 if err != nil {
131 return nil, err
132 }
133 if !response.Ok {
134 return nil, errors.New(response.Error)
135 }
136 return response, nil
197 return config, err
198 }
199 }
200
201 return config, nil
202 }
203
204 func buildSender(apiurl string, options ...MsgOption) sendConfig {
205 return sendConfig{
206 apiurl: apiurl,
207 options: options,
208 }
137209 }
138210
139211 type sendMode string
140212
141213 const (
142 chatUpdate sendMode = "chat.update"
143 chatPostMessage sendMode = "chat.postMessage"
144 chatDelete sendMode = "chat.delete"
214 chatUpdate sendMode = "chat.update"
215 chatPostMessage sendMode = "chat.postMessage"
216 chatDelete sendMode = "chat.delete"
217 chatPostEphemeral sendMode = "chat.postEphemeral"
218 chatResponse sendMode = "chat.responseURL"
219 chatMeMessage sendMode = "chat.meMessage"
220 chatUnfurl sendMode = "chat.unfurl"
145221 )
146222
147223 type sendConfig struct {
148 mode sendMode
149 values url.Values
224 apiurl string
225 options []MsgOption
226 mode sendMode
227 endpoint string
228 values url.Values
229 attachments []Attachment
230 responseType string
231 }
232
233 func (t sendConfig) BuildRequest(token, channelID string) (req *http.Request, _ func(*chatResponseFull) responseParser, err error) {
234 if t, err = applyMsgOptions(token, channelID, t.apiurl, t.options...); err != nil {
235 return nil, nil, err
236 }
237
238 switch t.mode {
239 case chatResponse:
240 return responseURLSender{
241 endpoint: t.endpoint,
242 values: t.values,
243 attachments: t.attachments,
244 responseType: t.responseType,
245 }.BuildRequest()
246 default:
247 return formSender{endpoint: t.endpoint, values: t.values}.BuildRequest()
248 }
249 }
250
251 type formSender struct {
252 endpoint string
253 values url.Values
254 }
255
256 func (t formSender) BuildRequest() (*http.Request, func(*chatResponseFull) responseParser, error) {
257 req, err := formReq(t.endpoint, t.values)
258 return req, func(resp *chatResponseFull) responseParser {
259 return newJSONParser(resp)
260 }, err
261 }
262
263 type responseURLSender struct {
264 endpoint string
265 values url.Values
266 attachments []Attachment
267 responseType string
268 }
269
270 func (t responseURLSender) BuildRequest() (*http.Request, func(*chatResponseFull) responseParser, error) {
271 req, err := jsonReq(t.endpoint, Msg{
272 Text: t.values.Get("text"),
273 Timestamp: t.values.Get("ts"),
274 Attachments: t.attachments,
275 ResponseType: t.responseType,
276 })
277 return req, func(resp *chatResponseFull) responseParser {
278 return newContentTypeParser(resp)
279 }, err
150280 }
151281
152282 // MsgOption option provided when sending a message.
155285 // MsgOptionPost posts a messages, this is the default.
156286 func MsgOptionPost() MsgOption {
157287 return func(config *sendConfig) error {
158 config.mode = chatPostMessage
288 config.endpoint = config.apiurl + string(chatPostMessage)
159289 config.values.Del("ts")
290 return nil
291 }
292 }
293
294 // MsgOptionPostEphemeral - posts an ephemeral message to the provided user.
295 func MsgOptionPostEphemeral(userID string) MsgOption {
296 return func(config *sendConfig) error {
297 config.endpoint = config.apiurl + string(chatPostEphemeral)
298 MsgOptionUser(userID)(config)
299 config.values.Del("ts")
300
301 return nil
302 }
303 }
304
305 // MsgOptionMeMessage posts a "me message" type from the calling user
306 func MsgOptionMeMessage() MsgOption {
307 return func(config *sendConfig) error {
308 config.endpoint = config.apiurl + string(chatMeMessage)
160309 return nil
161310 }
162311 }
164313 // MsgOptionUpdate updates a message based on the timestamp.
165314 func MsgOptionUpdate(timestamp string) MsgOption {
166315 return func(config *sendConfig) error {
167 config.mode = chatUpdate
316 config.endpoint = config.apiurl + string(chatUpdate)
168317 config.values.Add("ts", timestamp)
169318 return nil
170319 }
173322 // MsgOptionDelete deletes a message based on the timestamp.
174323 func MsgOptionDelete(timestamp string) MsgOption {
175324 return func(config *sendConfig) error {
176 config.mode = chatDelete
325 config.endpoint = config.apiurl + string(chatDelete)
177326 config.values.Add("ts", timestamp)
327 return nil
328 }
329 }
330
331 // MsgOptionUnfurl unfurls a message based on the timestamp.
332 func MsgOptionUnfurl(timestamp string, unfurls map[string]Attachment) MsgOption {
333 return func(config *sendConfig) error {
334 config.endpoint = config.apiurl + string(chatUnfurl)
335 config.values.Add("ts", timestamp)
336 unfurlsStr, err := json.Marshal(unfurls)
337 if err == nil {
338 config.values.Add("unfurls", string(unfurlsStr))
339 }
340 return err
341 }
342 }
343
344 // MsgOptionResponseURL supplies a url to use as the endpoint.
345 func MsgOptionResponseURL(url string, rt string) MsgOption {
346 return func(config *sendConfig) error {
347 config.mode = chatResponse
348 config.endpoint = url
349 config.responseType = rt
350 config.values.Del("ts")
178351 return nil
179352 }
180353 }
185358 if b != DEFAULT_MESSAGE_ASUSER {
186359 config.values.Set("as_user", "true")
187360 }
361 return nil
362 }
363 }
364
365 // MsgOptionUser set the user for the message.
366 func MsgOptionUser(userID string) MsgOption {
367 return func(config *sendConfig) error {
368 config.values.Set("user", userID)
369 return nil
370 }
371 }
372
373 // MsgOptionUsername set the username for the message.
374 func MsgOptionUsername(username string) MsgOption {
375 return func(config *sendConfig) error {
376 config.values.Set("username", username)
188377 return nil
189378 }
190379 }
194383 func MsgOptionText(text string, escape bool) MsgOption {
195384 return func(config *sendConfig) error {
196385 if escape {
197 text = escapeMessage(text)
386 text = slackutilsx.EscapeMessage(text)
198387 }
199388 config.values.Add("text", text)
200389 return nil
208397 return nil
209398 }
210399
211 attachments, err := json.Marshal(attachments)
400 config.attachments = attachments
401
402 // FIXME: We are setting the attachments on the message twice: above for
403 // the json version, and below for the html version. The marshalled bytes
404 // we put into config.values below don't work directly in the Msg version.
405
406 attachmentBytes, err := json.Marshal(attachments)
212407 if err == nil {
213 config.values.Set("attachments", string(attachments))
408 config.values.Set("attachments", string(attachmentBytes))
409 }
410
411 return err
412 }
413 }
414
415 // MsgOptionBlocks sets blocks for the message
416 func MsgOptionBlocks(blocks ...Block) MsgOption {
417 return func(config *sendConfig) error {
418 if blocks == nil {
419 return nil
420 }
421
422 blocks, err := json.Marshal(blocks)
423 if err == nil {
424 config.values.Set("blocks", string(blocks))
214425 }
215426 return err
216427 }
224435 }
225436 }
226437
438 // MsgOptionDisableLinkUnfurl disables link unfurling
439 func MsgOptionDisableLinkUnfurl() MsgOption {
440 return func(config *sendConfig) error {
441 config.values.Set("unfurl_links", "false")
442 return nil
443 }
444 }
445
227446 // MsgOptionDisableMediaUnfurl disables media unfurling.
228447 func MsgOptionDisableMediaUnfurl() MsgOption {
229448 return func(config *sendConfig) error {
240459 }
241460 }
242461
462 // MsgOptionTS sets the thread TS of the message to enable creating or replying to a thread
463 func MsgOptionTS(ts string) MsgOption {
464 return func(config *sendConfig) error {
465 config.values.Set("thread_ts", ts)
466 return nil
467 }
468 }
469
470 // MsgOptionBroadcast sets reply_broadcast to true
471 func MsgOptionBroadcast() MsgOption {
472 return func(config *sendConfig) error {
473 config.values.Set("reply_broadcast", "true")
474 return nil
475 }
476 }
477
478 // MsgOptionCompose combines multiple options into a single option.
479 func MsgOptionCompose(options ...MsgOption) MsgOption {
480 return func(c *sendConfig) error {
481 for _, opt := range options {
482 if err := opt(c); err != nil {
483 return err
484 }
485 }
486 return nil
487 }
488 }
489
490 // MsgOptionParse set parse option.
491 func MsgOptionParse(b bool) MsgOption {
492 return func(c *sendConfig) error {
493 var v string
494 if b {
495 v = "full"
496 } else {
497 v = "none"
498 }
499 c.values.Set("parse", v)
500 return nil
501 }
502 }
503
504 // MsgOptionIconURL sets an icon URL
505 func MsgOptionIconURL(iconURL string) MsgOption {
506 return func(c *sendConfig) error {
507 c.values.Set("icon_url", iconURL)
508 return nil
509 }
510 }
511
512 // MsgOptionIconEmoji sets an icon emoji
513 func MsgOptionIconEmoji(iconEmoji string) MsgOption {
514 return func(c *sendConfig) error {
515 c.values.Set("icon_emoji", iconEmoji)
516 return nil
517 }
518 }
519
520 // UnsafeMsgOptionEndpoint deliver the message to the specified endpoint.
521 // NOTE: USE AT YOUR OWN RISK: No issues relating to the use of this Option
522 // will be supported by the library, it is subject to change without notice that
523 // may result in compilation errors or runtime behaviour changes.
524 func UnsafeMsgOptionEndpoint(endpoint string, update func(url.Values)) MsgOption {
525 return func(config *sendConfig) error {
526 config.endpoint = endpoint
527 update(config.values)
528 return nil
529 }
530 }
531
243532 // MsgOptionPostMessageParameters maintain backwards compatibility.
244533 func MsgOptionPostMessageParameters(params PostMessageParameters) MsgOption {
245534 return func(config *sendConfig) error {
246535 if params.Username != DEFAULT_MESSAGE_USERNAME {
247 config.values.Set("username", string(params.Username))
536 config.values.Set("username", params.Username)
537 }
538
539 // chat.postEphemeral support
540 if params.User != DEFAULT_MESSAGE_USERNAME {
541 config.values.Set("user", params.User)
248542 }
249543
250544 // never generates an error.
251545 MsgOptionAsUser(params.AsUser)(config)
252546
253547 if params.Parse != DEFAULT_MESSAGE_PARSE {
254 config.values.Set("parse", string(params.Parse))
548 config.values.Set("parse", params.Parse)
255549 }
256550 if params.LinkNames != DEFAULT_MESSAGE_LINK_NAMES {
257551 config.values.Set("link_names", "1")
282576 if params.ThreadTimestamp != DEFAULT_MESSAGE_THREAD_TIMESTAMP {
283577 config.values.Set("thread_ts", params.ThreadTimestamp)
284578 }
285
286 return nil
287 }
288 }
579 if params.ReplyBroadcast != DEFAULT_MESSAGE_REPLY_BROADCAST {
580 config.values.Set("reply_broadcast", "true")
581 }
582
583 return nil
584 }
585 }
586
587 // PermalinkParameters are the parameters required to get a permalink to a
588 // message. Slack documentation can be found here:
589 // https://api.slack.com/methods/chat.getPermalink
590 type PermalinkParameters struct {
591 Channel string
592 Ts string
593 }
594
595 // GetPermalink returns the permalink for a message. It takes
596 // PermalinkParameters and returns a string containing the permalink. It
597 // returns an error if unable to retrieve the permalink.
598 func (api *Client) GetPermalink(params *PermalinkParameters) (string, error) {
599 return api.GetPermalinkContext(context.Background(), params)
600 }
601
602 // GetPermalinkContext returns the permalink for a message using a custom context.
603 func (api *Client) GetPermalinkContext(ctx context.Context, params *PermalinkParameters) (string, error) {
604 values := url.Values{
605 "token": {api.token},
606 "channel": {params.Channel},
607 "message_ts": {params.Ts},
608 }
609
610 response := struct {
611 Channel string `json:"channel"`
612 Permalink string `json:"permalink"`
613 SlackResponse
614 }{}
615 err := api.getMethod(ctx, "chat.getPermalink", values, &response)
616 if err != nil {
617 return "", err
618 }
619 return response.Permalink, response.Err()
620 }
0 package slack
1
2 import (
3 "encoding/json"
4 "net/http"
5 "testing"
6 )
7
8 func postMessageInvalidChannelHandler(rw http.ResponseWriter, r *http.Request) {
9 rw.Header().Set("Content-Type", "application/json")
10 response, _ := json.Marshal(chatResponseFull{
11 SlackResponse: SlackResponse{Ok: false, Error: "channel_not_found"},
12 })
13 rw.Write(response)
14 }
15
16 func TestPostMessageInvalidChannel(t *testing.T) {
17 http.HandleFunc("/chat.postMessage", postMessageInvalidChannelHandler)
18 once.Do(startServer)
19 api := New("testing-token", OptionAPIURL("http://"+serverAddr+"/"))
20 _, _, err := api.PostMessage("CXXXXXXXX", MsgOptionText("hello", false))
21 if err == nil {
22 t.Errorf("Expected error: channel_not_found; instead succeeded")
23 return
24 }
25
26 if err.Error() != "channel_not_found" {
27 t.Errorf("Expected error: channel_not_found; received: %s", err)
28 return
29 }
30 }
31
32 func TestGetPermalink(t *testing.T) {
33 channel := "C1H9RESGA"
34 timeStamp := "p135854651500008"
35
36 http.HandleFunc("/chat.getPermalink", func(rw http.ResponseWriter, r *http.Request) {
37
38 if got, want := r.Header.Get("Content-Type"), "application/x-www-form-urlencoded"; got != want {
39 t.Errorf("request uses unexpected content type: got %s, want %s", got, want)
40 }
41
42 if got, want := r.URL.Query().Get("channel"), channel; got != want {
43 t.Errorf("request contains unexpected channel: got %s, want %s", got, want)
44 }
45
46 if got, want := r.URL.Query().Get("message_ts"), timeStamp; got != want {
47 t.Errorf("request contains unexpected message timestamp: got %s, want %s", got, want)
48 }
49
50 rw.Header().Set("Content-Type", "application/json")
51 response := []byte("{\"ok\": true, \"channel\": \"" + channel + "\", \"permalink\": \"https://ghostbusters.slack.com/archives/" + channel + "/" + timeStamp + "\"}")
52 rw.Write(response)
53 })
54
55 once.Do(startServer)
56 api := New("testing-token", OptionAPIURL("http://"+serverAddr+"/"))
57 pp := PermalinkParameters{Channel: channel, Ts: timeStamp}
58 pl, err := api.GetPermalink(&pp)
59
60 if got, want := pl, "https://ghostbusters.slack.com/archives/C1H9RESGA/p135854651500008"; got != want {
61 t.Errorf("unexpected permalink: got %s, want %s", got, want)
62 }
63
64 if err != nil {
65 t.Errorf("unexpected error returned: %v", err)
66 }
67 }
00 package slack
11
2 import (
3 "context"
4 "net/url"
5 "strconv"
6 "strings"
7 )
8
29 // Conversation is the foundation for IM and BaseGroupConversation
3 type conversation struct {
10 type Conversation struct {
411 ID string `json:"id"`
512 Created JSONTime `json:"created"`
613 IsOpen bool `json:"is_open"`
815 Latest *Message `json:"latest,omitempty"`
916 UnreadCount int `json:"unread_count,omitempty"`
1017 UnreadCountDisplay int `json:"unread_count_display,omitempty"`
18 IsGroup bool `json:"is_group"`
19 IsShared bool `json:"is_shared"`
20 IsIM bool `json:"is_im"`
21 IsExtShared bool `json:"is_ext_shared"`
22 IsOrgShared bool `json:"is_org_shared"`
23 IsPendingExtShared bool `json:"is_pending_ext_shared"`
24 IsPrivate bool `json:"is_private"`
25 IsMpIM bool `json:"is_mpim"`
26 Unlinked int `json:"unlinked"`
27 NameNormalized string `json:"name_normalized"`
28 NumMembers int `json:"num_members"`
29 Priority float64 `json:"priority"`
30 User string `json:"user"`
31
32 // TODO support pending_shared
33 // TODO support previous_names
1134 }
1235
1336 // GroupConversation is the foundation for Group and Channel
14 type groupConversation struct {
15 conversation
37 type GroupConversation struct {
38 Conversation
1639 Name string `json:"name"`
1740 Creator string `json:"creator"`
1841 IsArchived bool `json:"is_archived"`
3457 Creator string `json:"creator"`
3558 LastSet JSONTime `json:"last_set"`
3659 }
60
61 type GetUsersInConversationParameters struct {
62 ChannelID string
63 Cursor string
64 Limit int
65 }
66
67 type GetConversationsForUserParameters struct {
68 UserID string
69 Cursor string
70 Types []string
71 Limit int
72 ExcludeArchived bool
73 }
74
75 type responseMetaData struct {
76 NextCursor string `json:"next_cursor"`
77 }
78
79 // GetUsersInConversation returns the list of users in a conversation
80 func (api *Client) GetUsersInConversation(params *GetUsersInConversationParameters) ([]string, string, error) {
81 return api.GetUsersInConversationContext(context.Background(), params)
82 }
83
84 // GetUsersInConversationContext returns the list of users in a conversation with a custom context
85 func (api *Client) GetUsersInConversationContext(ctx context.Context, params *GetUsersInConversationParameters) ([]string, string, error) {
86 values := url.Values{
87 "token": {api.token},
88 "channel": {params.ChannelID},
89 }
90 if params.Cursor != "" {
91 values.Add("cursor", params.Cursor)
92 }
93 if params.Limit != 0 {
94 values.Add("limit", strconv.Itoa(params.Limit))
95 }
96 response := struct {
97 Members []string `json:"members"`
98 ResponseMetaData responseMetaData `json:"response_metadata"`
99 SlackResponse
100 }{}
101
102 err := api.postMethod(ctx, "conversations.members", values, &response)
103 if err != nil {
104 return nil, "", err
105 }
106
107 if err := response.Err(); err != nil {
108 return nil, "", err
109 }
110
111 return response.Members, response.ResponseMetaData.NextCursor, nil
112 }
113
114 // GetConversationsForUser returns the list conversations for a given user
115 func (api *Client) GetConversationsForUser(params *GetConversationsForUserParameters) (channels []Channel, nextCursor string, err error) {
116 return api.GetConversationsForUserContext(context.Background(), params)
117 }
118
119 // GetConversationsForUserContext returns the list conversations for a given user with a custom context
120 func (api *Client) GetConversationsForUserContext(ctx context.Context, params *GetConversationsForUserParameters) (channels []Channel, nextCursor string, err error) {
121 values := url.Values{
122 "token": {api.token},
123 }
124 if params.UserID != "" {
125 values.Add("user", params.UserID)
126 }
127 if params.Cursor != "" {
128 values.Add("cursor", params.Cursor)
129 }
130 if params.Limit != 0 {
131 values.Add("limit", strconv.Itoa(params.Limit))
132 }
133 if params.Types != nil {
134 values.Add("types", strings.Join(params.Types, ","))
135 }
136 if params.ExcludeArchived {
137 values.Add("exclude_archived", "true")
138 }
139 response := struct {
140 Channels []Channel `json:"channels"`
141 ResponseMetaData responseMetaData `json:"response_metadata"`
142 SlackResponse
143 }{}
144 err = api.postMethod(ctx, "users.conversations", values, &response)
145 if err != nil {
146 return nil, "", err
147 }
148
149 return response.Channels, response.ResponseMetaData.NextCursor, response.Err()
150 }
151
152 // ArchiveConversation archives a conversation
153 func (api *Client) ArchiveConversation(channelID string) error {
154 return api.ArchiveConversationContext(context.Background(), channelID)
155 }
156
157 // ArchiveConversationContext archives a conversation with a custom context
158 func (api *Client) ArchiveConversationContext(ctx context.Context, channelID string) error {
159 values := url.Values{
160 "token": {api.token},
161 "channel": {channelID},
162 }
163
164 response := SlackResponse{}
165 err := api.postMethod(ctx, "conversations.archive", values, &response)
166 if err != nil {
167 return err
168 }
169
170 return response.Err()
171 }
172
173 // UnArchiveConversation reverses conversation archival
174 func (api *Client) UnArchiveConversation(channelID string) error {
175 return api.UnArchiveConversationContext(context.Background(), channelID)
176 }
177
178 // UnArchiveConversationContext reverses conversation archival with a custom context
179 func (api *Client) UnArchiveConversationContext(ctx context.Context, channelID string) error {
180 values := url.Values{
181 "token": {api.token},
182 "channel": {channelID},
183 }
184 response := SlackResponse{}
185 err := api.postMethod(ctx, "conversations.unarchive", values, &response)
186 if err != nil {
187 return err
188 }
189
190 return response.Err()
191 }
192
193 // SetTopicOfConversation sets the topic for a conversation
194 func (api *Client) SetTopicOfConversation(channelID, topic string) (*Channel, error) {
195 return api.SetTopicOfConversationContext(context.Background(), channelID, topic)
196 }
197
198 // SetTopicOfConversationContext sets the topic for a conversation with a custom context
199 func (api *Client) SetTopicOfConversationContext(ctx context.Context, channelID, topic string) (*Channel, error) {
200 values := url.Values{
201 "token": {api.token},
202 "channel": {channelID},
203 "topic": {topic},
204 }
205 response := struct {
206 SlackResponse
207 Channel *Channel `json:"channel"`
208 }{}
209 err := api.postMethod(ctx, "conversations.setTopic", values, &response)
210 if err != nil {
211 return nil, err
212 }
213
214 return response.Channel, response.Err()
215 }
216
217 // SetPurposeOfConversation sets the purpose for a conversation
218 func (api *Client) SetPurposeOfConversation(channelID, purpose string) (*Channel, error) {
219 return api.SetPurposeOfConversationContext(context.Background(), channelID, purpose)
220 }
221
222 // SetPurposeOfConversationContext sets the purpose for a conversation with a custom context
223 func (api *Client) SetPurposeOfConversationContext(ctx context.Context, channelID, purpose string) (*Channel, error) {
224 values := url.Values{
225 "token": {api.token},
226 "channel": {channelID},
227 "purpose": {purpose},
228 }
229 response := struct {
230 SlackResponse
231 Channel *Channel `json:"channel"`
232 }{}
233
234 err := api.postMethod(ctx, "conversations.setPurpose", values, &response)
235 if err != nil {
236 return nil, err
237 }
238
239 return response.Channel, response.Err()
240 }
241
242 // RenameConversation renames a conversation
243 func (api *Client) RenameConversation(channelID, channelName string) (*Channel, error) {
244 return api.RenameConversationContext(context.Background(), channelID, channelName)
245 }
246
247 // RenameConversationContext renames a conversation with a custom context
248 func (api *Client) RenameConversationContext(ctx context.Context, channelID, channelName string) (*Channel, error) {
249 values := url.Values{
250 "token": {api.token},
251 "channel": {channelID},
252 "name": {channelName},
253 }
254 response := struct {
255 SlackResponse
256 Channel *Channel `json:"channel"`
257 }{}
258
259 err := api.postMethod(ctx, "conversations.rename", values, &response)
260 if err != nil {
261 return nil, err
262 }
263
264 return response.Channel, response.Err()
265 }
266
267 // InviteUsersToConversation invites users to a channel
268 func (api *Client) InviteUsersToConversation(channelID string, users ...string) (*Channel, error) {
269 return api.InviteUsersToConversationContext(context.Background(), channelID, users...)
270 }
271
272 // InviteUsersToConversationContext invites users to a channel with a custom context
273 func (api *Client) InviteUsersToConversationContext(ctx context.Context, channelID string, users ...string) (*Channel, error) {
274 values := url.Values{
275 "token": {api.token},
276 "channel": {channelID},
277 "users": {strings.Join(users, ",")},
278 }
279 response := struct {
280 SlackResponse
281 Channel *Channel `json:"channel"`
282 }{}
283
284 err := api.postMethod(ctx, "conversations.invite", values, &response)
285 if err != nil {
286 return nil, err
287 }
288
289 return response.Channel, response.Err()
290 }
291
292 // KickUserFromConversation removes a user from a conversation
293 func (api *Client) KickUserFromConversation(channelID string, user string) error {
294 return api.KickUserFromConversationContext(context.Background(), channelID, user)
295 }
296
297 // KickUserFromConversationContext removes a user from a conversation with a custom context
298 func (api *Client) KickUserFromConversationContext(ctx context.Context, channelID string, user string) error {
299 values := url.Values{
300 "token": {api.token},
301 "channel": {channelID},
302 "user": {user},
303 }
304
305 response := SlackResponse{}
306 err := api.postMethod(ctx, "conversations.kick", values, &response)
307 if err != nil {
308 return err
309 }
310
311 return response.Err()
312 }
313
314 // CloseConversation closes a direct message or multi-person direct message
315 func (api *Client) CloseConversation(channelID string) (noOp bool, alreadyClosed bool, err error) {
316 return api.CloseConversationContext(context.Background(), channelID)
317 }
318
319 // CloseConversationContext closes a direct message or multi-person direct message with a custom context
320 func (api *Client) CloseConversationContext(ctx context.Context, channelID string) (noOp bool, alreadyClosed bool, err error) {
321 values := url.Values{
322 "token": {api.token},
323 "channel": {channelID},
324 }
325 response := struct {
326 SlackResponse
327 NoOp bool `json:"no_op"`
328 AlreadyClosed bool `json:"already_closed"`
329 }{}
330
331 err = api.postMethod(ctx, "conversations.close", values, &response)
332 if err != nil {
333 return false, false, err
334 }
335
336 return response.NoOp, response.AlreadyClosed, response.Err()
337 }
338
339 // CreateConversation initiates a public or private channel-based conversation
340 func (api *Client) CreateConversation(channelName string, isPrivate bool) (*Channel, error) {
341 return api.CreateConversationContext(context.Background(), channelName, isPrivate)
342 }
343
344 // CreateConversationContext initiates a public or private channel-based conversation with a custom context
345 func (api *Client) CreateConversationContext(ctx context.Context, channelName string, isPrivate bool) (*Channel, error) {
346 values := url.Values{
347 "token": {api.token},
348 "name": {channelName},
349 "is_private": {strconv.FormatBool(isPrivate)},
350 }
351 response, err := api.channelRequest(ctx, "conversations.create", values)
352 if err != nil {
353 return nil, err
354 }
355
356 return &response.Channel, nil
357 }
358
359 // GetConversationInfo retrieves information about a conversation
360 func (api *Client) GetConversationInfo(channelID string, includeLocale bool) (*Channel, error) {
361 return api.GetConversationInfoContext(context.Background(), channelID, includeLocale)
362 }
363
364 // GetConversationInfoContext retrieves information about a conversation with a custom context
365 func (api *Client) GetConversationInfoContext(ctx context.Context, channelID string, includeLocale bool) (*Channel, error) {
366 values := url.Values{
367 "token": {api.token},
368 "channel": {channelID},
369 "include_locale": {strconv.FormatBool(includeLocale)},
370 }
371 response, err := api.channelRequest(ctx, "conversations.info", values)
372 if err != nil {
373 return nil, err
374 }
375
376 return &response.Channel, response.Err()
377 }
378
379 // LeaveConversation leaves a conversation
380 func (api *Client) LeaveConversation(channelID string) (bool, error) {
381 return api.LeaveConversationContext(context.Background(), channelID)
382 }
383
384 // LeaveConversationContext leaves a conversation with a custom context
385 func (api *Client) LeaveConversationContext(ctx context.Context, channelID string) (bool, error) {
386 values := url.Values{
387 "token": {api.token},
388 "channel": {channelID},
389 }
390
391 response, err := api.channelRequest(ctx, "conversations.leave", values)
392 if err != nil {
393 return false, err
394 }
395
396 return response.NotInChannel, err
397 }
398
399 type GetConversationRepliesParameters struct {
400 ChannelID string
401 Timestamp string
402 Cursor string
403 Inclusive bool
404 Latest string
405 Limit int
406 Oldest string
407 }
408
409 // GetConversationReplies retrieves a thread of messages posted to a conversation
410 func (api *Client) GetConversationReplies(params *GetConversationRepliesParameters) (msgs []Message, hasMore bool, nextCursor string, err error) {
411 return api.GetConversationRepliesContext(context.Background(), params)
412 }
413
414 // GetConversationRepliesContext retrieves a thread of messages posted to a conversation with a custom context
415 func (api *Client) GetConversationRepliesContext(ctx context.Context, params *GetConversationRepliesParameters) (msgs []Message, hasMore bool, nextCursor string, err error) {
416 values := url.Values{
417 "token": {api.token},
418 "channel": {params.ChannelID},
419 "ts": {params.Timestamp},
420 }
421 if params.Cursor != "" {
422 values.Add("cursor", params.Cursor)
423 }
424 if params.Latest != "" {
425 values.Add("latest", params.Latest)
426 }
427 if params.Limit != 0 {
428 values.Add("limit", strconv.Itoa(params.Limit))
429 }
430 if params.Oldest != "" {
431 values.Add("oldest", params.Oldest)
432 }
433 if params.Inclusive {
434 values.Add("inclusive", "1")
435 } else {
436 values.Add("inclusive", "0")
437 }
438 response := struct {
439 SlackResponse
440 HasMore bool `json:"has_more"`
441 ResponseMetaData struct {
442 NextCursor string `json:"next_cursor"`
443 } `json:"response_metadata"`
444 Messages []Message `json:"messages"`
445 }{}
446
447 err = api.postMethod(ctx, "conversations.replies", values, &response)
448 if err != nil {
449 return nil, false, "", err
450 }
451
452 return response.Messages, response.HasMore, response.ResponseMetaData.NextCursor, response.Err()
453 }
454
455 type GetConversationsParameters struct {
456 Cursor string
457 ExcludeArchived string
458 Limit int
459 Types []string
460 }
461
462 // GetConversations returns the list of channels in a Slack team
463 func (api *Client) GetConversations(params *GetConversationsParameters) (channels []Channel, nextCursor string, err error) {
464 return api.GetConversationsContext(context.Background(), params)
465 }
466
467 // GetConversationsContext returns the list of channels in a Slack team with a custom context
468 func (api *Client) GetConversationsContext(ctx context.Context, params *GetConversationsParameters) (channels []Channel, nextCursor string, err error) {
469 values := url.Values{
470 "token": {api.token},
471 "exclude_archived": {params.ExcludeArchived},
472 }
473 if params.Cursor != "" {
474 values.Add("cursor", params.Cursor)
475 }
476 if params.Limit != 0 {
477 values.Add("limit", strconv.Itoa(params.Limit))
478 }
479 if params.Types != nil {
480 values.Add("types", strings.Join(params.Types, ","))
481 }
482 response := struct {
483 Channels []Channel `json:"channels"`
484 ResponseMetaData responseMetaData `json:"response_metadata"`
485 SlackResponse
486 }{}
487
488 err = api.postMethod(ctx, "conversations.list", values, &response)
489 if err != nil {
490 return nil, "", err
491 }
492
493 return response.Channels, response.ResponseMetaData.NextCursor, response.Err()
494 }
495
496 type OpenConversationParameters struct {
497 ChannelID string
498 ReturnIM bool
499 Users []string
500 }
501
502 // OpenConversation opens or resumes a direct message or multi-person direct message
503 func (api *Client) OpenConversation(params *OpenConversationParameters) (*Channel, bool, bool, error) {
504 return api.OpenConversationContext(context.Background(), params)
505 }
506
507 // OpenConversationContext opens or resumes a direct message or multi-person direct message with a custom context
508 func (api *Client) OpenConversationContext(ctx context.Context, params *OpenConversationParameters) (*Channel, bool, bool, error) {
509 values := url.Values{
510 "token": {api.token},
511 "return_im": {strconv.FormatBool(params.ReturnIM)},
512 }
513 if params.ChannelID != "" {
514 values.Add("channel", params.ChannelID)
515 }
516 if params.Users != nil {
517 values.Add("users", strings.Join(params.Users, ","))
518 }
519 response := struct {
520 Channel *Channel `json:"channel"`
521 NoOp bool `json:"no_op"`
522 AlreadyOpen bool `json:"already_open"`
523 SlackResponse
524 }{}
525
526 err := api.postMethod(ctx, "conversations.open", values, &response)
527 if err != nil {
528 return nil, false, false, err
529 }
530
531 return response.Channel, response.NoOp, response.AlreadyOpen, response.Err()
532 }
533
534 // JoinConversation joins an existing conversation
535 func (api *Client) JoinConversation(channelID string) (*Channel, string, []string, error) {
536 return api.JoinConversationContext(context.Background(), channelID)
537 }
538
539 // JoinConversationContext joins an existing conversation with a custom context
540 func (api *Client) JoinConversationContext(ctx context.Context, channelID string) (*Channel, string, []string, error) {
541 values := url.Values{"token": {api.token}, "channel": {channelID}}
542 response := struct {
543 Channel *Channel `json:"channel"`
544 Warning string `json:"warning"`
545 ResponseMetaData *struct {
546 Warnings []string `json:"warnings"`
547 } `json:"response_metadata"`
548 SlackResponse
549 }{}
550
551 err := api.postMethod(ctx, "conversations.join", values, &response)
552 if err != nil {
553 return nil, "", nil, err
554 }
555 if response.Err() != nil {
556 return nil, "", nil, response.Err()
557 }
558 var warnings []string
559 if response.ResponseMetaData != nil {
560 warnings = response.ResponseMetaData.Warnings
561 }
562 return response.Channel, response.Warning, warnings, nil
563 }
564
565 type GetConversationHistoryParameters struct {
566 ChannelID string
567 Cursor string
568 Inclusive bool
569 Latest string
570 Limit int
571 Oldest string
572 }
573
574 type GetConversationHistoryResponse struct {
575 SlackResponse
576 HasMore bool `json:"has_more"`
577 PinCount int `json:"pin_count"`
578 Latest string `json:"latest"`
579 ResponseMetaData struct {
580 NextCursor string `json:"next_cursor"`
581 } `json:"response_metadata"`
582 Messages []Message `json:"messages"`
583 }
584
585 // GetConversationHistory joins an existing conversation
586 func (api *Client) GetConversationHistory(params *GetConversationHistoryParameters) (*GetConversationHistoryResponse, error) {
587 return api.GetConversationHistoryContext(context.Background(), params)
588 }
589
590 // GetConversationHistoryContext joins an existing conversation with a custom context
591 func (api *Client) GetConversationHistoryContext(ctx context.Context, params *GetConversationHistoryParameters) (*GetConversationHistoryResponse, error) {
592 values := url.Values{"token": {api.token}, "channel": {params.ChannelID}}
593 if params.Cursor != "" {
594 values.Add("cursor", params.Cursor)
595 }
596 if params.Inclusive {
597 values.Add("inclusive", "1")
598 } else {
599 values.Add("inclusive", "0")
600 }
601 if params.Latest != "" {
602 values.Add("latest", params.Latest)
603 }
604 if params.Limit != 0 {
605 values.Add("limit", strconv.Itoa(params.Limit))
606 }
607 if params.Oldest != "" {
608 values.Add("oldest", params.Oldest)
609 }
610
611 response := GetConversationHistoryResponse{}
612
613 err := api.postMethod(ctx, "conversations.history", values, &response)
614 if err != nil {
615 return nil, err
616 }
617
618 return &response, response.Err()
619 }
11
22 import (
33 "encoding/json"
4 "net/http"
5 "reflect"
46 "testing"
57
68 "github.com/stretchr/testify/assert"
177179 assert.NotNil(t, im)
178180 assert.Equal(t, "D024BFF1M", im.ID)
179181 assert.Equal(t, true, im.IsIM)
182 assert.Equal(t, "U024BE7LH", im.User)
180183 assert.Equal(t, JSONTime(1360782804), im.Created)
181184 assert.Equal(t, false, im.IsUserDeleted)
182185 assert.Equal(t, true, im.IsOpen)
189192 im := &IM{}
190193 im.ID = "D024BFF1M"
191194 im.IsIM = true
195 im.User = "U024BE7LH"
192196 im.Created = JSONTime(1360782804)
193197 im.IsUserDeleted = false
194198 im.IsOpen = true
197201 im.UnreadCountDisplay = 0
198202 assertSimpleIM(t, im)
199203 }
204
205 func getTestMembers() []string {
206 return []string{"test"}
207 }
208
209 func getUsersInConversation(rw http.ResponseWriter, r *http.Request) {
210 rw.Header().Set("Content-Type", "application/json")
211 response, _ := json.Marshal(struct {
212 SlackResponse
213 Members []string `json:"members"`
214 ResponseMetaData responseMetaData `json:"response_metadata"`
215 }{
216 SlackResponse: SlackResponse{Ok: true},
217 Members: getTestMembers(),
218 ResponseMetaData: responseMetaData{NextCursor: ""},
219 })
220 rw.Write(response)
221 }
222
223 func TestGetUsersInConversation(t *testing.T) {
224 http.HandleFunc("/conversations.members", getUsersInConversation)
225 once.Do(startServer)
226 api := New("testing-token", OptionAPIURL("http://"+serverAddr+"/"))
227 params := GetUsersInConversationParameters{
228 ChannelID: "CXXXXXXXX",
229 }
230
231 expectedMembers := getTestMembers()
232
233 members, _, err := api.GetUsersInConversation(&params)
234 if err != nil {
235 t.Errorf("Unexpected error: %s", err)
236 return
237 }
238 if !reflect.DeepEqual(expectedMembers, members) {
239 t.Fatal(ErrIncorrectResponse)
240 }
241 }
242
243 func TestArchiveConversation(t *testing.T) {
244 http.HandleFunc("/conversations.archive", okJSONHandler)
245 once.Do(startServer)
246 api := New("testing-token", OptionAPIURL("http://"+serverAddr+"/"))
247 err := api.ArchiveConversation("CXXXXXXXX")
248 if err != nil {
249 t.Errorf("Unexpected error: %s", err)
250 return
251 }
252 }
253
254 func TestUnArchiveConversation(t *testing.T) {
255 http.HandleFunc("/conversations.unarchive", okJSONHandler)
256 once.Do(startServer)
257 api := New("testing-token", OptionAPIURL("http://"+serverAddr+"/"))
258 err := api.UnArchiveConversation("CXXXXXXXX")
259 if err != nil {
260 t.Errorf("Unexpected error: %s", err)
261 return
262 }
263 }
264
265 func getTestChannel() *Channel {
266 return &Channel{
267 GroupConversation: GroupConversation{
268 Topic: Topic{
269 Value: "response topic",
270 },
271 Purpose: Purpose{
272 Value: "response purpose",
273 },
274 }}
275 }
276
277 func okChannelJsonHandler(rw http.ResponseWriter, r *http.Request) {
278 rw.Header().Set("Content-Type", "application/json")
279 response, _ := json.Marshal(struct {
280 SlackResponse
281 Channel *Channel `json:"channel"`
282 }{
283 SlackResponse: SlackResponse{Ok: true},
284 Channel: getTestChannel(),
285 })
286 rw.Write(response)
287 }
288
289 func TestSetTopicOfConversation(t *testing.T) {
290 http.HandleFunc("/conversations.setTopic", okChannelJsonHandler)
291 once.Do(startServer)
292 api := New("testing-token", OptionAPIURL("http://"+serverAddr+"/"))
293 inputChannel := getTestChannel()
294 channel, err := api.SetTopicOfConversation("CXXXXXXXX", inputChannel.Topic.Value)
295 if err != nil {
296 t.Errorf("Unexpected error: %s", err)
297 return
298 }
299 if channel.Topic.Value != inputChannel.Topic.Value {
300 t.Fatalf(`topic = '%s', want '%s'`, channel.Topic.Value, inputChannel.Topic.Value)
301 }
302 }
303
304 func TestSetPurposeOfConversation(t *testing.T) {
305 http.HandleFunc("/conversations.setPurpose", okChannelJsonHandler)
306 once.Do(startServer)
307 api := New("testing-token", OptionAPIURL("http://"+serverAddr+"/"))
308 inputChannel := getTestChannel()
309 channel, err := api.SetPurposeOfConversation("CXXXXXXXX", inputChannel.Purpose.Value)
310 if err != nil {
311 t.Errorf("Unexpected error: %s", err)
312 return
313 }
314 if channel.Purpose.Value != inputChannel.Purpose.Value {
315 t.Fatalf(`purpose = '%s', want '%s'`, channel.Purpose.Value, inputChannel.Purpose.Value)
316 }
317 }
318
319 func TestRenameConversation(t *testing.T) {
320 http.HandleFunc("/conversations.rename", okChannelJsonHandler)
321 once.Do(startServer)
322 api := New("testing-token", OptionAPIURL("http://"+serverAddr+"/"))
323 inputChannel := getTestChannel()
324 channel, err := api.RenameConversation("CXXXXXXXX", inputChannel.Name)
325 if err != nil {
326 t.Errorf("Unexpected error: %s", err)
327 return
328 }
329 if channel.Name != inputChannel.Name {
330 t.Fatalf(`channelName = '%s', want '%s'`, channel.Name, inputChannel.Name)
331 }
332 }
333
334 func TestInviteUsersToConversation(t *testing.T) {
335 http.HandleFunc("/conversations.invite", okChannelJsonHandler)
336 once.Do(startServer)
337 api := New("testing-token", OptionAPIURL("http://"+serverAddr+"/"))
338 users := []string{"UXXXXXXX1", "UXXXXXXX2"}
339 channel, err := api.InviteUsersToConversation("CXXXXXXXX", users...)
340 if err != nil {
341 t.Errorf("Unexpected error: %s", err)
342 return
343 }
344 if channel == nil {
345 t.Error("channel should not be nil")
346 return
347 }
348 }
349
350 func TestKickUserFromConversation(t *testing.T) {
351 http.HandleFunc("/conversations.kick", okJSONHandler)
352 once.Do(startServer)
353 api := New("testing-token", OptionAPIURL("http://"+serverAddr+"/"))
354 err := api.KickUserFromConversation("CXXXXXXXX", "UXXXXXXXX")
355 if err != nil {
356 t.Errorf("Unexpected error: %s", err)
357 return
358 }
359 }
360
361 func closeConversationHandler(rw http.ResponseWriter, r *http.Request) {
362 rw.Header().Set("Content-Type", "application/json")
363 response, _ := json.Marshal(struct {
364 SlackResponse
365 NoOp bool `json:"no_op"`
366 AlreadyClosed bool `json:"already_closed"`
367 }{
368 SlackResponse: SlackResponse{Ok: true}})
369 rw.Write(response)
370 }
371
372 func TestCloseConversation(t *testing.T) {
373 http.HandleFunc("/conversations.close", closeConversationHandler)
374 once.Do(startServer)
375 api := New("testing-token", OptionAPIURL("http://"+serverAddr+"/"))
376 _, _, err := api.CloseConversation("CXXXXXXXX")
377 if err != nil {
378 t.Errorf("Unexpected error: %s", err)
379 return
380 }
381 }
382
383 func TestCreateConversation(t *testing.T) {
384 http.HandleFunc("/conversations.create", okChannelJsonHandler)
385 once.Do(startServer)
386 api := New("testing-token", OptionAPIURL("http://"+serverAddr+"/"))
387 channel, err := api.CreateConversation("CXXXXXXXX", false)
388 if err != nil {
389 t.Errorf("Unexpected error: %s", err)
390 return
391 }
392 if channel == nil {
393 t.Error("channel should not be nil")
394 return
395 }
396 }
397
398 func TestGetConversationInfo(t *testing.T) {
399 http.HandleFunc("/conversations.info", okChannelJsonHandler)
400 once.Do(startServer)
401 api := New("testing-token", OptionAPIURL("http://"+serverAddr+"/"))
402 channel, err := api.GetConversationInfo("CXXXXXXXX", false)
403 if err != nil {
404 t.Errorf("Unexpected error: %s", err)
405 return
406 }
407 if channel == nil {
408 t.Error("channel should not be nil")
409 return
410 }
411 }
412
413 func leaveConversationHandler(rw http.ResponseWriter, r *http.Request) {
414 rw.Header().Set("Content-Type", "application/json")
415 response, _ := json.Marshal(struct {
416 SlackResponse
417 NotInChannel bool `json:"not_in_channel"`
418 }{
419 SlackResponse: SlackResponse{Ok: true}})
420 rw.Write(response)
421 }
422
423 func TestLeaveConversation(t *testing.T) {
424 http.HandleFunc("/conversations.leave", leaveConversationHandler)
425 once.Do(startServer)
426 api := New("testing-token", OptionAPIURL("http://"+serverAddr+"/"))
427 _, err := api.LeaveConversation("CXXXXXXXX")
428 if err != nil {
429 t.Errorf("Unexpected error: %s", err)
430 return
431 }
432 }
433
434 func getConversationRepliesHandler(rw http.ResponseWriter, r *http.Request) {
435 rw.Header().Set("Content-Type", "application/json")
436 response, _ := json.Marshal(struct {
437 SlackResponse
438 HasMore bool `json:"has_more"`
439 ResponseMetaData struct {
440 NextCursor string `json:"next_cursor"`
441 } `json:"response_metadata"`
442 Messages []Message `json:"messages"`
443 }{
444 SlackResponse: SlackResponse{Ok: true},
445 Messages: []Message{}})
446 rw.Write(response)
447 }
448
449 func TestGetConversationReplies(t *testing.T) {
450 http.HandleFunc("/conversations.replies", getConversationRepliesHandler)
451 once.Do(startServer)
452 api := New("testing-token", OptionAPIURL("http://"+serverAddr+"/"))
453 params := GetConversationRepliesParameters{
454 ChannelID: "CXXXXXXXX",
455 Timestamp: "1234567890.123456",
456 }
457 _, _, _, err := api.GetConversationReplies(&params)
458 if err != nil {
459 t.Errorf("Unexpected error: %s", err)
460 return
461 }
462 }
463
464 func getConversationsHandler(rw http.ResponseWriter, r *http.Request) {
465 rw.Header().Set("Content-Type", "application/json")
466 response, _ := json.Marshal(struct {
467 SlackResponse
468 ResponseMetaData struct {
469 NextCursor string `json:"next_cursor"`
470 } `json:"response_metadata"`
471 Channels []Channel `json:"channels"`
472 }{
473 SlackResponse: SlackResponse{Ok: true},
474 Channels: []Channel{}})
475 rw.Write(response)
476 }
477
478 func TestGetConversations(t *testing.T) {
479 http.HandleFunc("/conversations.list", getConversationsHandler)
480 once.Do(startServer)
481 api := New("testing-token", OptionAPIURL("http://"+serverAddr+"/"))
482 params := GetConversationsParameters{}
483 _, _, err := api.GetConversations(&params)
484 if err != nil {
485 t.Errorf("Unexpected error: %s", err)
486 return
487 }
488 }
489
490 func openConversationHandler(rw http.ResponseWriter, r *http.Request) {
491 rw.Header().Set("Content-Type", "application/json")
492 response, _ := json.Marshal(struct {
493 SlackResponse
494 NoOp bool `json:"no_op"`
495 AlreadyOpen bool `json:"already_open"`
496 Channel *Channel `json:"channel"`
497 }{
498 SlackResponse: SlackResponse{Ok: true}})
499 rw.Write(response)
500 }
501
502 func TestOpenConversation(t *testing.T) {
503 http.HandleFunc("/conversations.open", openConversationHandler)
504 once.Do(startServer)
505 api := New("testing-token", OptionAPIURL("http://"+serverAddr+"/"))
506 params := OpenConversationParameters{ChannelID: "CXXXXXXXX"}
507 _, _, _, err := api.OpenConversation(&params)
508 if err != nil {
509 t.Errorf("Unexpected error: %s", err)
510 return
511 }
512 }
513
514 func joinConversationHandler(rw http.ResponseWriter, r *http.Request) {
515 rw.Header().Set("Content-Type", "application/json")
516 response, _ := json.Marshal(struct {
517 Channel *Channel `json:"channel"`
518 Warning string `json:"warning"`
519 ResponseMetaData *struct {
520 Warnings []string `json:"warnings"`
521 } `json:"response_metadata"`
522 SlackResponse
523 }{
524 SlackResponse: SlackResponse{Ok: true}})
525 rw.Write(response)
526 }
527
528 func TestJoinConversation(t *testing.T) {
529 http.HandleFunc("/conversations.join", joinConversationHandler)
530 once.Do(startServer)
531 api := New("testing-token", OptionAPIURL("http://"+serverAddr+"/"))
532 _, _, _, err := api.JoinConversation("CXXXXXXXX")
533 if err != nil {
534 t.Errorf("Unexpected error: %s", err)
535 return
536 }
537 }
538
539 func getConversationHistoryHandler(rw http.ResponseWriter, r *http.Request) {
540 rw.Header().Set("Content-Type", "application/json")
541 response, _ := json.Marshal(GetConversationHistoryResponse{
542 SlackResponse: SlackResponse{Ok: true}})
543 rw.Write(response)
544 }
545
546 func TestGetConversationHistory(t *testing.T) {
547 http.HandleFunc("/conversations.history", getConversationHistoryHandler)
548 once.Do(startServer)
549 api := New("testing-token", OptionAPIURL("http://"+serverAddr+"/"))
550 params := GetConversationHistoryParameters{ChannelID: "CXXXXXXXX"}
551 _, err := api.GetConversationHistory(&params)
552 if err != nil {
553 t.Errorf("Unexpected error: %s", err)
554 return
555 }
556 }
0 package slack
1
2 import (
3 "context"
4 "encoding/json"
5 "strings"
6 )
7
8 // InputType is the type of the dialog input type
9 type InputType string
10
11 const (
12 // InputTypeText textfield input
13 InputTypeText InputType = "text"
14 // InputTypeTextArea textarea input
15 InputTypeTextArea InputType = "textarea"
16 // InputTypeSelect select menus input
17 InputTypeSelect InputType = "select"
18 )
19
20 // DialogInput for dialogs input type text or menu
21 type DialogInput struct {
22 Type InputType `json:"type"`
23 Label string `json:"label"`
24 Name string `json:"name"`
25 Placeholder string `json:"placeholder"`
26 Optional bool `json:"optional"`
27 Hint string `json:"hint"`
28 }
29
30 // DialogTrigger ...
31 type DialogTrigger struct {
32 TriggerID string `json:"trigger_id"` //Required. Must respond within 3 seconds.
33 Dialog Dialog `json:"dialog"` //Required.
34 }
35
36 // Dialog as in Slack dialogs
37 // https://api.slack.com/dialogs#option_element_attributes#top-level_dialog_attributes
38 type Dialog struct {
39 TriggerID string `json:"trigger_id"` // Required
40 CallbackID string `json:"callback_id"` // Required
41 State string `json:"state,omitempty"` // Optional
42 Title string `json:"title"`
43 SubmitLabel string `json:"submit_label,omitempty"`
44 NotifyOnCancel bool `json:"notify_on_cancel"`
45 Elements []DialogElement `json:"elements"`
46 }
47
48 // DialogElement abstract type for dialogs.
49 type DialogElement interface{}
50
51 // DialogCallback DEPRECATED use InteractionCallback
52 type DialogCallback InteractionCallback
53
54 // DialogSubmissionCallback is sent from Slack when a user submits a form from within a dialog
55 type DialogSubmissionCallback struct {
56 State string `json:"state,omitempty"`
57 Submission map[string]string `json:"submission"`
58 }
59
60 // DialogOpenResponse response from `dialog.open`
61 type DialogOpenResponse struct {
62 SlackResponse
63 DialogResponseMetadata DialogResponseMetadata `json:"response_metadata"`
64 }
65
66 // DialogResponseMetadata lists the error messages
67 type DialogResponseMetadata struct {
68 Messages []string `json:"messages"`
69 }
70
71 // DialogInputValidationError is an error when user inputs incorrect value to form from within a dialog
72 type DialogInputValidationError struct {
73 Name string `json:"name"`
74 Error string `json:"error"`
75 }
76
77 // DialogInputValidationErrors lists the name of field and that error messages
78 type DialogInputValidationErrors struct {
79 Errors []DialogInputValidationError `json:"errors"`
80 }
81
82 // OpenDialog opens a dialog window where the triggerID originated from.
83 // EXPERIMENTAL: dialog functionality is currently experimental, api is not considered stable.
84 func (api *Client) OpenDialog(triggerID string, dialog Dialog) (err error) {
85 return api.OpenDialogContext(context.Background(), triggerID, dialog)
86 }
87
88 // OpenDialogContext opens a dialog window where the triggerId originated from with a custom context
89 // EXPERIMENTAL: dialog functionality is currently experimental, api is not considered stable.
90 func (api *Client) OpenDialogContext(ctx context.Context, triggerID string, dialog Dialog) (err error) {
91 if triggerID == "" {
92 return ErrParametersMissing
93 }
94
95 req := DialogTrigger{
96 TriggerID: triggerID,
97 Dialog: dialog,
98 }
99
100 encoded, err := json.Marshal(req)
101 if err != nil {
102 return err
103 }
104
105 response := &DialogOpenResponse{}
106 endpoint := api.endpoint + "dialog.open"
107 if err := postJSON(ctx, api.httpclient, endpoint, api.token, encoded, response, api); err != nil {
108 return err
109 }
110
111 if len(response.DialogResponseMetadata.Messages) > 0 {
112 response.Ok = false
113 response.Error += "\n" + strings.Join(response.DialogResponseMetadata.Messages, "\n")
114 }
115
116 return response.Err()
117 }
0 package slack
1
2 // SelectDataSource types of select datasource
3 type SelectDataSource string
4
5 const (
6 // DialogDataSourceStatic menu with static Options/OptionGroups
7 DialogDataSourceStatic SelectDataSource = "static"
8 // DialogDataSourceExternal dynamic datasource
9 DialogDataSourceExternal SelectDataSource = "external"
10 // DialogDataSourceConversations provides a list of conversations
11 DialogDataSourceConversations SelectDataSource = "conversations"
12 // DialogDataSourceChannels provides a list of channels
13 DialogDataSourceChannels SelectDataSource = "channels"
14 // DialogDataSourceUsers provides a list of users
15 DialogDataSourceUsers SelectDataSource = "users"
16 )
17
18 // DialogInputSelect dialog support for select boxes.
19 type DialogInputSelect struct {
20 DialogInput
21 Value string `json:"value,omitempty"` //Optional.
22 DataSource SelectDataSource `json:"data_source,omitempty"` //Optional. Allowed values: "users", "channels", "conversations", "external".
23 SelectedOptions []DialogSelectOption `json:"selected_options,omitempty"` //Optional. May hold at most one element, for use with "external" only.
24 Options []DialogSelectOption `json:"options,omitempty"` //One of options or option_groups is required.
25 OptionGroups []DialogOptionGroup `json:"option_groups,omitempty"` //Provide up to 100 options.
26 MinQueryLength int `json:"min_query_length,omitempty"` //Optional. minimum characters before query is sent.
27 Hint string `json:"hint,omitempty"` //Optional. Additional hint text.
28 }
29
30 // DialogSelectOption is an option for the user to select from the menu
31 type DialogSelectOption struct {
32 Label string `json:"label"`
33 Value string `json:"value"`
34 }
35
36 // DialogOptionGroup is a collection of options for creating a segmented table
37 type DialogOptionGroup struct {
38 Label string `json:"label"`
39 Options []DialogSelectOption `json:"options"`
40 }
41
42 // NewStaticSelectDialogInput constructor for a `static` datasource menu input
43 func NewStaticSelectDialogInput(name, label string, options []DialogSelectOption) *DialogInputSelect {
44 return &DialogInputSelect{
45 DialogInput: DialogInput{
46 Type: InputTypeSelect,
47 Name: name,
48 Label: label,
49 Optional: true,
50 },
51 DataSource: DialogDataSourceStatic,
52 Options: options,
53 }
54 }
55
56 // NewGroupedSelectDialogInput creates grouped options select input for Dialogs.
57 func NewGroupedSelectDialogInput(name, label string, options []DialogOptionGroup) *DialogInputSelect {
58 return &DialogInputSelect{
59 DialogInput: DialogInput{
60 Type: InputTypeSelect,
61 Name: name,
62 Label: label,
63 },
64 DataSource: DialogDataSourceStatic,
65 OptionGroups: options}
66 }
67
68 // NewDialogOptionGroup creates a DialogOptionGroup from several select options
69 func NewDialogOptionGroup(label string, options ...DialogSelectOption) DialogOptionGroup {
70 return DialogOptionGroup{
71 Label: label,
72 Options: options,
73 }
74 }
75
76 // NewConversationsSelect returns a `Conversations` select
77 func NewConversationsSelect(name, label string) *DialogInputSelect {
78 return newPresetSelect(name, label, DialogDataSourceConversations)
79 }
80
81 // NewChannelsSelect returns a `Channels` select
82 func NewChannelsSelect(name, label string) *DialogInputSelect {
83 return newPresetSelect(name, label, DialogDataSourceChannels)
84 }
85
86 // NewUsersSelect returns a `Users` select
87 func NewUsersSelect(name, label string) *DialogInputSelect {
88 return newPresetSelect(name, label, DialogDataSourceUsers)
89 }
90
91 func newPresetSelect(name, label string, dataSourceType SelectDataSource) *DialogInputSelect {
92 return &DialogInputSelect{
93 DialogInput: DialogInput{
94 Type: InputTypeSelect,
95 Label: label,
96 Name: name,
97 },
98 DataSource: dataSourceType,
99 }
100 }
0 package slack
1
2 import (
3 "testing"
4
5 "github.com/stretchr/testify/assert"
6 )
7
8 func selectOptionsFromArray(options ...string) []DialogSelectOption {
9 selectOptions := make([]DialogSelectOption, len(options))
10 for idx, value := range options {
11 selectOptions[idx] = DialogSelectOption{
12 Label: value,
13 Value: value,
14 }
15 }
16 return selectOptions
17 }
18
19 func selectOptionsFromMap(options map[string]string) []DialogSelectOption {
20 selectOptions := make([]DialogSelectOption, len(options))
21 idx := 0
22 var option DialogSelectOption
23 for key, value := range options {
24 option = DialogSelectOption{
25 Label: key,
26 Value: value,
27 }
28 selectOptions[idx] = option
29 idx++
30 }
31 return selectOptions
32 }
33
34 func TestSelectOptionsFromArray(t *testing.T) {
35 options := []string{"opt 1"}
36 expectedOptions := selectOptionsFromArray(options...)
37 assert.Equal(t, len(options), len(expectedOptions))
38
39 firstOption := expectedOptions[0]
40 assert.Equal(t, "opt 1", firstOption.Label)
41 assert.Equal(t, "opt 1", firstOption.Value)
42 }
43
44 func TestOptionsFromMap(t *testing.T) {
45 options := make(map[string]string)
46 options["key"] = "myValue"
47
48 selectOptions := selectOptionsFromMap(options)
49 assert.Equal(t, 1, len(selectOptions))
50
51 firstOption := selectOptions[0]
52 assert.Equal(t, "key", firstOption.Label)
53 assert.Equal(t, "myValue", firstOption.Value)
54 }
55
56 func TestStaticSelectFromArray(t *testing.T) {
57 name := "static select"
58 label := "Static Select Label"
59 expectedOptions := selectOptionsFromArray("opt 1", "opt 2", "opt 3")
60
61 selectInput := NewStaticSelectDialogInput(name, label, expectedOptions)
62 assert.Equal(t, name, selectInput.Name)
63 assert.Equal(t, label, selectInput.Label)
64 assert.Equal(t, expectedOptions, selectInput.Options)
65 }
66
67 func TestStaticSelectFromDictionary(t *testing.T) {
68 name := "static select"
69 label := "Static Select Label"
70
71 optionsMap := make(map[string]string)
72 optionsMap["option_1"] = "First"
73 optionsMap["option_2"] = "Second"
74 optionsMap["option_3"] = "Third"
75 expectedOptions := selectOptionsFromMap(optionsMap)
76
77 selectInput := NewStaticSelectDialogInput(name, label, expectedOptions)
78 assert.Equal(t, name, selectInput.Name)
79 assert.Equal(t, label, selectInput.Label)
80 assert.Equal(t, expectedOptions, selectInput.Options)
81 }
82
83 func TestNewDialogOptionGroup(t *testing.T) {
84 expectedOptions := selectOptionsFromArray("option_1", "option_2")
85
86 label := "GroupLabel"
87 optionGroup := NewDialogOptionGroup(label, expectedOptions...)
88
89 assert.Equal(t, label, optionGroup.Label)
90 assert.Equal(t, expectedOptions, optionGroup.Options)
91
92 }
93
94 func TestStaticGroupedSelect(t *testing.T) {
95
96 groupOpt1 := NewDialogOptionGroup("group1", selectOptionsFromArray("G1_01", "G1_02")...)
97 groupOpt2 := NewDialogOptionGroup("group2", selectOptionsFromArray("G2_01", "G2_02", "G2_03")...)
98
99 options := []DialogOptionGroup{groupOpt1, groupOpt2}
100
101 groupSelect := NewGroupedSelectDialogInput("groupSelect", "User Label", options)
102 assert.Equal(t, InputTypeSelect, groupSelect.Type)
103 assert.Equal(t, "groupSelect", groupSelect.Name)
104 assert.Equal(t, "User Label", groupSelect.Label)
105 assert.Nil(t, groupSelect.Options)
106 assert.NotNil(t, groupSelect.OptionGroups)
107 assert.Equal(t, 2, len(groupSelect.OptionGroups))
108 }
109
110 func TestConversationSelect(t *testing.T) {
111 convoSelect := NewConversationsSelect("", "")
112 assert.Equal(t, InputTypeSelect, convoSelect.Type)
113 assert.Equal(t, DialogDataSourceConversations, convoSelect.DataSource)
114 }
115
116 func TestChannelSelect(t *testing.T) {
117 convoSelect := NewChannelsSelect("", "")
118 assert.Equal(t, InputTypeSelect, convoSelect.Type)
119 assert.Equal(t, DialogDataSourceChannels, convoSelect.DataSource)
120 }
121
122 func TestUserSelect(t *testing.T) {
123 convoSelect := NewUsersSelect("", "")
124 assert.Equal(t, InputTypeSelect, convoSelect.Type)
125 assert.Equal(t, DialogDataSourceUsers, convoSelect.DataSource)
126 }
0 package slack
1
2 import (
3 "encoding/json"
4 "fmt"
5 "testing"
6
7 "net/http"
8
9 "github.com/stretchr/testify/assert"
10 )
11
12 // Dialogs
13 var simpleDialog = `{
14 "callback_id":"ryde-46e2b0",
15 "title":"Request a Ride",
16 "submit_label":"Request",
17 "notify_on_cancel":true
18 }`
19
20 var simpleTextElement = `{
21 "label": "testing label",
22 "name": "testing name",
23 "type": "text",
24 "placeholder": "testing placeholder",
25 "optional": true,
26 "value": "testing value",
27 "max_length": 1000,
28 "min_length": 10,
29 "hint": "testing hint",
30 "subtype": "email"
31 }`
32
33 var simpleSelectElement = `{
34 "label": "testing label",
35 "name": "testing name",
36 "type": "select",
37 "placeholder": "testing placeholder",
38 "optional": true,
39 "value": "testing value",
40 "data_source": "users",
41 "selected_options": [],
42 "options": [{"label": "option 1", "value": "1"}],
43 "option_groups": []
44 }`
45
46 func unmarshalDialog() (*Dialog, error) {
47 dialog := &Dialog{}
48 // Unmarshall the simple dialog json
49 if err := json.Unmarshal([]byte(simpleDialog), &dialog); err != nil {
50 return nil, err
51 }
52
53 // Unmarshall and append the text element
54 textElement := &TextInputElement{}
55 if err := json.Unmarshal([]byte(simpleTextElement), &textElement); err != nil {
56 return nil, err
57 }
58
59 // Unmarshall and append the select element
60 selectElement := &DialogInputSelect{}
61 if err := json.Unmarshal([]byte(simpleSelectElement), &selectElement); err != nil {
62 return nil, err
63 }
64
65 dialog.Elements = []DialogElement{
66 textElement,
67 selectElement,
68 }
69
70 return dialog, nil
71 }
72
73 func TestSimpleDialog(t *testing.T) {
74 dialog, err := unmarshalDialog()
75 assert.Nil(t, err)
76 assertSimpleDialog(t, dialog)
77 }
78
79 func TestCreateSimpleDialog(t *testing.T) {
80 dialog := &Dialog{}
81 dialog.CallbackID = "ryde-46e2b0"
82 dialog.Title = "Request a Ride"
83 dialog.SubmitLabel = "Request"
84 dialog.NotifyOnCancel = true
85
86 textElement := &TextInputElement{}
87 textElement.Label = "testing label"
88 textElement.Name = "testing name"
89 textElement.Type = "text"
90 textElement.Placeholder = "testing placeholder"
91 textElement.Optional = true
92 textElement.Value = "testing value"
93 textElement.MaxLength = 1000
94 textElement.MinLength = 10
95 textElement.Hint = "testing hint"
96 textElement.Subtype = "email"
97
98 selectElement := &DialogInputSelect{}
99 selectElement.Label = "testing label"
100 selectElement.Name = "testing name"
101 selectElement.Type = "select"
102 selectElement.Placeholder = "testing placeholder"
103 selectElement.Optional = true
104 selectElement.Value = "testing value"
105 selectElement.DataSource = "users"
106 selectElement.SelectedOptions = []DialogSelectOption{}
107 selectElement.Options = []DialogSelectOption{
108 {Label: "option 1", Value: "1"},
109 }
110 selectElement.OptionGroups = []DialogOptionGroup{}
111
112 dialog.Elements = []DialogElement{
113 textElement,
114 selectElement,
115 }
116
117 assertSimpleDialog(t, dialog)
118 }
119
120 func assertSimpleDialog(t *testing.T, dialog *Dialog) {
121 assert.NotNil(t, dialog)
122
123 // Test the main dialog fields
124 assert.Equal(t, "ryde-46e2b0", dialog.CallbackID)
125 assert.Equal(t, "Request a Ride", dialog.Title)
126 assert.Equal(t, "Request", dialog.SubmitLabel)
127 assert.Equal(t, true, dialog.NotifyOnCancel)
128
129 // Test the text element is correctly parsed
130 textElement := dialog.Elements[0].(*TextInputElement)
131 assert.Equal(t, "testing label", textElement.Label)
132 assert.Equal(t, "testing name", textElement.Name)
133 assert.Equal(t, InputTypeText, textElement.Type)
134 assert.Equal(t, "testing placeholder", textElement.Placeholder)
135 assert.Equal(t, true, textElement.Optional)
136 assert.Equal(t, "testing value", textElement.Value)
137 assert.Equal(t, 1000, textElement.MaxLength)
138 assert.Equal(t, 10, textElement.MinLength)
139 assert.Equal(t, "testing hint", textElement.Hint)
140 assert.Equal(t, InputSubtypeEmail, textElement.Subtype)
141
142 // Test the select element is correctly parsed
143 selectElement := dialog.Elements[1].(*DialogInputSelect)
144 assert.Equal(t, "testing label", selectElement.Label)
145 assert.Equal(t, "testing name", selectElement.Name)
146 assert.Equal(t, InputTypeSelect, selectElement.Type)
147 assert.Equal(t, "testing placeholder", selectElement.Placeholder)
148 assert.Equal(t, true, selectElement.Optional)
149 assert.Equal(t, "testing value", selectElement.Value)
150 assert.Equal(t, DialogDataSourceUsers, selectElement.DataSource)
151 assert.Equal(t, []DialogSelectOption{}, selectElement.SelectedOptions)
152 assert.Equal(t, "option 1", selectElement.Options[0].Label)
153 assert.Equal(t, "1", selectElement.Options[0].Value)
154 assert.Equal(t, 0, len(selectElement.OptionGroups))
155 }
156
157 // Callbacks
158 var simpleCallback = `{
159 "type": "dialog_submission",
160 "submission": {
161 "name": "Sigourney Dreamweaver",
162 "email": "sigdre@example.com",
163 "phone": "+1 800-555-1212",
164 "meal": "burrito",
165 "comment": "No sour cream please",
166 "team_channel": "C0LFFBKPB",
167 "who_should_sing": "U0MJRG1AL"
168 },
169 "callback_id": "employee_offsite_1138b",
170 "team": {
171 "id": "T1ABCD2E12",
172 "domain": "coverbands"
173 },
174 "user": {
175 "id": "W12A3BCDEF",
176 "name": "dreamweaver"
177 },
178 "channel": {
179 "id": "C1AB2C3DE",
180 "name": "coverthon-1999"
181 },
182 "action_ts": "936893340.702759",
183 "token": "M1AqUUw3FqayAbqNtsGMch72",
184 "response_url": "https://hooks.slack.com/app/T012AB0A1/123456789/JpmK0yzoZDeRiqfeduTBYXWQ"
185 }`
186
187 func unmarshalCallback(j string) (*DialogCallback, error) {
188 callback := &DialogCallback{}
189 if err := json.Unmarshal([]byte(j), &callback); err != nil {
190 return nil, err
191 }
192 return callback, nil
193 }
194
195 func TestSimpleCallback(t *testing.T) {
196 callback, err := unmarshalCallback(simpleCallback)
197 assert.Nil(t, err)
198 assertSimpleCallback(t, callback)
199 }
200
201 func assertSimpleCallback(t *testing.T, callback *DialogCallback) {
202 assert.NotNil(t, callback)
203 assert.Equal(t, InteractionTypeDialogSubmission, callback.Type)
204 assert.Equal(t, "employee_offsite_1138b", callback.CallbackID)
205 assert.Equal(t, "T1ABCD2E12", callback.Team.ID)
206 assert.Equal(t, "coverbands", callback.Team.Domain)
207 assert.Equal(t, "C1AB2C3DE", callback.Channel.ID)
208 assert.Equal(t, "coverthon-1999", callback.Channel.Name)
209 assert.Equal(t, "W12A3BCDEF", callback.User.ID)
210 assert.Equal(t, "dreamweaver", callback.User.Name)
211 assert.Equal(t, "936893340.702759", callback.ActionTs)
212 assert.Equal(t, "M1AqUUw3FqayAbqNtsGMch72", callback.Token)
213 assert.Equal(t, "https://hooks.slack.com/app/T012AB0A1/123456789/JpmK0yzoZDeRiqfeduTBYXWQ", callback.ResponseURL)
214 assert.Equal(t, "Sigourney Dreamweaver", callback.Submission["name"])
215 assert.Equal(t, "sigdre@example.com", callback.Submission["email"])
216 assert.Equal(t, "+1 800-555-1212", callback.Submission["phone"])
217 assert.Equal(t, "burrito", callback.Submission["meal"])
218 assert.Equal(t, "No sour cream please", callback.Submission["comment"])
219 assert.Equal(t, "C0LFFBKPB", callback.Submission["team_channel"])
220 assert.Equal(t, "U0MJRG1AL", callback.Submission["who_should_sing"])
221 }
222
223 // Suggestion Callbacks
224 var simpleSuggestionCallback = `{
225 "type": "dialog_suggestion",
226 "token": "W3VDvuzi2nRLsiaDOsmJranO",
227 "action_ts": "1528203589.238335",
228 "team": {
229 "id": "T24BK35ML",
230 "domain": "hooli-hq"
231 },
232 "user": {
233 "id": "U900MV5U7",
234 "name": "gbelson"
235 },
236 "channel": {
237 "id": "C012AB3CD",
238 "name": "triage-platform"
239 },
240 "name": "external_data",
241 "value": "test",
242 "callback_id": "bugs"
243 }`
244
245 func unmarshalSuggestionCallback(j string) (*InteractionCallback, error) {
246 callback := &InteractionCallback{}
247 if err := json.Unmarshal([]byte(j), &callback); err != nil {
248 return nil, err
249 }
250 return callback, nil
251 }
252
253 func TestSimpleSuggestionCallback(t *testing.T) {
254 callback, err := unmarshalSuggestionCallback(simpleSuggestionCallback)
255 assert.Nil(t, err)
256 assertSimpleSuggestionCallback(t, callback)
257 }
258
259 func assertSimpleSuggestionCallback(t *testing.T, callback *InteractionCallback) {
260 assert.NotNil(t, callback)
261 assert.Equal(t, InteractionTypeDialogSuggestion, callback.Type)
262 assert.Equal(t, "W3VDvuzi2nRLsiaDOsmJranO", callback.Token)
263 assert.Equal(t, "1528203589.238335", callback.ActionTs)
264 assert.Equal(t, "T24BK35ML", callback.Team.ID)
265 assert.Equal(t, "hooli-hq", callback.Team.Domain)
266 assert.Equal(t, "U900MV5U7", callback.User.ID)
267 assert.Equal(t, "gbelson", callback.User.Name)
268 assert.Equal(t, "C012AB3CD", callback.Channel.ID)
269 assert.Equal(t, "triage-platform", callback.Channel.Name)
270 assert.Equal(t, "external_data", callback.Name)
271 assert.Equal(t, "test", callback.Value)
272 assert.Equal(t, "bugs", callback.CallbackID)
273 }
274
275 func openDialogHandler(rw http.ResponseWriter, r *http.Request) {
276 rw.Header().Set("Content-Type", "application/json")
277 response, _ := json.Marshal(struct {
278 SlackResponse
279 }{
280 SlackResponse: SlackResponse{Ok: true},
281 })
282 rw.Write(response)
283 }
284
285 func TestOpenDialog(t *testing.T) {
286 http.HandleFunc("/dialog.open", openDialogHandler)
287 once.Do(startServer)
288 api := New("testing-token", OptionAPIURL("http://"+serverAddr+"/"))
289 dialog, err := unmarshalDialog()
290 if err != nil {
291 t.Errorf("Unexpected error: %s", err)
292 return
293 }
294 err = api.OpenDialog("TXXXXXXXX", *dialog)
295 if err != nil {
296 t.Errorf("Unexpected error: %s", err)
297 return
298 }
299 err = api.OpenDialog("", *dialog)
300 if err == nil {
301 t.Errorf("Did not error with empty trigger, %s", err)
302 return
303 }
304 }
305
306 const (
307 triggerID = "trigger_xyz"
308 callbackID = "callback_xyz"
309 notifyOnCancel = false
310 title = "Dialog_title"
311 submitLabel = "Send"
312 token = "xoxa-123-123-123-213"
313 )
314
315 func _mocDialog() *Dialog {
316 triggerID := triggerID
317 callbackID := callbackID
318 notifyOnCancel := notifyOnCancel
319 title := title
320 submitLabel := submitLabel
321
322 return &Dialog{
323 TriggerID: triggerID,
324 CallbackID: callbackID,
325 NotifyOnCancel: notifyOnCancel,
326 Title: title,
327 SubmitLabel: submitLabel,
328 }
329 }
330
331 func TestDialogCreate(t *testing.T) {
332 dialog := _mocDialog()
333 if dialog == nil {
334 t.Errorf("Should be able to construct a dialog")
335 t.Fail()
336 }
337 }
338
339 func ExampleDialog() {
340 dialog := _mocDialog()
341 fmt.Println(*dialog)
342 // Output:
343 // {trigger_xyz callback_xyz Dialog_title Send false []}
344 }
0 package slack
1
2 // TextInputSubtype Accepts email, number, tel, or url. In some form factors, optimized input is provided for this subtype.
3 type TextInputSubtype string
4
5 // TextInputOption handle to extra inputs options.
6 type TextInputOption func(*TextInputElement)
7
8 const (
9 // InputSubtypeEmail email keyboard
10 InputSubtypeEmail TextInputSubtype = "email"
11 // InputSubtypeNumber numeric keyboard
12 InputSubtypeNumber TextInputSubtype = "number"
13 // InputSubtypeTel Phone keyboard
14 InputSubtypeTel TextInputSubtype = "tel"
15 // InputSubtypeURL Phone keyboard
16 InputSubtypeURL TextInputSubtype = "url"
17 )
18
19 // TextInputElement subtype of DialogInput
20 // https://api.slack.com/dialogs#option_element_attributes#text_element_attributes
21 type TextInputElement struct {
22 DialogInput
23 MaxLength int `json:"max_length,omitempty"`
24 MinLength int `json:"min_length,omitempty"`
25 Hint string `json:"hint,omitempty"`
26 Subtype TextInputSubtype `json:"subtype"`
27 Value string `json:"value"`
28 }
29
30 // NewTextInput constructor for a `text` input
31 func NewTextInput(name, label, text string, options ...TextInputOption) *TextInputElement {
32 t := &TextInputElement{
33 DialogInput: DialogInput{
34 Type: InputTypeText,
35 Name: name,
36 Label: label,
37 },
38 Value: text,
39 }
40
41 for _, opt := range options {
42 opt(t)
43 }
44
45 return t
46 }
47
48 // NewTextAreaInput constructor for a `textarea` input
49 func NewTextAreaInput(name, label, text string) *TextInputElement {
50 return &TextInputElement{
51 DialogInput: DialogInput{
52 Type: InputTypeTextArea,
53 Name: name,
54 Label: label,
55 },
56 Value: text,
57 }
58 }
0 package slack
1
2 import (
3 "testing"
4
5 "github.com/stretchr/testify/assert"
6 )
7
8 func TestNewTextInput(t *testing.T) {
9 name := "internalName"
10 label := "Human Readable"
11 value := "Pre filled text"
12 textInput := NewTextInput(name, label, value)
13 assert.Equal(t, InputTypeText, textInput.Type)
14 assert.Equal(t, name, textInput.Name)
15 assert.Equal(t, label, textInput.Label)
16 assert.Equal(t, value, textInput.Value)
17 }
18
19 func TestNewTextAreaInput(t *testing.T) {
20 name := "internalName"
21 label := "Human Readable"
22 value := "Pre filled text"
23 textInput := NewTextAreaInput(name, label, value)
24 assert.Equal(t, InputTypeTextArea, textInput.Type)
25 assert.Equal(t, name, textInput.Name)
26 assert.Equal(t, label, textInput.Label)
27 assert.Equal(t, value, textInput.Value)
28 }
00 package slack
11
22 import (
3 "errors"
3 "context"
44 "net/url"
55 "strconv"
66 "strings"
3434 SlackResponse
3535 }
3636
37 func dndRequest(path string, values url.Values, debug bool) (*dndResponseFull, error) {
37 func (api *Client) dndRequest(ctx context.Context, path string, values url.Values) (*dndResponseFull, error) {
3838 response := &dndResponseFull{}
39 err := post(path, values, response, debug)
39 err := api.postMethod(ctx, path, values, response)
4040 if err != nil {
4141 return nil, err
4242 }
43 if !response.Ok {
44 return nil, errors.New(response.Error)
45 }
46 return response, nil
43
44 return response, response.Err()
4745 }
4846
4947 // EndDND ends the user's scheduled Do Not Disturb session
5048 func (api *Client) EndDND() error {
49 return api.EndDNDContext(context.Background())
50 }
51
52 // EndDNDContext ends the user's scheduled Do Not Disturb session with a custom context
53 func (api *Client) EndDNDContext(ctx context.Context) error {
5154 values := url.Values{
52 "token": {api.config.token},
55 "token": {api.token},
5356 }
5457
5558 response := &SlackResponse{}
56 if err := post("dnd.endDnd", values, response, api.debug); err != nil {
59
60 if err := api.postMethod(ctx, "dnd.endDnd", values, response); err != nil {
5761 return err
5862 }
59 if !response.Ok {
60 return errors.New(response.Error)
61 }
62 return nil
63
64 return response.Err()
6365 }
6466
6567 // EndSnooze ends the current user's snooze mode
6668 func (api *Client) EndSnooze() (*DNDStatus, error) {
69 return api.EndSnoozeContext(context.Background())
70 }
71
72 // EndSnoozeContext ends the current user's snooze mode with a custom context
73 func (api *Client) EndSnoozeContext(ctx context.Context) (*DNDStatus, error) {
6774 values := url.Values{
68 "token": {api.config.token},
75 "token": {api.token},
6976 }
7077
71 response, err := dndRequest("dnd.endSnooze", values, api.debug)
78 response, err := api.dndRequest(ctx, "dnd.endSnooze", values)
7279 if err != nil {
7380 return nil, err
7481 }
7784
7885 // GetDNDInfo provides information about a user's current Do Not Disturb settings.
7986 func (api *Client) GetDNDInfo(user *string) (*DNDStatus, error) {
87 return api.GetDNDInfoContext(context.Background(), user)
88 }
89
90 // GetDNDInfoContext provides information about a user's current Do Not Disturb settings with a custom context.
91 func (api *Client) GetDNDInfoContext(ctx context.Context, user *string) (*DNDStatus, error) {
8092 values := url.Values{
81 "token": {api.config.token},
93 "token": {api.token},
8294 }
8395 if user != nil {
8496 values.Set("user", *user)
8597 }
86 response, err := dndRequest("dnd.info", values, api.debug)
98
99 response, err := api.dndRequest(ctx, "dnd.info", values)
87100 if err != nil {
88101 return nil, err
89102 }
92105
93106 // GetDNDTeamInfo provides information about a user's current Do Not Disturb settings.
94107 func (api *Client) GetDNDTeamInfo(users []string) (map[string]DNDStatus, error) {
108 return api.GetDNDTeamInfoContext(context.Background(), users)
109 }
110
111 // GetDNDTeamInfoContext provides information about a user's current Do Not Disturb settings with a custom context.
112 func (api *Client) GetDNDTeamInfoContext(ctx context.Context, users []string) (map[string]DNDStatus, error) {
95113 values := url.Values{
96 "token": {api.config.token},
114 "token": {api.token},
97115 "users": {strings.Join(users, ",")},
98116 }
99117 response := &dndTeamInfoResponse{}
100 if err := post("dnd.teamInfo", values, response, api.debug); err != nil {
118
119 if err := api.postMethod(ctx, "dnd.teamInfo", values, response); err != nil {
101120 return nil, err
102121 }
103 if !response.Ok {
104 return nil, errors.New(response.Error)
122
123 if response.Err() != nil {
124 return nil, response.Err()
105125 }
126
106127 return response.Users, nil
107128 }
108129
110131 // settings. If a snooze session is not already active for the user, invoking
111132 // this method will begin one for the specified duration.
112133 func (api *Client) SetSnooze(minutes int) (*DNDStatus, error) {
134 return api.SetSnoozeContext(context.Background(), minutes)
135 }
136
137 // SetSnoozeContext adjusts the snooze duration for a user's Do Not Disturb settings with a custom context.
138 // For more information see the SetSnooze docs
139 func (api *Client) SetSnoozeContext(ctx context.Context, minutes int) (*DNDStatus, error) {
113140 values := url.Values{
114 "token": {api.config.token},
141 "token": {api.token},
115142 "num_minutes": {strconv.Itoa(minutes)},
116143 }
117 response, err := dndRequest("dnd.setSnooze", values, api.debug)
144
145 response, err := api.dndRequest(ctx, "dnd.setSnooze", values)
118146 if err != nil {
119147 return nil, err
120148 }
1111 w.Write([]byte(`{ "ok": true }`))
1212 })
1313 once.Do(startServer)
14 SLACK_API = "http://" + serverAddr + "/"
15 api := New("testing-token")
14 api := New("testing-token", OptionAPIURL("http://"+serverAddr+"/"))
1615 err := api.EndDND()
1716 if err != nil {
1817 t.Fatalf("Unexpected error: %s", err)
3534 SnoozeInfo: SnoozeInfo{SnoozeEnabled: false},
3635 }
3736 once.Do(startServer)
38 SLACK_API = "http://" + serverAddr + "/"
39 api := New("testing-token")
37 api := New("testing-token", OptionAPIURL("http://"+serverAddr+"/"))
4038 snoozeState, err := api.EndSnooze()
4139 if err != nil {
4240 t.Fatalf("Unexpected error: %s", err)
7169 },
7270 }
7371 once.Do(startServer)
74 SLACK_API = "http://" + serverAddr + "/"
75 api := New("testing-token")
72 api := New("testing-token", OptionAPIURL("http://"+serverAddr+"/"))
7673 userDNDInfoResponse, err := api.GetDNDInfo(nil)
7774 if err != nil {
7875 t.Fatalf("Unexpected error: %s", err)
103100 }`))
104101 })
105102 usersDNDInfo := map[string]DNDStatus{
106 "U023BECGF": DNDStatus{
103 "U023BECGF": {
107104 Enabled: true,
108105 NextStartTimestamp: 1450387800,
109106 NextEndTimestamp: 1450423800,
110107 },
111 "U058CJVAA": DNDStatus{
108 "U058CJVAA": {
112109 Enabled: false,
113110 NextStartTimestamp: 1,
114111 NextEndTimestamp: 1,
115112 },
116113 }
117114 once.Do(startServer)
118 SLACK_API = "http://" + serverAddr + "/"
119 api := New("testing-token")
115 api := New("testing-token", OptionAPIURL("http://"+serverAddr+"/"))
120116 usersDNDInfoResponse, err := api.GetDNDTeamInfo(nil)
121117 if err != nil {
122118 t.Fatalf("Unexpected error: %s", err)
145141 },
146142 }
147143 once.Do(startServer)
148 SLACK_API = "http://" + serverAddr + "/"
149 api := New("testing-token")
144 api := New("testing-token", OptionAPIURL("http://"+serverAddr+"/"))
150145 snoozeResponse, err := api.SetSnooze(60)
151146 if err != nil {
152147 t.Fatalf("Unexpected error: %s", err)
00 package slack
11
22 import (
3 "errors"
3 "context"
44 "net/url"
55 )
66
1111
1212 // GetEmoji retrieves all the emojis
1313 func (api *Client) GetEmoji() (map[string]string, error) {
14 return api.GetEmojiContext(context.Background())
15 }
16
17 // GetEmojiContext retrieves all the emojis with a custom context
18 func (api *Client) GetEmojiContext(ctx context.Context) (map[string]string, error) {
1419 values := url.Values{
15 "token": {api.config.token},
20 "token": {api.token},
1621 }
1722 response := &emojiResponseFull{}
18 err := post("emoji.list", values, response, api.debug)
23
24 err := api.postMethod(ctx, "emoji.list", values, response)
1925 if err != nil {
2026 return nil, err
2127 }
22 if !response.Ok {
23 return nil, errors.New(response.Error)
28
29 if response.Err() != nil {
30 return nil, response.Err()
2431 }
32
2533 return response.Emoji, nil
2634 }
1919 http.HandleFunc("/emoji.list", getEmojiHandler)
2020
2121 once.Do(startServer)
22 SLACK_API = "http://" + serverAddr + "/"
23 api := New("testing-token")
22 api := New("testing-token", OptionAPIURL("http://"+serverAddr+"/"))
2423 emojisResponse := map[string]string{
2524 "bowtie": "https://my.slack.com/emoji/bowtie/46ec6f2bb0.png",
2625 "squirrel": "https://my.slack.com/emoji/squirrel/f35f40c0e0.png",
0 package slack
1
2 import "github.com/nlopes/slack/internal/errorsx"
3
4 // Errors returned by various methods.
5 const (
6 ErrAlreadyDisconnected = errorsx.String("Invalid call to Disconnect - Slack API is already disconnected")
7 ErrRTMDisconnected = errorsx.String("disconnect received while trying to connect")
8 ErrParametersMissing = errorsx.String("received empty parameters")
9 ErrInvalidConfiguration = errorsx.String("invalid configuration")
10 ErrMissingHeaders = errorsx.String("missing headers")
11 ErrExpiredTimestamp = errorsx.String("timestamp is too old")
12 )
13
14 // internal errors
15 const (
16 errPaginationComplete = errorsx.String("pagination complete")
17 )
0 ### Block Examples
1
2 The examples provided replicate the template examples provided by slack. The template builder can be found at https://api.slack.com/tools/block-kit-builder.
3
4 Due to the nature how slack expects different components to be configured, building complex block using the provided functions can be very verbose, but allows for maximum flexibility.
5
6 The examples below should cover implementing most supported block elements.
7
8 For additional information on Blocks, see the [Block Kit website](https://api.slack.com/block-kit).
9
10 ### Using examples with the Block Kit Builder website
11 When generating examples, they will be printed to the screen as a complete message that is meant to be sent back to slack as a direct response, or throuogh the ResponseURL provided. To test your examples in the Block Kit Builder, you must take the contents of the `blocks` property and paste the results into the builder.
12
13 For example, when printing a simple header, the output will be
14
15 ```
16 {
17 "replace_original": false,
18 "delete_original": false,
19 "blocks": [
20 {
21 "type": "section",
22 "text": {
23 "type": "plain_text",
24 "text": "Example Header Text"
25 }
26 },
27 {
28 "type": "divider"
29 }
30 ]
31 }
32 ```
33
34 To preview this block on the builder website, you should copy just the contents of the blocks:
35
36 ```
37 [
38 {
39 "type": "section",
40 "text": {
41 "type": "plain_text",
42 "text": "Example Header Text"
43 }
44 },
45 {
46 "type": "divider"
47 }
48 ]
49 ```
50
51 #### Example 1 - Approval
52 The first example demonstrates usage of Sections, Fields and Action buttons. You can view the [Approval Example](https://api.slack.com/tools/block-kit-builder?blocks=%5B%0A%09%7B%0A%09%09%22type%22%3A%20%22section%22%2C%0A%09%09%22text%22%3A%20%7B%0A%09%09%09%22type%22%3A%20%22mrkdwn%22%2C%0A%09%09%09%22text%22%3A%20%22You%20have%20a%20new%20request%3A%5Cn*%3CfakeLink.toEmployeeProfile.com%7CFred%20Enriquez%20-%20New%20device%20request%3E*%22%0A%09%09%7D%0A%09%7D%2C%0A%09%7B%0A%09%09%22type%22%3A%20%22section%22%2C%0A%09%09%22fields%22%3A%20%5B%0A%09%09%09%7B%0A%09%09%09%09%22type%22%3A%20%22mrkdwn%22%2C%0A%09%09%09%09%22text%22%3A%20%22*Type%3A*%5CnComputer%20(laptop)%22%0A%09%09%09%7D%2C%0A%09%09%09%7B%0A%09%09%09%09%22type%22%3A%20%22mrkdwn%22%2C%0A%09%09%09%09%22text%22%3A%20%22*When%3A*%5CnSubmitted%20Aut%2010%22%0A%09%09%09%7D%2C%0A%09%09%09%7B%0A%09%09%09%09%22type%22%3A%20%22mrkdwn%22%2C%0A%09%09%09%09%22text%22%3A%20%22*Last%20Update%3A*%5CnMar%2010%2C%202015%20(3%20years%2C%205%20months)%22%0A%09%09%09%7D%2C%0A%09%09%09%7B%0A%09%09%09%09%22type%22%3A%20%22mrkdwn%22%2C%0A%09%09%09%09%22text%22%3A%20%22*Reason%3A*%5CnAll%20vowel%20keys%20aren%27t%20working.%22%0A%09%09%09%7D%2C%0A%09%09%09%7B%0A%09%09%09%09%22type%22%3A%20%22mrkdwn%22%2C%0A%09%09%09%09%22text%22%3A%20%22*Specs%3A*%5Cn%5C%22Cheetah%20Pro%2015%5C%22%20-%20Fast%2C%20really%20fast%5C%22%22%0A%09%09%09%7D%0A%09%09%5D%0A%09%7D%2C%0A%09%7B%0A%09%09%22type%22%3A%20%22actions%22%2C%0A%09%09%22elements%22%3A%20%5B%0A%09%09%09%7B%0A%09%09%09%09%22type%22%3A%20%22button%22%2C%0A%09%09%09%09%22text%22%3A%20%7B%0A%09%09%09%09%09%22type%22%3A%20%22plain_text%22%2C%0A%09%09%09%09%09%22emoji%22%3A%20true%2C%0A%09%09%09%09%09%22text%22%3A%20%22Approve%22%0A%09%09%09%09%7D%2C%0A%09%09%09%09%22value%22%3A%20%22click_me_123%22%0A%09%09%09%7D%2C%0A%09%09%09%7B%0A%09%09%09%09%22type%22%3A%20%22button%22%2C%0A%09%09%09%09%22text%22%3A%20%7B%0A%09%09%09%09%09%22type%22%3A%20%22plain_text%22%2C%0A%09%09%09%09%09%22emoji%22%3A%20true%2C%0A%09%09%09%09%09%22text%22%3A%20%22Deny%22%0A%09%09%09%09%7D%2C%0A%09%09%09%09%22value%22%3A%20%22click_me_123%22%0A%09%09%09%7D%0A%09%09%5D%0A%09%7D%0A%5D) on the block kit builder website. This example can be generated with the function named `exampleOne`.
53
54 #### Example 2 - Approval - With Images
55 The secoond example adds additional complexity by introducing images as accessories to main blocks of text. You can view this [Approval Example with Images](https://api.slack.com/tools/block-kit-builder?blocks=%5B%0A%09%7B%0A%09%09%22type%22%3A%20%22section%22%2C%0A%09%09%22text%22%3A%20%7B%0A%09%09%09%22type%22%3A%20%22mrkdwn%22%2C%0A%09%09%09%22text%22%3A%20%22You%20have%20a%20new%20request%3A%5Cn*%3Cgoogle.com%7CFred%20Enriquez%20-%20Time%20Off%20request%3E*%22%0A%09%09%7D%0A%09%7D%2C%0A%09%7B%0A%09%09%22type%22%3A%20%22section%22%2C%0A%09%09%22text%22%3A%20%7B%0A%09%09%09%22type%22%3A%20%22mrkdwn%22%2C%0A%09%09%09%22text%22%3A%20%22*Type%3A*%5CnPaid%20time%20off%5Cn*When%3A*%5CnAug%2010-Aug%2013%5Cn*Hours%3A*%2016.0%20(2%20days)%5Cn*Remaining%20balance%3A*%2032.0%20hours%20(4%20days)%5Cn*Comments%3A*%20%5C%22Family%20in%20town%2C%20going%20camping!%5C%22%22%0A%09%09%7D%2C%0A%09%09%22accessory%22%3A%20%7B%0A%09%09%09%22type%22%3A%20%22image%22%2C%0A%09%09%09%22image_url%22%3A%20%22https%3A%2F%2Fapi.slack.com%2Fimg%2Fblocks%2Fbkb_template_images%2FapprovalsNewDevice.png%22%2C%0A%09%09%09%22alt_text%22%3A%20%22computer%20thumbnail%22%0A%09%09%7D%0A%09%7D%2C%0A%09%7B%0A%09%09%22type%22%3A%20%22actions%22%2C%0A%09%09%22elements%22%3A%20%5B%0A%09%09%09%7B%0A%09%09%09%09%22type%22%3A%20%22button%22%2C%0A%09%09%09%09%22text%22%3A%20%7B%0A%09%09%09%09%09%22type%22%3A%20%22plain_text%22%2C%0A%09%09%09%09%09%22emoji%22%3A%20true%2C%0A%09%09%09%09%09%22text%22%3A%20%22Approve%22%0A%09%09%09%09%7D%2C%0A%09%09%09%09%22value%22%3A%20%22click_me_123%22%0A%09%09%09%7D%2C%0A%09%09%09%7B%0A%09%09%09%09%22type%22%3A%20%22button%22%2C%0A%09%09%09%09%22text%22%3A%20%7B%0A%09%09%09%09%09%22type%22%3A%20%22plain_text%22%2C%0A%09%09%09%09%09%22emoji%22%3A%20true%2C%0A%09%09%09%09%09%22text%22%3A%20%22Deny%22%0A%09%09%09%09%7D%2C%0A%09%09%09%09%22value%22%3A%20%22click_me_123%22%0A%09%09%09%7D%0A%09%09%5D%0A%09%7D%0A%5D) on the block kit builder website. This example can be generated with the function named `exampleTwo`.
56
57 #### Example 3 - Notifications
58 This example shows how to add actions to your block that will trigger an interactive message to your application. You can view the rendered example for [Notifications](https://api.slack.com/tools/block-kit-builder?blocks=%5B%0A%09%7B%0A%09%09%22type%22%3A%20%22section%22%2C%0A%09%09%22text%22%3A%20%7B%0A%09%09%09%22type%22%3A%20%22plain_text%22%2C%0A%09%09%09%22emoji%22%3A%20true%2C%0A%09%09%09%22text%22%3A%20%22Looks%20like%20you%20have%20a%20scheduling%20conflict%20with%20this%20event%3A%22%0A%09%09%7D%0A%09%7D%2C%0A%09%7B%0A%09%09%22type%22%3A%20%22divider%22%0A%09%7D%2C%0A%09%7B%0A%09%09%22type%22%3A%20%22section%22%2C%0A%09%09%22text%22%3A%20%7B%0A%09%09%09%22type%22%3A%20%22mrkdwn%22%2C%0A%09%09%09%22text%22%3A%20%22*%3CfakeLink.toUserProfiles.com%7CIris%20%2F%20Zelda%201-1%3E*%5CnTuesday%2C%20January%2021%204%3A00-4%3A30pm%5CnBuilding%202%20-%20Havarti%20Cheese%20(3)%5Cn2%20guests%22%0A%09%09%7D%2C%0A%09%09%22accessory%22%3A%20%7B%0A%09%09%09%22type%22%3A%20%22image%22%2C%0A%09%09%09%22image_url%22%3A%20%22https%3A%2F%2Fapi.slack.com%2Fimg%2Fblocks%2Fbkb_template_images%2Fnotifications.png%22%2C%0A%09%09%09%22alt_text%22%3A%20%22calendar%20thumbnail%22%0A%09%09%7D%0A%09%7D%2C%0A%09%7B%0A%09%09%22type%22%3A%20%22context%22%2C%0A%09%09%22elements%22%3A%20%5B%0A%09%09%09%7B%0A%09%09%09%09%22type%22%3A%20%22image%22%2C%0A%09%09%09%09%22image_url%22%3A%20%22https%3A%2F%2Fapi.slack.com%2Fimg%2Fblocks%2Fbkb_template_images%2FnotificationsWarningIcon.png%22%2C%0A%09%09%09%09%22alt_text%22%3A%20%22notifications%20warning%20icon%22%0A%09%09%09%7D%2C%0A%09%09%09%7B%0A%09%09%09%09%22type%22%3A%20%22mrkdwn%22%2C%0A%09%09%09%09%22text%22%3A%20%22*Conflicts%20with%20Team%20Huddle%3A%204%3A15-4%3A30pm*%22%0A%09%09%09%7D%0A%09%09%5D%0A%09%7D%2C%0A%09%7B%0A%09%09%22type%22%3A%20%22divider%22%0A%09%7D%2C%0A%09%7B%0A%09%09%22type%22%3A%20%22section%22%2C%0A%09%09%22text%22%3A%20%7B%0A%09%09%09%22type%22%3A%20%22mrkdwn%22%2C%0A%09%09%09%22text%22%3A%20%22*Propose%20a%20new%20time%3A*%22%0A%09%09%7D%0A%09%7D%2C%0A%09%7B%0A%09%09%22type%22%3A%20%22section%22%2C%0A%09%09%22text%22%3A%20%7B%0A%09%09%09%22type%22%3A%20%22mrkdwn%22%2C%0A%09%09%09%22text%22%3A%20%22*Today%20-%204%3A30-5pm*%5CnEveryone%20is%20available%3A%20%40iris%2C%20%40zelda%22%0A%09%09%7D%2C%0A%09%09%22accessory%22%3A%20%7B%0A%09%09%09%22type%22%3A%20%22button%22%2C%0A%09%09%09%22text%22%3A%20%7B%0A%09%09%09%09%22type%22%3A%20%22plain_text%22%2C%0A%09%09%09%09%22emoji%22%3A%20true%2C%0A%09%09%09%09%22text%22%3A%20%22Choose%22%0A%09%09%09%7D%2C%0A%09%09%09%22value%22%3A%20%22click_me_123%22%0A%09%09%7D%0A%09%7D%2C%0A%09%7B%0A%09%09%22type%22%3A%20%22section%22%2C%0A%09%09%22text%22%3A%20%7B%0A%09%09%09%22type%22%3A%20%22mrkdwn%22%2C%0A%09%09%09%22text%22%3A%20%22*Tomorrow%20-%204-4%3A30pm*%5CnEveryone%20is%20available%3A%20%40iris%2C%20%40zelda%22%0A%09%09%7D%2C%0A%09%09%22accessory%22%3A%20%7B%0A%09%09%09%22type%22%3A%20%22button%22%2C%0A%09%09%09%22text%22%3A%20%7B%0A%09%09%09%09%22type%22%3A%20%22plain_text%22%2C%0A%09%09%09%09%22emoji%22%3A%20true%2C%0A%09%09%09%09%22text%22%3A%20%22Choose%22%0A%09%09%09%7D%2C%0A%09%09%09%22value%22%3A%20%22click_me_123%22%0A%09%09%7D%0A%09%7D%2C%0A%09%7B%0A%09%09%22type%22%3A%20%22section%22%2C%0A%09%09%22text%22%3A%20%7B%0A%09%09%09%22type%22%3A%20%22mrkdwn%22%2C%0A%09%09%09%22text%22%3A%20%22*Tomorrow%20-%206-6%3A30pm*%5CnSome%20people%20aren%27t%20available%3A%20%40iris%2C%20~%40zelda~%22%0A%09%09%7D%2C%0A%09%09%22accessory%22%3A%20%7B%0A%09%09%09%22type%22%3A%20%22button%22%2C%0A%09%09%09%22text%22%3A%20%7B%0A%09%09%09%09%22type%22%3A%20%22plain_text%22%2C%0A%09%09%09%09%22emoji%22%3A%20true%2C%0A%09%09%09%09%22text%22%3A%20%22Choose%22%0A%09%09%09%7D%2C%0A%09%09%09%22value%22%3A%20%22click_me_123%22%0A%09%09%7D%0A%09%7D%2C%0A%09%7B%0A%09%09%22type%22%3A%20%22section%22%2C%0A%09%09%22text%22%3A%20%7B%0A%09%09%09%22type%22%3A%20%22mrkdwn%22%2C%0A%09%09%09%22text%22%3A%20%22*%3Cfakelink.ToMoreTimes.com%7CShow%20more%20times%3E*%22%0A%09%09%7D%0A%09%7D%0A%5D) on the block builder website. Refer to the function `exampleThree` for details on how this block can be generated.
59
60 #### Example 4 - Polls
61 The Polls example displays results and allows the end user to vote, displaying a count and images of recent voters. You can view the rendered [Poll Example](https://api.slack.com/tools/block-kit-builder?blocks=%5B%0A%09%7B%0A%09%09%22type%22%3A%20%22section%22%2C%0A%09%09%22text%22%3A%20%7B%0A%09%09%09%22type%22%3A%20%22mrkdwn%22%2C%0A%09%09%09%22text%22%3A%20%22*Where%20should%20we%20order%20lunch%20from%3F*%20Poll%20by%20%3CfakeLink.toUser.com%7CMark%3E%22%0A%09%09%7D%0A%09%7D%2C%0A%09%7B%0A%09%09%22type%22%3A%20%22divider%22%0A%09%7D%2C%0A%09%7B%0A%09%09%22type%22%3A%20%22section%22%2C%0A%09%09%22text%22%3A%20%7B%0A%09%09%09%22type%22%3A%20%22mrkdwn%22%2C%0A%09%09%09%22text%22%3A%20%22%3Asushi%3A%20*Ace%20Wasabi%20Rock-n-Roll%20Sushi%20Bar*%5CnThe%20best%20landlocked%20sushi%20restaurant.%22%0A%09%09%7D%2C%0A%09%09%22accessory%22%3A%20%7B%0A%09%09%09%22type%22%3A%20%22button%22%2C%0A%09%09%09%22text%22%3A%20%7B%0A%09%09%09%09%22type%22%3A%20%22plain_text%22%2C%0A%09%09%09%09%22emoji%22%3A%20true%2C%0A%09%09%09%09%22text%22%3A%20%22Vote%22%0A%09%09%09%7D%2C%0A%09%09%09%22value%22%3A%20%22click_me_123%22%0A%09%09%7D%0A%09%7D%2C%0A%09%7B%0A%09%09%22type%22%3A%20%22context%22%2C%0A%09%09%22elements%22%3A%20%5B%0A%09%09%09%7B%0A%09%09%09%09%22type%22%3A%20%22image%22%2C%0A%09%09%09%09%22image_url%22%3A%20%22https%3A%2F%2Fapi.slack.com%2Fimg%2Fblocks%2Fbkb_template_images%2Fprofile_1.png%22%2C%0A%09%09%09%09%22alt_text%22%3A%20%22Michael%20Scott%22%0A%09%09%09%7D%2C%0A%09%09%09%7B%0A%09%09%09%09%22type%22%3A%20%22image%22%2C%0A%09%09%09%09%22image_url%22%3A%20%22https%3A%2F%2Fapi.slack.com%2Fimg%2Fblocks%2Fbkb_template_images%2Fprofile_2.png%22%2C%0A%09%09%09%09%22alt_text%22%3A%20%22Dwight%20Schrute%22%0A%09%09%09%7D%2C%0A%09%09%09%7B%0A%09%09%09%09%22type%22%3A%20%22image%22%2C%0A%09%09%09%09%22image_url%22%3A%20%22https%3A%2F%2Fapi.slack.com%2Fimg%2Fblocks%2Fbkb_template_images%2Fprofile_3.png%22%2C%0A%09%09%09%09%22alt_text%22%3A%20%22Pam%20Beasely%22%0A%09%09%09%7D%2C%0A%09%09%09%7B%0A%09%09%09%09%22type%22%3A%20%22plain_text%22%2C%0A%09%09%09%09%22emoji%22%3A%20true%2C%0A%09%09%09%09%22text%22%3A%20%223%20votes%22%0A%09%09%09%7D%0A%09%09%5D%0A%09%7D%2C%0A%09%7B%0A%09%09%22type%22%3A%20%22section%22%2C%0A%09%09%22text%22%3A%20%7B%0A%09%09%09%22type%22%3A%20%22mrkdwn%22%2C%0A%09%09%09%22text%22%3A%20%22%3Ahamburger%3A%20*Super%20Hungryman%20Hamburgers*%5CnOnly%20for%20the%20hungriest%20of%20the%20hungry.%22%0A%09%09%7D%2C%0A%09%09%22accessory%22%3A%20%7B%0A%09%09%09%22type%22%3A%20%22button%22%2C%0A%09%09%09%22text%22%3A%20%7B%0A%09%09%09%09%22type%22%3A%20%22plain_text%22%2C%0A%09%09%09%09%22emoji%22%3A%20true%2C%0A%09%09%09%09%22text%22%3A%20%22Vote%22%0A%09%09%09%7D%2C%0A%09%09%09%22value%22%3A%20%22click_me_123%22%0A%09%09%7D%0A%09%7D%2C%0A%09%7B%0A%09%09%22type%22%3A%20%22context%22%2C%0A%09%09%22elements%22%3A%20%5B%0A%09%09%09%7B%0A%09%09%09%09%22type%22%3A%20%22image%22%2C%0A%09%09%09%09%22image_url%22%3A%20%22https%3A%2F%2Fapi.slack.com%2Fimg%2Fblocks%2Fbkb_template_images%2Fprofile_4.png%22%2C%0A%09%09%09%09%22alt_text%22%3A%20%22Angela%22%0A%09%09%09%7D%2C%0A%09%09%09%7B%0A%09%09%09%09%22type%22%3A%20%22image%22%2C%0A%09%09%09%09%22image_url%22%3A%20%22https%3A%2F%2Fapi.slack.com%2Fimg%2Fblocks%2Fbkb_template_images%2Fprofile_2.png%22%2C%0A%09%09%09%09%22alt_text%22%3A%20%22Dwight%20Schrute%22%0A%09%09%09%7D%2C%0A%09%09%09%7B%0A%09%09%09%09%22type%22%3A%20%22plain_text%22%2C%0A%09%09%09%09%22emoji%22%3A%20true%2C%0A%09%09%09%09%22text%22%3A%20%222%20votes%22%0A%09%09%09%7D%0A%09%09%5D%0A%09%7D%2C%0A%09%7B%0A%09%09%22type%22%3A%20%22section%22%2C%0A%09%09%22text%22%3A%20%7B%0A%09%09%09%22type%22%3A%20%22mrkdwn%22%2C%0A%09%09%09%22text%22%3A%20%22%3Aramen%3A%20*Kagawa-Ya%20Udon%20Noodle%20Shop*%5CnDo%20you%20like%20to%20shop%20for%20noodles%3F%20We%20have%20noodles.%22%0A%09%09%7D%2C%0A%09%09%22accessory%22%3A%20%7B%0A%09%09%09%22type%22%3A%20%22button%22%2C%0A%09%09%09%22text%22%3A%20%7B%0A%09%09%09%09%22type%22%3A%20%22plain_text%22%2C%0A%09%09%09%09%22emoji%22%3A%20true%2C%0A%09%09%09%09%22text%22%3A%20%22Vote%22%0A%09%09%09%7D%2C%0A%09%09%09%22value%22%3A%20%22click_me_123%22%0A%09%09%7D%0A%09%7D%2C%0A%09%7B%0A%09%09%22type%22%3A%20%22context%22%2C%0A%09%09%22elements%22%3A%20%5B%0A%09%09%09%7B%0A%09%09%09%09%22type%22%3A%20%22mrkdwn%22%2C%0A%09%09%09%09%22text%22%3A%20%22No%20votes%22%0A%09%09%09%7D%0A%09%09%5D%0A%09%7D%2C%0A%09%7B%0A%09%09%22type%22%3A%20%22divider%22%0A%09%7D%2C%0A%09%7B%0A%09%09%22type%22%3A%20%22actions%22%2C%0A%09%09%22elements%22%3A%20%5B%0A%09%09%09%7B%0A%09%09%09%09%22type%22%3A%20%22button%22%2C%0A%09%09%09%09%22text%22%3A%20%7B%0A%09%09%09%09%09%22type%22%3A%20%22plain_text%22%2C%0A%09%09%09%09%09%22emoji%22%3A%20true%2C%0A%09%09%09%09%09%22text%22%3A%20%22Add%20a%20suggestion%22%0A%09%09%09%09%7D%2C%0A%09%09%09%09%22value%22%3A%20%22click_me_123%22%0A%09%09%09%7D%0A%09%09%5D%0A%09%7D%0A%5D) on the block kit builder website. Refer to the function named `exampleFour` for more information on generating this block type.
62
63 #### Example 5 - Search Results
64 This example introduces overflow elements, allowing you to populate a select style dropdown with fields. These fields can be static, loaded from an external source. You can view the rendered [Search Results Example](https://api.slack.com/tools/block-kit-builder?blocks=%5B%0A%09%7B%0A%09%09%22type%22%3A%20%22section%22%2C%0A%09%09%22text%22%3A%20%7B%0A%09%09%09%22type%22%3A%20%22mrkdwn%22%2C%0A%09%09%09%22text%22%3A%20%22We%20found%20*205%20Hotels*%20in%20New%20Orleans%2C%20LA%20from%20*12%2F14%20to%2012%2F17*%22%0A%09%09%7D%2C%0A%09%09%22accessory%22%3A%20%7B%0A%09%09%09%22type%22%3A%20%22overflow%22%2C%0A%09%09%09%22options%22%3A%20%5B%0A%09%09%09%09%7B%0A%09%09%09%09%09%22text%22%3A%20%7B%0A%09%09%09%09%09%09%22type%22%3A%20%22plain_text%22%2C%0A%09%09%09%09%09%09%22emoji%22%3A%20true%2C%0A%09%09%09%09%09%09%22text%22%3A%20%22Option%20One%22%0A%09%09%09%09%09%7D%2C%0A%09%09%09%09%09%22value%22%3A%20%22value-0%22%0A%09%09%09%09%7D%2C%0A%09%09%09%09%7B%0A%09%09%09%09%09%22text%22%3A%20%7B%0A%09%09%09%09%09%09%22type%22%3A%20%22plain_text%22%2C%0A%09%09%09%09%09%09%22emoji%22%3A%20true%2C%0A%09%09%09%09%09%09%22text%22%3A%20%22Option%20Two%22%0A%09%09%09%09%09%7D%2C%0A%09%09%09%09%09%22value%22%3A%20%22value-1%22%0A%09%09%09%09%7D%2C%0A%09%09%09%09%7B%0A%09%09%09%09%09%22text%22%3A%20%7B%0A%09%09%09%09%09%09%22type%22%3A%20%22plain_text%22%2C%0A%09%09%09%09%09%09%22emoji%22%3A%20true%2C%0A%09%09%09%09%09%09%22text%22%3A%20%22Option%20Three%22%0A%09%09%09%09%09%7D%2C%0A%09%09%09%09%09%22value%22%3A%20%22value-2%22%0A%09%09%09%09%7D%2C%0A%09%09%09%09%7B%0A%09%09%09%09%09%22text%22%3A%20%7B%0A%09%09%09%09%09%09%22type%22%3A%20%22plain_text%22%2C%0A%09%09%09%09%09%09%22emoji%22%3A%20true%2C%0A%09%09%09%09%09%09%22text%22%3A%20%22Option%20Four%22%0A%09%09%09%09%09%7D%2C%0A%09%09%09%09%09%22value%22%3A%20%22value-3%22%0A%09%09%09%09%7D%0A%09%09%09%5D%0A%09%09%7D%0A%09%7D%2C%0A%09%7B%0A%09%09%22type%22%3A%20%22divider%22%0A%09%7D%2C%0A%09%7B%0A%09%09%22type%22%3A%20%22section%22%2C%0A%09%09%22text%22%3A%20%7B%0A%09%09%09%22type%22%3A%20%22mrkdwn%22%2C%0A%09%09%09%22text%22%3A%20%22*%3CfakeLink.toHotelPage.com%7CWindsor%20Court%20Hotel%3E*%5Cn%E2%98%85%E2%98%85%E2%98%85%E2%98%85%E2%98%85%5Cn%24340%20per%20night%5CnRated%3A%209.4%20-%20Excellent%22%0A%09%09%7D%2C%0A%09%09%22accessory%22%3A%20%7B%0A%09%09%09%22type%22%3A%20%22image%22%2C%0A%09%09%09%22image_url%22%3A%20%22https%3A%2F%2Fapi.slack.com%2Fimg%2Fblocks%2Fbkb_template_images%2FtripAgent_1.png%22%2C%0A%09%09%09%22alt_text%22%3A%20%22Windsor%20Court%20Hotel%20thumbnail%22%0A%09%09%7D%0A%09%7D%2C%0A%09%7B%0A%09%09%22type%22%3A%20%22context%22%2C%0A%09%09%22elements%22%3A%20%5B%0A%09%09%09%7B%0A%09%09%09%09%22type%22%3A%20%22image%22%2C%0A%09%09%09%09%22image_url%22%3A%20%22https%3A%2F%2Fapi.slack.com%2Fimg%2Fblocks%2Fbkb_template_images%2FtripAgentLocationMarker.png%22%2C%0A%09%09%09%09%22alt_text%22%3A%20%22Location%20Pin%20Icon%22%0A%09%09%09%7D%2C%0A%09%09%09%7B%0A%09%09%09%09%22type%22%3A%20%22plain_text%22%2C%0A%09%09%09%09%22emoji%22%3A%20true%2C%0A%09%09%09%09%22text%22%3A%20%22Location%3A%20Central%20Business%20District%22%0A%09%09%09%7D%0A%09%09%5D%0A%09%7D%2C%0A%09%7B%0A%09%09%22type%22%3A%20%22divider%22%0A%09%7D%2C%0A%09%7B%0A%09%09%22type%22%3A%20%22section%22%2C%0A%09%09%22text%22%3A%20%7B%0A%09%09%09%22type%22%3A%20%22mrkdwn%22%2C%0A%09%09%09%22text%22%3A%20%22*%3CfakeLink.toHotelPage.com%7CThe%20Ritz-Carlton%20New%20Orleans%3E*%5Cn%E2%98%85%E2%98%85%E2%98%85%E2%98%85%E2%98%85%5Cn%24340%20per%20night%5CnRated%3A%209.1%20-%20Excellent%22%0A%09%09%7D%2C%0A%09%09%22accessory%22%3A%20%7B%0A%09%09%09%22type%22%3A%20%22image%22%2C%0A%09%09%09%22image_url%22%3A%20%22https%3A%2F%2Fapi.slack.com%2Fimg%2Fblocks%2Fbkb_template_images%2FtripAgent_2.png%22%2C%0A%09%09%09%22alt_text%22%3A%20%22Ritz-Carlton%20New%20Orleans%20thumbnail%22%0A%09%09%7D%0A%09%7D%2C%0A%09%7B%0A%09%09%22type%22%3A%20%22context%22%2C%0A%09%09%22elements%22%3A%20%5B%0A%09%09%09%7B%0A%09%09%09%09%22type%22%3A%20%22image%22%2C%0A%09%09%09%09%22image_url%22%3A%20%22https%3A%2F%2Fapi.slack.com%2Fimg%2Fblocks%2Fbkb_template_images%2FtripAgentLocationMarker.png%22%2C%0A%09%09%09%09%22alt_text%22%3A%20%22Location%20Pin%20Icon%22%0A%09%09%09%7D%2C%0A%09%09%09%7B%0A%09%09%09%09%22type%22%3A%20%22plain_text%22%2C%0A%09%09%09%09%22emoji%22%3A%20true%2C%0A%09%09%09%09%22text%22%3A%20%22Location%3A%20French%20Quarter%22%0A%09%09%09%7D%0A%09%09%5D%0A%09%7D%2C%0A%09%7B%0A%09%09%22type%22%3A%20%22divider%22%0A%09%7D%2C%0A%09%7B%0A%09%09%22type%22%3A%20%22section%22%2C%0A%09%09%22text%22%3A%20%7B%0A%09%09%09%22type%22%3A%20%22mrkdwn%22%2C%0A%09%09%09%22text%22%3A%20%22*%3CfakeLink.toHotelPage.com%7COmni%20Royal%20Orleans%20Hotel%3E*%5Cn%E2%98%85%E2%98%85%E2%98%85%E2%98%85%E2%98%85%5Cn%24419%20per%20night%5CnRated%3A%208.8%20-%20Excellent%22%0A%09%09%7D%2C%0A%09%09%22accessory%22%3A%20%7B%0A%09%09%09%22type%22%3A%20%22image%22%2C%0A%09%09%09%22image_url%22%3A%20%22https%3A%2F%2Fapi.slack.com%2Fimg%2Fblocks%2Fbkb_template_images%2FtripAgent_3.png%22%2C%0A%09%09%09%22alt_text%22%3A%20%22Omni%20Royal%20Orleans%20Hotel%20thumbnail%22%0A%09%09%7D%0A%09%7D%2C%0A%09%7B%0A%09%09%22type%22%3A%20%22context%22%2C%0A%09%09%22elements%22%3A%20%5B%0A%09%09%09%7B%0A%09%09%09%09%22type%22%3A%20%22image%22%2C%0A%09%09%09%09%22image_url%22%3A%20%22https%3A%2F%2Fapi.slack.com%2Fimg%2Fblocks%2Fbkb_template_images%2FtripAgentLocationMarker.png%22%2C%0A%09%09%09%09%22alt_text%22%3A%20%22Location%20Pin%20Icon%22%0A%09%09%09%7D%2C%0A%09%09%09%7B%0A%09%09%09%09%22type%22%3A%20%22plain_text%22%2C%0A%09%09%09%09%22emoji%22%3A%20true%2C%0A%09%09%09%09%22text%22%3A%20%22Location%3A%20French%20Quarter%22%0A%09%09%09%7D%0A%09%09%5D%0A%09%7D%2C%0A%09%7B%0A%09%09%22type%22%3A%20%22divider%22%0A%09%7D%2C%0A%09%7B%0A%09%09%22type%22%3A%20%22actions%22%2C%0A%09%09%22elements%22%3A%20%5B%0A%09%09%09%7B%0A%09%09%09%09%22type%22%3A%20%22button%22%2C%0A%09%09%09%09%22text%22%3A%20%7B%0A%09%09%09%09%09%22type%22%3A%20%22plain_text%22%2C%0A%09%09%09%09%09%22emoji%22%3A%20true%2C%0A%09%09%09%09%09%22text%22%3A%20%22Next%202%20Results%22%0A%09%09%09%09%7D%2C%0A%09%09%09%09%22value%22%3A%20%22click_me_123%22%0A%09%09%09%7D%0A%09%09%5D%0A%09%7D%0A%5D) on the block kit builder website. Refer to the function named `exampleFive` for more information on generating this block.
65
66 #### Example 6 - Search Results with Options and Actions
67
68 Using a combination of overflow elements containing selectable options and actions, this examples allows you to prompt the user with multiple actions in a single response. You can view the rendered [Search Results with Actions](https://api.slack.com/tools/block-kit-builder?blocks=%5B%0A%09%7B%0A%09%09%22type%22%3A%20%22section%22%2C%0A%09%09%22text%22%3A%20%7B%0A%09%09%09%22type%22%3A%20%22mrkdwn%22%2C%0A%09%09%09%22text%22%3A%20%22%3Amag%3A%20Search%20results%20for%20*Cata*%22%0A%09%09%7D%0A%09%7D%2C%0A%09%7B%0A%09%09%22type%22%3A%20%22divider%22%0A%09%7D%2C%0A%09%7B%0A%09%09%22type%22%3A%20%22section%22%2C%0A%09%09%22text%22%3A%20%7B%0A%09%09%09%22type%22%3A%20%22mrkdwn%22%2C%0A%09%09%09%22text%22%3A%20%22*%3CfakeLink.toYourApp.com%7CUse%20Case%20Catalogue%3E*%5CnUse%20Case%20Catalogue%20for%20the%20following%20departments%2Froles...%22%0A%09%09%7D%2C%0A%09%09%22accessory%22%3A%20%7B%0A%09%09%09%22type%22%3A%20%22static_select%22%2C%0A%09%09%09%22placeholder%22%3A%20%7B%0A%09%09%09%09%22type%22%3A%20%22plain_text%22%2C%0A%09%09%09%09%22emoji%22%3A%20true%2C%0A%09%09%09%09%22text%22%3A%20%22Manage%22%0A%09%09%09%7D%2C%0A%09%09%09%22options%22%3A%20%5B%0A%09%09%09%09%7B%0A%09%09%09%09%09%22text%22%3A%20%7B%0A%09%09%09%09%09%09%22type%22%3A%20%22plain_text%22%2C%0A%09%09%09%09%09%09%22emoji%22%3A%20true%2C%0A%09%09%09%09%09%09%22text%22%3A%20%22Edit%20it%22%0A%09%09%09%09%09%7D%2C%0A%09%09%09%09%09%22value%22%3A%20%22value-0%22%0A%09%09%09%09%7D%2C%0A%09%09%09%09%7B%0A%09%09%09%09%09%22text%22%3A%20%7B%0A%09%09%09%09%09%09%22type%22%3A%20%22plain_text%22%2C%0A%09%09%09%09%09%09%22emoji%22%3A%20true%2C%0A%09%09%09%09%09%09%22text%22%3A%20%22Read%20it%22%0A%09%09%09%09%09%7D%2C%0A%09%09%09%09%09%22value%22%3A%20%22value-1%22%0A%09%09%09%09%7D%2C%0A%09%09%09%09%7B%0A%09%09%09%09%09%22text%22%3A%20%7B%0A%09%09%09%09%09%09%22type%22%3A%20%22plain_text%22%2C%0A%09%09%09%09%09%09%22emoji%22%3A%20true%2C%0A%09%09%09%09%09%09%22text%22%3A%20%22Save%20it%22%0A%09%09%09%09%09%7D%2C%0A%09%09%09%09%09%22value%22%3A%20%22value-2%22%0A%09%09%09%09%7D%0A%09%09%09%5D%0A%09%09%7D%0A%09%7D%2C%0A%09%7B%0A%09%09%22type%22%3A%20%22section%22%2C%0A%09%09%22text%22%3A%20%7B%0A%09%09%09%22type%22%3A%20%22mrkdwn%22%2C%0A%09%09%09%22text%22%3A%20%22*%3CfakeLink.toYourApp.com%7CCustomer%20Support%20-%20Workflow%20Diagram%20Catalogue%3E*%5CnThis%20resource%20was%20put%20together%20by%20members%20of...%22%0A%09%09%7D%2C%0A%09%09%22accessory%22%3A%20%7B%0A%09%09%09%22type%22%3A%20%22static_select%22%2C%0A%09%09%09%22placeholder%22%3A%20%7B%0A%09%09%09%09%22type%22%3A%20%22plain_text%22%2C%0A%09%09%09%09%22emoji%22%3A%20true%2C%0A%09%09%09%09%22text%22%3A%20%22Manage%22%0A%09%09%09%7D%2C%0A%09%09%09%22options%22%3A%20%5B%0A%09%09%09%09%7B%0A%09%09%09%09%09%22text%22%3A%20%7B%0A%09%09%09%09%09%09%22type%22%3A%20%22plain_text%22%2C%0A%09%09%09%09%09%09%22emoji%22%3A%20true%2C%0A%09%09%09%09%09%09%22text%22%3A%20%22Manage%20it%22%0A%09%09%09%09%09%7D%2C%0A%09%09%09%09%09%22value%22%3A%20%22value-0%22%0A%09%09%09%09%7D%2C%0A%09%09%09%09%7B%0A%09%09%09%09%09%22text%22%3A%20%7B%0A%09%09%09%09%09%09%22type%22%3A%20%22plain_text%22%2C%0A%09%09%09%09%09%09%22emoji%22%3A%20true%2C%0A%09%09%09%09%09%09%22text%22%3A%20%22Read%20it%22%0A%09%09%09%09%09%7D%2C%0A%09%09%09%09%09%22value%22%3A%20%22value-1%22%0A%09%09%09%09%7D%2C%0A%09%09%09%09%7B%0A%09%09%09%09%09%22text%22%3A%20%7B%0A%09%09%09%09%09%09%22type%22%3A%20%22plain_text%22%2C%0A%09%09%09%09%09%09%22emoji%22%3A%20true%2C%0A%09%09%09%09%09%09%22text%22%3A%20%22Save%20it%22%0A%09%09%09%09%09%7D%2C%0A%09%09%09%09%09%22value%22%3A%20%22value-2%22%0A%09%09%09%09%7D%0A%09%09%09%5D%0A%09%09%7D%0A%09%7D%2C%0A%09%7B%0A%09%09%22type%22%3A%20%22section%22%2C%0A%09%09%22text%22%3A%20%7B%0A%09%09%09%22type%22%3A%20%22mrkdwn%22%2C%0A%09%09%09%22text%22%3A%20%22*%3CfakeLink.toYourApp.com%7CSelf-Serve%20Learning%20Options%20Catalogue%3E*%5CnSee%20the%20learning%20and%20development%20options%20we...%22%0A%09%09%7D%2C%0A%09%09%22accessory%22%3A%20%7B%0A%09%09%09%22type%22%3A%20%22static_select%22%2C%0A%09%09%09%22placeholder%22%3A%20%7B%0A%09%09%09%09%22type%22%3A%20%22plain_text%22%2C%0A%09%09%09%09%22emoji%22%3A%20true%2C%0A%09%09%09%09%22text%22%3A%20%22Manage%22%0A%09%09%09%7D%2C%0A%09%09%09%22options%22%3A%20%5B%0A%09%09%09%09%7B%0A%09%09%09%09%09%22text%22%3A%20%7B%0A%09%09%09%09%09%09%22type%22%3A%20%22plain_text%22%2C%0A%09%09%09%09%09%09%22emoji%22%3A%20true%2C%0A%09%09%09%09%09%09%22text%22%3A%20%22Manage%20it%22%0A%09%09%09%09%09%7D%2C%0A%09%09%09%09%09%22value%22%3A%20%22value-0%22%0A%09%09%09%09%7D%2C%0A%09%09%09%09%7B%0A%09%09%09%09%09%22text%22%3A%20%7B%0A%09%09%09%09%09%09%22type%22%3A%20%22plain_text%22%2C%0A%09%09%09%09%09%09%22emoji%22%3A%20true%2C%0A%09%09%09%09%09%09%22text%22%3A%20%22Read%20it%22%0A%09%09%09%09%09%7D%2C%0A%09%09%09%09%09%22value%22%3A%20%22value-1%22%0A%09%09%09%09%7D%2C%0A%09%09%09%09%7B%0A%09%09%09%09%09%22text%22%3A%20%7B%0A%09%09%09%09%09%09%22type%22%3A%20%22plain_text%22%2C%0A%09%09%09%09%09%09%22emoji%22%3A%20true%2C%0A%09%09%09%09%09%09%22text%22%3A%20%22Save%20it%22%0A%09%09%09%09%09%7D%2C%0A%09%09%09%09%09%22value%22%3A%20%22value-2%22%0A%09%09%09%09%7D%0A%09%09%09%5D%0A%09%09%7D%0A%09%7D%2C%0A%09%7B%0A%09%09%22type%22%3A%20%22section%22%2C%0A%09%09%22text%22%3A%20%7B%0A%09%09%09%22type%22%3A%20%22mrkdwn%22%2C%0A%09%09%09%22text%22%3A%20%22*%3CfakeLink.toYourApp.com%7CUse%20Case%20Catalogue%20-%20CF%20Presentation%20-%20%5BJune%2012%2C%202018%5D%3E*%5CnThis%20is%20presentation%20will%20continue%20to%20be%20updated%20as...%22%0A%09%09%7D%2C%0A%09%09%22accessory%22%3A%20%7B%0A%09%09%09%22type%22%3A%20%22static_select%22%2C%0A%09%09%09%22placeholder%22%3A%20%7B%0A%09%09%09%09%22type%22%3A%20%22plain_text%22%2C%0A%09%09%09%09%22emoji%22%3A%20true%2C%0A%09%09%09%09%22text%22%3A%20%22Manage%22%0A%09%09%09%7D%2C%0A%09%09%09%22options%22%3A%20%5B%0A%09%09%09%09%7B%0A%09%09%09%09%09%22text%22%3A%20%7B%0A%09%09%09%09%09%09%22type%22%3A%20%22plain_text%22%2C%0A%09%09%09%09%09%09%22emoji%22%3A%20true%2C%0A%09%09%09%09%09%09%22text%22%3A%20%22Manage%20it%22%0A%09%09%09%09%09%7D%2C%0A%09%09%09%09%09%22value%22%3A%20%22value-0%22%0A%09%09%09%09%7D%2C%0A%09%09%09%09%7B%0A%09%09%09%09%09%22text%22%3A%20%7B%0A%09%09%09%09%09%09%22type%22%3A%20%22plain_text%22%2C%0A%09%09%09%09%09%09%22emoji%22%3A%20true%2C%0A%09%09%09%09%09%09%22text%22%3A%20%22Read%20it%22%0A%09%09%09%09%09%7D%2C%0A%09%09%09%09%09%22value%22%3A%20%22value-1%22%0A%09%09%09%09%7D%2C%0A%09%09%09%09%7B%0A%09%09%09%09%09%22text%22%3A%20%7B%0A%09%09%09%09%09%09%22type%22%3A%20%22plain_text%22%2C%0A%09%09%09%09%09%09%22emoji%22%3A%20true%2C%0A%09%09%09%09%09%09%22text%22%3A%20%22Save%20it%22%0A%09%09%09%09%09%7D%2C%0A%09%09%09%09%09%22value%22%3A%20%22value-2%22%0A%09%09%09%09%7D%0A%09%09%09%5D%0A%09%09%7D%0A%09%7D%2C%0A%09%7B%0A%09%09%22type%22%3A%20%22section%22%2C%0A%09%09%22text%22%3A%20%7B%0A%09%09%09%22type%22%3A%20%22mrkdwn%22%2C%0A%09%09%09%22text%22%3A%20%22*%3CfakeLink.toYourApp.com%7CComprehensive%20Benefits%20Catalogue%20-%202019%3E*%5CnInformation%20about%20all%20the%20benfits%20we%20offer%20is...%22%0A%09%09%7D%2C%0A%09%09%22accessory%22%3A%20%7B%0A%09%09%09%22type%22%3A%20%22static_select%22%2C%0A%09%09%09%22placeholder%22%3A%20%7B%0A%09%09%09%09%22type%22%3A%20%22plain_text%22%2C%0A%09%09%09%09%22emoji%22%3A%20true%2C%0A%09%09%09%09%22text%22%3A%20%22Manage%22%0A%09%09%09%7D%2C%0A%09%09%09%22options%22%3A%20%5B%0A%09%09%09%09%7B%0A%09%09%09%09%09%22text%22%3A%20%7B%0A%09%09%09%09%09%09%22type%22%3A%20%22plain_text%22%2C%0A%09%09%09%09%09%09%22emoji%22%3A%20true%2C%0A%09%09%09%09%09%09%22text%22%3A%20%22Manage%20it%22%0A%09%09%09%09%09%7D%2C%0A%09%09%09%09%09%22value%22%3A%20%22value-0%22%0A%09%09%09%09%7D%2C%0A%09%09%09%09%7B%0A%09%09%09%09%09%22text%22%3A%20%7B%0A%09%09%09%09%09%09%22type%22%3A%20%22plain_text%22%2C%0A%09%09%09%09%09%09%22emoji%22%3A%20true%2C%0A%09%09%09%09%09%09%22text%22%3A%20%22Read%20it%22%0A%09%09%09%09%09%7D%2C%0A%09%09%09%09%09%22value%22%3A%20%22value-1%22%0A%09%09%09%09%7D%2C%0A%09%09%09%09%7B%0A%09%09%09%09%09%22text%22%3A%20%7B%0A%09%09%09%09%09%09%22type%22%3A%20%22plain_text%22%2C%0A%09%09%09%09%09%09%22emoji%22%3A%20true%2C%0A%09%09%09%09%09%09%22text%22%3A%20%22Save%20it%22%0A%09%09%09%09%09%7D%2C%0A%09%09%09%09%09%22value%22%3A%20%22value-2%22%0A%09%09%09%09%7D%0A%09%09%09%5D%0A%09%09%7D%0A%09%7D%2C%0A%09%7B%0A%09%09%22type%22%3A%20%22divider%22%0A%09%7D%2C%0A%09%7B%0A%09%09%22type%22%3A%20%22actions%22%2C%0A%09%09%22elements%22%3A%20%5B%0A%09%09%09%7B%0A%09%09%09%09%22type%22%3A%20%22button%22%2C%0A%09%09%09%09%22text%22%3A%20%7B%0A%09%09%09%09%09%22type%22%3A%20%22plain_text%22%2C%0A%09%09%09%09%09%22emoji%22%3A%20true%2C%0A%09%09%09%09%09%22text%22%3A%20%22Next%205%20Results%22%0A%09%09%09%09%7D%2C%0A%09%09%09%09%22value%22%3A%20%22click_me_123%22%0A%09%09%09%7D%0A%09%09%5D%0A%09%7D%0A%5D) example on the block kit builder website. Refer to the function named `exampleSix` for more information on building this block.
0 package main
1
2 import (
3 "encoding/json"
4 "fmt"
5
6 "github.com/nlopes/slack"
7 )
8
9 // The functions below mock the different templates slack has as examples on their website.
10 //
11 // Refer to README.md for more information on the examples and how to use them.
12
13 func main() {
14
15 fmt.Println("--- Begin Example One ---")
16 exampleOne()
17 fmt.Println("--- End Example One ---")
18
19 fmt.Println("--- Begin Example Two ---")
20 exampleTwo()
21 fmt.Println("--- End Example Two ---")
22
23 fmt.Println("--- Begin Example Three ---")
24 exampleThree()
25 fmt.Println("--- End Example Three ---")
26
27 fmt.Println("--- Begin Example Four ---")
28 exampleFour()
29 fmt.Println("--- End Example Four ---")
30
31 fmt.Println("--- Begin Example Five ---")
32 exampleFive()
33 fmt.Println("--- End Example Five ---")
34
35 fmt.Println("--- Begin Example Six ---")
36 exampleSix()
37 fmt.Println("--- End Example Six ---")
38
39 fmt.Println("--- Begin Example Unmarshalling ---")
40 unmarshalExample()
41 fmt.Println("--- End Example Unmarshalling ---")
42 }
43
44 // approvalRequest mocks the simple "Approval" template located on block kit builder website
45 func exampleOne() {
46
47 // Header Section
48 headerText := slack.NewTextBlockObject("mrkdwn", "You have a new request:\n*<fakeLink.toEmployeeProfile.com|Fred Enriquez - New device request>*", false, false)
49 headerSection := slack.NewSectionBlock(headerText, nil, nil)
50
51 // Fields
52 typeField := slack.NewTextBlockObject("mrkdwn", "*Type:*\nComputer (laptop)", false, false)
53 whenField := slack.NewTextBlockObject("mrkdwn", "*When:*\nSubmitted Aut 10", false, false)
54 lastUpdateField := slack.NewTextBlockObject("mrkdwn", "*Last Update:*\nMar 10, 2015 (3 years, 5 months)", false, false)
55 reasonField := slack.NewTextBlockObject("mrkdwn", "*Reason:*\nAll vowel keys aren't working.", false, false)
56 specsField := slack.NewTextBlockObject("mrkdwn", "*Specs:*\n\"Cheetah Pro 15\" - Fast, really fast\"", false, false)
57
58 fieldSlice := make([]*slack.TextBlockObject, 0)
59 fieldSlice = append(fieldSlice, typeField)
60 fieldSlice = append(fieldSlice, whenField)
61 fieldSlice = append(fieldSlice, lastUpdateField)
62 fieldSlice = append(fieldSlice, reasonField)
63 fieldSlice = append(fieldSlice, specsField)
64
65 fieldsSection := slack.NewSectionBlock(nil, fieldSlice, nil)
66
67 // Approve and Deny Buttons
68 approveBtnTxt := slack.NewTextBlockObject("plain_text", "Approve", false, false)
69 approveBtn := slack.NewButtonBlockElement("", "click_me_123", approveBtnTxt)
70
71 denyBtnTxt := slack.NewTextBlockObject("plain_text", "Deny", false, false)
72 denyBtn := slack.NewButtonBlockElement("", "click_me_123", denyBtnTxt)
73
74 actionBlock := slack.NewActionBlock("", approveBtn, denyBtn)
75
76 // Build Message with blocks created above
77
78 msg := slack.NewBlockMessage(
79 headerSection,
80 fieldsSection,
81 actionBlock,
82 )
83
84 b, err := json.MarshalIndent(msg, "", " ")
85 if err != nil {
86 fmt.Println(err)
87 return
88 }
89
90 fmt.Println(string(b))
91
92 }
93
94 // exampleTwo mocks the more complex "Approval" template located on block kit builder website
95 // which includes an accessory image next to the approval request
96 func exampleTwo() {
97
98 // Header Section
99 headerText := slack.NewTextBlockObject("mrkdwn", "You have a new request:\n*<google.com|Fred Enriquez - Time Off request>*", false, false)
100 headerSection := slack.NewSectionBlock(headerText, nil, nil)
101
102 approvalText := slack.NewTextBlockObject("mrkdwn", "*Type:*\nPaid time off\n*When:*\nAug 10-Aug 13\n*Hours:* 16.0 (2 days)\n*Remaining balance:* 32.0 hours (4 days)\n*Comments:* \"Family in town, going camping!\"", false, false)
103 approvalImage := slack.NewImageBlockElement("https://api.slack.com/img/blocks/bkb_template_images/approvalsNewDevice.png", "computer thumbnail")
104
105 fieldsSection := slack.NewSectionBlock(approvalText, nil, slack.NewAccessory(approvalImage))
106
107 // Approve and Deny Buttons
108 approveBtnTxt := slack.NewTextBlockObject("plain_text", "Approve", false, false)
109 approveBtn := slack.NewButtonBlockElement("", "click_me_123", approveBtnTxt)
110
111 denyBtnTxt := slack.NewTextBlockObject("plain_text", "Deny", false, false)
112 denyBtn := slack.NewButtonBlockElement("", "click_me_123", denyBtnTxt)
113
114 actionBlock := slack.NewActionBlock("", approveBtn, denyBtn)
115
116 // Build Message with blocks created above
117
118 msg := slack.NewBlockMessage(
119 headerSection,
120 fieldsSection,
121 actionBlock,
122 )
123
124 b, err := json.MarshalIndent(msg, "", " ")
125 if err != nil {
126 fmt.Println(err)
127 return
128 }
129
130 fmt.Println(string(b))
131
132 }
133
134 // exampleThree generates the notification example from the block kit builder website
135 func exampleThree() {
136
137 // Shared Assets for example
138 chooseBtnText := slack.NewTextBlockObject("plain_text", "Choose", true, false)
139 chooseBtnEle := slack.NewButtonBlockElement("", "click_me_123", chooseBtnText)
140 divSection := slack.NewDividerBlock()
141
142 // Header Section
143 headerText := slack.NewTextBlockObject("plain_text", "Looks like you have a scheduling conflict with this event:", false, false)
144 headerSection := slack.NewSectionBlock(headerText, nil, nil)
145
146 // Schedule Info Section
147 scheduleText := slack.NewTextBlockObject("mrkdwn", "*<fakeLink.toUserProfiles.com|Iris / Zelda 1-1>*\nTuesday, January 21 4:00-4:30pm\nBuilding 2 - Havarti Cheese (3)\n2 guests", false, false)
148 scheduleAccessory := slack.NewImageBlockElement("https://api.slack.com/img/blocks/bkb_template_images/notifications.png", "calendar thumbnail")
149 schedeuleSection := slack.NewSectionBlock(scheduleText, nil, slack.NewAccessory(scheduleAccessory))
150
151 // Conflict Section
152 conflictImage := slack.NewImageBlockElement("https://api.slack.com/img/blocks/bkb_template_images/notificationsWarningIcon.png", "notifications warning icon")
153 conflictText := slack.NewTextBlockObject("mrkdwn", "*Conflicts with Team Huddle: 4:15-4:30pm*", false, false)
154
155 conflictSection := slack.NewContextBlock(
156 "",
157 []slack.MixedElement{conflictImage, conflictText}...,
158 )
159
160 // Proposese Text
161 proposeText := slack.NewTextBlockObject("mrkdwn", "*Propose a new time:*", false, false)
162 proposeSection := slack.NewSectionBlock(proposeText, nil, nil)
163
164 // Option 1
165 optionOneText := slack.NewTextBlockObject("mrkdwn", "*Today - 4:30-5pm*\nEveryone is available: @iris, @zelda", false, false)
166 optionOneSection := slack.NewSectionBlock(optionOneText, nil, slack.NewAccessory(chooseBtnEle))
167
168 // Option 2
169 optionTwoText := slack.NewTextBlockObject("mrkdwn", "*Tomorrow - 4-4:30pm*\nEveryone is available: @iris, @zelda", false, false)
170 optionTwoSection := slack.NewSectionBlock(optionTwoText, nil, slack.NewAccessory(chooseBtnEle))
171
172 // Option 3
173 optionThreeText := slack.NewTextBlockObject("mrkdwn", "*Tomorrow - 6-6:30pm*\nSome people aren't available: @iris, ~@zelda~", false, false)
174 optionThreeSection := slack.NewSectionBlock(optionThreeText, nil, slack.NewAccessory(chooseBtnEle))
175
176 // Show More Times Link
177 showMoreText := slack.NewTextBlockObject("mrkdwn", "*<fakelink.ToMoreTimes.com|Show more times>*", false, false)
178 showMoreSection := slack.NewSectionBlock(showMoreText, nil, nil)
179
180 // Build Message with blocks created above
181 msg := slack.NewBlockMessage(
182 headerSection,
183 divSection,
184 schedeuleSection,
185 conflictSection,
186 divSection,
187 proposeSection,
188 optionOneSection,
189 optionTwoSection,
190 optionThreeSection,
191 showMoreSection,
192 )
193
194 b, err := json.MarshalIndent(msg, "", " ")
195 if err != nil {
196 fmt.Println(err)
197 return
198 }
199
200 fmt.Println(string(b))
201
202 }
203
204 // exampleFour profiles a poll example block
205 func exampleFour() {
206
207 // Shared Assets for example
208 divSection := slack.NewDividerBlock()
209 voteBtnText := slack.NewTextBlockObject("plain_text", "Vote", true, false)
210 voteBtnEle := slack.NewButtonBlockElement("", "click_me_123", voteBtnText)
211 profileOne := slack.NewImageBlockElement("https://api.slack.com/img/blocks/bkb_template_images/profile_1.png", "Michael Scott")
212 profileTwo := slack.NewImageBlockElement("https://api.slack.com/img/blocks/bkb_template_images/profile_2.png", "Dwight Schrute")
213 profileThree := slack.NewImageBlockElement("https://api.slack.com/img/blocks/bkb_template_images/profile_3.png", "Pam Beasely")
214 profileFour := slack.NewImageBlockElement("https://api.slack.com/img/blocks/bkb_template_images/profile_4.png", "Angela")
215
216 // Header Section
217 headerText := slack.NewTextBlockObject("mrkdwn", "*Where should we order lunch from?* Poll by <fakeLink.toUser.com|Mark>", false, false)
218 headerSection := slack.NewSectionBlock(headerText, nil, nil)
219
220 // Option One Info
221 optOneText := slack.NewTextBlockObject("mrkdwn", ":sushi: *Ace Wasabi Rock-n-Roll Sushi Bar*\nThe best landlocked sushi restaurant.", false, false)
222 optOneSection := slack.NewSectionBlock(optOneText, nil, slack.NewAccessory(voteBtnEle))
223
224 // Option One Votes
225 optOneVoteText := slack.NewTextBlockObject("plain_text", "3 votes", true, false)
226 optOneContext := slack.NewContextBlock("", []slack.MixedElement{profileOne, profileTwo, profileThree, optOneVoteText}...)
227
228 // Option Two Info
229 optTwoText := slack.NewTextBlockObject("mrkdwn", ":hamburger: *Super Hungryman Hamburgers*\nOnly for the hungriest of the hungry.", false, false)
230 optTwoSection := slack.NewSectionBlock(optTwoText, nil, slack.NewAccessory(voteBtnEle))
231
232 // Option Two Votes
233 optTwoVoteText := slack.NewTextBlockObject("plain_text", "2 votes", true, false)
234 optTwoContext := slack.NewContextBlock("", []slack.MixedElement{profileFour, profileTwo, optTwoVoteText}...)
235
236 // Option Three Info
237 optThreeText := slack.NewTextBlockObject("mrkdwn", ":ramen: *Kagawa-Ya Udon Noodle Shop*\nDo you like to shop for noodles? We have noodles.", false, false)
238 optThreeSection := slack.NewSectionBlock(optThreeText, nil, slack.NewAccessory(voteBtnEle))
239
240 // Option Three Votes
241 optThreeVoteText := slack.NewTextBlockObject("plain_text", "No votes", true, false)
242 optThreeContext := slack.NewContextBlock("", []slack.MixedElement{optThreeVoteText}...)
243
244 // Suggestions Action
245 btnTxt := slack.NewTextBlockObject("plain_text", "Add a suggestion", false, false)
246 nextBtn := slack.NewButtonBlockElement("", "click_me_123", btnTxt)
247 actionBlock := slack.NewActionBlock("", nextBtn)
248
249 // Build Message with blocks created above
250 msg := slack.NewBlockMessage(
251 headerSection,
252 divSection,
253 optOneSection,
254 optOneContext,
255 optTwoSection,
256 optTwoContext,
257 optThreeSection,
258 optThreeContext,
259 divSection,
260 actionBlock,
261 )
262
263 b, err := json.MarshalIndent(msg, "", " ")
264 if err != nil {
265 fmt.Println(err)
266 return
267 }
268
269 fmt.Println(string(b))
270
271 }
272
273 func exampleFive() {
274
275 // Build Header Section Block, includes text and overflow menu
276
277 headerText := slack.NewTextBlockObject("mrkdwn", "We found *205 Hotels* in New Orleans, LA from *12/14 to 12/17*", false, false)
278
279 // Build Text Objects associated with each option
280 overflowOptionTextOne := slack.NewTextBlockObject("plain_text", "Option One", false, false)
281 overflowOptionTextTwo := slack.NewTextBlockObject("plain_text", "Option Two", false, false)
282 overflowOptionTextThree := slack.NewTextBlockObject("plain_text", "Option Three", false, false)
283
284 // Build each option, providing a value for the option
285 overflowOptionOne := slack.NewOptionBlockObject("value-0", overflowOptionTextOne)
286 overflowOptionTwo := slack.NewOptionBlockObject("value-1", overflowOptionTextTwo)
287 overflowOptionThree := slack.NewOptionBlockObject("value-2", overflowOptionTextThree)
288
289 // Build overflow section
290 overflow := slack.NewOverflowBlockElement("", overflowOptionOne, overflowOptionTwo, overflowOptionThree)
291
292 // Create the header section
293 headerSection := slack.NewSectionBlock(headerText, nil, slack.NewAccessory(overflow))
294
295 // Shared Divider
296 divSection := slack.NewDividerBlock()
297
298 // Shared Objects
299 locationPinImage := slack.NewImageBlockElement("https://api.slack.com/img/blocks/bkb_template_images/tripAgentLocationMarker.png", "Location Pin Icon")
300
301 // First Hotel Listing
302 hotelOneInfo := slack.NewTextBlockObject("mrkdwn", "*<fakeLink.toHotelPage.com|Windsor Court Hotel>*\n★★★★★\n$340 per night\nRated: 9.4 - Excellent", false, false)
303 hotelOneImage := slack.NewImageBlockElement("https://api.slack.com/img/blocks/bkb_template_images/tripAgent_1.png", "Windsor Court Hotel thumbnail")
304 hotelOneLoc := slack.NewTextBlockObject("plain_text", "Location: Central Business District", true, false)
305
306 hotelOneSection := slack.NewSectionBlock(hotelOneInfo, nil, slack.NewAccessory(hotelOneImage))
307 hotelOneContext := slack.NewContextBlock("", []slack.MixedElement{locationPinImage, hotelOneLoc}...)
308
309 // Second Hotel Listing
310 hotelTwoInfo := slack.NewTextBlockObject("mrkdwn", "*<fakeLink.toHotelPage.com|The Ritz-Carlton New Orleans>*\n★★★★★\n$340 per night\nRated: 9.1 - Excellent", false, false)
311 hotelTwoImage := slack.NewImageBlockElement("https://api.slack.com/img/blocks/bkb_template_images/tripAgent_2.png", "Ritz-Carlton New Orleans thumbnail")
312 hotelTwoLoc := slack.NewTextBlockObject("plain_text", "Location: French Quarter", true, false)
313
314 hotelTwoSection := slack.NewSectionBlock(hotelTwoInfo, nil, slack.NewAccessory(hotelTwoImage))
315 hotelTwoContext := slack.NewContextBlock("", []slack.MixedElement{locationPinImage, hotelTwoLoc}...)
316
317 // Third Hotel Listing
318 hotelThreeInfo := slack.NewTextBlockObject("mrkdwn", "*<fakeLink.toHotelPage.com|Omni Royal Orleans Hotel>*\n★★★★★\n$419 per night\nRated: 8.8 - Excellent", false, false)
319 hotelThreeImage := slack.NewImageBlockElement("https://api.slack.com/img/blocks/bkb_template_images/tripAgent_3.png", "https://api.slack.com/img/blocks/bkb_template_images/tripAgent_3.png")
320 hotelThreeLoc := slack.NewTextBlockObject("plain_text", "Location: French Quarter", true, false)
321
322 hotelThreeSection := slack.NewSectionBlock(hotelThreeInfo, nil, slack.NewAccessory(hotelThreeImage))
323 hotelThreeContext := slack.NewContextBlock("", []slack.MixedElement{locationPinImage, hotelThreeLoc}...)
324
325 // Action button
326 btnTxt := slack.NewTextBlockObject("plain_text", "Next 2 Results", false, false)
327 nextBtn := slack.NewButtonBlockElement("", "click_me_123", btnTxt)
328 actionBlock := slack.NewActionBlock("", nextBtn)
329
330 // Build Message with blocks created above
331 msg := slack.NewBlockMessage(
332 headerSection,
333 divSection,
334 hotelOneSection,
335 hotelOneContext,
336 divSection,
337 hotelTwoSection,
338 hotelTwoContext,
339 divSection,
340 hotelThreeSection,
341 hotelThreeContext,
342 divSection,
343 actionBlock,
344 )
345
346 b, err := json.MarshalIndent(msg, "", " ")
347 if err != nil {
348 fmt.Println(err)
349 return
350 }
351
352 fmt.Println(string(b))
353
354 }
355
356 func exampleSix() {
357
358 // Shared Assets for example
359 divSection := slack.NewDividerBlock()
360
361 // Shared Available Options
362 manageTxt := slack.NewTextBlockObject("plain_text", "Manage", true, false)
363 editTxt := slack.NewTextBlockObject("plain_text", "Edit it", false, false)
364 readTxt := slack.NewTextBlockObject("plain_text", "Read it", false, false)
365 saveTxt := slack.NewTextBlockObject("plain_text", "Save it", false, false)
366
367 editOpt := slack.NewOptionBlockObject("value-0", editTxt)
368 readOpt := slack.NewOptionBlockObject("value-1", readTxt)
369 saveOpt := slack.NewOptionBlockObject("value-2", saveTxt)
370
371 availableOption := slack.NewOptionsSelectBlockElement("static_select", manageTxt, "", editOpt, readOpt, saveOpt)
372
373 // Header Section
374 headerText := slack.NewTextBlockObject("mrkdwn", ":mag: Search results for *Cata*", false, false)
375 headerSection := slack.NewSectionBlock(headerText, nil, nil)
376
377 // Result One
378 resultOneTxt := slack.NewTextBlockObject("mrkdwn", "*<fakeLink.toYourApp.com|Use Case Catalogue>*\nUse Case Catalogue for the following departments/roles...", false, false)
379 resultOneSection := slack.NewSectionBlock(resultOneTxt, nil, slack.NewAccessory(availableOption))
380
381 // Result Two
382 resultTwoTxt := slack.NewTextBlockObject("mrkdwn", "*<fakeLink.toYourApp.com|Customer Support - Workflow Diagram Catalogue>*\nThis resource was put together by members of...", false, false)
383 resultTwoSection := slack.NewSectionBlock(resultTwoTxt, nil, slack.NewAccessory(availableOption))
384
385 // Result Three
386 resultThreeTxt := slack.NewTextBlockObject("mrkdwn", "*<fakeLink.toYourApp.com|Self-Serve Learning Options Catalogue>*\nSee the learning and development options we...", false, false)
387 resultThreeSection := slack.NewSectionBlock(resultThreeTxt, nil, slack.NewAccessory(availableOption))
388
389 // Result Four
390 resultFourTxt := slack.NewTextBlockObject("mrkdwn", "*<fakeLink.toYourApp.com|Use Case Catalogue - CF Presentation - [June 12, 2018]>*\nThis is presentation will continue to be updated as...", false, false)
391 resultFourSection := slack.NewSectionBlock(resultFourTxt, nil, slack.NewAccessory(availableOption))
392
393 // Result Five
394 resultFiveTxt := slack.NewTextBlockObject("mrkdwn", "*<fakeLink.toYourApp.com|Comprehensive Benefits Catalogue - 2019>*\nInformation about all the benfits we offer is...", false, false)
395 resultFiveSection := slack.NewSectionBlock(resultFiveTxt, nil, slack.NewAccessory(availableOption))
396
397 // Next Results Button
398 // Suggestions Action
399 btnTxt := slack.NewTextBlockObject("plain_text", "Next 5 Results", false, false)
400 nextBtn := slack.NewButtonBlockElement("", "click_me_123", btnTxt)
401 actionBlock := slack.NewActionBlock("", nextBtn)
402
403 // Build Message with blocks created above
404 msg := slack.NewBlockMessage(
405 headerSection,
406 divSection,
407 resultOneSection,
408 resultTwoSection,
409 resultThreeSection,
410 resultFourSection,
411 resultFiveSection,
412 divSection,
413 actionBlock,
414 )
415
416 b, err := json.MarshalIndent(msg, "", " ")
417 if err != nil {
418 fmt.Println(err)
419 return
420 }
421
422 fmt.Println(string(b))
423
424 }
425
426 func unmarshalExample() {
427 var msgBlocks []slack.Block
428
429 // Append ActionBlock for marshalling
430 btnTxt := slack.NewTextBlockObject("plain_text", "Add a suggestion", false, false)
431 nextBtn := slack.NewButtonBlockElement("", "click_me_123", btnTxt)
432 approveBtnTxt := slack.NewTextBlockObject("plain_text", "Approve", false, false)
433 approveBtn := slack.NewButtonBlockElement("", "click_me_123", approveBtnTxt)
434 msgBlocks = append(msgBlocks, slack.NewActionBlock("", nextBtn, approveBtn))
435
436 // Append ContextBlock for marshalling
437 profileOne := slack.NewImageBlockElement("https://api.slack.com/img/blocks/bkb_template_images/profile_1.png", "Michael Scott")
438 profileTwo := slack.NewImageBlockElement("https://api.slack.com/img/blocks/bkb_template_images/profile_2.png", "Dwight Schrute")
439 textBlockObj := slack.NewTextBlockObject("mrkdwn", "*<fakeLink.toHotelPage.com|Omni Royal Orleans Hotel>*\n★★★★★\n$419 per night\nRated: 8.8 - Excellent", false, false)
440 msgBlocks = append(msgBlocks, slack.NewContextBlock("", []slack.MixedElement{profileOne, profileTwo, textBlockObj}...))
441
442 // Append ImageBlock for marshalling
443 msgBlocks = append(msgBlocks, slack.NewImageBlock("https://api.slack.com/img/blocks/bkb_template_images/profile_2.png", "some profile", "image-block", textBlockObj))
444
445 // Append DividerBlock for marshalling
446 msgBlocks = append(msgBlocks, slack.NewDividerBlock())
447
448 // Append SectionBlock for marshalling
449 approvalText := slack.NewTextBlockObject("mrkdwn", "*Type:*\nPaid time off\n*When:*\nAug 10-Aug 13\n*Hours:* 16.0 (2 days)\n*Remaining balance:* 32.0 hours (4 days)\n*Comments:* \"Family in town, going camping!\"", false, false)
450 approvalImage := slack.NewImageBlockElement("https://api.slack.com/img/blocks/bkb_template_images/approvalsNewDevice.png", "computer thumbnail")
451 msgBlocks = append(msgBlocks, slack.NewSectionBlock(approvalText, nil, slack.NewAccessory(approvalImage)), nil)
452
453 // Build Message with blocks created above
454 msg := slack.NewBlockMessage(msgBlocks...)
455
456 b, err := json.Marshal(&msg)
457 if err != nil {
458 fmt.Println(err)
459 return
460 }
461
462 fmt.Println(string(b))
463
464 // Unmarshal message
465 m := slack.Message{}
466 if err := json.Unmarshal(b, &m); err != nil {
467 fmt.Println(err)
468 return
469 }
470
471 var respBlocks []slack.Block
472 for _, block := range m.Blocks.BlockSet {
473 // Need to implement a type switch to determine Block type since the
474 // response from Slack could include any/all types under "blocks" key
475 switch block.BlockType() {
476 case slack.MBTContext:
477 var respMixedElements []slack.MixedElement
478 contextElements := block.(*slack.ContextBlock).ContextElements.Elements
479 // Need to implement a type switch for ContextElements for same reason as Blocks
480 for _, elem := range contextElements {
481 switch elem.MixedElementType() {
482 case slack.MixedElementImage:
483 // Assert the block's type to manipulate/extract values
484 imageBlockElem := elem.(*slack.ImageBlockElement)
485 imageBlockElem.ImageURL = "https://api.slack.com/img/blocks/bkb_template_images/profile_1.png"
486 imageBlockElem.AltText = "MichaelScott"
487 respMixedElements = append(respMixedElements, imageBlockElem)
488 case slack.MixedElementText:
489 textBlockElem := elem.(*slack.TextBlockObject)
490 textBlockElem.Text = "go go go go go"
491 respMixedElements = append(respMixedElements, textBlockElem)
492 }
493 }
494 respBlocks = append(respBlocks, slack.NewContextBlock("new block", respMixedElements...))
495 case slack.MBTAction:
496 actionBlock := block.(*slack.ActionBlock)
497 // Need to implement a type switch for BlockElements for same reason as Blocks
498 for _, elem := range actionBlock.Elements.ElementSet {
499 switch elem.ElementType() {
500 case slack.METImage:
501 imageElem := elem.(*slack.ImageBlockElement)
502 fmt.Printf("do something with image block element: %v\n", imageElem)
503 case slack.METButton:
504 buttonElem := elem.(*slack.ButtonBlockElement)
505 fmt.Printf("do something with button block element: %v\n", buttonElem)
506 case slack.METOverflow:
507 overflowElem := elem.(*slack.OverflowBlockElement)
508 fmt.Printf("do something with overflow block element: %v\n", overflowElem)
509 case slack.METDatepicker:
510 datepickerElem := elem.(*slack.DatePickerBlockElement)
511 fmt.Printf("do something with datepicker block element: %v\n", datepickerElem)
512 }
513 }
514 respBlocks = append(respBlocks, block)
515 case slack.MBTImage:
516 // Simply re-append the block if you want to include it in the response
517 respBlocks = append(respBlocks, block)
518 case slack.MBTSection:
519 respBlocks = append(respBlocks, block)
520 case slack.MBTDivider:
521 respBlocks = append(respBlocks, block)
522 }
523 }
524
525 // Build new Message with Blocks obtained/edited from callback
526 respMsg := slack.NewBlockMessage(respBlocks...)
527
528 b, err = json.Marshal(&respMsg)
529 if err != nil {
530 fmt.Println(err)
531 return
532 }
533
534 fmt.Println(string(b))
535 }
1313 return
1414 }
1515 for _, channel := range channels {
16 fmt.Println(channel.ID)
16 fmt.Println(channel.Name)
17 // channel is of type conversation & groupConversation
18 // see all available methods in `conversation.go`
1719 }
1820 }
0 package main
1
2 import (
3 "fmt"
4 "log"
5 "net/url"
6 "os"
7
8 "github.com/nlopes/slack"
9 )
10
11 func main() {
12 api := slack.New(
13 "YOUR TOKEN HERE",
14 slack.OptionDebug(true),
15 slack.OptionLog(log.New(os.Stdout, "slack-bot: ", log.Lshortfile|log.LstdFlags)),
16 )
17
18 // turn on the batch_presence_aware option
19 rtm := api.NewRTM(slack.RTMOptionConnParams(url.Values{
20 "batch_presence_aware": {"1"},
21 }))
22 go rtm.ManageConnection()
23
24 for msg := range rtm.IncomingEvents {
25 fmt.Print("Event Received: ")
26 switch ev := msg.Data.(type) {
27 case *slack.HelloEvent:
28 // Replace USER-ID-N here with your User IDs
29 rtm.SendMessage(rtm.NewSubscribeUserPresence([]string{
30 "USER-ID-1",
31 "USER-ID-2",
32 }))
33
34 case *slack.ConnectedEvent:
35 fmt.Println("Infos:", ev.Info)
36 fmt.Println("Connection counter:", ev.ConnectionCount)
37 // Replace C2147483705 with your Channel ID
38 rtm.SendMessage(rtm.NewOutgoingMessage("Hello world", "C2147483705"))
39
40 case *slack.MessageEvent:
41 fmt.Printf("Message: %v\n", ev)
42
43 case *slack.PresenceChangeEvent:
44 fmt.Printf("Presence Change: %v\n", ev)
45
46 case *slack.LatencyReport:
47 fmt.Printf("Current latency: %v\n", ev.Value)
48
49 case *slack.RTMError:
50 fmt.Printf("Error: %s\n", ev.Error())
51
52 case *slack.InvalidAuthEvent:
53 fmt.Printf("Invalid credentials")
54 return
55
56 default:
57
58 // Ignore other events..
59 // fmt.Printf("Unexpected: %v\n", msg.Data)
60 }
61 }
62 }
0 package main
1
2 import (
3 "bytes"
4 "encoding/json"
5 "fmt"
6 "net/http"
7
8 "github.com/nlopes/slack"
9 "github.com/nlopes/slack/slackevents"
10 )
11
12 // You more than likely want your "Bot User OAuth Access Token" which starts with "xoxb-"
13 var api = slack.New("TOKEN")
14
15 func main() {
16 http.HandleFunc("/events-endpoint", func(w http.ResponseWriter, r *http.Request) {
17 buf := new(bytes.Buffer)
18 buf.ReadFrom(r.Body)
19 body := buf.String()
20 eventsAPIEvent, e := slackevents.ParseEvent(json.RawMessage(body), slackevents.OptionVerifyToken(&slackevents.TokenComparator{VerificationToken: "TOKEN"}))
21 if e != nil {
22 w.WriteHeader(http.StatusInternalServerError)
23 }
24
25 if eventsAPIEvent.Type == slackevents.URLVerification {
26 var r *slackevents.ChallengeResponse
27 err := json.Unmarshal([]byte(body), &r)
28 if err != nil {
29 w.WriteHeader(http.StatusInternalServerError)
30 }
31 w.Header().Set("Content-Type", "text")
32 w.Write([]byte(r.Challenge))
33 }
34 if eventsAPIEvent.Type == slackevents.CallbackEvent {
35 innerEvent := eventsAPIEvent.InnerEvent
36 switch ev := innerEvent.Data.(type) {
37 case *slackevents.AppMentionEvent:
38 api.PostMessage(ev.Channel, slack.MsgOptionText("Yes, hello.", false))
39 }
40 }
41 })
42 fmt.Println("[INFO] Server listening")
43 http.ListenAndServe(":3000", nil)
44 }
0 package main
1
2 import (
3 "fmt"
4
5 "github.com/nlopes/slack"
6 )
7
8 func main() {
9 api := slack.New("YOUR_TOKEN_HERE")
10
11 userID := "USER_ID"
12
13 _, _, channelID, err := api.OpenIMChannel(userID)
14
15 if err != nil {
16 fmt.Printf("%s\n", err)
17 }
18
19 api.PostMessage(channelID, slack.MsgOptionText("Hello World!", false))
20 }
77
88 func main() {
99 api := slack.New("YOUR_TOKEN_HERE")
10 params := slack.PostMessageParameters{}
1110 attachment := slack.Attachment{
1211 Pretext: "some pretext",
1312 Text: "some text",
2120 },
2221 */
2322 }
24 params.Attachments = []slack.Attachment{attachment}
25 channelID, timestamp, err := api.PostMessage("CHANNEL_ID", "Some text", params)
23
24 channelID, timestamp, err := api.PostMessage("CHANNEL_ID", slack.MsgOptionText("Some text", false), slack.MsgOptionAttachments(attachment))
2625 if err != nil {
2726 fmt.Printf("%s\n", err)
2827 return
1919 flag.BoolVar(&debug, "debug", false, "Show JSON output")
2020 flag.Parse()
2121
22 api := slack.New(apiToken)
23 if debug {
24 api.SetDebug(true)
25 }
22 api := slack.New(apiToken, slack.OptionDebug(debug))
2623
2724 var (
2825 postAsUserName string
2926 postAsUserID string
3027 postToChannelID string
28 channels []slack.Channel
3129 )
3230
3331 // Find the user to post as.
5048 // If the channel exists, that means we just need to unarchive it
5149 if err.Error() == "name_taken" {
5250 err = nil
53 channels, err := api.GetChannels(false)
54 if err != nil {
51 if channels, err = api.GetChannels(false); err != nil {
5552 fmt.Println("Could not retrieve channels")
5653 return
5754 }
7976 fmt.Printf("Posting as %s (%s) in channel %s\n", postAsUserName, postAsUserID, postToChannelID)
8077
8178 // Post a message.
82 postParams := slack.PostMessageParameters{}
83 channelID, timestamp, err := api.PostMessage(postToChannelID, "Is this any good?", postParams)
79 channelID, timestamp, err := api.PostMessage(postToChannelID, slack.MsgOptionText("Is this any good?", false))
8480 if err != nil {
8581 fmt.Printf("Error posting message: %s\n", err)
8682 return
9086 msgRef := slack.NewRefToMessage(channelID, timestamp)
9187
9288 // Add message pin to channel
93 if err := api.AddPin(channelID, msgRef); err != nil {
89 if err = api.AddPin(channelID, msgRef); err != nil {
9490 fmt.Printf("Error adding pin: %s\n", err)
9591 return
9692 }
1616 flag.BoolVar(&debug, "debug", false, "Show JSON output")
1717 flag.Parse()
1818
19 api := slack.New(apiToken)
20 if debug {
21 api.SetDebug(true)
22 }
19 api := slack.New(apiToken, slack.OptionDebug(debug))
2320
2421 var (
2522 postAsUserName string
5552 fmt.Printf("Posting as %s (%s) in DM with %s (%s), channel %s\n", postAsUserName, postAsUserID, postToUserName, postToUserID, postToChannelID)
5653
5754 // Post a message.
58 postParams := slack.PostMessageParameters{}
59 channelID, timestamp, err := api.PostMessage(postToChannelID, "Is this any good?", postParams)
55 channelID, timestamp, err := api.PostMessage(postToChannelID, slack.MsgOptionText("Is this any good?", false))
6056 if err != nil {
6157 fmt.Printf("Error posting message: %s\n", err)
6258 return
6662 msgRef := slack.NewRefToMessage(channelID, timestamp)
6763
6864 // React with :+1:
69 if err := api.AddReaction("+1", msgRef); err != nil {
65 if err = api.AddReaction("+1", msgRef); err != nil {
7066 fmt.Printf("Error adding reaction: %s\n", err)
7167 return
7268 }
7369
7470 // React with :-1:
75 if err := api.AddReaction("cry", msgRef); err != nil {
71 if err = api.AddReaction("cry", msgRef); err != nil {
7672 fmt.Printf("Error adding reaction: %s\n", err)
7773 return
7874 }
0 package main
1
2 import (
3 "encoding/json"
4 "flag"
5 "fmt"
6 "io"
7 "io/ioutil"
8 "net/http"
9
10 "github.com/nlopes/slack"
11 )
12
13 func main() {
14 var (
15 signingSecret string
16 )
17
18 flag.StringVar(&signingSecret, "secret", "YOUR_SIGNING_SECRET_HERE", "Your Slack app's signing secret")
19 flag.Parse()
20
21 http.HandleFunc("/slash", func(w http.ResponseWriter, r *http.Request) {
22
23 verifier, err := slack.NewSecretsVerifier(r.Header, signingSecret)
24 if err != nil {
25 w.WriteHeader(http.StatusInternalServerError)
26 return
27 }
28
29 r.Body = ioutil.NopCloser(io.TeeReader(r.Body, &verifier))
30 s, err := slack.SlashCommandParse(r)
31 if err != nil {
32 w.WriteHeader(http.StatusInternalServerError)
33 return
34 }
35
36 if err = verifier.Ensure(); err != nil {
37 w.WriteHeader(http.StatusUnauthorized)
38 return
39 }
40
41 switch s.Command {
42 case "/echo":
43 params := &slack.Msg{Text: s.Text}
44 b, err := json.Marshal(params)
45 if err != nil {
46 w.WriteHeader(http.StatusInternalServerError)
47 return
48 }
49 w.Header().Set("Content-Type", "application/json")
50 w.Write(b)
51 default:
52 w.WriteHeader(http.StatusInternalServerError)
53 return
54 }
55 })
56 fmt.Println("[INFO] Server listening")
57 http.ListenAndServe(":3000", nil)
58 }
1616 flag.BoolVar(&debug, "debug", false, "Show JSON output")
1717 flag.Parse()
1818
19 api := slack.New(apiToken)
20 if debug {
21 api.SetDebug(true)
22 }
19 api := slack.New(apiToken, slack.OptionDebug(debug))
2320
2421 // Get all stars for the usr.
2522 params := slack.NewStarsParameters()
77
88 func main() {
99 api := slack.New("YOUR_TOKEN_HERE")
10 //Example for single user
10 //Example for single user
1111 billingActive, err := api.GetBillableInfo("U023BECGF")
1212 if err != nil {
1313 fmt.Printf("%s\n", err)
1515 }
1616 fmt.Printf("ID: U023BECGF, BillingActive: %v\n\n\n", billingActive["U023BECGF"])
1717
18 //Example for team
19 billingActiveForTeam, err := api.GetBillableInfoForTeam()
20 for id, value := range billingActiveForTeam {
21 fmt.Printf("ID: %v, BillingActive: %v\n", id, value)
22 }
18 //Example for team
19 billingActiveForTeam, _ := api.GetBillableInfoForTeam()
20 for id, value := range billingActiveForTeam {
21 fmt.Printf("ID: %v, BillingActive: %v\n", id, value)
22 }
2323
2424 }
0 package main
1
2 import (
3 "encoding/json"
4 "fmt"
5 "strconv"
6 "time"
7
8 "github.com/nlopes/slack"
9 )
10
11 func main() {
12 attachment := slack.Attachment{
13 Color: "good",
14 Fallback: "You successfully posted by Incoming Webhook URL!",
15 AuthorName: "nlopes/slack",
16 AuthorSubname: "github.com",
17 AuthorLink: "https://github.com/nlopes/slack",
18 AuthorIcon: "https://avatars2.githubusercontent.com/u/652790",
19 Text: "<!channel> All text in Slack uses the same system of escaping: chat messages, direct messages, file comments, etc. :smile:\nSee <https://api.slack.com/docs/message-formatting#linking_to_channels_and_users>",
20 Footer: "slack api",
21 FooterIcon: "https://platform.slack-edge.com/img/default_application_icon.png",
22 Ts: json.Number(strconv.FormatInt(time.Now().Unix(), 10)),
23 }
24 msg := slack.WebhookMessage{
25 Attachments: []slack.Attachment{attachment},
26 }
27
28 err := slack.PostWebhook("YOUR_WEBHOOK_URL_HERE", &msg)
29 if err != nil {
30 fmt.Println(err)
31 }
32 }
88 )
99
1010 func main() {
11 api := slack.New("YOUR TOKEN HERE")
12 logger := log.New(os.Stdout, "slack-bot: ", log.Lshortfile|log.LstdFlags)
13 slack.SetLogger(logger)
14 api.SetDebug(true)
11 api := slack.New(
12 "YOUR TOKEN HERE",
13 slack.OptionDebug(true),
14 slack.OptionLog(log.New(os.Stdout, "slack-bot: ", log.Lshortfile|log.LstdFlags)),
15 )
1516
1617 rtm := api.NewRTM()
1718 go rtm.ManageConnection()
2526 case *slack.ConnectedEvent:
2627 fmt.Println("Infos:", ev.Info)
2728 fmt.Println("Connection counter:", ev.ConnectionCount)
28 // Replace #general with your Channel ID
29 rtm.SendMessage(rtm.NewOutgoingMessage("Hello world", "#general"))
29 // Replace C2147483705 with your Channel ID
30 rtm.SendMessage(rtm.NewOutgoingMessage("Hello world", "C2147483705"))
3031
3132 case *slack.MessageEvent:
3233 fmt.Printf("Message: %v\n", ev)
00 package slack
11
22 import (
3 "errors"
3 "context"
4 "fmt"
45 "io"
56 "net/url"
67 "strconv"
8485 CommentsCount int `json:"comments_count"`
8586 NumStars int `json:"num_stars"`
8687 IsStarred bool `json:"is_starred"`
88 Shares Share `json:"shares"`
89 }
90
91 type Share struct {
92 Public map[string][]ShareFileInfo `json:"public"`
93 Private map[string][]ShareFileInfo `json:"private"`
94 }
95
96 type ShareFileInfo struct {
97 ReplyUsers []string `json:"reply_users"`
98 ReplyUsersCount int `json:"reply_users_count"`
99 ReplyCount int `json:"reply_count"`
100 Ts string `json:"ts"`
101 ThreadTs string `json:"thread_ts"`
102 LatestReply string `json:"latest_reply"`
103 ChannelName string `json:"channel_name"`
104 TeamID string `json:"team_id"`
87105 }
88106
89107 // FileUploadParameters contains all the parameters necessary (including the optional ones) for an UploadFile() request.
90108 //
91109 // There are three ways to upload a file. You can either set Content if file is small, set Reader if file is large,
92110 // or provide a local file path in File to upload it from your filesystem.
111 //
112 // Note that when using the Reader option, you *must* specify the Filename, otherwise the Slack API isn't happy.
93113 type FileUploadParameters struct {
94 File string
95 Content string
96 Reader io.Reader
97 Filetype string
98 Filename string
99 Title string
100 InitialComment string
101 Channels []string
114 File string
115 Content string
116 Reader io.Reader
117 Filetype string
118 Filename string
119 Title string
120 InitialComment string
121 Channels []string
122 ThreadTimestamp string
102123 }
103124
104125 // GetFilesParameters contains all the parameters necessary (including the optional ones) for a GetFiles() request
112133 Page int
113134 }
114135
136 // ListFilesParameters contains all the parameters necessary (including the optional ones) for a ListFiles() request
137 type ListFilesParameters struct {
138 Limit int
139 User string
140 Channel string
141 Types string
142 Cursor string
143 }
144
115145 type fileResponseFull struct {
116146 File `json:"file"`
117147 Paging `json:"paging"`
118 Comments []Comment `json:"comments"`
119 Files []File `json:"files"`
148 Comments []Comment `json:"comments"`
149 Files []File `json:"files"`
150 Metadata ResponseMetadata `json:"response_metadata"`
120151
121152 SlackResponse
122153 }
134165 }
135166 }
136167
137 func fileRequest(path string, values url.Values, debug bool) (*fileResponseFull, error) {
168 func (api *Client) fileRequest(ctx context.Context, path string, values url.Values) (*fileResponseFull, error) {
138169 response := &fileResponseFull{}
139 err := post(path, values, response, debug)
170 err := api.postMethod(ctx, path, values, response)
140171 if err != nil {
141172 return nil, err
142173 }
143 if !response.Ok {
144 return nil, errors.New(response.Error)
145 }
146 return response, nil
174
175 return response, response.Err()
147176 }
148177
149178 // GetFileInfo retrieves a file and related comments
150179 func (api *Client) GetFileInfo(fileID string, count, page int) (*File, []Comment, *Paging, error) {
151 values := url.Values{
152 "token": {api.config.token},
180 return api.GetFileInfoContext(context.Background(), fileID, count, page)
181 }
182
183 // GetFileInfoContext retrieves a file and related comments with a custom context
184 func (api *Client) GetFileInfoContext(ctx context.Context, fileID string, count, page int) (*File, []Comment, *Paging, error) {
185 values := url.Values{
186 "token": {api.token},
153187 "file": {fileID},
154188 "count": {strconv.Itoa(count)},
155189 "page": {strconv.Itoa(page)},
156190 }
157 response, err := fileRequest("files.info", values, api.debug)
191
192 response, err := api.fileRequest(ctx, "files.info", values)
158193 if err != nil {
159194 return nil, nil, nil, err
160195 }
161196 return &response.File, response.Comments, &response.Paging, nil
197 }
198
199 // GetFile retreives a given file from its private download URL
200 func (api *Client) GetFile(downloadURL string, writer io.Writer) error {
201 return downloadFile(api.httpclient, api.token, downloadURL, writer, api)
162202 }
163203
164204 // GetFiles retrieves all files according to the parameters given
165205 func (api *Client) GetFiles(params GetFilesParameters) ([]File, *Paging, error) {
166 values := url.Values{
167 "token": {api.config.token},
168 }
206 return api.GetFilesContext(context.Background(), params)
207 }
208
209 // ListFiles retrieves all files according to the parameters given. Uses cursor based pagination.
210 func (api *Client) ListFiles(params ListFilesParameters) ([]File, *ListFilesParameters, error) {
211 return api.ListFilesContext(context.Background(), params)
212 }
213
214 // ListFilesContext retrieves all files according to the parameters given with a custom context. Uses cursor based pagination.
215 func (api *Client) ListFilesContext(ctx context.Context, params ListFilesParameters) ([]File, *ListFilesParameters, error) {
216 values := url.Values{
217 "token": {api.token},
218 }
219
169220 if params.User != DEFAULT_FILES_USER {
170221 values.Add("user", params.User)
171222 }
172223 if params.Channel != DEFAULT_FILES_CHANNEL {
173224 values.Add("channel", params.Channel)
174225 }
226 if params.Limit != DEFAULT_FILES_COUNT {
227 values.Add("limit", strconv.Itoa(params.Limit))
228 }
229 if params.Cursor != "" {
230 values.Add("cursor", params.Cursor)
231 }
232
233 response, err := api.fileRequest(ctx, "files.list", values)
234 if err != nil {
235 return nil, nil, err
236 }
237
238 params.Cursor = response.Metadata.Cursor
239
240 return response.Files, &params, nil
241 }
242
243 // GetFilesContext retrieves all files according to the parameters given with a custom context
244 func (api *Client) GetFilesContext(ctx context.Context, params GetFilesParameters) ([]File, *Paging, error) {
245 values := url.Values{
246 "token": {api.token},
247 }
248 if params.User != DEFAULT_FILES_USER {
249 values.Add("user", params.User)
250 }
251 if params.Channel != DEFAULT_FILES_CHANNEL {
252 values.Add("channel", params.Channel)
253 }
175254 if params.TimestampFrom != DEFAULT_FILES_TS_FROM {
176255 values.Add("ts_from", strconv.FormatInt(int64(params.TimestampFrom), 10))
177256 }
187266 if params.Page != DEFAULT_FILES_PAGE {
188267 values.Add("page", strconv.Itoa(params.Page))
189268 }
190 response, err := fileRequest("files.list", values, api.debug)
269
270 response, err := api.fileRequest(ctx, "files.list", values)
191271 if err != nil {
192272 return nil, nil, err
193273 }
196276
197277 // UploadFile uploads a file
198278 func (api *Client) UploadFile(params FileUploadParameters) (file *File, err error) {
279 return api.UploadFileContext(context.Background(), params)
280 }
281
282 // UploadFileContext uploads a file and setting a custom context
283 func (api *Client) UploadFileContext(ctx context.Context, params FileUploadParameters) (file *File, err error) {
199284 // Test if user token is valid. This helps because client.Do doesn't like this for some reason. XXX: More
200285 // investigation needed, but for now this will do.
201286 _, err = api.AuthTest()
204289 }
205290 response := &fileResponseFull{}
206291 values := url.Values{
207 "token": {api.config.token},
292 "token": {api.token},
208293 }
209294 if params.Filetype != "" {
210295 values.Add("filetype", params.Filetype)
218303 if params.InitialComment != "" {
219304 values.Add("initial_comment", params.InitialComment)
220305 }
306 if params.ThreadTimestamp != "" {
307 values.Add("thread_ts", params.ThreadTimestamp)
308 }
221309 if len(params.Channels) != 0 {
222310 values.Add("channels", strings.Join(params.Channels, ","))
223311 }
224312 if params.Content != "" {
225313 values.Add("content", params.Content)
226 err = post("files.upload", values, response, api.debug)
314 err = api.postMethod(ctx, "files.upload", values, response)
227315 } else if params.File != "" {
228 err = postLocalWithMultipartResponse("files.upload", params.File, "file", values, response, api.debug)
316 err = postLocalWithMultipartResponse(ctx, api.httpclient, api.endpoint+"files.upload", params.File, "file", values, response, api)
229317 } else if params.Reader != nil {
230 err = postWithMultipartResponse("files.upload", params.Filename, "file", values, params.Reader, response, api.debug)
231 }
318 if params.Filename == "" {
319 return nil, fmt.Errorf("files.upload: FileUploadParameters.Filename is mandatory when using FileUploadParameters.Reader")
320 }
321 err = postWithMultipartResponse(ctx, api.httpclient, api.endpoint+"files.upload", params.Filename, "file", values, params.Reader, response, api)
322 }
323
232324 if err != nil {
233325 return nil, err
234326 }
235 if !response.Ok {
236 return nil, errors.New(response.Error)
237 }
238 return &response.File, nil
327
328 return &response.File, response.Err()
329 }
330
331 // DeleteFileComment deletes a file's comment
332 func (api *Client) DeleteFileComment(commentID, fileID string) error {
333 return api.DeleteFileCommentContext(context.Background(), fileID, commentID)
334 }
335
336 // DeleteFileCommentContext deletes a file's comment with a custom context
337 func (api *Client) DeleteFileCommentContext(ctx context.Context, fileID, commentID string) (err error) {
338 if fileID == "" || commentID == "" {
339 return ErrParametersMissing
340 }
341
342 values := url.Values{
343 "token": {api.token},
344 "file": {fileID},
345 "id": {commentID},
346 }
347 _, err = api.fileRequest(ctx, "files.comments.delete", values)
348 return err
239349 }
240350
241351 // DeleteFile deletes a file
242352 func (api *Client) DeleteFile(fileID string) error {
243 values := url.Values{
244 "token": {api.config.token},
353 return api.DeleteFileContext(context.Background(), fileID)
354 }
355
356 // DeleteFileContext deletes a file with a custom context
357 func (api *Client) DeleteFileContext(ctx context.Context, fileID string) (err error) {
358 values := url.Values{
359 "token": {api.token},
245360 "file": {fileID},
246361 }
247 _, err := fileRequest("files.delete", values, api.debug)
248 if err != nil {
249 return err
250 }
251 return nil
252
362
363 _, err = api.fileRequest(ctx, "files.delete", values)
364 return err
253365 }
254366
255367 // RevokeFilePublicURL disables public/external sharing for a file
256368 func (api *Client) RevokeFilePublicURL(fileID string) (*File, error) {
257 values := url.Values{
258 "token": {api.config.token},
369 return api.RevokeFilePublicURLContext(context.Background(), fileID)
370 }
371
372 // RevokeFilePublicURLContext disables public/external sharing for a file with a custom context
373 func (api *Client) RevokeFilePublicURLContext(ctx context.Context, fileID string) (*File, error) {
374 values := url.Values{
375 "token": {api.token},
259376 "file": {fileID},
260377 }
261 response, err := fileRequest("files.revokePublicURL", values, api.debug)
378
379 response, err := api.fileRequest(ctx, "files.revokePublicURL", values)
262380 if err != nil {
263381 return nil, err
264382 }
267385
268386 // ShareFilePublicURL enabled public/external sharing for a file
269387 func (api *Client) ShareFilePublicURL(fileID string) (*File, []Comment, *Paging, error) {
270 values := url.Values{
271 "token": {api.config.token},
388 return api.ShareFilePublicURLContext(context.Background(), fileID)
389 }
390
391 // ShareFilePublicURLContext enabled public/external sharing for a file with a custom context
392 func (api *Client) ShareFilePublicURLContext(ctx context.Context, fileID string) (*File, []Comment, *Paging, error) {
393 values := url.Values{
394 "token": {api.token},
272395 "file": {fileID},
273396 }
274 response, err := fileRequest("files.sharedPublicURL", values, api.debug)
397
398 response, err := api.fileRequest(ctx, "files.sharedPublicURL", values)
275399 if err != nil {
276400 return nil, nil, nil, err
277401 }
0 package slack
1
2 import (
3 "bytes"
4 "encoding/json"
5 "io/ioutil"
6 "log"
7 "net/http"
8 "net/url"
9 "reflect"
10 "strings"
11 "testing"
12 )
13
14 type fileCommentHandler struct {
15 gotParams map[string]string
16 }
17
18 func newFileCommentHandler() *fileCommentHandler {
19 return &fileCommentHandler{
20 gotParams: make(map[string]string),
21 }
22 }
23
24 func (h *fileCommentHandler) accumulateFormValue(k string, r *http.Request) {
25 if v := r.FormValue(k); v != "" {
26 h.gotParams[k] = v
27 }
28 }
29
30 func (h *fileCommentHandler) handler(w http.ResponseWriter, r *http.Request) {
31 h.accumulateFormValue("token", r)
32 h.accumulateFormValue("file", r)
33 h.accumulateFormValue("id", r)
34
35 w.Header().Set("Content-Type", "application/json")
36 if h.gotParams["id"] == "trigger-error" {
37 w.Write([]byte(`{ "ok": false, "error": "errored" }`))
38 } else {
39 w.Write([]byte(`{ "ok": true }`))
40 }
41 }
42
43 type mockHTTPClient struct{}
44
45 func (m *mockHTTPClient) Do(*http.Request) (*http.Response, error) {
46 return &http.Response{StatusCode: 200, Body: ioutil.NopCloser(bytes.NewBufferString(`OK`))}, nil
47 }
48
49 func TestSlack_GetFile(t *testing.T) {
50 api := &Client{
51 endpoint: "http://" + serverAddr + "/",
52 token: "testing-token",
53 httpclient: &mockHTTPClient{},
54 }
55
56 tests := []struct {
57 title string
58 downloadURL string
59 expectError bool
60 }{
61 {
62 title: "Testing with valid file",
63 downloadURL: "https://files.slack.com/files-pri/T99999999-FGGGGGGGG/download/test.csv",
64 expectError: false,
65 },
66 {
67 title: "Testing with invalid file (empty URL)",
68 downloadURL: "",
69 expectError: true,
70 },
71 }
72
73 for _, test := range tests {
74 err := api.GetFile(test.downloadURL, &bytes.Buffer{})
75
76 if !test.expectError && err != nil {
77 log.Fatalf("%s: Unexpected error: %s in test", test.title, err)
78 } else if test.expectError == true && err == nil {
79 log.Fatalf("Expected error but got none")
80 }
81 }
82 }
83
84 func TestSlack_DeleteFileComment(t *testing.T) {
85 once.Do(startServer)
86 api := New("testing-token", OptionAPIURL("http://"+serverAddr+"/"))
87 tests := []struct {
88 title string
89 body url.Values
90 wantParams map[string]string
91 expectError bool
92 }{
93 {
94 title: "Testing with proper body",
95 body: url.Values{
96 "file": {"file12345"},
97 "id": {"id12345"},
98 },
99 wantParams: map[string]string{
100 "token": "testing-token",
101 "file": "file12345",
102 "id": "id12345",
103 },
104 expectError: false,
105 },
106 {
107 title: "Testing with false body",
108 body: url.Values{
109 "file": {""},
110 "id": {""},
111 },
112 wantParams: map[string]string{},
113 expectError: true,
114 },
115 {
116 title: "Testing with error",
117 body: url.Values{
118 "file": {"file12345"},
119 "id": {"trigger-error"},
120 },
121 wantParams: map[string]string{
122 "token": "testing-token",
123 "file": "file12345",
124 "id": "trigger-error",
125 },
126 expectError: true,
127 },
128 }
129
130 var fch *fileCommentHandler
131 http.HandleFunc("/files.comments.delete", func(w http.ResponseWriter, r *http.Request) {
132 fch.handler(w, r)
133 })
134
135 for _, test := range tests {
136 fch = newFileCommentHandler()
137 err := api.DeleteFileComment(test.body["id"][0], test.body["file"][0])
138
139 if !test.expectError && err != nil {
140 log.Fatalf("%s: Unexpected error: %s in test", test.title, err)
141 } else if test.expectError == true && err == nil {
142 log.Fatalf("Expected error but got none")
143 }
144
145 if !reflect.DeepEqual(fch.gotParams, test.wantParams) {
146 log.Fatalf("%s: Got params [%#v]\nBut received [%#v]\n", test.title, fch.gotParams, test.wantParams)
147 }
148 }
149 }
150
151 func authTestHandler(rw http.ResponseWriter, r *http.Request) {
152 rw.Header().Set("Content-Type", "application/json")
153 response, _ := json.Marshal(authTestResponseFull{
154 SlackResponse: SlackResponse{Ok: true}})
155 rw.Write(response)
156 }
157
158 func uploadFileHandler(rw http.ResponseWriter, r *http.Request) {
159 rw.Header().Set("Content-Type", "application/json")
160 response, _ := json.Marshal(fileResponseFull{
161 SlackResponse: SlackResponse{Ok: true}})
162 rw.Write(response)
163 }
164
165 func TestUploadFile(t *testing.T) {
166 http.HandleFunc("/auth.test", authTestHandler)
167 http.HandleFunc("/files.upload", uploadFileHandler)
168 once.Do(startServer)
169 api := New("testing-token", OptionAPIURL("http://"+serverAddr+"/"))
170 params := FileUploadParameters{
171 Filename: "test.txt", Content: "test content",
172 Channels: []string{"CXXXXXXXX"}}
173 if _, err := api.UploadFile(params); err != nil {
174 t.Errorf("Unexpected error: %s", err)
175 }
176
177 reader := bytes.NewBufferString("test reader")
178 params = FileUploadParameters{
179 Filename: "test.txt",
180 Reader: reader,
181 Channels: []string{"CXXXXXXXX"}}
182 if _, err := api.UploadFile(params); err != nil {
183 t.Errorf("Unexpected error: %s", err)
184 }
185
186 largeByt := make([]byte, 107374200)
187 reader = bytes.NewBuffer(largeByt)
188 params = FileUploadParameters{
189 Filename: "test.txt", Reader: reader,
190 Channels: []string{"CXXXXXXXX"}}
191 if _, err := api.UploadFile(params); err != nil {
192 t.Errorf("Unexpected error: %s", err)
193 }
194 }
195
196 func TestUploadFileWithoutFilename(t *testing.T) {
197 once.Do(startServer)
198 api := New("testing-token", OptionAPIURL("http://"+serverAddr+"/"))
199
200 reader := bytes.NewBufferString("test reader")
201 params := FileUploadParameters{
202 Reader: reader,
203 Channels: []string{"CXXXXXXXX"}}
204 _, err := api.UploadFile(params)
205 if err == nil {
206 t.Fatal("Expected error when omitting filename, instead got nil")
207 }
208
209 if !strings.Contains(err.Error(), ".Filename is mandatory") {
210 t.Errorf("Error message should mention empty FileUploadParameters.Filename")
211 }
212 }
0 module github.com/nlopes/slack
1
2 require (
3 github.com/davecgh/go-spew v1.1.1 // indirect
4 github.com/gorilla/websocket v1.2.0
5 github.com/pkg/errors v0.8.0
6 github.com/pmezard/go-difflib v1.0.0 // indirect
7 github.com/stretchr/testify v1.2.2
8 )
0 github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8=
1 github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
2 github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
3 github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
4 github.com/gorilla/websocket v1.2.0 h1:VJtLvh6VQym50czpZzx07z/kw9EgAxI3x1ZB8taTMQQ=
5 github.com/gorilla/websocket v1.2.0/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoAMk2YaljkQ=
6 github.com/nlopes/slack v0.1.0/go.mod h1:jVI4BBK3lSktibKahxBF74txcK2vyvkza1z/+rRnVAM=
7 github.com/nlopes/slack v0.5.0 h1:NbIae8Kd0NpqaEI3iUrsuS0KbcEDhzhc939jLW5fNm0=
8 github.com/nlopes/slack v0.5.0/go.mod h1:jVI4BBK3lSktibKahxBF74txcK2vyvkza1z/+rRnVAM=
9 github.com/pkg/errors v0.8.0 h1:WdK/asTD0HN+q6hsWO3/vpuAkAr+tw6aNJNDFFf0+qw=
10 github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
11 github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
12 github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
13 github.com/stretchr/testify v1.2.0/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
14 github.com/stretchr/testify v1.2.2 h1:bSDNvY7ZPG5RlJ8otE/7V6gMiyenm9RtJ7IUVIAoJ1w=
15 github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
16 github.com/victorcoder/slack-test v0.0.0-20190131110821-6f9a569c10af h1:JFxr+No3ZWgCtxnnTWCybnB/z0Iy3qLmdj3u2NV5o48=
17 github.com/victorcoder/slack-test v0.0.0-20190131110821-6f9a569c10af/go.mod h1:dStM4ShMus8J3hiq66ExbbzGLkwyZ+RQJePwFhWCCvQ=
18 github.com/victorcoder/slack-test v0.0.0-20190131113129-a43b3bb77f43 h1:wtFekkaAAQibpy3iE4Hhx2Gi9pZAbITOSfVP7GXk5eM=
19 github.com/victorcoder/slack-test v0.0.0-20190131113129-a43b3bb77f43/go.mod h1:dStM4ShMus8J3hiq66ExbbzGLkwyZ+RQJePwFhWCCvQ=
20 golang.org/x/net v0.0.0-20180108090419-434ec0c7fe37 h1:BkNcmLtAVeWe9h5k0jt24CQgaG5vb4x/doFbAiEC/Ho=
21 golang.org/x/net v0.0.0-20180108090419-434ec0c7fe37/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
00 package slack
11
22 import (
3 "errors"
3 "context"
44 "net/url"
55 "strconv"
66 )
77
88 // Group contains all the information for a group
99 type Group struct {
10 groupConversation
10 GroupConversation
1111 IsGroup bool `json:"is_group"`
1212 }
1313
2626 SlackResponse
2727 }
2828
29 func groupRequest(path string, values url.Values, debug bool) (*groupResponseFull, error) {
29 func (api *Client) groupRequest(ctx context.Context, path string, values url.Values) (*groupResponseFull, error) {
3030 response := &groupResponseFull{}
31 err := post(path, values, response, debug)
32 if err != nil {
33 return nil, err
34 }
35 if !response.Ok {
36 return nil, errors.New(response.Error)
37 }
38 return response, nil
31 err := api.postMethod(ctx, path, values, response)
32 if err != nil {
33 return nil, err
34 }
35
36 return response, response.Err()
3937 }
4038
4139 // ArchiveGroup archives a private group
4240 func (api *Client) ArchiveGroup(group string) error {
43 values := url.Values{
44 "token": {api.config.token},
45 "channel": {group},
46 }
47 _, err := groupRequest("groups.archive", values, api.debug)
48 if err != nil {
49 return err
50 }
51 return nil
41 return api.ArchiveGroupContext(context.Background(), group)
42 }
43
44 // ArchiveGroupContext archives a private group
45 func (api *Client) ArchiveGroupContext(ctx context.Context, group string) error {
46 values := url.Values{
47 "token": {api.token},
48 "channel": {group},
49 }
50
51 _, err := api.groupRequest(ctx, "groups.archive", values)
52 return err
5253 }
5354
5455 // UnarchiveGroup unarchives a private group
5556 func (api *Client) UnarchiveGroup(group string) error {
56 values := url.Values{
57 "token": {api.config.token},
58 "channel": {group},
59 }
60 _, err := groupRequest("groups.unarchive", values, api.debug)
61 if err != nil {
62 return err
63 }
64 return nil
57 return api.UnarchiveGroupContext(context.Background(), group)
58 }
59
60 // UnarchiveGroupContext unarchives a private group
61 func (api *Client) UnarchiveGroupContext(ctx context.Context, group string) error {
62 values := url.Values{
63 "token": {api.token},
64 "channel": {group},
65 }
66
67 _, err := api.groupRequest(ctx, "groups.unarchive", values)
68 return err
6569 }
6670
6771 // CreateGroup creates a private group
6872 func (api *Client) CreateGroup(group string) (*Group, error) {
69 values := url.Values{
70 "token": {api.config.token},
73 return api.CreateGroupContext(context.Background(), group)
74 }
75
76 // CreateGroupContext creates a private group
77 func (api *Client) CreateGroupContext(ctx context.Context, group string) (*Group, error) {
78 values := url.Values{
79 "token": {api.token},
7180 "name": {group},
7281 }
73 response, err := groupRequest("groups.create", values, api.debug)
82
83 response, err := api.groupRequest(ctx, "groups.create", values)
7484 if err != nil {
7585 return nil, err
7686 }
8494 // 3. Creates a new group with the name of the existing group.
8595 // 4. Adds all members of the existing group to the new group.
8696 func (api *Client) CreateChildGroup(group string) (*Group, error) {
87 values := url.Values{
88 "token": {api.config.token},
89 "channel": {group},
90 }
91 response, err := groupRequest("groups.createChild", values, api.debug)
97 return api.CreateChildGroupContext(context.Background(), group)
98 }
99
100 // CreateChildGroupContext creates a new private group archiving the old one with a custom context
101 // For more information see CreateChildGroup
102 func (api *Client) CreateChildGroupContext(ctx context.Context, group string) (*Group, error) {
103 values := url.Values{
104 "token": {api.token},
105 "channel": {group},
106 }
107
108 response, err := api.groupRequest(ctx, "groups.createChild", values)
92109 if err != nil {
93110 return nil, err
94111 }
95112 return &response.Group, nil
96 }
97
98 // CloseGroup closes a private group
99 func (api *Client) CloseGroup(group string) (bool, bool, error) {
100 values := url.Values{
101 "token": {api.config.token},
102 "channel": {group},
103 }
104 response, err := imRequest("groups.close", values, api.debug)
105 if err != nil {
106 return false, false, err
107 }
108 return response.NoOp, response.AlreadyClosed, nil
109113 }
110114
111115 // GetGroupHistory fetches all the history for a private group
112116 func (api *Client) GetGroupHistory(group string, params HistoryParameters) (*History, error) {
113 values := url.Values{
114 "token": {api.config.token},
117 return api.GetGroupHistoryContext(context.Background(), group, params)
118 }
119
120 // GetGroupHistoryContext fetches all the history for a private group with a custom context
121 func (api *Client) GetGroupHistoryContext(ctx context.Context, group string, params HistoryParameters) (*History, error) {
122 values := url.Values{
123 "token": {api.token},
115124 "channel": {group},
116125 }
117126 if params.Latest != DEFAULT_HISTORY_LATEST {
137146 values.Add("unreads", "0")
138147 }
139148 }
140 response, err := groupRequest("groups.history", values, api.debug)
149
150 response, err := api.groupRequest(ctx, "groups.history", values)
141151 if err != nil {
142152 return nil, err
143153 }
146156
147157 // InviteUserToGroup invites a specific user to a private group
148158 func (api *Client) InviteUserToGroup(group, user string) (*Group, bool, error) {
149 values := url.Values{
150 "token": {api.config.token},
159 return api.InviteUserToGroupContext(context.Background(), group, user)
160 }
161
162 // InviteUserToGroupContext invites a specific user to a private group with a custom context
163 func (api *Client) InviteUserToGroupContext(ctx context.Context, group, user string) (*Group, bool, error) {
164 values := url.Values{
165 "token": {api.token},
151166 "channel": {group},
152167 "user": {user},
153168 }
154 response, err := groupRequest("groups.invite", values, api.debug)
169
170 response, err := api.groupRequest(ctx, "groups.invite", values)
155171 if err != nil {
156172 return nil, false, err
157173 }
160176
161177 // LeaveGroup makes authenticated user leave the group
162178 func (api *Client) LeaveGroup(group string) error {
163 values := url.Values{
164 "token": {api.config.token},
165 "channel": {group},
166 }
167 _, err := groupRequest("groups.leave", values, api.debug)
168 if err != nil {
169 return err
170 }
171 return nil
179 return api.LeaveGroupContext(context.Background(), group)
180 }
181
182 // LeaveGroupContext makes authenticated user leave the group with a custom context
183 func (api *Client) LeaveGroupContext(ctx context.Context, group string) (err error) {
184 values := url.Values{
185 "token": {api.token},
186 "channel": {group},
187 }
188
189 _, err = api.groupRequest(ctx, "groups.leave", values)
190 return err
172191 }
173192
174193 // KickUserFromGroup kicks a user from a group
175194 func (api *Client) KickUserFromGroup(group, user string) error {
176 values := url.Values{
177 "token": {api.config.token},
195 return api.KickUserFromGroupContext(context.Background(), group, user)
196 }
197
198 // KickUserFromGroupContext kicks a user from a group with a custom context
199 func (api *Client) KickUserFromGroupContext(ctx context.Context, group, user string) (err error) {
200 values := url.Values{
201 "token": {api.token},
178202 "channel": {group},
179203 "user": {user},
180204 }
181 _, err := groupRequest("groups.kick", values, api.debug)
182 if err != nil {
183 return err
184 }
185 return nil
205
206 _, err = api.groupRequest(ctx, "groups.kick", values)
207 return err
186208 }
187209
188210 // GetGroups retrieves all groups
189211 func (api *Client) GetGroups(excludeArchived bool) ([]Group, error) {
190 values := url.Values{
191 "token": {api.config.token},
212 return api.GetGroupsContext(context.Background(), excludeArchived)
213 }
214
215 // GetGroupsContext retrieves all groups with a custom context
216 func (api *Client) GetGroupsContext(ctx context.Context, excludeArchived bool) ([]Group, error) {
217 values := url.Values{
218 "token": {api.token},
192219 }
193220 if excludeArchived {
194221 values.Add("exclude_archived", "1")
195222 }
196 response, err := groupRequest("groups.list", values, api.debug)
223
224 response, err := api.groupRequest(ctx, "groups.list", values)
197225 if err != nil {
198226 return nil, err
199227 }
202230
203231 // GetGroupInfo retrieves the given group
204232 func (api *Client) GetGroupInfo(group string) (*Group, error) {
205 values := url.Values{
206 "token": {api.config.token},
207 "channel": {group},
208 }
209 response, err := groupRequest("groups.info", values, api.debug)
233 return api.GetGroupInfoContext(context.Background(), group)
234 }
235
236 // GetGroupInfoContext retrieves the given group with a custom context
237 func (api *Client) GetGroupInfoContext(ctx context.Context, group string) (*Group, error) {
238 values := url.Values{
239 "token": {api.token},
240 "channel": {group},
241 "include_locale": {strconv.FormatBool(true)},
242 }
243
244 response, err := api.groupRequest(ctx, "groups.info", values)
210245 if err != nil {
211246 return nil, err
212247 }
219254 // calls (just one per channel). This is useful for when reading scroll-back history, or following a busy live
220255 // channel. A timeout of 5 seconds is a good starting point. Be sure to flush these calls on shutdown/logout.
221256 func (api *Client) SetGroupReadMark(group, ts string) error {
222 values := url.Values{
223 "token": {api.config.token},
257 return api.SetGroupReadMarkContext(context.Background(), group, ts)
258 }
259
260 // SetGroupReadMarkContext sets the read mark on a private group with a custom context
261 // For more details see SetGroupReadMark
262 func (api *Client) SetGroupReadMarkContext(ctx context.Context, group, ts string) (err error) {
263 values := url.Values{
264 "token": {api.token},
224265 "channel": {group},
225266 "ts": {ts},
226267 }
227 _, err := groupRequest("groups.mark", values, api.debug)
228 if err != nil {
229 return err
230 }
231 return nil
268
269 _, err = api.groupRequest(ctx, "groups.mark", values)
270 return err
232271 }
233272
234273 // OpenGroup opens a private group
235274 func (api *Client) OpenGroup(group string) (bool, bool, error) {
236 values := url.Values{
237 "token": {api.config.token},
238 "channel": {group},
239 }
240 response, err := groupRequest("groups.open", values, api.debug)
275 return api.OpenGroupContext(context.Background(), group)
276 }
277
278 // OpenGroupContext opens a private group with a custom context
279 func (api *Client) OpenGroupContext(ctx context.Context, group string) (bool, bool, error) {
280 values := url.Values{
281 "token": {api.token},
282 "channel": {group},
283 }
284
285 response, err := api.groupRequest(ctx, "groups.open", values)
241286 if err != nil {
242287 return false, false, err
243288 }
248293 // XXX: They return a channel, not a group. What is this crap? :(
249294 // Inconsistent api it seems.
250295 func (api *Client) RenameGroup(group, name string) (*Channel, error) {
251 values := url.Values{
252 "token": {api.config.token},
296 return api.RenameGroupContext(context.Background(), group, name)
297 }
298
299 // RenameGroupContext renames a group with a custom context
300 func (api *Client) RenameGroupContext(ctx context.Context, group, name string) (*Channel, error) {
301 values := url.Values{
302 "token": {api.token},
253303 "channel": {group},
254304 "name": {name},
255305 }
306
256307 // XXX: the created entry in this call returns a string instead of a number
257308 // so I may have to do some workaround to solve it.
258 response, err := groupRequest("groups.rename", values, api.debug)
309 response, err := api.groupRequest(ctx, "groups.rename", values)
259310 if err != nil {
260311 return nil, err
261312 }
262313 return &response.Channel, nil
263
264314 }
265315
266316 // SetGroupPurpose sets the group purpose
267317 func (api *Client) SetGroupPurpose(group, purpose string) (string, error) {
268 values := url.Values{
269 "token": {api.config.token},
318 return api.SetGroupPurposeContext(context.Background(), group, purpose)
319 }
320
321 // SetGroupPurposeContext sets the group purpose with a custom context
322 func (api *Client) SetGroupPurposeContext(ctx context.Context, group, purpose string) (string, error) {
323 values := url.Values{
324 "token": {api.token},
270325 "channel": {group},
271326 "purpose": {purpose},
272327 }
273 response, err := groupRequest("groups.setPurpose", values, api.debug)
328
329 response, err := api.groupRequest(ctx, "groups.setPurpose", values)
274330 if err != nil {
275331 return "", err
276332 }
279335
280336 // SetGroupTopic sets the group topic
281337 func (api *Client) SetGroupTopic(group, topic string) (string, error) {
282 values := url.Values{
283 "token": {api.config.token},
338 return api.SetGroupTopicContext(context.Background(), group, topic)
339 }
340
341 // SetGroupTopicContext sets the group topic with a custom context
342 func (api *Client) SetGroupTopicContext(ctx context.Context, group, topic string) (string, error) {
343 values := url.Values{
344 "token": {api.token},
284345 "channel": {group},
285346 "topic": {topic},
286347 }
287 response, err := groupRequest("groups.setTopic", values, api.debug)
348
349 response, err := api.groupRequest(ctx, "groups.setTopic", values)
288350 if err != nil {
289351 return "", err
290352 }
+49
-25
im.go less more
00 package slack
11
22 import (
3 "errors"
3 "context"
44 "net/url"
55 "strconv"
66 )
2121
2222 // IM contains information related to the Direct Message channel
2323 type IM struct {
24 conversation
25 IsIM bool `json:"is_im"`
26 User string `json:"user"`
27 IsUserDeleted bool `json:"is_user_deleted"`
24 Conversation
25 IsUserDeleted bool `json:"is_user_deleted"`
2826 }
2927
30 func imRequest(path string, values url.Values, debug bool) (*imResponseFull, error) {
28 func (api *Client) imRequest(ctx context.Context, path string, values url.Values) (*imResponseFull, error) {
3129 response := &imResponseFull{}
32 err := post(path, values, response, debug)
30 err := api.postMethod(ctx, path, values, response)
3331 if err != nil {
3432 return nil, err
3533 }
36 if !response.Ok {
37 return nil, errors.New(response.Error)
38 }
39 return response, nil
34
35 return response, response.Err()
4036 }
4137
4238 // CloseIMChannel closes the direct message channel
4339 func (api *Client) CloseIMChannel(channel string) (bool, bool, error) {
40 return api.CloseIMChannelContext(context.Background(), channel)
41 }
42
43 // CloseIMChannelContext closes the direct message channel with a custom context
44 func (api *Client) CloseIMChannelContext(ctx context.Context, channel string) (bool, bool, error) {
4445 values := url.Values{
45 "token": {api.config.token},
46 "token": {api.token},
4647 "channel": {channel},
4748 }
48 response, err := imRequest("im.close", values, api.debug)
49
50 response, err := api.imRequest(ctx, "im.close", values)
4951 if err != nil {
5052 return false, false, err
5153 }
5557 // OpenIMChannel opens a direct message channel to the user provided as argument
5658 // Returns some status and the channel ID
5759 func (api *Client) OpenIMChannel(user string) (bool, bool, string, error) {
60 return api.OpenIMChannelContext(context.Background(), user)
61 }
62
63 // OpenIMChannelContext opens a direct message channel to the user provided as argument with a custom context
64 // Returns some status and the channel ID
65 func (api *Client) OpenIMChannelContext(ctx context.Context, user string) (bool, bool, string, error) {
5866 values := url.Values{
59 "token": {api.config.token},
67 "token": {api.token},
6068 "user": {user},
6169 }
62 response, err := imRequest("im.open", values, api.debug)
70
71 response, err := api.imRequest(ctx, "im.open", values)
6372 if err != nil {
6473 return false, false, "", err
6574 }
6877
6978 // MarkIMChannel sets the read mark of a direct message channel to a specific point
7079 func (api *Client) MarkIMChannel(channel, ts string) (err error) {
80 return api.MarkIMChannelContext(context.Background(), channel, ts)
81 }
82
83 // MarkIMChannelContext sets the read mark of a direct message channel to a specific point with a custom context
84 func (api *Client) MarkIMChannelContext(ctx context.Context, channel, ts string) error {
7185 values := url.Values{
72 "token": {api.config.token},
86 "token": {api.token},
7387 "channel": {channel},
7488 "ts": {ts},
7589 }
76 _, err = imRequest("im.mark", values, api.debug)
77 if err != nil {
78 return err
79 }
80 return
90
91 _, err := api.imRequest(ctx, "im.mark", values)
92 return err
8193 }
8294
8395 // GetIMHistory retrieves the direct message channel history
8496 func (api *Client) GetIMHistory(channel string, params HistoryParameters) (*History, error) {
97 return api.GetIMHistoryContext(context.Background(), channel, params)
98 }
99
100 // GetIMHistoryContext retrieves the direct message channel history with a custom context
101 func (api *Client) GetIMHistoryContext(ctx context.Context, channel string, params HistoryParameters) (*History, error) {
85102 values := url.Values{
86 "token": {api.config.token},
103 "token": {api.token},
87104 "channel": {channel},
88105 }
89106 if params.Latest != DEFAULT_HISTORY_LATEST {
109126 values.Add("unreads", "0")
110127 }
111128 }
112 response, err := imRequest("im.history", values, api.debug)
129
130 response, err := api.imRequest(ctx, "im.history", values)
113131 if err != nil {
114132 return nil, err
115133 }
118136
119137 // GetIMChannels returns the list of direct message channels
120138 func (api *Client) GetIMChannels() ([]IM, error) {
139 return api.GetIMChannelsContext(context.Background())
140 }
141
142 // GetIMChannelsContext returns the list of direct message channels with a custom context
143 func (api *Client) GetIMChannelsContext(ctx context.Context) ([]IM, error) {
121144 values := url.Values{
122 "token": {api.config.token},
145 "token": {api.token},
123146 }
124 response, err := imRequest("im.list", values, api.debug)
147
148 response, err := api.imRequest(ctx, "im.list", values)
125149 if err != nil {
126150 return nil, err
127151 }
00 package slack
11
22 import (
3 "bytes"
34 "fmt"
5 "strconv"
46 "time"
57 )
68
126128 return time.Unix(int64(t), 0)
127129 }
128130
131 // UnmarshalJSON will unmarshal both string and int JSON values
132 func (t *JSONTime) UnmarshalJSON(buf []byte) error {
133 s := bytes.Trim(buf, `"`)
134
135 v, err := strconv.Atoi(string(s))
136 if err != nil {
137 return err
138 }
139
140 *t = JSONTime(int64(v))
141 return nil
142 }
143
129144 // Team contains details about a team
130145 type Team struct {
131146 ID string `json:"id"`
140155 Image72 string `json:"image_72,omitempty"`
141156 }
142157
143 // Info contains various details about Users, Channels, Bots and the authenticated user.
158 // Info contains various details about the authenticated user and team.
144159 // It is returned by StartRTM or included in the "ConnectedEvent" RTM event.
145160 type Info struct {
146 URL string `json:"url,omitempty"`
147 User *UserDetails `json:"self,omitempty"`
148 Team *Team `json:"team,omitempty"`
149 Users []User `json:"users,omitempty"`
150 Channels []Channel `json:"channels,omitempty"`
151 Groups []Group `json:"groups,omitempty"`
152 Bots []Bot `json:"bots,omitempty"`
153 IMs []IM `json:"ims,omitempty"`
161 URL string `json:"url,omitempty"`
162 User *UserDetails `json:"self,omitempty"`
163 Team *Team `json:"team,omitempty"`
154164 }
155165
156166 type infoResponseFull struct {
157167 Info
158 WebResponse
168 SlackResponse
159169 }
160170
161 // GetBotByID returns a bot given a bot id
171 // GetBotByID is deprecated and returns nil
162172 func (info Info) GetBotByID(botID string) *Bot {
163 for _, bot := range info.Bots {
164 if bot.ID == botID {
165 return &bot
166 }
167 }
168173 return nil
169174 }
170175
171 // GetUserByID returns a user given a user id
176 // GetUserByID is deprecated and returns nil
172177 func (info Info) GetUserByID(userID string) *User {
173 for _, user := range info.Users {
174 if user.ID == userID {
175 return &user
176 }
177 }
178178 return nil
179179 }
180180
181 // GetChannelByID returns a channel given a channel id
181 // GetChannelByID is deprecated and returns nil
182182 func (info Info) GetChannelByID(channelID string) *Channel {
183 for _, channel := range info.Channels {
184 if channel.ID == channelID {
185 return &channel
186 }
187 }
188183 return nil
189184 }
190185
191 // GetGroupByID returns a group given a group id
186 // GetGroupByID is deprecated and returns nil
192187 func (info Info) GetGroupByID(groupID string) *Group {
193 for _, group := range info.Groups {
194 if group.ID == groupID {
195 return &group
196 }
197 }
198188 return nil
199189 }
200190
201 // GetIMByID returns an IM given an IM id
191 // GetIMByID is deprecated and returns nil
202192 func (info Info) GetIMByID(imID string) *IM {
203 for _, im := range info.IMs {
204 if im.ID == imID {
205 return &im
206 }
207 }
208193 return nil
209194 }
0 package slack
1
2 import (
3 "encoding/json"
4 )
5
6 // InteractionType type of interactions
7 type InteractionType string
8
9 // ActionType type represents the type of action (attachment, block, etc.)
10 type actionType string
11
12 // action is an interface that should be implemented by all callback action types
13 type action interface {
14 actionType() actionType
15 }
16
17 // Types of interactions that can be received.
18 const (
19 InteractionTypeDialogCancellation = InteractionType("dialog_cancellation")
20 InteractionTypeDialogSubmission = InteractionType("dialog_submission")
21 InteractionTypeDialogSuggestion = InteractionType("dialog_suggestion")
22 InteractionTypeInteractionMessage = InteractionType("interactive_message")
23 InteractionTypeMessageAction = InteractionType("message_action")
24 InteractionTypeBlockActions = InteractionType("block_actions")
25 )
26
27 // InteractionCallback is sent from slack when a user interactions with a button or dialog.
28 type InteractionCallback struct {
29 Type InteractionType `json:"type"`
30 Token string `json:"token"`
31 CallbackID string `json:"callback_id"`
32 ResponseURL string `json:"response_url"`
33 TriggerID string `json:"trigger_id"`
34 ActionTs string `json:"action_ts"`
35 Team Team `json:"team"`
36 Channel Channel `json:"channel"`
37 User User `json:"user"`
38 OriginalMessage Message `json:"original_message"`
39 Message Message `json:"message"`
40 Name string `json:"name"`
41 Value string `json:"value"`
42 MessageTs string `json:"message_ts"`
43 AttachmentID string `json:"attachment_id"`
44 ActionCallback ActionCallbacks `json:"actions"`
45 DialogSubmissionCallback
46 }
47
48 // ActionCallback is a convenience struct defined to allow dynamic unmarshalling of
49 // the "actions" value in Slack's JSON response, which varies depending on block type
50 type ActionCallbacks struct {
51 AttachmentActions []*AttachmentAction
52 BlockActions []*BlockAction
53 }
54
55 // UnmarshalJSON implements the Marshaller interface in order to delegate
56 // marshalling and allow for proper type assertion when decoding the response
57 func (a *ActionCallbacks) UnmarshalJSON(data []byte) error {
58 var raw []json.RawMessage
59 err := json.Unmarshal(data, &raw)
60 if err != nil {
61 return err
62 }
63
64 for _, r := range raw {
65 var obj map[string]interface{}
66 err := json.Unmarshal(r, &obj)
67 if err != nil {
68 return err
69 }
70
71 if _, ok := obj["block_id"].(string); ok {
72 action, err := unmarshalAction(r, &BlockAction{})
73 if err != nil {
74 return err
75 }
76
77 a.BlockActions = append(a.BlockActions, action.(*BlockAction))
78 return nil
79 }
80
81 action, err := unmarshalAction(r, &AttachmentAction{})
82 if err != nil {
83 return err
84 }
85 a.AttachmentActions = append(a.AttachmentActions, action.(*AttachmentAction))
86 }
87
88 return nil
89 }
90
91 func unmarshalAction(r json.RawMessage, callbackAction action) (action, error) {
92 err := json.Unmarshal(r, callbackAction)
93 if err != nil {
94 return nil, err
95 }
96 return callbackAction, nil
97 }
0 package slack
1
2 import (
3 "encoding/json"
4 "testing"
5
6 "github.com/stretchr/testify/assert"
7 )
8
9 const (
10 dialogSubmissionCallback = `{
11 "type": "dialog_submission",
12 "submission": {
13 "name": "Sigourney Dreamweaver",
14 "email": "sigdre@example.com",
15 "phone": "+1 800-555-1212",
16 "meal": "burrito",
17 "comment": "No sour cream please",
18 "team_channel": "C0LFFBKPB",
19 "who_should_sing": "U0MJRG1AL"
20 },
21 "callback_id": "employee_offsite_1138b",
22 "team": {
23 "id": "T1ABCD2E12",
24 "domain": "coverbands"
25 },
26 "user": {
27 "id": "W12A3BCDEF",
28 "name": "dreamweaver"
29 },
30 "channel": {
31 "id": "C1AB2C3DE",
32 "name": "coverthon-1999"
33 },
34 "action_ts": "936893340.702759",
35 "token": "M1AqUUw3FqayAbqNtsGMch72",
36 "response_url": "https://hooks.slack.com/app/T012AB0A1/123456789/JpmK0yzoZDeRiqfeduTBYXWQ"
37 }`
38 actionCallback = `{}`
39 )
40
41 func assertInteractionCallback(t *testing.T, callback InteractionCallback, encoded string) {
42 var decoded InteractionCallback
43 assert.Nil(t, json.Unmarshal([]byte(encoded), &decoded))
44 assert.Equal(t, decoded, callback)
45 }
46
47 func TestDialogCallback(t *testing.T) {
48 expected := InteractionCallback{
49 Type: InteractionTypeDialogSubmission,
50 Token: "M1AqUUw3FqayAbqNtsGMch72",
51 CallbackID: "employee_offsite_1138b",
52 ResponseURL: "https://hooks.slack.com/app/T012AB0A1/123456789/JpmK0yzoZDeRiqfeduTBYXWQ",
53 ActionTs: "936893340.702759",
54 Team: Team{ID: "T1ABCD2E12", Name: "", Domain: "coverbands"},
55 Channel: Channel{
56 GroupConversation: GroupConversation{
57 Conversation: Conversation{
58 ID: "C1AB2C3DE",
59 },
60 Name: "coverthon-1999",
61 },
62 },
63 User: User{
64 ID: "W12A3BCDEF",
65 Name: "dreamweaver",
66 },
67 DialogSubmissionCallback: DialogSubmissionCallback{
68 Submission: map[string]string{
69 "team_channel": "C0LFFBKPB",
70 "who_should_sing": "U0MJRG1AL",
71 "name": "Sigourney Dreamweaver",
72 "email": "sigdre@example.com",
73 "phone": "+1 800-555-1212",
74 "meal": "burrito",
75 "comment": "No sour cream please",
76 },
77 },
78 }
79 assertInteractionCallback(t, expected, dialogSubmissionCallback)
80 }
81
82 func TestActionCallback(t *testing.T) {
83 assertInteractionCallback(t, InteractionCallback{}, actionCallback)
84 }
0 package errorsx
1
2 // String representing an error, useful for declaring string constants as errors.
3 type String string
4
5 func (t String) Error() string {
6 return string(t)
7 }
0 package timex
1
2 import "time"
3
4 // Max returns the maximum duration
5 func Max(values ...time.Duration) time.Duration {
6 var (
7 max time.Duration
8 )
9
10 for _, v := range values {
11 if v > max {
12 max = v
13 }
14 }
15
16 return max
17 }
0 package slack
1
2 import (
3 "fmt"
4 )
5
6 // logger is a logger interface compatible with both stdlib and some
7 // 3rd party loggers.
8 type logger interface {
9 Output(int, string) error
10 }
11
12 // ilogger represents the internal logging api we use.
13 type ilogger interface {
14 logger
15 Print(...interface{})
16 Printf(string, ...interface{})
17 Println(...interface{})
18 }
19
20 type debug interface {
21 Debug() bool
22
23 // Debugf print a formatted debug line.
24 Debugf(format string, v ...interface{})
25 // Debugln print a debug line.
26 Debugln(v ...interface{})
27 }
28
29 // internalLog implements the additional methods used by our internal logging.
30 type internalLog struct {
31 logger
32 }
33
34 // Println replicates the behaviour of the standard logger.
35 func (t internalLog) Println(v ...interface{}) {
36 t.Output(2, fmt.Sprintln(v...))
37 }
38
39 // Printf replicates the behaviour of the standard logger.
40 func (t internalLog) Printf(format string, v ...interface{}) {
41 t.Output(2, fmt.Sprintf(format, v...))
42 }
43
44 // Print replicates the behaviour of the standard logger.
45 func (t internalLog) Print(v ...interface{}) {
46 t.Output(2, fmt.Sprint(v...))
47 }
48
49 type discard struct{}
50
51 func (t discard) Debug() bool {
52 return false
53 }
54
55 // Debugf print a formatted debug line.
56 func (t discard) Debugf(format string, v ...interface{}) {}
57
58 // Debugln print a debug line.
59 func (t discard) Debugln(v ...interface{}) {}
0 package slack
1
2 import (
3 "bytes"
4 "log"
5 "testing"
6
7 "github.com/stretchr/testify/assert"
8 )
9
10 func TestLogging(t *testing.T) {
11 buf := bytes.NewBufferString("")
12 logger := internalLog{logger: log.New(buf, "", 0|log.Lshortfile)}
13 logger.Println("test line 123")
14 assert.Equal(t, buf.String(), "logger_test.go:14: test line 123\n")
15 buf.Truncate(0)
16 logger.Print("test line 123")
17 assert.Equal(t, buf.String(), "logger_test.go:17: test line 123\n")
18 buf.Truncate(0)
19 logger.Printf("test line 123\n")
20 assert.Equal(t, buf.String(), "logger_test.go:20: test line 123\n")
21 buf.Truncate(0)
22 logger.Output(1, "test line 123\n")
23 assert.Equal(t, buf.String(), "logger_test.go:23: test line 123\n")
24 buf.Truncate(0)
25 }
11
22 // OutgoingMessage is used for the realtime API, and seems incomplete.
33 type OutgoingMessage struct {
4 ID int `json:"id"`
5 Channel string `json:"channel,omitempty"`
6 Text string `json:"text,omitempty"`
7 Type string `json:"type,omitempty"`
8 ThreadTimestamp string `json:"thread_ts,omitempty"`
4 ID int `json:"id"`
5 // channel ID
6 Channel string `json:"channel,omitempty"`
7 Text string `json:"text,omitempty"`
8 Type string `json:"type,omitempty"`
9 ThreadTimestamp string `json:"thread_ts,omitempty"`
10 ThreadBroadcast bool `json:"reply_broadcast,omitempty"`
11 IDs []string `json:"ids,omitempty"`
912 }
1013
1114 // Message is an auxiliary type to allow us to have a message containing sub messages
1215 type Message struct {
1316 Msg
1417 SubMessage *Msg `json:"message,omitempty"`
18 PreviousMessage *Msg `json:"previous_message,omitempty"`
1519 }
1620
1721 // Msg contains information about a slack message
2428 Timestamp string `json:"ts,omitempty"`
2529 ThreadTimestamp string `json:"thread_ts,omitempty"`
2630 IsStarred bool `json:"is_starred,omitempty"`
27 PinnedTo []string `json:"pinned_to, omitempty"`
31 PinnedTo []string `json:"pinned_to,omitempty"`
2832 Attachments []Attachment `json:"attachments,omitempty"`
2933 Edited *Edited `json:"edited,omitempty"`
34 LastRead string `json:"last_read,omitempty"`
35 Subscribed bool `json:"subscribed,omitempty"`
36 UnreadCount int `json:"unread_count,omitempty"`
3037
3138 // Message Subtypes
3239 SubType string `json:"subtype,omitempty"`
6370 ParentUserId string `json:"parent_user_id,omitempty"`
6471
6572 // file_share, file_comment, file_mention
66 File *File `json:"file,omitempty"`
73 Files []File `json:"files,omitempty"`
6774
6875 // file_share
6976 Upload bool `json:"upload,omitempty"`
8087
8188 // reactions
8289 Reactions []ItemReaction `json:"reactions,omitempty"`
90
91 // slash commands and interactive messages
92 ResponseType string `json:"response_type,omitempty"`
93 ReplaceOriginal bool `json:"replace_original"`
94 DeleteOriginal bool `json:"delete_original"`
95
96 // Block type Message
97 Blocks Blocks `json:"blocks,omitempty"`
8398 }
99
100 const (
101 // ResponseTypeInChannel in channel response for slash commands.
102 ResponseTypeInChannel = "in_channel"
103 // ResponseTypeEphemeral ephemeral respone for slash commands.
104 ResponseTypeEphemeral = "ephemeral"
105 )
84106
85107 // Icon is used for bot messages
86108 type Icon struct {
107129
108130 // Ping contains information about a Ping Event
109131 type Ping struct {
110 ID int `json:"id"`
111 Type string `json:"type"`
132 ID int `json:"id"`
133 Type string `json:"type"`
134 Timestamp int64 `json:"timestamp"`
112135 }
113136
114137 // Pong contains information about a Pong Event
115138 type Pong struct {
116 Type string `json:"type"`
117 ReplyTo int `json:"reply_to"`
139 Type string `json:"type"`
140 ReplyTo int `json:"reply_to"`
141 Timestamp int64 `json:"timestamp"`
118142 }
119143
120144 // NewOutgoingMessage prepares an OutgoingMessage that the user can
121145 // use to send a message. Use this function to properly set the
122146 // messageID.
123 func (rtm *RTM) NewOutgoingMessage(text string, channel string) *OutgoingMessage {
147 func (rtm *RTM) NewOutgoingMessage(text string, channelID string, options ...RTMsgOption) *OutgoingMessage {
124148 id := rtm.idGen.Next()
125 return &OutgoingMessage{
149 msg := OutgoingMessage{
126150 ID: id,
127151 Type: "message",
128 Channel: channel,
152 Channel: channelID,
129153 Text: text,
154 }
155 for _, option := range options {
156 option(&msg)
157 }
158 return &msg
159 }
160
161 // NewSubscribeUserPresence prepares an OutgoingMessage that the user can
162 // use to subscribe presence events for the specified users.
163 func (rtm *RTM) NewSubscribeUserPresence(ids []string) *OutgoingMessage {
164 return &OutgoingMessage{
165 Type: "presence_sub",
166 IDs: ids,
130167 }
131168 }
132169
133170 // NewTypingMessage prepares an OutgoingMessage that the user can
134171 // use to send as a typing indicator. Use this function to properly set the
135172 // messageID.
136 func (rtm *RTM) NewTypingMessage(channel string) *OutgoingMessage {
173 func (rtm *RTM) NewTypingMessage(channelID string) *OutgoingMessage {
137174 id := rtm.idGen.Next()
138175 return &OutgoingMessage{
139176 ID: id,
140177 Type: "typing",
141 Channel: channel,
178 Channel: channelID,
142179 }
143180 }
181
182 // RTMsgOption allows configuration of various options available for sending an RTM message
183 type RTMsgOption func(*OutgoingMessage)
184
185 // RTMsgOptionTS sets thead timestamp of an outgoing message in order to respond to a thread
186 func RTMsgOptionTS(threadTimestamp string) RTMsgOption {
187 return func(msg *OutgoingMessage) {
188 msg.ThreadTimestamp = threadTimestamp
189 }
190 }
191
192 // RTMsgOptionBroadcast sets broadcast reply to channel to "true"
193 func RTMsgOptionBroadcast() RTMsgOption {
194 return func(msg *OutgoingMessage) {
195 msg.ThreadBroadcast = true
196 }
197 }
8383 "type": "message",
8484 "subtype": "file_share",
8585 "text": "<@U2147483697|tester> uploaded a file: <https:\/\/test.slack.com\/files\/tester\/abc\/test.txt|test.txt> and commented: test comment here",
86 "file": {
86 "files": [{
8787 "id": "abc",
8888 "created": 1433314757,
8989 "timestamp": 1433314757,
124124 "user": "U2147483697",
125125 "comment": "test comment here"
126126 }
127 },
127 }],
128128 "user": "U2147483697",
129129 "upload": true,
130130 "ts": "1433314757.000006"
147147 "type": "message",
148148 "subtype": "file_share",
149149 "text": "<@U2147483697|tester> shared a file: <https:\/\/test.slack.com\/files\/tester\/abc\/test_post|test post>",
150 "file": {
150 "files": [{
151151 "id": "abc",
152152 "created": 1433315398,
153153 "timestamp": 1433315398,
178178 "groups": [],
179179 "ims": [],
180180 "comments_count": 1
181 },
181 }],
182182 "user": "U2147483697",
183183 "upload": false,
184184 "ts": "1433315416.000008"
201201 "type": "message",
202202 "subtype": "file_comment",
203203 "text": "<@U2147483697|tester> commented on <@U2147483697|tester>'s file <https:\/\/test.slack.com\/files\/tester\/abc\/test_post|test post>: another comment",
204 "file": {
204 "files": [{
205205 "id": "abc",
206206 "created": 1433315398,
207207 "timestamp": 1433315398,
232232 "groups": [],
233233 "ims": [],
234234 "comments_count": 2
235 },
235 }],
236236 "comment": {
237237 "id": "xyz",
238238 "created": 1433316360,
754754 "subtype": "file_share",
755755 "ts": "1358877455.000010",
756756 "text": "<@cal> uploaded a file: <https:...7.png|7.png>",
757 "file": {
757 "files": [{
758758 "id" : "F2147483862",
759759 "created" : 1356032811,
760760 "timestamp" : 1356032811,
793793 "initial_comment": {},
794794 "num_stars": 7,
795795 "is_starred": true
796 },
796 }],
797797 "user": "U2147483697",
798798 "upload": true
799799 }`
809809 assert.Equal(t, "<@cal> uploaded a file: <https:...7.png|7.png>", message.Text)
810810 assert.Equal(t, "U2147483697", message.User)
811811 assert.True(t, message.Upload)
812 assert.NotNil(t, message.File)
813 }
812 assert.NotNil(t, message.Files[0])
813 }
+305
-124
misc.go less more
11
22 import (
33 "bytes"
4 "context"
45 "encoding/json"
6 "errors"
57 "fmt"
68 "io"
79 "io/ioutil"
10 "mime"
811 "mime/multipart"
912 "net/http"
1013 "net/http/httputil"
1114 "net/url"
1215 "os"
1316 "path/filepath"
17 "strconv"
1418 "strings"
1519 "time"
1620 )
1721
18 // HTTPRequester defines the minimal interface needed for an http.Client to be implemented.
19 //
20 // Use it in conjunction with the SetHTTPClient function to allow for other capabilities
21 // like a tracing http.Client
22 type HTTPRequester interface {
23 Do(*http.Request) (*http.Response, error)
24 }
25
26 var customHTTPClient HTTPRequester
27
28 // HTTPClient sets a custom http.Client
29 // deprecated: in favor of SetHTTPClient()
30 var HTTPClient = &http.Client{}
31
32 type WebResponse struct {
33 Ok bool `json:"ok"`
34 Error *WebError `json:"error"`
35 }
36
37 type WebError string
38
39 func (s WebError) Error() string {
40 return string(s)
41 }
42
43 func fileUploadReq(path, fieldname, filename string, values url.Values, r io.Reader) (*http.Request, error) {
44 body := &bytes.Buffer{}
45 wr := multipart.NewWriter(body)
46
47 ioWriter, err := wr.CreateFormFile(fieldname, filename)
48 if err != nil {
49 wr.Close()
22 // SlackResponse handles parsing out errors from the web api.
23 type SlackResponse struct {
24 Ok bool `json:"ok"`
25 Error string `json:"error"`
26 }
27
28 func (t SlackResponse) Err() error {
29 if t.Ok {
30 return nil
31 }
32
33 // handle pure text based responses like chat.post
34 // which while they have a slack response in their data structure
35 // it doesn't actually get set during parsing.
36 if strings.TrimSpace(t.Error) == "" {
37 return nil
38 }
39
40 return errors.New(t.Error)
41 }
42
43 // StatusCodeError represents an http response error.
44 // type httpStatusCode interface { HTTPStatusCode() int } to handle it.
45 type statusCodeError struct {
46 Code int
47 Status string
48 }
49
50 func (t statusCodeError) Error() string {
51 return fmt.Sprintf("slack server error: %s", t.Status)
52 }
53
54 func (t statusCodeError) HTTPStatusCode() int {
55 return t.Code
56 }
57
58 func (t statusCodeError) Retryable() bool {
59 if t.Code >= 500 || t.Code == http.StatusTooManyRequests {
60 return true
61 }
62 return false
63 }
64
65 // RateLimitedError represents the rate limit respond from slack
66 type RateLimitedError struct {
67 RetryAfter time.Duration
68 }
69
70 func (e *RateLimitedError) Error() string {
71 return fmt.Sprintf("slack rate limit exceeded, retry after %s", e.RetryAfter)
72 }
73
74 func (e *RateLimitedError) Retryable() bool {
75 return true
76 }
77
78 func fileUploadReq(ctx context.Context, path string, values url.Values, r io.Reader) (*http.Request, error) {
79 req, err := http.NewRequest("POST", path, r)
80 if err != nil {
5081 return nil, err
5182 }
52 _, err = io.Copy(ioWriter, r)
53 if err != nil {
54 wr.Close()
55 return nil, err
56 }
57 // Close the multipart writer or the footer won't be written
58 wr.Close()
59 req, err := http.NewRequest("POST", path, body)
60 if err != nil {
61 return nil, err
62 }
63 req.Header.Add("Content-Type", wr.FormDataContentType())
83
84 req = req.WithContext(ctx)
6485 req.URL.RawQuery = (values).Encode()
6586 return req, nil
6687 }
6788
68 func parseResponseBody(body io.ReadCloser, intf *interface{}, debug bool) error {
89 func downloadFile(client httpClient, token string, downloadURL string, writer io.Writer, d debug) error {
90 if downloadURL == "" {
91 return fmt.Errorf("received empty download URL")
92 }
93
94 req, err := http.NewRequest("GET", downloadURL, &bytes.Buffer{})
95 if err != nil {
96 return err
97 }
98
99 var bearer = "Bearer " + token
100 req.Header.Add("Authorization", bearer)
101 req.WithContext(context.Background())
102
103 resp, err := client.Do(req)
104 if err != nil {
105 return err
106 }
107
108 defer resp.Body.Close()
109
110 err = checkStatusCode(resp, d)
111 if err != nil {
112 return err
113 }
114
115 _, err = io.Copy(writer, resp.Body)
116
117 return err
118 }
119
120 func formReq(endpoint string, values url.Values) (req *http.Request, err error) {
121 if req, err = http.NewRequest("POST", endpoint, strings.NewReader(values.Encode())); err != nil {
122 return nil, err
123 }
124
125 req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
126 return req, nil
127 }
128
129 func jsonReq(endpoint string, body interface{}) (req *http.Request, err error) {
130 buffer := bytes.NewBuffer([]byte{})
131 if err = json.NewEncoder(buffer).Encode(body); err != nil {
132 return nil, err
133 }
134
135 if req, err = http.NewRequest("POST", endpoint, buffer); err != nil {
136 return nil, err
137 }
138
139 req.Header.Set("Content-Type", "application/json; charset=utf-8")
140 return req, nil
141 }
142
143 func parseResponseBody(body io.ReadCloser, intf interface{}, d debug) error {
69144 response, err := ioutil.ReadAll(body)
70145 if err != nil {
71146 return err
72147 }
73148
74 // FIXME: will be api.Debugf
75 if debug {
76 logger.Printf("parseResponseBody: %s\n", string(response))
77 }
78
79 err = json.Unmarshal(response, &intf)
80 if err != nil {
81 return err
82 }
83
84 return nil
85 }
86
87 func postLocalWithMultipartResponse(path, fpath, fieldname string, values url.Values, intf interface{}, debug bool) error {
149 if d.Debug() {
150 d.Debugln("parseResponseBody", string(response))
151 }
152
153 return json.Unmarshal(response, intf)
154 }
155
156 func postLocalWithMultipartResponse(ctx context.Context, client httpClient, method, fpath, fieldname string, values url.Values, intf interface{}, d debug) error {
88157 fullpath, err := filepath.Abs(fpath)
89158 if err != nil {
90159 return err
94163 return err
95164 }
96165 defer file.Close()
97 return postWithMultipartResponse(path, filepath.Base(fpath), fieldname, values, file, intf, debug)
98 }
99
100 func postWithMultipartResponse(path, name, fieldname string, values url.Values, r io.Reader, intf interface{}, debug bool) error {
101 req, err := fileUploadReq(SLACK_API+path, fieldname, name, values, r)
102 if err != nil {
103 return err
104 }
105 resp, err := getHTTPClient().Do(req)
166
167 return postWithMultipartResponse(ctx, client, method, filepath.Base(fpath), fieldname, values, file, intf, d)
168 }
169
170 func postWithMultipartResponse(ctx context.Context, client httpClient, path, name, fieldname string, values url.Values, r io.Reader, intf interface{}, d debug) error {
171 pipeReader, pipeWriter := io.Pipe()
172 wr := multipart.NewWriter(pipeWriter)
173 errc := make(chan error)
174 go func() {
175 defer pipeWriter.Close()
176 ioWriter, err := wr.CreateFormFile(fieldname, name)
177 if err != nil {
178 errc <- err
179 return
180 }
181 _, err = io.Copy(ioWriter, r)
182 if err != nil {
183 errc <- err
184 return
185 }
186 if err = wr.Close(); err != nil {
187 errc <- err
188 return
189 }
190 }()
191 req, err := fileUploadReq(ctx, path, values, pipeReader)
192 if err != nil {
193 return err
194 }
195 req.Header.Add("Content-Type", wr.FormDataContentType())
196 req = req.WithContext(ctx)
197 resp, err := client.Do(req)
198
106199 if err != nil {
107200 return err
108201 }
109202 defer resp.Body.Close()
110203
111 // Slack seems to send an HTML body along with 5xx error codes. Don't parse it.
112 if resp.StatusCode != 200 {
113 logResponse(resp, debug)
114 return fmt.Errorf("Slack server error: %s.", resp.Status)
115 }
116
117 return parseResponseBody(resp.Body, &intf, debug)
118 }
119
120 func postForm(endpoint string, values url.Values, intf interface{}, debug bool) error {
204 err = checkStatusCode(resp, d)
205 if err != nil {
206 return err
207 }
208
209 select {
210 case err = <-errc:
211 return err
212 default:
213 return newJSONParser(intf)(resp)
214 }
215 }
216
217 func doPost(ctx context.Context, client httpClient, req *http.Request, parser responseParser, d debug) error {
218 req = req.WithContext(ctx)
219 resp, err := client.Do(req)
220 if err != nil {
221 return err
222 }
223 defer resp.Body.Close()
224
225 err = checkStatusCode(resp, d)
226 if err != nil {
227 return err
228 }
229
230 return parser(resp)
231 }
232
233 // post JSON.
234 func postJSON(ctx context.Context, client httpClient, endpoint, token string, json []byte, intf interface{}, d debug) error {
235 reqBody := bytes.NewBuffer(json)
236 req, err := http.NewRequest("POST", endpoint, reqBody)
237 if err != nil {
238 return err
239 }
240 req.Header.Set("Content-Type", "application/json")
241 req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", token))
242
243 return doPost(ctx, client, req, newJSONParser(intf), d)
244 }
245
246 // post a url encoded form.
247 func postForm(ctx context.Context, client httpClient, endpoint string, values url.Values, intf interface{}, d debug) error {
121248 reqBody := strings.NewReader(values.Encode())
122249 req, err := http.NewRequest("POST", endpoint, reqBody)
123250 if err != nil {
124251 return err
125252 }
126253 req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
127
128 resp, err := getHTTPClient().Do(req)
129 if err != nil {
130 return err
131 }
132 defer resp.Body.Close()
254 return doPost(ctx, client, req, newJSONParser(intf), d)
255 }
256
257 func getResource(ctx context.Context, client httpClient, endpoint string, values url.Values, intf interface{}, d debug) error {
258 req, err := http.NewRequest("GET", endpoint, nil)
259 if err != nil {
260 return err
261 }
262 req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
263 req.URL.RawQuery = values.Encode()
264
265 return doPost(ctx, client, req, newJSONParser(intf), d)
266 }
267
268 func parseAdminResponse(ctx context.Context, client httpClient, method string, teamName string, values url.Values, intf interface{}, d debug) error {
269 endpoint := fmt.Sprintf(WEBAPIURLFormat, teamName, method, time.Now().Unix())
270 return postForm(ctx, client, endpoint, values, intf, d)
271 }
272
273 func logResponse(resp *http.Response, d debug) error {
274 if d.Debug() {
275 text, err := httputil.DumpResponse(resp, true)
276 if err != nil {
277 return err
278 }
279 d.Debugln(string(text))
280 }
281
282 return nil
283 }
284
285 func okJSONHandler(rw http.ResponseWriter, r *http.Request) {
286 rw.Header().Set("Content-Type", "application/json")
287 response, _ := json.Marshal(SlackResponse{
288 Ok: true,
289 })
290 rw.Write(response)
291 }
292
293 // timerReset safely reset a timer, see time.Timer.Reset for details.
294 func timerReset(t *time.Timer, d time.Duration) {
295 if !t.Stop() {
296 <-t.C
297 }
298 t.Reset(d)
299 }
300
301 func checkStatusCode(resp *http.Response, d debug) error {
302 if resp.StatusCode == http.StatusTooManyRequests {
303 retry, err := strconv.ParseInt(resp.Header.Get("Retry-After"), 10, 64)
304 if err != nil {
305 return err
306 }
307 return &RateLimitedError{time.Duration(retry) * time.Second}
308 }
133309
134310 // Slack seems to send an HTML body along with 5xx error codes. Don't parse it.
135 if resp.StatusCode != 200 {
136 logResponse(resp, debug)
137 return fmt.Errorf("Slack server error: %s.", resp.Status)
138 }
139
140 return parseResponseBody(resp.Body, &intf, debug)
141 }
142
143 func post(path string, values url.Values, intf interface{}, debug bool) error {
144 return postForm(SLACK_API+path, values, intf, debug)
145 }
146
147 func parseAdminResponse(method string, teamName string, values url.Values, intf interface{}, debug bool) error {
148 endpoint := fmt.Sprintf(SLACK_WEB_API_FORMAT, teamName, method, time.Now().Unix())
149 return postForm(endpoint, values, intf, debug)
150 }
151
152 func logResponse(resp *http.Response, debug bool) error {
153 if debug {
154 text, err := httputil.DumpResponse(resp, true)
311 if resp.StatusCode != http.StatusOK {
312 logResponse(resp, d)
313 return statusCodeError{Code: resp.StatusCode, Status: resp.Status}
314 }
315
316 return nil
317 }
318
319 type responseParser func(*http.Response) error
320
321 func newJSONParser(dst interface{}) responseParser {
322 return func(resp *http.Response) error {
323 return json.NewDecoder(resp.Body).Decode(dst)
324 }
325 }
326
327 func newTextParser(dst interface{}) responseParser {
328 return func(resp *http.Response) error {
329 b, err := ioutil.ReadAll(resp.Body)
155330 if err != nil {
156331 return err
157332 }
158333
159 logger.Print(string(text))
160 }
161
162 return nil
163 }
164
165 func getHTTPClient() HTTPRequester {
166 if customHTTPClient != nil {
167 return customHTTPClient
168 }
169
170 return HTTPClient
171 }
172
173 // SetHTTPClient allows you to specify a custom http.Client
174 // Use this instead of the package level HTTPClient variable if you want to use a custom client like the
175 // Stackdriver Trace HTTPClient https://godoc.org/cloud.google.com/go/trace#HTTPClient
176 func SetHTTPClient(client HTTPRequester) {
177 customHTTPClient = client
178 }
334 if !bytes.Equal(b, []byte("ok")) {
335 return errors.New(string(b))
336 }
337
338 return nil
339 }
340 }
341
342 func newContentTypeParser(dst interface{}) responseParser {
343 return func(req *http.Response) (err error) {
344 var (
345 ctype string
346 )
347
348 if ctype, _, err = mime.ParseMediaType(req.Header.Get("Content-Type")); err != nil {
349 return err
350 }
351
352 switch ctype {
353 case "application/json":
354 return newJSONParser(dst)(req)
355 default:
356 return newTextParser(dst)(req)
357 }
358 }
359 }
00 package slack
11
22 import (
3 "context"
34 "log"
45 "net/http"
56 "net/url"
67 "sync"
78 "testing"
9
10 "github.com/nlopes/slack/slackutilsx"
811 )
912
1013 var (
3437 func TestParseResponse(t *testing.T) {
3538 parseResponseOnce.Do(setParseResponseHandler)
3639 once.Do(startServer)
37 SLACK_API = "http://" + serverAddr + "/"
40 APIURL := "http://" + serverAddr + "/"
3841 values := url.Values{
3942 "token": {validToken},
4043 }
44
4145 responsePartial := &SlackResponse{}
42 err := post("parseResponse", values, responsePartial, false)
46 err := postForm(context.Background(), http.DefaultClient, APIURL+"parseResponse", values, responsePartial, discard{})
4347 if err != nil {
4448 t.Errorf("Unexpected error: %s", err)
4549 }
4852 func TestParseResponseNoToken(t *testing.T) {
4953 parseResponseOnce.Do(setParseResponseHandler)
5054 once.Do(startServer)
51 SLACK_API = "http://" + serverAddr + "/"
55 APIURL := "http://" + serverAddr + "/"
5256 values := url.Values{}
57
5358 responsePartial := &SlackResponse{}
54 err := post("parseResponse", values, responsePartial, false)
59 err := postForm(context.Background(), http.DefaultClient, APIURL+"parseResponse", values, responsePartial, discard{})
5560 if err != nil {
5661 t.Errorf("Unexpected error: %s", err)
5762 return
5863 }
59 if responsePartial.Ok == true {
64 if responsePartial.Ok {
6065 t.Errorf("Unexpected error: %s", err)
6166 } else if responsePartial.Error != "not_authed" {
6267 t.Errorf("got %v; want %v", responsePartial.Error, "not_authed")
6671 func TestParseResponseInvalidToken(t *testing.T) {
6772 parseResponseOnce.Do(setParseResponseHandler)
6873 once.Do(startServer)
69 SLACK_API = "http://" + serverAddr + "/"
74 APIURL := "http://" + serverAddr + "/"
7075 values := url.Values{
7176 "token": {"whatever"},
7277 }
7378 responsePartial := &SlackResponse{}
74 err := post("parseResponse", values, responsePartial, false)
79 err := postForm(context.Background(), http.DefaultClient, APIURL+"parseResponse", values, responsePartial, discard{})
7580 if err != nil {
7681 t.Errorf("Unexpected error: %s", err)
7782 return
7883 }
79 if responsePartial.Ok == true {
84 if responsePartial.Ok {
8085 t.Errorf("Unexpected error: %s", err)
8186 } else if responsePartial.Error != "invalid_auth" {
8287 t.Errorf("got %v; want %v", responsePartial.Error, "invalid_auth")
8388 }
8489 }
90
91 func TestRetryable(t *testing.T) {
92 for _, e := range []error{
93 &RateLimitedError{},
94 statusCodeError{Code: http.StatusInternalServerError},
95 statusCodeError{Code: http.StatusTooManyRequests},
96 } {
97 r, ok := e.(slackutilsx.Retryable)
98 if !ok {
99 t.Errorf("expected %#v to implement Retryable", e)
100 }
101 if !r.Retryable() {
102 t.Errorf("expected %#v to be Retryable", e)
103 }
104 }
105 }
00 package slack
11
22 import (
3 "errors"
3 "context"
44 "net/url"
55 )
66
7 // OAuthResponseIncomingWebhook ...
78 type OAuthResponseIncomingWebhook struct {
89 URL string `json:"url"`
910 Channel string `json:"channel"`
1112 ConfigurationURL string `json:"configuration_url"`
1213 }
1314
15 // OAuthResponseBot ...
1416 type OAuthResponseBot struct {
1517 BotUserID string `json:"bot_user_id"`
1618 BotAccessToken string `json:"bot_access_token"`
1719 }
1820
21 // OAuthResponse ...
1922 type OAuthResponse struct {
2023 AccessToken string `json:"access_token"`
2124 Scope string `json:"scope"`
2831 }
2932
3033 // GetOAuthToken retrieves an AccessToken
31 func GetOAuthToken(clientID, clientSecret, code, redirectURI string, debug bool) (accessToken string, scope string, err error) {
32 response, err := GetOAuthResponse(clientID, clientSecret, code, redirectURI, debug)
34 func GetOAuthToken(client httpClient, clientID, clientSecret, code, redirectURI string) (accessToken string, scope string, err error) {
35 return GetOAuthTokenContext(context.Background(), client, clientID, clientSecret, code, redirectURI)
36 }
37
38 // GetOAuthTokenContext retrieves an AccessToken with a custom context
39 func GetOAuthTokenContext(ctx context.Context, client httpClient, clientID, clientSecret, code, redirectURI string) (accessToken string, scope string, err error) {
40 response, err := GetOAuthResponseContext(ctx, client, clientID, clientSecret, code, redirectURI)
3341 if err != nil {
3442 return "", "", err
3543 }
3644 return response.AccessToken, response.Scope, nil
3745 }
3846
39 func GetOAuthResponse(clientID, clientSecret, code, redirectURI string, debug bool) (resp *OAuthResponse, err error) {
47 func GetOAuthResponse(client httpClient, clientID, clientSecret, code, redirectURI string) (resp *OAuthResponse, err error) {
48 return GetOAuthResponseContext(context.Background(), client, clientID, clientSecret, code, redirectURI)
49 }
50
51 func GetOAuthResponseContext(ctx context.Context, client httpClient, clientID, clientSecret, code, redirectURI string) (resp *OAuthResponse, err error) {
4052 values := url.Values{
4153 "client_id": {clientID},
4254 "client_secret": {clientSecret},
4456 "redirect_uri": {redirectURI},
4557 }
4658 response := &OAuthResponse{}
47 err = post("oauth.access", values, response, debug)
48 if err != nil {
59 if err = postForm(ctx, client, APIURL+"oauth.access", values, response, discard{}); err != nil {
4960 return nil, err
5061 }
51 if !response.Ok {
52 return nil, errors.New(response.Error)
53 }
54 return response, nil
62 return response, response.Err()
5563 }
00 package slack
11
22 import (
3 "context"
34 "errors"
45 "net/url"
56 )
1213
1314 // AddPin pins an item in a channel
1415 func (api *Client) AddPin(channel string, item ItemRef) error {
16 return api.AddPinContext(context.Background(), channel, item)
17 }
18
19 // AddPinContext pins an item in a channel with a custom context
20 func (api *Client) AddPinContext(ctx context.Context, channel string, item ItemRef) error {
1521 values := url.Values{
1622 "channel": {channel},
17 "token": {api.config.token},
23 "token": {api.token},
1824 }
1925 if item.Timestamp != "" {
20 values.Set("timestamp", string(item.Timestamp))
26 values.Set("timestamp", item.Timestamp)
2127 }
2228 if item.File != "" {
23 values.Set("file", string(item.File))
29 values.Set("file", item.File)
2430 }
2531 if item.Comment != "" {
26 values.Set("file_comment", string(item.Comment))
32 values.Set("file_comment", item.Comment)
2733 }
34
2835 response := &SlackResponse{}
29 if err := post("pins.add", values, response, api.debug); err != nil {
36 if err := api.postMethod(ctx, "pins.add", values, response); err != nil {
3037 return err
3138 }
32 if !response.Ok {
33 return errors.New(response.Error)
34 }
35 return nil
39
40 return response.Err()
3641 }
3742
3843 // RemovePin un-pins an item from a channel
3944 func (api *Client) RemovePin(channel string, item ItemRef) error {
45 return api.RemovePinContext(context.Background(), channel, item)
46 }
47
48 // RemovePinContext un-pins an item from a channel with a custom context
49 func (api *Client) RemovePinContext(ctx context.Context, channel string, item ItemRef) error {
4050 values := url.Values{
4151 "channel": {channel},
42 "token": {api.config.token},
52 "token": {api.token},
4353 }
4454 if item.Timestamp != "" {
45 values.Set("timestamp", string(item.Timestamp))
55 values.Set("timestamp", item.Timestamp)
4656 }
4757 if item.File != "" {
48 values.Set("file", string(item.File))
58 values.Set("file", item.File)
4959 }
5060 if item.Comment != "" {
51 values.Set("file_comment", string(item.Comment))
61 values.Set("file_comment", item.Comment)
5262 }
63
5364 response := &SlackResponse{}
54 if err := post("pins.remove", values, response, api.debug); err != nil {
65 if err := api.postMethod(ctx, "pins.remove", values, response); err != nil {
5566 return err
5667 }
57 if !response.Ok {
58 return errors.New(response.Error)
59 }
60 return nil
68
69 return response.Err()
6170 }
6271
6372 // ListPins returns information about the items a user reacted to.
6473 func (api *Client) ListPins(channel string) ([]Item, *Paging, error) {
74 return api.ListPinsContext(context.Background(), channel)
75 }
76
77 // ListPinsContext returns information about the items a user reacted to with a custom context.
78 func (api *Client) ListPinsContext(ctx context.Context, channel string) ([]Item, *Paging, error) {
6579 values := url.Values{
6680 "channel": {channel},
67 "token": {api.config.token},
81 "token": {api.token},
6882 }
83
6984 response := &listPinsResponseFull{}
70 err := post("pins.list", values, response, api.debug)
85 err := api.postMethod(ctx, "pins.list", values, response)
7186 if err != nil {
7287 return nil, nil, err
7388 }
3737
3838 func TestSlack_AddPin(t *testing.T) {
3939 once.Do(startServer)
40 SLACK_API = "http://" + serverAddr + "/"
41 api := New("testing-token")
40 api := New("testing-token", OptionAPIURL("http://"+serverAddr+"/"))
4241 tests := []struct {
4342 channel string
4443 ref ItemRef
8584
8685 func TestSlack_RemovePin(t *testing.T) {
8786 once.Do(startServer)
88 SLACK_API = "http://" + serverAddr + "/"
89 api := New("testing-token")
87 api := New("testing-token", OptionAPIURL("http://"+serverAddr+"/"))
9088 tests := []struct {
9189 channel string
9290 ref ItemRef
133131
134132 func TestSlack_ListPins(t *testing.T) {
135133 once.Do(startServer)
136 SLACK_API = "http://" + serverAddr + "/"
137 api := New("testing-token")
134 api := New("testing-token", OptionAPIURL("http://"+serverAddr+"/"))
138135 rh := newPinsHandler()
139136 http.HandleFunc("/pins.list", func(w http.ResponseWriter, r *http.Request) { rh.handler(w, r) })
140137 rh.response = `{"ok": true,
198195 NewMessageItem("C1", &Message{Msg: Msg{
199196 Text: "hello",
200197 Reactions: []ItemReaction{
201 ItemReaction{Name: "astonished", Count: 3, Users: []string{"U1", "U2", "U3"}},
202 ItemReaction{Name: "clock1", Count: 3, Users: []string{"U1", "U2"}},
198 {Name: "astonished", Count: 3, Users: []string{"U1", "U2", "U3"}},
199 {Name: "clock1", Count: 3, Users: []string{"U1", "U2"}},
203200 },
204201 }}),
205202 NewFileItem(&File{Name: "toy"}),
00 package slack
11
22 import (
3 "errors"
3 "context"
44 "net/url"
55 "strconv"
66 )
128128
129129 // AddReaction adds a reaction emoji to a message, file or file comment.
130130 func (api *Client) AddReaction(name string, item ItemRef) error {
131 values := url.Values{
132 "token": {api.config.token},
131 return api.AddReactionContext(context.Background(), name, item)
132 }
133
134 // AddReactionContext adds a reaction emoji to a message, file or file comment with a custom context.
135 func (api *Client) AddReactionContext(ctx context.Context, name string, item ItemRef) error {
136 values := url.Values{
137 "token": {api.token},
133138 }
134139 if name != "" {
135140 values.Set("name", name)
136141 }
137142 if item.Channel != "" {
138 values.Set("channel", string(item.Channel))
143 values.Set("channel", item.Channel)
139144 }
140145 if item.Timestamp != "" {
141 values.Set("timestamp", string(item.Timestamp))
146 values.Set("timestamp", item.Timestamp)
142147 }
143148 if item.File != "" {
144 values.Set("file", string(item.File))
149 values.Set("file", item.File)
145150 }
146151 if item.Comment != "" {
147 values.Set("file_comment", string(item.Comment))
148 }
152 values.Set("file_comment", item.Comment)
153 }
154
149155 response := &SlackResponse{}
150 if err := post("reactions.add", values, response, api.debug); err != nil {
156 if err := api.postMethod(ctx, "reactions.add", values, response); err != nil {
151157 return err
152158 }
153 if !response.Ok {
154 return errors.New(response.Error)
155 }
156 return nil
159
160 return response.Err()
157161 }
158162
159163 // RemoveReaction removes a reaction emoji from a message, file or file comment.
160164 func (api *Client) RemoveReaction(name string, item ItemRef) error {
161 values := url.Values{
162 "token": {api.config.token},
165 return api.RemoveReactionContext(context.Background(), name, item)
166 }
167
168 // RemoveReactionContext removes a reaction emoji from a message, file or file comment with a custom context.
169 func (api *Client) RemoveReactionContext(ctx context.Context, name string, item ItemRef) error {
170 values := url.Values{
171 "token": {api.token},
163172 }
164173 if name != "" {
165174 values.Set("name", name)
166175 }
167176 if item.Channel != "" {
168 values.Set("channel", string(item.Channel))
177 values.Set("channel", item.Channel)
169178 }
170179 if item.Timestamp != "" {
171 values.Set("timestamp", string(item.Timestamp))
180 values.Set("timestamp", item.Timestamp)
172181 }
173182 if item.File != "" {
174 values.Set("file", string(item.File))
183 values.Set("file", item.File)
175184 }
176185 if item.Comment != "" {
177 values.Set("file_comment", string(item.Comment))
178 }
186 values.Set("file_comment", item.Comment)
187 }
188
179189 response := &SlackResponse{}
180 if err := post("reactions.remove", values, response, api.debug); err != nil {
190 if err := api.postMethod(ctx, "reactions.remove", values, response); err != nil {
181191 return err
182192 }
183 if !response.Ok {
184 return errors.New(response.Error)
185 }
186 return nil
193
194 return response.Err()
187195 }
188196
189197 // GetReactions returns details about the reactions on an item.
190198 func (api *Client) GetReactions(item ItemRef, params GetReactionsParameters) ([]ItemReaction, error) {
191 values := url.Values{
192 "token": {api.config.token},
199 return api.GetReactionsContext(context.Background(), item, params)
200 }
201
202 // GetReactionsContext returns details about the reactions on an item with a custom context
203 func (api *Client) GetReactionsContext(ctx context.Context, item ItemRef, params GetReactionsParameters) ([]ItemReaction, error) {
204 values := url.Values{
205 "token": {api.token},
193206 }
194207 if item.Channel != "" {
195 values.Set("channel", string(item.Channel))
208 values.Set("channel", item.Channel)
196209 }
197210 if item.Timestamp != "" {
198 values.Set("timestamp", string(item.Timestamp))
211 values.Set("timestamp", item.Timestamp)
199212 }
200213 if item.File != "" {
201 values.Set("file", string(item.File))
214 values.Set("file", item.File)
202215 }
203216 if item.Comment != "" {
204 values.Set("file_comment", string(item.Comment))
217 values.Set("file_comment", item.Comment)
205218 }
206219 if params.Full != DEFAULT_REACTIONS_FULL {
207220 values.Set("full", strconv.FormatBool(params.Full))
208221 }
222
209223 response := &getReactionsResponseFull{}
210 if err := post("reactions.get", values, response, api.debug); err != nil {
224 if err := api.postMethod(ctx, "reactions.get", values, response); err != nil {
211225 return nil, err
212226 }
213 if !response.Ok {
214 return nil, errors.New(response.Error)
215 }
227
228 if err := response.Err(); err != nil {
229 return nil, err
230 }
231
216232 return response.extractReactions(), nil
217233 }
218234
219235 // ListReactions returns information about the items a user reacted to.
220236 func (api *Client) ListReactions(params ListReactionsParameters) ([]ReactedItem, *Paging, error) {
221 values := url.Values{
222 "token": {api.config.token},
237 return api.ListReactionsContext(context.Background(), params)
238 }
239
240 // ListReactionsContext returns information about the items a user reacted to with a custom context.
241 func (api *Client) ListReactionsContext(ctx context.Context, params ListReactionsParameters) ([]ReactedItem, *Paging, error) {
242 values := url.Values{
243 "token": {api.token},
223244 }
224245 if params.User != DEFAULT_REACTIONS_USER {
225246 values.Add("user", params.User)
233254 if params.Full != DEFAULT_REACTIONS_FULL {
234255 values.Add("full", strconv.FormatBool(params.Full))
235256 }
257
236258 response := &listReactionsResponseFull{}
237 err := post("reactions.list", values, response, api.debug)
259 err := api.postMethod(ctx, "reactions.list", values, response)
238260 if err != nil {
239261 return nil, nil, err
240262 }
241 if !response.Ok {
242 return nil, nil, errors.New(response.Error)
243 }
263
264 if err := response.Err(); err != nil {
265 return nil, nil, err
266 }
267
244268 return response.extractReactedItems(), &response.Paging, nil
245269 }
4040
4141 func TestSlack_AddReaction(t *testing.T) {
4242 once.Do(startServer)
43 SLACK_API = "http://" + serverAddr + "/"
44 api := New("testing-token")
43 api := New("testing-token", OptionAPIURL("http://"+serverAddr+"/"))
4544 tests := []struct {
4645 name string
4746 ref ItemRef
8988
9089 func TestSlack_RemoveReaction(t *testing.T) {
9190 once.Do(startServer)
92 SLACK_API = "http://" + serverAddr + "/"
93 api := New("testing-token")
91 api := New("testing-token", OptionAPIURL("http://"+serverAddr+"/"))
9492 tests := []struct {
9593 name string
9694 ref ItemRef
138136
139137 func TestSlack_GetReactions(t *testing.T) {
140138 once.Do(startServer)
141 SLACK_API = "http://" + serverAddr + "/"
142 api := New("testing-token")
139 api := New("testing-token", OptionAPIURL("http://"+serverAddr+"/"))
143140 tests := []struct {
144141 ref ItemRef
145142 params GetReactionsParameters
171168 ]
172169 }}`,
173170 []ItemReaction{
174 ItemReaction{Name: "astonished", Count: 3, Users: []string{"U1", "U2", "U3"}},
175 ItemReaction{Name: "clock1", Count: 3, Users: []string{"U1", "U2"}},
171 {Name: "astonished", Count: 3, Users: []string{"U1", "U2", "U3"}},
172 {Name: "clock1", Count: 3, Users: []string{"U1", "U2"}},
176173 },
177174 },
178175 {
199196 ]
200197 }}`,
201198 []ItemReaction{
202 ItemReaction{Name: "astonished", Count: 3, Users: []string{"U1", "U2", "U3"}},
203 ItemReaction{Name: "clock1", Count: 3, Users: []string{"U1", "U2"}},
199 {Name: "astonished", Count: 3, Users: []string{"U1", "U2", "U3"}},
200 {Name: "clock1", Count: 3, Users: []string{"U1", "U2"}},
204201 },
205202 },
206203 {
228225 ]
229226 }}`,
230227 []ItemReaction{
231 ItemReaction{Name: "astonished", Count: 3, Users: []string{"U1", "U2", "U3"}},
232 ItemReaction{Name: "clock1", Count: 3, Users: []string{"U1", "U2"}},
228 {Name: "astonished", Count: 3, Users: []string{"U1", "U2", "U3"}},
229 {Name: "clock1", Count: 3, Users: []string{"U1", "U2"}},
233230 },
234231 },
235232 }
253250
254251 func TestSlack_ListReactions(t *testing.T) {
255252 once.Do(startServer)
256 SLACK_API = "http://" + serverAddr + "/"
257 api := New("testing-token")
253 api := New("testing-token", OptionAPIURL("http://"+serverAddr+"/"))
258254 rh := newReactionsHandler()
259255 http.HandleFunc("/reactions.list", func(w http.ResponseWriter, r *http.Request) { rh.handler(w, r) })
260256 rh.response = `{"ok": true,
315311 "pages": 1
316312 }}`
317313 want := []ReactedItem{
318 ReactedItem{
314 {
319315 Item: NewMessageItem("C1", &Message{Msg: Msg{
320316 Text: "hello",
321317 Reactions: []ItemReaction{
322 ItemReaction{Name: "astonished", Count: 3, Users: []string{"U1", "U2", "U3"}},
323 ItemReaction{Name: "clock1", Count: 3, Users: []string{"U1", "U2"}},
318 {Name: "astonished", Count: 3, Users: []string{"U1", "U2", "U3"}},
319 {Name: "clock1", Count: 3, Users: []string{"U1", "U2"}},
324320 },
325321 }}),
326322 Reactions: []ItemReaction{
327 ItemReaction{Name: "astonished", Count: 3, Users: []string{"U1", "U2", "U3"}},
328 ItemReaction{Name: "clock1", Count: 3, Users: []string{"U1", "U2"}},
329 },
330 },
331 ReactedItem{
323 {Name: "astonished", Count: 3, Users: []string{"U1", "U2", "U3"}},
324 {Name: "clock1", Count: 3, Users: []string{"U1", "U2"}},
325 },
326 },
327 {
332328 Item: NewFileItem(&File{Name: "toy"}),
333329 Reactions: []ItemReaction{
334 ItemReaction{Name: "clock1", Count: 3, Users: []string{"U1", "U2"}},
335 },
336 },
337 ReactedItem{
330 {Name: "clock1", Count: 3, Users: []string{"U1", "U2"}},
331 },
332 },
333 {
338334 Item: NewFileCommentItem(&File{Name: "toy"}, &Comment{Comment: "cool toy"}),
339335 Reactions: []ItemReaction{
340 ItemReaction{Name: "astonished", Count: 3, Users: []string{"U1", "U2", "U3"}},
336 {Name: "astonished", Count: 3, Users: []string{"U1", "U2", "U3"}},
341337 },
342338 },
343339 }
0 package slack
1
2 import (
3 "context"
4 "net/url"
5 "time"
6 )
7
8 type Reminder struct {
9 ID string `json:"id"`
10 Creator string `json:"creator"`
11 User string `json:"user"`
12 Text string `json:"text"`
13 Recurring bool `json:"recurring"`
14 Time time.Time `json:"time"`
15 CompleteTS int `json:"complete_ts"`
16 }
17
18 type reminderResp struct {
19 SlackResponse
20 Reminder Reminder `json:"reminder"`
21 }
22
23 func (api *Client) doReminder(ctx context.Context, path string, values url.Values) (*Reminder, error) {
24 response := &reminderResp{}
25 if err := api.postMethod(ctx, path, values, response); err != nil {
26 return nil, err
27 }
28 return &response.Reminder, response.Err()
29 }
30
31 // AddChannelReminder adds a reminder for a channel.
32 //
33 // See https://api.slack.com/methods/reminders.add (NOTE: the ability to set
34 // reminders on a channel is currently undocumented but has been tested to
35 // work)
36 func (api *Client) AddChannelReminder(channelID, text, time string) (*Reminder, error) {
37 values := url.Values{
38 "token": {api.token},
39 "text": {text},
40 "time": {time},
41 "channel": {channelID},
42 }
43 return api.doReminder(context.Background(), "reminders.add", values)
44 }
45
46 // AddUserReminder adds a reminder for a user.
47 //
48 // See https://api.slack.com/methods/reminders.add (NOTE: the ability to set
49 // reminders on a channel is currently undocumented but has been tested to
50 // work)
51 func (api *Client) AddUserReminder(userID, text, time string) (*Reminder, error) {
52 values := url.Values{
53 "token": {api.token},
54 "text": {text},
55 "time": {time},
56 "user": {userID},
57 }
58 return api.doReminder(context.Background(), "reminders.add", values)
59 }
60
61 // DeleteReminder deletes an existing reminder.
62 //
63 // See https://api.slack.com/methods/reminders.delete
64 func (api *Client) DeleteReminder(id string) error {
65 values := url.Values{
66 "token": {api.token},
67 "reminder": {id},
68 }
69 response := &SlackResponse{}
70 if err := api.postMethod(context.Background(), "reminders.delete", values, response); err != nil {
71 return err
72 }
73 return response.Err()
74 }
0 package slack
1
2 import (
3 "net/http"
4 "reflect"
5 "testing"
6 )
7
8 type remindersHandler struct {
9 gotParams map[string]string
10 response string
11 }
12
13 func newRemindersHandler() *remindersHandler {
14 return &remindersHandler{
15 gotParams: make(map[string]string),
16 }
17 }
18
19 func (rh *remindersHandler) accumulateFormValue(k string, r *http.Request) {
20 if v := r.FormValue(k); v != "" {
21 rh.gotParams[k] = v
22 }
23 }
24
25 func (rh *remindersHandler) handler(w http.ResponseWriter, r *http.Request) {
26 rh.accumulateFormValue("channel", r)
27 rh.accumulateFormValue("user", r)
28 rh.accumulateFormValue("text", r)
29 rh.accumulateFormValue("time", r)
30 rh.accumulateFormValue("reminder", r)
31 w.Header().Set("Content-Type", "application/json")
32 if rh.gotParams["text"] == "trigger-error" || rh.gotParams["reminder"] == "trigger-error" {
33 w.Write([]byte(`{ "ok": false, "error": "oh no" }`))
34 } else {
35 w.Write([]byte(`{ "ok": true }`))
36 }
37 }
38
39 func TestSlack_AddReminder(t *testing.T) {
40 once.Do(startServer)
41 api := New("testing-token", OptionAPIURL("http://"+serverAddr+"/"))
42 tests := []struct {
43 chanID string
44 userID string
45 text string
46 time string
47 wantParams map[string]string
48 expectErr bool
49 }{
50 {
51 "someChannelID",
52 "",
53 "hello world",
54 "tomorrow at 9am",
55 map[string]string{
56 "text": "hello world",
57 "time": "tomorrow at 9am",
58 "channel": "someChannelID",
59 },
60 false,
61 },
62 {
63 "someChannelID",
64 "",
65 "trigger-error",
66 "tomorrow at 9am",
67 map[string]string{
68 "text": "trigger-error",
69 "time": "tomorrow at 9am",
70 "channel": "someChannelID",
71 },
72 true,
73 },
74 {
75 "",
76 "someUserID",
77 "hello world",
78 "tomorrow at 9am",
79 map[string]string{
80 "text": "hello world",
81 "time": "tomorrow at 9am",
82 "user": "someUserID",
83 },
84 false,
85 },
86 {
87 "",
88 "someUserID",
89 "trigger-error",
90 "tomorrow at 9am",
91 map[string]string{
92 "text": "trigger-error",
93 "time": "tomorrow at 9am",
94 "user": "someUserID",
95 },
96 true,
97 },
98 }
99 var rh *remindersHandler
100 http.HandleFunc("/reminders.add", func(w http.ResponseWriter, r *http.Request) { rh.handler(w, r) })
101 for i, test := range tests {
102 rh = newRemindersHandler()
103 var err error
104 if test.chanID != "" {
105 _, err = api.AddChannelReminder(test.chanID, test.text, test.time)
106 } else {
107 _, err = api.AddUserReminder(test.userID, test.text, test.time)
108 }
109 if test.expectErr == false && err != nil {
110 t.Fatalf("%d: Unexpected error: %s", i, err)
111 } else if test.expectErr == true && err == nil {
112 t.Fatalf("%d: Expected error but got none!", i)
113 }
114 if !reflect.DeepEqual(rh.gotParams, test.wantParams) {
115 t.Errorf("%d: Got params %#v, want %#v", i, rh.gotParams, test.wantParams)
116 }
117 }
118 }
119
120 func TestSlack_DeleteReminder(t *testing.T) {
121 once.Do(startServer)
122 api := New("testing-token", OptionAPIURL("http://"+serverAddr+"/"))
123 tests := []struct {
124 reminder string
125 wantParams map[string]string
126 expectErr bool
127 }{
128 {
129 "foo",
130 map[string]string{
131 "reminder": "foo",
132 },
133 false,
134 },
135 {
136 "trigger-error",
137 map[string]string{
138 "reminder": "trigger-error",
139 },
140 true,
141 },
142 }
143 var rh *remindersHandler
144 http.HandleFunc("/reminders.delete", func(w http.ResponseWriter, r *http.Request) { rh.handler(w, r) })
145 for i, test := range tests {
146 rh = newRemindersHandler()
147 err := api.DeleteReminder(test.reminder)
148 if test.expectErr == false && err != nil {
149 t.Fatalf("%d: Unexpected error: %s", i, err)
150 } else if test.expectErr == true && err == nil {
151 t.Fatalf("%d: Expected error but got none!", i)
152 }
153 if !reflect.DeepEqual(rh.gotParams, test.wantParams) {
154 t.Errorf("%d: Got params %#v, want %#v", i, rh.gotParams, test.wantParams)
155 }
156 }
157 }
00 package slack
11
22 import (
3 "context"
34 "encoding/json"
4 "fmt"
55 "net/url"
6 "sync"
67 "time"
8
9 "github.com/gorilla/websocket"
710 )
811
9 // StartRTM calls the "rtm.start" endpoint and returns the provided URL and the full Info
10 // block.
12 const (
13 websocketDefaultTimeout = 10 * time.Second
14 defaultPingInterval = 30 * time.Second
15 )
16
17 const (
18 rtmEventTypeAck = ""
19 rtmEventTypeHello = "hello"
20 rtmEventTypeGoodbye = "goodbye"
21 rtmEventTypePong = "pong"
22 rtmEventTypeDesktopNotification = "desktop_notification"
23 )
24
25 // StartRTM calls the "rtm.start" endpoint and returns the provided URL and the full Info block.
1126 //
12 // To have a fully managed Websocket connection, use `NewRTM`, and call `ManageConnection()`
13 // on it.
27 // To have a fully managed Websocket connection, use `NewRTM`, and call `ManageConnection()` on it.
1428 func (api *Client) StartRTM() (info *Info, websocketURL string, err error) {
29 ctx, cancel := context.WithTimeout(context.Background(), websocketDefaultTimeout)
30 defer cancel()
31
32 return api.StartRTMContext(ctx)
33 }
34
35 // StartRTMContext calls the "rtm.start" endpoint and returns the provided URL and the full Info block with a custom context.
36 //
37 // To have a fully managed Websocket connection, use `NewRTM`, and call `ManageConnection()` on it.
38 func (api *Client) StartRTMContext(ctx context.Context) (info *Info, websocketURL string, err error) {
1539 response := &infoResponseFull{}
16 err = post("rtm.start", url.Values{"token": {api.config.token}}, response, api.debug)
40 err = api.postMethod(ctx, "rtm.start", url.Values{"token": {api.token}}, response)
1741 if err != nil {
18 return nil, "", fmt.Errorf("post: %s", err)
19 }
20 if !response.Ok {
21 return nil, "", response.Error
42 return nil, "", err
2243 }
2344
24 // websocket.Dial does not accept url without the port (yet)
25 // Fixed by: https://github.com/golang/net/commit/5058c78c3627b31e484a81463acd51c7cecc06f3
26 // but slack returns the address with no port, so we have to fix it
2745 api.Debugln("Using URL:", response.Info.URL)
28 websocketURL, err = websocketizeURLPort(response.Info.URL)
46 return &response.Info, response.Info.URL, response.Err()
47 }
48
49 // ConnectRTM calls the "rtm.connect" endpoint and returns the provided URL and the compact Info block.
50 //
51 // To have a fully managed Websocket connection, use `NewRTM`, and call `ManageConnection()` on it.
52 func (api *Client) ConnectRTM() (info *Info, websocketURL string, err error) {
53 ctx, cancel := context.WithTimeout(context.Background(), websocketDefaultTimeout)
54 defer cancel()
55
56 return api.ConnectRTMContext(ctx)
57 }
58
59 // ConnectRTMContext calls the "rtm.connect" endpoint and returns the
60 // provided URL and the compact Info block with a custom context.
61 //
62 // To have a fully managed Websocket connection, use `NewRTM`, and call `ManageConnection()` on it.
63 func (api *Client) ConnectRTMContext(ctx context.Context) (info *Info, websocketURL string, err error) {
64 response := &infoResponseFull{}
65 err = api.postMethod(ctx, "rtm.connect", url.Values{"token": {api.token}}, response)
2966 if err != nil {
30 return nil, "", fmt.Errorf("parsing response URL: %s", err)
67 api.Debugf("Failed to connect to RTM: %s", err)
68 return nil, "", err
3169 }
3270
33 return &response.Info, websocketURL, nil
71 api.Debugln("Using URL:", response.Info.URL)
72 return &response.Info, response.Info.URL, response.Err()
3473 }
3574
36 // ConnectRTM calls the "rtm.connect" endpoint and returns the provided URL and the compact Info
37 // block.
38 //
39 // To have a fully managed Websocket connection, use `NewRTM`, and call `ManageConnection()`
40 // on it.
41 func (api *Client) ConnectRTM() (info *Info, websocketURL string, err error) {
42 response := &infoResponseFull{}
43 err = post("rtm.connect", url.Values{"token": {api.config.token}}, response, api.debug)
44 if err != nil {
45 return nil, "", fmt.Errorf("post: %s", err)
75 // RTMOption options for the managed RTM.
76 type RTMOption func(*RTM)
77
78 // RTMOptionUseStart as of 11th July 2017 you should prefer setting this to false, see:
79 // https://api.slack.com/changelog/2017-04-start-using-rtm-connect-and-stop-using-rtm-start
80 func RTMOptionUseStart(b bool) RTMOption {
81 return func(rtm *RTM) {
82 rtm.useRTMStart = b
4683 }
47 if !response.Ok {
48 return nil, "", response.Error
84 }
85
86 // RTMOptionDialer takes a gorilla websocket Dialer and uses it as the
87 // Dialer when opening the websocket for the RTM connection.
88 func RTMOptionDialer(d *websocket.Dialer) RTMOption {
89 return func(rtm *RTM) {
90 rtm.dialer = d
4991 }
92 }
5093
51 // websocket.Dial does not accept url without the port (yet)
52 // Fixed by: https://github.com/golang/net/commit/5058c78c3627b31e484a81463acd51c7cecc06f3
53 // but slack returns the address with no port, so we have to fix it
54 api.Debugln("Using URL:", response.Info.URL)
55 websocketURL, err = websocketizeURLPort(response.Info.URL)
56 if err != nil {
57 return nil, "", fmt.Errorf("parsing response URL: %s", err)
94 // RTMOptionPingInterval determines how often to deliver a ping message to slack.
95 func RTMOptionPingInterval(d time.Duration) RTMOption {
96 return func(rtm *RTM) {
97 rtm.pingInterval = d
98 rtm.resetDeadman()
5899 }
100 }
59101
60 return &response.Info, websocketURL, nil
102 // RTMOptionConnParams installs parameters to embed into the connection URL.
103 func RTMOptionConnParams(connParams url.Values) RTMOption {
104 return func(rtm *RTM) {
105 rtm.connParams = connParams
106 }
61107 }
62108
63109 // NewRTM returns a RTM, which provides a fully managed connection to
64110 // Slack's websocket-based Real-Time Messaging protocol.
65 func (api *Client) NewRTM() *RTM {
66 return api.NewRTMWithOptions(nil)
67 }
68
69 // NewRTMWithOptions returns a RTM, which provides a fully managed connection to
70 // Slack's websocket-based Real-Time Messaging protocol.
71 // This also allows to configure various options available for RTM API.
72 func (api *Client) NewRTMWithOptions(options *RTMOptions) *RTM {
111 func (api *Client) NewRTM(options ...RTMOption) *RTM {
73112 result := &RTM{
74113 Client: *api,
75114 IncomingEvents: make(chan RTMEvent, 50),
76115 outgoingMessages: make(chan OutgoingMessage, 20),
77 pings: make(map[int]time.Time),
78 isConnected: false,
79 wasIntentional: true,
116 pingInterval: defaultPingInterval,
117 pingDeadman: time.NewTimer(deadmanDuration(defaultPingInterval)),
80118 killChannel: make(chan bool),
119 disconnected: make(chan struct{}),
120 disconnectedm: &sync.Once{},
81121 forcePing: make(chan bool),
82122 rawEvents: make(chan json.RawMessage),
83123 idGen: NewSafeID(1),
124 mu: &sync.Mutex{},
84125 }
85126
86 if options != nil {
87 result.useRTMStart = options.UseRTMStart
88 } else {
89 result.useRTMStart = true
127 for _, opt := range options {
128 opt(result)
90129 }
91130
92131 return result
00 package slack
11
22 import (
3 "errors"
3 "context"
44 "net/url"
55 "strconv"
66 )
99 DEFAULT_SEARCH_SORT = "score"
1010 DEFAULT_SEARCH_SORT_DIR = "desc"
1111 DEFAULT_SEARCH_HIGHLIGHT = false
12 DEFAULT_SEARCH_COUNT = 100
12 DEFAULT_SEARCH_COUNT = 20
1313 DEFAULT_SEARCH_PAGE = 1
1414 )
1515
3535 }
3636
3737 type SearchMessage struct {
38 Type string `json:"type"`
39 Channel CtxChannel `json:"channel"`
40 User string `json:"user"`
41 Username string `json:"username"`
42 Timestamp string `json:"ts"`
43 Text string `json:"text"`
44 Permalink string `json:"permalink"`
45 Previous CtxMessage `json:"previous"`
46 Previous2 CtxMessage `json:"previous_2"`
47 Next CtxMessage `json:"next"`
48 Next2 CtxMessage `json:"next_2"`
38 Type string `json:"type"`
39 Channel CtxChannel `json:"channel"`
40 User string `json:"user"`
41 Username string `json:"username"`
42 Timestamp string `json:"ts"`
43 Blocks Blocks `json:"blocks,omitempty"`
44 Text string `json:"text"`
45 Permalink string `json:"permalink"`
46 Attachments []Attachment `json:"attachments"`
47 Previous CtxMessage `json:"previous"`
48 Previous2 CtxMessage `json:"previous_2"`
49 Next CtxMessage `json:"next"`
50 Next2 CtxMessage `json:"next_2"`
4951 }
5052
5153 type SearchMessages struct {
7981 }
8082 }
8183
82 func (api *Client) _search(path, query string, params SearchParameters, files, messages bool) (response *searchResponseFull, error error) {
84 func (api *Client) _search(ctx context.Context, path, query string, params SearchParameters, files, messages bool) (response *searchResponseFull, error error) {
8385 values := url.Values{
84 "token": {api.config.token},
86 "token": {api.token},
8587 "query": {query},
8688 }
8789 if params.Sort != DEFAULT_SEARCH_SORT {
99101 if params.Page != DEFAULT_SEARCH_PAGE {
100102 values.Add("page", strconv.Itoa(params.Page))
101103 }
104
102105 response = &searchResponseFull{}
103 err := post(path, values, response, api.debug)
106 err := api.postMethod(ctx, path, values, response)
104107 if err != nil {
105108 return nil, err
106109 }
107 if !response.Ok {
108 return nil, errors.New(response.Error)
109 }
110 return response, nil
110
111 return response, response.Err()
111112
112113 }
113114
114115 func (api *Client) Search(query string, params SearchParameters) (*SearchMessages, *SearchFiles, error) {
115 response, err := api._search("search.all", query, params, true, true)
116 return api.SearchContext(context.Background(), query, params)
117 }
118
119 func (api *Client) SearchContext(ctx context.Context, query string, params SearchParameters) (*SearchMessages, *SearchFiles, error) {
120 response, err := api._search(ctx, "search.all", query, params, true, true)
116121 if err != nil {
117122 return nil, nil, err
118123 }
120125 }
121126
122127 func (api *Client) SearchFiles(query string, params SearchParameters) (*SearchFiles, error) {
123 response, err := api._search("search.files", query, params, true, false)
128 return api.SearchFilesContext(context.Background(), query, params)
129 }
130
131 func (api *Client) SearchFilesContext(ctx context.Context, query string, params SearchParameters) (*SearchFiles, error) {
132 response, err := api._search(ctx, "search.files", query, params, true, false)
124133 if err != nil {
125134 return nil, err
126135 }
128137 }
129138
130139 func (api *Client) SearchMessages(query string, params SearchParameters) (*SearchMessages, error) {
131 response, err := api._search("search.messages", query, params, false, true)
140 return api.SearchMessagesContext(context.Background(), query, params)
141 }
142
143 func (api *Client) SearchMessagesContext(ctx context.Context, query string, params SearchParameters) (*SearchMessages, error) {
144 response, err := api._search(ctx, "search.messages", query, params, false, true)
132145 if err != nil {
133146 return nil, err
134147 }
0 package slack
1
2 import (
3 "crypto/hmac"
4 "crypto/sha256"
5 "encoding/hex"
6 "fmt"
7 "hash"
8 "net/http"
9 "strconv"
10 "strings"
11 "time"
12 )
13
14 // Signature headers
15 const (
16 hSignature = "X-Slack-Signature"
17 hTimestamp = "X-Slack-Request-Timestamp"
18 )
19
20 // SecretsVerifier contains the information needed to verify that the request comes from Slack
21 type SecretsVerifier struct {
22 signature []byte
23 hmac hash.Hash
24 }
25
26 func unsafeSignatureVerifier(header http.Header, secret string) (_ SecretsVerifier, err error) {
27 var (
28 bsignature []byte
29 )
30
31 signature := header.Get(hSignature)
32 stimestamp := header.Get(hTimestamp)
33
34 if signature == "" || stimestamp == "" {
35 return SecretsVerifier{}, ErrMissingHeaders
36 }
37
38 if bsignature, err = hex.DecodeString(strings.TrimPrefix(signature, "v0=")); err != nil {
39 return SecretsVerifier{}, err
40 }
41
42 hash := hmac.New(sha256.New, []byte(secret))
43 if _, err = hash.Write([]byte(fmt.Sprintf("v0:%s:", stimestamp))); err != nil {
44 return SecretsVerifier{}, err
45 }
46
47 return SecretsVerifier{
48 signature: bsignature,
49 hmac: hash,
50 }, nil
51 }
52
53 // NewSecretsVerifier returns a SecretsVerifier object in exchange for an http.Header object and signing secret
54 func NewSecretsVerifier(header http.Header, secret string) (sv SecretsVerifier, err error) {
55 var (
56 timestamp int64
57 )
58
59 stimestamp := header.Get(hTimestamp)
60
61 if sv, err = unsafeSignatureVerifier(header, secret); err != nil {
62 return SecretsVerifier{}, err
63 }
64
65 if timestamp, err = strconv.ParseInt(stimestamp, 10, 64); err != nil {
66 return SecretsVerifier{}, err
67 }
68
69 diff := absDuration(time.Since(time.Unix(timestamp, 0)))
70 if diff > 5*time.Minute {
71 return SecretsVerifier{}, ErrExpiredTimestamp
72 }
73
74 return sv, err
75 }
76
77 func (v *SecretsVerifier) Write(body []byte) (n int, err error) {
78 return v.hmac.Write(body)
79 }
80
81 // Ensure compares the signature sent from Slack with the actual computed hash to judge validity
82 func (v SecretsVerifier) Ensure() error {
83 computed := v.hmac.Sum(nil)
84 // use hmac.Equal prevent leaking timing information.
85 if hmac.Equal(computed, v.signature) {
86 return nil
87 }
88
89 return fmt.Errorf("Expected signing signature: %s, but computed: %s", hex.EncodeToString(v.signature), hex.EncodeToString(computed))
90 }
91
92 func abs64(n int64) int64 {
93 y := n >> 63
94 return (n ^ y) - y
95 }
96
97 func absDuration(n time.Duration) time.Duration {
98 return time.Duration(abs64(int64(n)))
99 }
0 package slack
1
2 import (
3 "io"
4 "log"
5 "net/http"
6 "testing"
7 )
8
9 const (
10 validSigningSecret = "e6b19c573432dcc6b075501d51b51bb8"
11 invalidSigningSecret = "e6b19c573432dcc6b075501d51b51boo"
12 validBody = `{"token":"aF5ynEYQH0dFN9imlgcADxDB","team_id":"XXXXXXXXX","api_app_id":"YYYYYYYYY","event":{"type":"app_mention","user":"AAAAAAAAA","text":"<@EEEEEEEEE> hello world","client_msg_id":"477cc591-ch73-a14z-4db8-g0cd76321bec","ts":"1531431954.000073","channel":"TTTTTTTTT","event_ts":"1531431954.000073"},"type":"event_callback","event_id":"TvBP7LRED7","event_time":1531431954,"authed_users":["EEEEEEEEE"]}`
13 invalidBody = `{"token":"12345678abcdefghlmnopqrs","team_id":"XXXXXXXXX","api_app_id":"YYYYYYYYY","event":{"type":"app_mention","user":"AAAAAAAAA","text":"<@EEEEEEEEE> hello world","client_msg_id":"477cc591-ch73-a14z-4db8-g0cd76321bec","ts":"1531431954.000073","channel":"TTTTTTTTT","event_ts":"1531431954.000073"},"type":"event_callback","event_id":"TvBP7LRED7","event_time":1531431954,"authed_users":["EEEEEEEEE"]}`
14 )
15
16 func newHeader(valid bool) http.Header {
17 h := http.Header{}
18 if valid {
19 h.Set("X-Slack-Signature", "v0=adada4ed31709aef585c2580ca3267678c6a8eaeb7e0c1aca3ee57b656886b2c")
20 h.Set("X-Slack-Request-Timestamp", "1531431954")
21 } else {
22 h.Set("X-Slack-Signature", "")
23 }
24 return h
25 }
26
27 func TestExpiredTimestamp(t *testing.T) {
28 _, err := NewSecretsVerifier(newHeader(true), "abcdefg12345")
29 if err == nil {
30 t.Fatal("expected an error but got none")
31 }
32 }
33
34 func TestUnsafeSignatureVerifier(t *testing.T) {
35 tests := []struct {
36 title string
37 header http.Header
38 signingSecret string
39 expectError bool
40 }{
41 {
42 title: "Testing with acceptable params",
43 header: newHeader(true),
44 signingSecret: "abcdefg12345",
45 expectError: false,
46 },
47 {
48 title: "Testing with unacceptable params",
49 header: newHeader(false),
50 signingSecret: "abcdefg12345",
51 expectError: true,
52 },
53 }
54
55 for _, test := range tests {
56 _, err := unsafeSignatureVerifier(test.header, test.signingSecret)
57
58 if !test.expectError && err != nil {
59 log.Fatalf("%s: Unexpected error: %s in test", test.title, err)
60 } else if test.expectError == true && err == nil {
61 log.Fatalf("Expected error but got none")
62 }
63 }
64 }
65
66 func TestEnsure(t *testing.T) {
67 tests := []struct {
68 title string
69 header http.Header
70 signingSecret string
71 body string
72 expectError bool
73 }{
74 {
75 title: "Testing with acceptable signing secret and valid body",
76 header: newHeader(true),
77 signingSecret: validSigningSecret,
78 body: validBody,
79 expectError: false,
80 },
81 {
82 title: "Testing with unacceptable signing secret and valid body",
83 header: newHeader(true),
84 signingSecret: invalidSigningSecret,
85 body: validBody,
86 expectError: true,
87 },
88 {
89 title: "Testing with acceptable signing secret and invalid body",
90 header: newHeader(true),
91 signingSecret: validSigningSecret,
92 body: invalidBody,
93 expectError: true,
94 },
95 }
96
97 for _, test := range tests {
98 sv, err := unsafeSignatureVerifier(test.header, test.signingSecret)
99 if err != nil {
100 t.Fatalf("unexpected error: %s", err)
101 }
102 io.WriteString(&sv, test.body)
103
104 err = sv.Ensure()
105
106 if !test.expectError && err != nil {
107 log.Fatalf("%s: Unexpected error: %s in test", test.title, err)
108 } else if test.expectError == true && err == nil {
109 log.Fatalf("Expected error but got none")
110 }
111 }
112
113 }
00 package slack
11
22 import (
3 "errors"
3 "context"
4 "fmt"
45 "log"
6 "net/http"
57 "net/url"
68 "os"
79 )
810
9 var logger *log.Logger // A logger that can be set by consumers
10 /*
11 Added as a var so that we can change this for testing purposes
12 */
13 var SLACK_API string = "https://slack.com/api/"
14 var SLACK_WEB_API_FORMAT string = "https://%s.slack.com/api/users.admin.%s?t=%s"
11 const (
12 // APIURL of the slack api.
13 APIURL = "https://slack.com/api/"
14 // WEBAPIURLFormat ...
15 WEBAPIURLFormat = "https://%s.slack.com/api/users.admin.%s?t=%d"
16 )
1517
16 type SlackResponse struct {
17 Ok bool `json:"ok"`
18 Error string `json:"error"`
18 // httpClient defines the minimal interface needed for an http.Client to be implemented.
19 type httpClient interface {
20 Do(*http.Request) (*http.Response, error)
1921 }
2022
23 // ResponseMetadata holds pagination metadata
24 type ResponseMetadata struct {
25 Cursor string `json:"next_cursor"`
26 }
27
28 func (t *ResponseMetadata) initialize() *ResponseMetadata {
29 if t != nil {
30 return t
31 }
32
33 return &ResponseMetadata{}
34 }
35
36 // AuthTestResponse ...
2137 type AuthTestResponse struct {
2238 URL string `json:"url"`
2339 Team string `json:"team"`
2440 User string `json:"user"`
2541 TeamID string `json:"team_id"`
2642 UserID string `json:"user_id"`
43 // EnterpriseID is only returned when an enterprise id present
44 EnterpriseID string `json:"enterprise_id,omitempty"`
2745 }
2846
2947 type authTestResponseFull struct {
3149 AuthTestResponse
3250 }
3351
52 // Client for the slack api.
53 type ParamOption func(*url.Values)
54
3455 type Client struct {
35 config struct {
36 token string
37 }
38 info Info
39 debug bool
56 token string
57 endpoint string
58 debug bool
59 log ilogger
60 httpclient httpClient
4061 }
4162
42 // SetLogger let's library users supply a logger, so that api debugging
43 // can be logged along with the application's debugging info.
44 func SetLogger(l *log.Logger) {
45 logger = l
63 // Option defines an option for a Client
64 type Option func(*Client)
65
66 // OptionHTTPClient - provide a custom http client to the slack client.
67 func OptionHTTPClient(client httpClient) func(*Client) {
68 return func(c *Client) {
69 c.httpclient = client
70 }
4671 }
4772
48 func New(token string) *Client {
49 s := &Client{}
50 s.config.token = token
73 // OptionDebug enable debugging for the client
74 func OptionDebug(b bool) func(*Client) {
75 return func(c *Client) {
76 c.debug = b
77 }
78 }
79
80 // OptionLog set logging for client.
81 func OptionLog(l logger) func(*Client) {
82 return func(c *Client) {
83 c.log = internalLog{logger: l}
84 }
85 }
86
87 // OptionAPIURL set the url for the client. only useful for testing.
88 func OptionAPIURL(u string) func(*Client) {
89 return func(c *Client) { c.endpoint = u }
90 }
91
92 // New builds a slack client from the provided token and options.
93 func New(token string, options ...Option) *Client {
94 s := &Client{
95 token: token,
96 endpoint: APIURL,
97 httpclient: &http.Client{},
98 log: log.New(os.Stderr, "nlopes/slack", log.LstdFlags|log.Lshortfile),
99 }
100
101 for _, opt := range options {
102 opt(s)
103 }
104
51105 return s
52106 }
53107
54108 // AuthTest tests if the user is able to do authenticated requests or not
55109 func (api *Client) AuthTest() (response *AuthTestResponse, error error) {
110 return api.AuthTestContext(context.Background())
111 }
112
113 // AuthTestContext tests if the user is able to do authenticated requests or not with a custom context
114 func (api *Client) AuthTestContext(ctx context.Context) (response *AuthTestResponse, err error) {
115 api.Debugf("Challenging auth...")
56116 responseFull := &authTestResponseFull{}
57 err := post("auth.test", url.Values{"token": {api.config.token}}, responseFull, api.debug)
117 err = api.postMethod(ctx, "auth.test", url.Values{"token": {api.token}}, responseFull)
58118 if err != nil {
59119 return nil, err
60120 }
61 if !responseFull.Ok {
62 return nil, errors.New(responseFull.Error)
63 }
64 return &responseFull.AuthTestResponse, nil
121
122 return &responseFull.AuthTestResponse, responseFull.Err()
65123 }
66124
67 // SetDebug switches the api into debug mode
68 // When in debug mode, it logs various info about what its doing
69 // If you ever use this in production, don't call SetDebug(true)
70 func (api *Client) SetDebug(debug bool) {
71 api.debug = debug
72 if debug && logger == nil {
73 logger = log.New(os.Stdout, "nlopes/slack", log.LstdFlags | log.Lshortfile)
125 // Debugf print a formatted debug line.
126 func (api *Client) Debugf(format string, v ...interface{}) {
127 if api.debug {
128 api.log.Output(2, fmt.Sprintf(format, v...))
74129 }
75130 }
76131
77 func (api *Client) Debugf(format string, v ...interface{}) {
132 // Debugln print a debug line.
133 func (api *Client) Debugln(v ...interface{}) {
78134 if api.debug {
79 logger.Printf(format, v...)
135 api.log.Output(2, fmt.Sprintln(v...))
80136 }
81137 }
82138
83 func (api *Client) Debugln(v ...interface{}) {
84 if api.debug {
85 logger.Println(v...)
86 }
139 // Debug returns if debug is enabled.
140 func (api *Client) Debug() bool {
141 return api.debug
87142 }
143
144 // post to a slack web method.
145 func (api *Client) postMethod(ctx context.Context, path string, values url.Values, intf interface{}) error {
146 return postForm(ctx, api.httpclient, api.endpoint+path, values, intf, api)
147 }
148
149 // get a slack web method.
150 func (api *Client) getMethod(ctx context.Context, path string, values url.Values, intf interface{}) error {
151 return getResource(ctx, api.httpclient, api.endpoint+path, values, intf, api)
152 }
0 package slackevents
1
2 import (
3 "encoding/json"
4
5 "github.com/nlopes/slack"
6 )
7
8 type MessageActionResponse struct {
9 ResponseType string `json:"response_type"`
10 ReplaceOriginal bool `json:"replace_original"`
11 Text string `json:"text"`
12 }
13
14 type MessageActionEntity struct {
15 ID string `json:"id"`
16 Domain string `json:"domain"`
17 Name string `json:"name"`
18 }
19
20 type MessageAction struct {
21 Type string `json:"type"`
22 Actions []slack.AttachmentAction `json:"actions"`
23 CallbackID string `json:"callback_id"`
24 Team MessageActionEntity `json:"team"`
25 Channel MessageActionEntity `json:"channel"`
26 User MessageActionEntity `json:"user"`
27 ActionTimestamp json.Number `json:"action_ts"`
28 MessageTimestamp json.Number `json:"message_ts"`
29 AttachmentID json.Number `json:"attachment_id"`
30 Token string `json:"token"`
31 Message slack.Message `json:"message"`
32 OriginalMessage slack.Message `json:"original_message"`
33 ResponseURL string `json:"response_url"`
34 TriggerID string `json:"trigger_id"`
35 }
0 // inner_events.go provides EventsAPI particular inner events
1
2 package slackevents
3
4 import "encoding/json"
5
6 // EventsAPIInnerEvent the inner event of a EventsAPI event_callback Event.
7 type EventsAPIInnerEvent struct {
8 Type string `json:"type"`
9 Data interface{}
10 }
11
12 // AppMentionEvent is an (inner) EventsAPI subscribable event.
13 type AppMentionEvent struct {
14 Type string `json:"type"`
15 User string `json:"user"`
16 Text string `json:"text"`
17 TimeStamp string `json:"ts"`
18 ThreadTimeStamp string `json:"thread_ts"`
19 Channel string `json:"channel"`
20 EventTimeStamp json.Number `json:"event_ts"`
21 }
22
23 // AppHomeOpenedEvent Your Slack app home was opened.
24 type AppHomeOpenedEvent struct {
25 Type string `json:"type"`
26 User string `json:"user"`
27 Channel string `json:"channel"`
28 EventTimeStamp json.Number `json:"event_ts"`
29 }
30
31 // AppUninstalledEvent Your Slack app was uninstalled.
32 type AppUninstalledEvent struct {
33 Type string `json:"type"`
34 }
35
36 // GridMigrationFinishedEvent An enterprise grid migration has finished on this workspace.
37 type GridMigrationFinishedEvent struct {
38 Type string `json:"type"`
39 EnterpriseID string `json:"enterprise_id"`
40 }
41
42 // GridMigrationStartedEvent An enterprise grid migration has started on this workspace.
43 type GridMigrationStartedEvent struct {
44 Type string `json:"type"`
45 EnterpriseID string `json:"enterprise_id"`
46 }
47
48 // LinkSharedEvent A message was posted containing one or more links relevant to your application
49 type LinkSharedEvent struct {
50 Type string `json:"type"`
51 User string `json:"user"`
52 TimeStamp string `json:"ts"`
53 Channel string `json:"channel"`
54 MessageTimeStamp json.Number `json:"message_ts"`
55 Links []sharedLinks `json:"links"`
56 }
57
58 type sharedLinks struct {
59 Domain string `json:"domain"`
60 URL string `json:"url"`
61 }
62
63 // MessageEvent occurs when a variety of types of messages has been posted.
64 // Parse ChannelType to see which
65 // if ChannelType = "group", this is a private channel message
66 // if ChannelType = "channel", this message was sent to a channel
67 // if ChannelType = "im", this is a private message
68 // if ChannelType = "mim", A message was posted in a multiparty direct message channel
69 // TODO: Improve this so that it is not required to manually parse ChannelType
70 type MessageEvent struct {
71 // Basic Message Event - https://api.slack.com/events/message
72 Type string `json:"type"`
73 User string `json:"user"`
74 Text string `json:"text"`
75 ThreadTimeStamp string `json:"thread_ts"`
76 TimeStamp string `json:"ts"`
77 Channel string `json:"channel"`
78 ChannelType string `json:"channel_type"`
79 EventTimeStamp json.Number `json:"event_ts"`
80
81 // Edited Message
82 Message *MessageEvent `json:"message,omitempty"`
83 PreviousMessage *MessageEvent `json:"previous_message,omitempty"`
84 Edited *Edited `json:"edited,omitempty"`
85
86 // Message Subtypes
87 SubType string `json:"subtype,omitempty"`
88
89 // bot_message (https://api.slack.com/events/message/bot_message)
90 BotID string `json:"bot_id,omitempty"`
91 Username string `json:"username,omitempty"`
92 Icons *Icon `json:"icons,omitempty"`
93
94 Upload bool `json:"upload"`
95 Files []File `json:"files"`
96 }
97
98 // MemberJoinedChannelEvent A member join a channel
99 type MemberJoinedChannelEvent struct {
100 Type string `json:"type"`
101 User string `json:"user"`
102 Channel string `json:"channel"`
103 ChannelType string `json:"channel_type"`
104 Team string `json:"team"`
105 Inviter string `json:"inviter"`
106 }
107
108 type pinEvent struct {
109 Type string `json:"type"`
110 User string `json:"user"`
111 Item Item `json:"item"`
112 Channel string `json:"channel_id"`
113 EventTimestamp string `json:"event_ts"`
114 HasPins bool `json:"has_pins,omitempty"`
115 }
116
117 // PinAddedEvent An item was pinned to a channel - https://api.slack.com/events/pin_added
118 type PinAddedEvent pinEvent
119
120 // PinRemovedEvent An item was unpinned from a channel - https://api.slack.com/events/pin_removed
121 type PinRemovedEvent pinEvent
122
123 type tokens struct {
124 Oauth []string `json:"oauth"`
125 Bot []string `json:"bot"`
126 }
127
128 // TokensRevokedEvent APP's API tokes are revoked - https://api.slack.com/events/tokens_revoked
129 type TokensRevokedEvent struct {
130 Type string `json:"type"`
131 Tokens tokens `json:"tokens"`
132 }
133
134 // JSONTime exists so that we can have a String method converting the date
135 type JSONTime int64
136
137 // Comment contains all the information relative to a comment
138 type Comment struct {
139 ID string `json:"id,omitempty"`
140 Created JSONTime `json:"created,omitempty"`
141 Timestamp JSONTime `json:"timestamp,omitempty"`
142 User string `json:"user,omitempty"`
143 Comment string `json:"comment,omitempty"`
144 }
145
146 // File is a file upload
147 type File struct {
148 ID string `json:"id"`
149 Created int `json:"created"`
150 Timestamp int `json:"timestamp"`
151 Name string `json:"name"`
152 Title string `json:"title"`
153 Mimetype string `json:"mimetype"`
154 Filetype string `json:"filetype"`
155 PrettyType string `json:"pretty_type"`
156 User string `json:"user"`
157 Editable bool `json:"editable"`
158 Size int `json:"size"`
159 Mode string `json:"mode"`
160 IsExternal bool `json:"is_external"`
161 ExternalType string `json:"external_type"`
162 IsPublic bool `json:"is_public"`
163 PublicURLShared bool `json:"public_url_shared"`
164 DisplayAsBot bool `json:"display_as_bot"`
165 Username string `json:"username"`
166 URLPrivate string `json:"url_private"`
167 URLPrivateDownload string `json:"url_private_download"`
168 Thumb64 string `json:"thumb_64"`
169 Thumb80 string `json:"thumb_80"`
170 Thumb360 string `json:"thumb_360"`
171 Thumb360W int `json:"thumb_360_w"`
172 Thumb360H int `json:"thumb_360_h"`
173 Thumb480 string `json:"thumb_480"`
174 Thumb480W int `json:"thumb_480_w"`
175 Thumb480H int `json:"thumb_480_h"`
176 Thumb160 string `json:"thumb_160"`
177 Thumb720 string `json:"thumb_720"`
178 Thumb720W int `json:"thumb_720_w"`
179 Thumb720H int `json:"thumb_720_h"`
180 Thumb800 string `json:"thumb_800"`
181 Thumb800W int `json:"thumb_800_w"`
182 Thumb800H int `json:"thumb_800_h"`
183 Thumb960 string `json:"thumb_960"`
184 Thumb960W int `json:"thumb_960_w"`
185 Thumb960H int `json:"thumb_960_h"`
186 Thumb1024 string `json:"thumb_1024"`
187 Thumb1024W int `json:"thumb_1024_w"`
188 Thumb1024H int `json:"thumb_1024_h"`
189 ImageExifRotation int `json:"image_exif_rotation"`
190 OriginalW int `json:"original_w"`
191 OriginalH int `json:"original_h"`
192 Permalink string `json:"permalink"`
193 PermalinkPublic string `json:"permalink_public"`
194 }
195
196 // Edited is included when a Message is edited
197 type Edited struct {
198 User string `json:"user"`
199 TimeStamp string `json:"ts"`
200 }
201
202 // Icon is used for bot messages
203 type Icon struct {
204 IconURL string `json:"icon_url,omitempty"`
205 IconEmoji string `json:"icon_emoji,omitempty"`
206 }
207
208 // Item is any type of slack message - message, file, or file comment.
209 type Item struct {
210 Type string `json:"type"`
211 Channel string `json:"channel,omitempty"`
212 Message *ItemMessage `json:"message,omitempty"`
213 File *File `json:"file,omitempty"`
214 Comment *Comment `json:"comment,omitempty"`
215 Timestamp string `json:"ts,omitempty"`
216 }
217
218 // ItemMessage is the event message
219 type ItemMessage struct {
220 Type string `json:"type"`
221 User string `json:"user"`
222 Text string `json:"text"`
223 Timestamp string `json:"ts"`
224 PinnedTo []string `json:"pinned_to"`
225 ReplaceOriginal bool `json:"replace_original"`
226 DeleteOriginal bool `json:"delete_original"`
227 }
228
229 // IsEdited checks if the MessageEvent is caused by an edit
230 func (e MessageEvent) IsEdited() bool {
231 return e.Message != nil &&
232 e.Message.Edited != nil
233 }
234
235 const (
236 // AppMention is an Events API subscribable event
237 AppMention = "app_mention"
238 // AppHomeOpened Your Slack app home was opened
239 AppHomeOpened = "app_home_opened"
240 // AppUninstalled Your Slack app was uninstalled.
241 AppUninstalled = "app_uninstalled"
242 // GridMigrationFinished An enterprise grid migration has finished on this workspace.
243 GridMigrationFinished = "grid_migration_finished"
244 // GridMigrationStarted An enterprise grid migration has started on this workspace.
245 GridMigrationStarted = "grid_migration_started"
246 // LinkShared A message was posted containing one or more links relevant to your application
247 LinkShared = "link_shared"
248 // Message A message was posted to a channel, private channel (group), im, or mim
249 Message = "message"
250 // Member Joined Channel
251 MemberJoinedChannel = "member_joined_channel"
252 // PinAdded An item was pinned to a channel
253 PinAdded = "pin_added"
254 // PinRemoved An item was unpinned from a channel
255 PinRemoved = "pin_removed"
256 // TokensRevoked APP's API tokes are revoked
257 TokensRevoked = "tokens_revoked"
258 )
259
260 // EventsAPIInnerEventMapping maps INNER Event API events to their corresponding struct
261 // implementations. The structs should be instances of the unmarshalling
262 // target for the matching event type.
263 var EventsAPIInnerEventMapping = map[string]interface{}{
264 AppMention: AppMentionEvent{},
265 AppHomeOpened: AppHomeOpenedEvent{},
266 AppUninstalled: AppUninstalledEvent{},
267 GridMigrationFinished: GridMigrationFinishedEvent{},
268 GridMigrationStarted: GridMigrationStartedEvent{},
269 LinkShared: LinkSharedEvent{},
270 Message: MessageEvent{},
271 MemberJoinedChannel: MemberJoinedChannelEvent{},
272 PinAdded: PinAddedEvent{},
273 PinRemoved: PinRemovedEvent{},
274 TokensRevoked: TokensRevokedEvent{},
275 }
0 package slackevents
1
2 import (
3 "encoding/json"
4 "testing"
5 )
6
7 func TestAppMention(t *testing.T) {
8 rawE := []byte(`
9 {
10 "type": "app_mention",
11 "user": "U061F7AUR",
12 "text": "<@U0LAN0Z89> is it everything a river should be?",
13 "ts": "1515449522.000016",
14 "thread_ts": "1515449522.000016",
15 "channel": "C0LAN2Q65",
16 "event_ts": "1515449522000016"
17 }
18 `)
19 err := json.Unmarshal(rawE, &AppMentionEvent{})
20 if err != nil {
21 t.Error(err)
22 }
23 }
24
25 func TestAppUninstalled(t *testing.T) {
26 rawE := []byte(`
27 {
28 "type": "app_uninstalled"
29 }
30 `)
31 err := json.Unmarshal(rawE, &AppUninstalledEvent{})
32 if err != nil {
33 t.Error(err)
34 }
35 }
36
37 func TestGridMigrationFinishedEvent(t *testing.T) {
38 rawE := []byte(`
39 {
40 "type": "grid_migration_finished",
41 "enterprise_id": "EXXXXXXXX"
42 }
43 `)
44 err := json.Unmarshal(rawE, &GridMigrationFinishedEvent{})
45 if err != nil {
46 t.Error(err)
47 }
48 }
49
50 func TestGridMigrationStartedEvent(t *testing.T) {
51 rawE := []byte(`
52 {
53 "token": "XXYYZZ",
54 "team_id": "TXXXXXXXX",
55 "api_app_id": "AXXXXXXXXX",
56 "event": {
57 "type": "grid_migration_started",
58 "enterprise_id": "EXXXXXXXX"
59 },
60 "type": "event_callback",
61 "event_id": "EvXXXXXXXX",
62 "event_time": 1234567890
63 }
64 `)
65 err := json.Unmarshal(rawE, &GridMigrationStartedEvent{})
66 if err != nil {
67 t.Error(err)
68 }
69 }
70
71 func TestLinkSharedEvent(t *testing.T) {
72 rawE := []byte(`
73 {
74 "type": "link_shared",
75 "channel": "Cxxxxxx",
76 "user": "Uxxxxxxx",
77 "message_ts": "123456789.9875",
78 "links":
79 [
80 {
81 "domain": "example.com",
82 "url": "https://example.com/12345"
83 },
84 {
85 "domain": "example.com",
86 "url": "https://example.com/67890"
87 },
88 {
89 "domain": "another-example.com",
90 "url": "https://yet.another-example.com/v/abcde"
91 }
92 ]
93 }
94 `)
95 err := json.Unmarshal(rawE, &LinkSharedEvent{})
96 if err != nil {
97 t.Error(err)
98 }
99 }
100
101 func TestMessageEvent(t *testing.T) {
102 rawE := []byte(`
103 {
104 "type": "message",
105 "channel": "G024BE91L",
106 "user": "U2147483697",
107 "text": "Live long and prospect.",
108 "ts": "1355517523.000005",
109 "event_ts": "1355517523.000005",
110 "channel_type": "channel",
111 "message": {
112 "text": "To infinity and beyond.",
113 "edited": {
114 "user": "U2147483697",
115 "ts": "1355517524.000000"
116 }
117 },
118 "previous_message": {
119 "text": "Live long and prospect."
120 }
121 }
122 `)
123 err := json.Unmarshal(rawE, &MessageEvent{})
124 if err != nil {
125 t.Error(err)
126 }
127 }
128
129 func TestBotMessageEvent(t *testing.T) {
130 rawE := []byte(`
131 {
132 "type": "message",
133 "subtype": "bot_message",
134 "ts": "1358877455.000010",
135 "text": "Pushing is the answer",
136 "bot_id": "BB12033",
137 "username": "github",
138 "icons": {}
139 }
140 `)
141 err := json.Unmarshal(rawE, &MessageEvent{})
142 if err != nil {
143 t.Error(err)
144 }
145 }
146
147 func TestPinAdded(t *testing.T) {
148 rawE := []byte(`
149 {
150 "type": "pin_added",
151 "user": "U061F7AUR",
152 "item": {
153 "type": "message",
154 "channel":"C0LAN2Q65",
155 "message":{
156 "type":"message",
157 "user":"U061F7AUR",
158 "text": "<@U0LAN0Z89> is it everything a river should be?",
159 "ts":"1539904112.000100",
160 "pinned_to":["C0LAN2Q65"],
161 "replace_original":false,
162 "delete_original":false
163 }
164 },
165 "channel_id":"C0LAN2Q65",
166 "event_ts": "1515449522000016"
167 }
168 `)
169 err := json.Unmarshal(rawE, &PinAddedEvent{})
170 if err != nil {
171 t.Error(err)
172 }
173 }
174
175 func TestPinRemoved(t *testing.T) {
176 rawE := []byte(`
177 {
178 "type": "pin_removed",
179 "user": "U061F7AUR",
180 "item": {
181 "type": "message",
182 "channel":"C0LAN2Q65",
183 "message":{
184 "type":"message",
185 "user":"U061F7AUR",
186 "text": "<@U0LAN0Z89> is it everything a river should be?",
187 "ts":"1539904112.000100",
188 "pinned_to":["C0LAN2Q65"],
189 "replace_original":false,
190 "delete_original":false
191 }
192 },
193 "channel_id":"C0LAN2Q65",
194 "event_ts": "1515449522000016"
195 }
196 `)
197 err := json.Unmarshal(rawE, &PinRemovedEvent{})
198 if err != nil {
199 t.Error(err)
200 }
201 }
202
203 func TestTokensRevoked(t *testing.T) {
204 rawE := []byte(`
205 {
206 "type": "tokens_revoked",
207 "tokens": {
208 "oauth": [
209 "OUXXXXXXXX"
210 ],
211 "bot": [
212 "BUXXXXXXXX"
213 ]
214 }
215 }
216 `)
217 tre := TokensRevokedEvent{}
218 err := json.Unmarshal(rawE, &tre)
219 if err != nil {
220 t.Error(err)
221 }
222
223 if tre.Type != "tokens_revoked" {
224 t.Fail()
225 }
226
227 if len(tre.Tokens.Bot) != 1 || tre.Tokens.Bot[0] != "BUXXXXXXXX" {
228 t.Fail()
229 }
230
231 if len(tre.Tokens.Oauth) != 1 || tre.Tokens.Oauth[0] != "OUXXXXXXXX" {
232 t.Fail()
233 }
234 }
0 // outer_events.go provides EventsAPI particular outer events
1
2 package slackevents
3
4 import (
5 "encoding/json"
6 )
7
8 // EventsAPIEvent is the base EventsAPIEvent
9 type EventsAPIEvent struct {
10 Token string `json:"token"`
11 TeamID string `json:"team_id"`
12 Type string `json:"type"`
13 Data interface{}
14 InnerEvent EventsAPIInnerEvent
15 }
16
17 // EventsAPIURLVerificationEvent received when configuring a EventsAPI driven app
18 type EventsAPIURLVerificationEvent struct {
19 Token string `json:"token"`
20 Challenge string `json:"challenge"`
21 Type string `json:"type"`
22 }
23
24 // ChallengeResponse is a response to a EventsAPIEvent URLVerification challenge
25 type ChallengeResponse struct {
26 Challenge string
27 }
28
29 // EventsAPICallbackEvent is the main (outer) EventsAPI event.
30 type EventsAPICallbackEvent struct {
31 Type string `json:"type"`
32 Token string `json:"token"`
33 TeamID string `json:"team_id"`
34 APIAppID string `json:"api_app_id"`
35 InnerEvent *json.RawMessage `json:"event"`
36 AuthedUsers []string `json:"authed_users"`
37 AuthedTeams []string `json:"authed_teams"`
38 EventID string `json:"event_id"`
39 EventTime int `json:"event_time"`
40 }
41
42 // EventsAPIAppRateLimited indicates your app's event subscriptions are being rate limited
43 type EventsAPIAppRateLimited struct {
44 Type string `json:"type"`
45 Token string `json:"token"`
46 TeamID string `json:"team_id"`
47 MinuteRateLimited int `json:"minute_rate_limited"`
48 APIAppID string `json:"api_app_id"`
49 }
50
51 const (
52 // CallbackEvent is the "outer" event of an EventsAPI event.
53 CallbackEvent = "event_callback"
54 // URLVerification is an event used when configuring your EventsAPI app
55 URLVerification = "url_verification"
56 // AppRateLimited indicates your app's event subscriptions are being rate limited
57 AppRateLimited = "app_rate_limited"
58 )
59
60 // EventsAPIEventMap maps OUTTER Event API events to their corresponding struct
61 // implementations. The structs should be instances of the unmarshalling
62 // target for the matching event type.
63 var EventsAPIEventMap = map[string]interface{}{
64 CallbackEvent: EventsAPICallbackEvent{},
65 URLVerification: EventsAPIURLVerificationEvent{},
66 AppRateLimited: EventsAPIAppRateLimited{},
67 }
0 package slackevents
1
2 import (
3 "encoding/json"
4 "testing"
5 )
6
7 func TestURLVerificationEvent(t *testing.T) {
8 rawE := []byte(`
9 {
10 "token": "Jhj5dZrVaK7ZwHHjRyZWjbDl",
11 "challenge": "3eZbrw1aBm2rZgRNFdxV2595E9CY3gmdALWMmHkvFXO7tYXAYM8P",
12 "type": "url_verification"
13 }
14 `)
15 err := json.Unmarshal(rawE, &EventsAPIURLVerificationEvent{})
16 if err != nil {
17 t.Error(err)
18 }
19 }
20
21 func TestCallBackEvent(t *testing.T) {
22 rawE := []byte(`
23 {
24 "token": "XXYYZZ",
25 "team_id": "TXXXXXXXX",
26 "api_app_id": "AXXXXXXXXX",
27 "event": {
28 "type": "app_mention",
29 "event_ts": "1234567890.123456",
30 "user": "UXXXXXXX1"
31 },
32 "type": "event_callback",
33 "authed_users": [ "UXXXXXXX1" ],
34 "event_id": "Ev08MFMKH6",
35 "event_time": 1234567890
36 }
37 `)
38 err := json.Unmarshal(rawE, &EventsAPICallbackEvent{})
39 if err != nil {
40 t.Error(err)
41 }
42 }
43
44 func TestAppRateLimitedEvent(t *testing.T) {
45 rawE := []byte(`
46 {
47 "token": "Jhj5dZrVaK7ZwHHjRyZWjbDl",
48 "type": "app_rate_limited",
49 "team_id": "T123456",
50 "minute_rate_limited": 1518467820,
51 "api_app_id": "A123456"
52 }
53 `)
54 err := json.Unmarshal(rawE, &EventsAPIAppRateLimited{})
55 if err != nil {
56 t.Error(err)
57 }
58 }
0 package slackevents
1
2 import (
3 "crypto/subtle"
4 "encoding/json"
5 "errors"
6 "fmt"
7 "reflect"
8
9 "github.com/nlopes/slack"
10 )
11
12 // eventsMap checks both slack.EventsMapping and
13 // and slackevents.EventsAPIInnerEventMapping. If the event
14 // exists, returns the the unmarshalled struct instance of
15 // target for the matching event type.
16 // TODO: Consider moving all events into its own package?
17 func eventsMap(t string) (interface{}, bool) {
18 // Must parse EventsAPI FIRST as both RTM and EventsAPI
19 // have a type: "Message" event.
20 // TODO: Handle these cases more explicitly.
21 v, exists := EventsAPIInnerEventMapping[t]
22 if exists {
23 return v, exists
24 }
25 v, exists = slack.EventMapping[t]
26 if exists {
27 return v, exists
28 }
29 return v, exists
30 }
31
32 func parseOuterEvent(rawE json.RawMessage) (EventsAPIEvent, error) {
33 e := &EventsAPIEvent{}
34 err := json.Unmarshal(rawE, e)
35 if err != nil {
36 return EventsAPIEvent{
37 "",
38 "",
39 "unmarshalling_error",
40 &slack.UnmarshallingErrorEvent{ErrorObj: err},
41 EventsAPIInnerEvent{},
42 }, err
43 }
44 if e.Type == CallbackEvent {
45 cbEvent := &EventsAPICallbackEvent{}
46 err = json.Unmarshal(rawE, cbEvent)
47 if err != nil {
48 return EventsAPIEvent{
49 "",
50 "",
51 "unmarshalling_error",
52 &slack.UnmarshallingErrorEvent{ErrorObj: err},
53 EventsAPIInnerEvent{},
54 }, err
55 }
56 return EventsAPIEvent{
57 e.Token,
58 e.TeamID,
59 e.Type,
60 cbEvent,
61 EventsAPIInnerEvent{},
62 }, nil
63 }
64 urlVE := &EventsAPIURLVerificationEvent{}
65 err = json.Unmarshal(rawE, urlVE)
66 if err != nil {
67 return EventsAPIEvent{
68 "",
69 "",
70 "unmarshalling_error",
71 &slack.UnmarshallingErrorEvent{ErrorObj: err},
72 EventsAPIInnerEvent{},
73 }, err
74 }
75 return EventsAPIEvent{
76 e.Token,
77 e.TeamID,
78 e.Type,
79 urlVE,
80 EventsAPIInnerEvent{},
81 }, nil
82 }
83
84 func parseInnerEvent(e *EventsAPICallbackEvent) (EventsAPIEvent, error) {
85 iE := &slack.Event{}
86 rawInnerJSON := e.InnerEvent
87 err := json.Unmarshal(*rawInnerJSON, iE)
88 if err != nil {
89 return EventsAPIEvent{
90 e.Token,
91 e.TeamID,
92 "unmarshalling_error",
93 &slack.UnmarshallingErrorEvent{ErrorObj: err},
94 EventsAPIInnerEvent{},
95 }, err
96 }
97 v, exists := eventsMap(iE.Type)
98 if !exists {
99 return EventsAPIEvent{
100 e.Token,
101 e.TeamID,
102 iE.Type,
103 nil,
104 EventsAPIInnerEvent{},
105 }, fmt.Errorf("Inner Event does not exist! %s", iE.Type)
106 }
107 t := reflect.TypeOf(v)
108 recvEvent := reflect.New(t).Interface()
109 err = json.Unmarshal(*rawInnerJSON, recvEvent)
110 if err != nil {
111 return EventsAPIEvent{
112 e.Token,
113 e.TeamID,
114 "unmarshalling_error",
115 &slack.UnmarshallingErrorEvent{ErrorObj: err},
116 EventsAPIInnerEvent{},
117 }, err
118 }
119 return EventsAPIEvent{
120 e.Token,
121 e.TeamID,
122 e.Type,
123 e,
124 EventsAPIInnerEvent{iE.Type, recvEvent},
125 }, nil
126 }
127
128 type Config struct {
129 VerificationToken string
130 TokenVerified bool
131 }
132
133 type Option func(cfg *Config)
134
135 type verifier interface {
136 Verify(token string) bool
137 }
138
139 func OptionVerifyToken(v verifier) Option {
140 return func(cfg *Config) {
141 cfg.TokenVerified = v.Verify(cfg.VerificationToken)
142 }
143 }
144
145 // OptionNoVerifyToken skips the check of the Slack verification token
146 func OptionNoVerifyToken() Option {
147 return func(cfg *Config) {
148 cfg.TokenVerified = true
149 }
150 }
151
152 type TokenComparator struct {
153 VerificationToken string
154 }
155
156 func (c TokenComparator) Verify(t string) bool {
157 return subtle.ConstantTimeCompare([]byte(c.VerificationToken), []byte(t)) == 1
158 }
159
160 // ParseEvent parses the outter and inner events (if applicable) of an events
161 // api event returning a EventsAPIEvent type. If the event is a url_verification event,
162 // the inner event is empty.
163 func ParseEvent(rawEvent json.RawMessage, opts ...Option) (EventsAPIEvent, error) {
164 e, err := parseOuterEvent(rawEvent)
165 if err != nil {
166 return EventsAPIEvent{}, err
167 }
168
169 cfg := &Config{}
170 cfg.VerificationToken = e.Token
171 for _, opt := range opts {
172 opt(cfg)
173 }
174
175 if !cfg.TokenVerified {
176 return EventsAPIEvent{}, errors.New("Invalid verification token")
177 }
178
179 if e.Type == CallbackEvent {
180 cbEvent := e.Data.(*EventsAPICallbackEvent)
181 innerEvent, err := parseInnerEvent(cbEvent)
182 if err != nil {
183 err := fmt.Errorf("EventsAPI Error parsing inner event: %s, %s", innerEvent.Type, err)
184 return EventsAPIEvent{
185 "",
186 "",
187 "unmarshalling_error",
188 &slack.UnmarshallingErrorEvent{ErrorObj: err},
189 EventsAPIInnerEvent{},
190 }, err
191 }
192 return innerEvent, nil
193 }
194 urlVerificationEvent := &EventsAPIURLVerificationEvent{}
195 err = json.Unmarshal(rawEvent, urlVerificationEvent)
196 if err != nil {
197 return EventsAPIEvent{
198 "",
199 "",
200 "unmarshalling_error",
201 &slack.UnmarshallingErrorEvent{ErrorObj: err},
202 EventsAPIInnerEvent{},
203 }, err
204 }
205 return EventsAPIEvent{
206 e.Token,
207 e.TeamID,
208 e.Type,
209 urlVerificationEvent,
210 EventsAPIInnerEvent{},
211 }, nil
212 }
213
214 func ParseActionEvent(payloadString string, opts ...Option) (MessageAction, error) {
215 byteString := []byte(payloadString)
216 action := MessageAction{}
217 err := json.Unmarshal(byteString, &action)
218 if err != nil {
219 return MessageAction{}, errors.New("MessageAction unmarshalling failed")
220 }
221
222 cfg := &Config{}
223 cfg.VerificationToken = action.Token
224 for _, opt := range opts {
225 opt(cfg)
226 }
227
228 if !cfg.TokenVerified {
229 return MessageAction{}, errors.New("invalid verification token")
230 } else {
231 return action, nil
232 }
233 }
0 package slackevents
1
2 import (
3 "encoding/json"
4 "fmt"
5 "testing"
6
7 "github.com/nlopes/slack"
8 )
9
10 func TestParserOuterCallBackEvent(t *testing.T) {
11 eventsAPIRawCallbackEvent := `
12 {
13 "token": "XXYYZZ",
14 "team_id": "TXXXXXXXX",
15 "api_app_id": "AXXXXXXXXX",
16 "event": {
17 "type": "app_mention",
18 "event_ts": "1234567890.123456",
19 "user": "UXXXXXXX1"
20 },
21 "type": "event_callback",
22 "authed_users": [ "UXXXXXXX1" ],
23 "event_id": "Ev08MFMKH6",
24 "event_time": 1234567890
25 }
26 `
27 msg, e := ParseEvent(json.RawMessage(eventsAPIRawCallbackEvent), OptionVerifyToken(&TokenComparator{"XXYYZZ"}))
28 if e != nil {
29 fmt.Println(e)
30 t.Fail()
31 }
32 switch ev := msg.Data.(type) {
33 case *EventsAPICallbackEvent:
34 {
35 }
36 case *slack.UnmarshallingErrorEvent:
37 {
38 fmt.Println("Unmarshalling Error!")
39 fmt.Println(ev)
40 t.Fail()
41 }
42 default:
43 {
44 fmt.Println(ev)
45 t.Fail()
46 }
47 }
48 }
49
50 func TestParseURLVerificationEvent(t *testing.T) {
51 urlVerificationEvent := `
52 {
53 "token": "fake-token",
54 "challenge": "aljdsflaji3jj",
55 "type": "url_verification"
56 }
57 `
58 msg, e := ParseEvent(json.RawMessage(urlVerificationEvent), OptionVerifyToken(&TokenComparator{"fake-token"}))
59 if e != nil {
60 fmt.Println(e)
61 t.Fail()
62 }
63 switch ev := msg.Data.(type) {
64 case *EventsAPIURLVerificationEvent:
65 {
66 }
67 default:
68 {
69 fmt.Println(ev)
70 t.Fail()
71 }
72 }
73 }
74
75 func TestThatOuterCallbackEventHasInnerEvent(t *testing.T) {
76 eventsAPIRawCallbackEvent := `
77 {
78 "token": "XXYYZZ",
79 "team_id": "TXXXXXXXX",
80 "api_app_id": "AXXXXXXXXX",
81 "event": {
82 "type": "app_mention",
83 "event_ts": "1234567890.123456",
84 "user": "UXXXXXXX1"
85 },
86 "type": "event_callback",
87 "authed_users": [ "UXXXXXXX1" ],
88 "event_id": "Ev08MFMKH6",
89 "event_time": 1234567890
90 }
91 `
92 msg, e := ParseEvent(json.RawMessage(eventsAPIRawCallbackEvent), OptionVerifyToken(&TokenComparator{"XXYYZZ"}))
93 if e != nil {
94 fmt.Println(e)
95 t.Fail()
96 }
97 switch outterEvent := msg.Data.(type) {
98 case *EventsAPICallbackEvent:
99 {
100 switch innerEvent := msg.InnerEvent.Data.(type) {
101 case *AppMentionEvent:
102 {
103 }
104 default:
105 fmt.Println(innerEvent)
106 t.Fail()
107 }
108 }
109 default:
110 {
111 fmt.Println(outterEvent)
112 t.Fail()
113 }
114 }
115 }
116
117 func TestBadTokenVerification(t *testing.T) {
118 urlVerificationEvent := `
119 {
120 "token": "fake-token",
121 "challenge": "aljdsflaji3jj",
122 "type": "url_verification"
123 }
124 `
125 _, e := ParseEvent(json.RawMessage(urlVerificationEvent), OptionVerifyToken(TokenComparator{"real-token"}))
126 if e == nil {
127 t.Fail()
128 }
129 }
130
131 func TestNoTokenVerification(t *testing.T) {
132 urlVerificationEvent := `
133 {
134 "token": "fake-token",
135 "challenge": "aljdsflaji3jj",
136 "type": "url_verification"
137 }
138 `
139 _, e := ParseEvent(json.RawMessage(urlVerificationEvent), OptionNoVerifyToken())
140 if e != nil {
141 fmt.Println(e)
142 t.Fail()
143 }
144 }
0 EXAMPLES := $(shell find examples/ -maxdepth 1 -type d -exec sh -c 'echo $(basename {})' \;)
1 EXLIST := $(subst examples/,,$(EXAMPLES))
2
3 ifeq ($(TRAVIS_BUILD_DIR),)
4 GOPATH := $(GOPATH)
5 else
6 GOPATH := $(GOPATH):$(TRAVIS_BUILD_DIR)
7 endif
8
9 all: chmod clean lint test coverage $(EXLIST)
10
11 # this chmod is a side effect of some windows-development related issues
12 chmod:
13 chmod +x script/*
14
15 lint:
16 @script/lint
17
18 test:
19 @script/test
20
21 coverage:
22 @script/coverage
23
24 $(EXLIST):
25 @echo $@
26 @go test -v ./examples/$@
27 @gocov test ./examples/$@ | gocov report
28
29 clean:
30 @rm -rf bin/ pkg/
31
32 .PHONY: all chmod clean lint test coverage $(EXLIST)
0 # slacktest
1
2 code in this package was shamelessly copied from https://github.com/lusis/slack-test.
3 since we were unable to get a response from the maintainer and its lack of license
4 have left us in an odd state. but wanted to move it here for maintenance purposes.
0 package slacktest
1
2 import (
3 "fmt"
4
5 slack "github.com/nlopes/slack"
6 )
7
8 const defaultBotName = "TestSlackBot"
9 const defaultBotID = "U023BECGF"
10 const defaultTeamID = "T024BE7LD"
11 const defaultNonBotUserID = "W012A3CDE"
12 const defaultNonBotUserName = "Egon Spengler"
13 const defaultTeamName = "SlackTest Team"
14 const defaultTeamDomain = "testdomain"
15
16 var defaultCreatedTs = nowAsJSONTime()
17
18 var defaultTeam = &slack.Team{
19 ID: defaultTeamID,
20 Name: defaultTeamName,
21 Domain: defaultTeamDomain,
22 }
23
24 var defaultBotInfo = &slack.UserDetails{
25 ID: defaultBotID,
26 Name: defaultBotName,
27 Created: defaultCreatedTs,
28 ManualPresence: "true",
29 Prefs: slack.UserPrefs{},
30 }
31
32 var okWebResponse = slack.SlackResponse{
33 Ok: true,
34 }
35
36 var defaultChannelsListJSON = fmt.Sprintf(`
37 {
38 "ok": true,
39 "channels": [%s, %s]
40 }
41 `, defaultGeneralChannelJSON, defaultExtraChannelJSON)
42
43 var defaultGroupsListJSON = fmt.Sprintf(`
44 {
45 "ok": true,
46 "groups": [%s]
47 }
48 `, defaultGroupJSON)
49
50 var defaultAuthTestJSON = fmt.Sprintf(`
51 {
52 "ok": true,
53 "url": "https://localhost.localdomain/",
54 "team": "%s",
55 "user": "%s",
56 "team_id": "%s",
57 "user_id": "%s"
58 }
59 `, defaultTeamName, defaultNonBotUserName, defaultTeamID, defaultNonBotUserID)
60
61 var defaultUsersInfoJSON = fmt.Sprintf(`
62 {
63 "ok":true,
64 %s
65 }
66 `, defaultNonBotUser)
67
68 var defaultGeneralChannelJSON = fmt.Sprintf(`
69 {
70 "id": "C024BE91L",
71 "name": "general",
72 "is_channel": true,
73 "created": %d,
74 "creator": "%s",
75 "is_archived": false,
76 "is_general": true,
77
78 "members": [
79 "W012A3CDE"
80 ],
81
82 "topic": {
83 "value": "Fun times",
84 "creator": "%s",
85 "last_set": %d
86 },
87 "purpose": {
88 "value": "This channel is for fun",
89 "creator": "%s",
90 "last_set": %d
91 },
92
93 "is_member": true
94 }
95 `, nowAsJSONTime(), defaultNonBotUserID, defaultNonBotUserID, nowAsJSONTime(), defaultNonBotUserID, nowAsJSONTime())
96
97 var defaultExtraChannelJSON = fmt.Sprintf(`
98 {
99 "id": "C024BE92L",
100 "name": "bot-playground",
101 "is_channel": true,
102 "created": %d,
103 "creator": "%s",
104 "is_archived": false,
105 "is_general": true,
106
107 "members": [
108 "W012A3CDE"
109 ],
110
111 "topic": {
112 "value": "Fun times",
113 "creator": "%s",
114 "last_set": %d
115 },
116 "purpose": {
117 "value": "This channel is for fun",
118 "creator": "%s",
119 "last_set": %d
120 },
121
122 "is_member": true
123 }
124 `, nowAsJSONTime(), defaultNonBotUserID, defaultNonBotUserID, nowAsJSONTime(), defaultNonBotUserID, nowAsJSONTime())
125
126 var defaultGroupJSON = fmt.Sprintf(`{
127 "id": "G024BE91L",
128 "name": "secretplans",
129 "is_group": true,
130 "created": %d,
131 "creator": "%s",
132 "is_archived": false,
133 "members": [
134 "W012A3CDE"
135 ],
136 "topic": {
137 "value": "Secret plans on hold",
138 "creator": "%s",
139 "last_set": %d
140 },
141 "purpose": {
142 "value": "Discuss secret plans that no-one else should know",
143 "creator": "%s",
144 "last_set": %d
145 }
146 }`, nowAsJSONTime(), defaultNonBotUserID, defaultNonBotUserID, nowAsJSONTime(), defaultNonBotUserID, nowAsJSONTime())
147
148 var defaultNonBotUser = fmt.Sprintf(`
149 "user": {
150 "id": "%s",
151 "team_id": "%s",
152 "name": "spengler",
153 "deleted": false,
154 "color": "9f69e7",
155 "real_name": "%s",
156 "tz": "America/Los_Angeles",
157 "tz_label": "Pacific Daylight Time",
158 "tz_offset": -25200,
159 "profile": {
160 "avatar_hash": "ge3b51ca72de",
161 "status_text": "Print is dead",
162 "status_emoji": ":books:",
163 "real_name": "%s",
164 "display_name": "spengler",
165 "real_name_normalized": "%s",
166 "display_name_normalized": "spengler",
167 "email": "spengler@ghostbusters.example.com",
168 "image_24": "https://localhost.localdomain/avatar/e3b51ca72dee4ef87916ae2b9240df50.jpg",
169 "image_32": "https://localhost.localdomain/avatar/e3b51ca72dee4ef87916ae2b9240df50.jpg",
170 "image_48": "https://localhost.localdomain/avatar/e3b51ca72dee4ef87916ae2b9240df50.jpg",
171 "image_72": "https://localhost.localdomain/avatar/e3b51ca72dee4ef87916ae2b9240df50.jpg",
172 "image_192": "https://localhost.localdomain/avatar/e3b51ca72dee4ef87916ae2b9240df50.jpg",
173 "image_512": "https://localhost.localdomain/avatar/e3b51ca72dee4ef87916ae2b9240df50.jpg",
174 "team": "%s"
175 },
176 "is_admin": true,
177 "is_owner": false,
178 "is_primary_owner": false,
179 "is_restricted": false,
180 "is_ultra_restricted": false,
181 "is_bot": false,
182 "is_stranger": false,
183 "updated": 1502138686,
184 "is_app_user": false,
185 "has_2fa": false,
186 "locale": "en-US"
187 }
188 `, defaultNonBotUserID, defaultTeamID, defaultNonBotUserName, defaultNonBotUserName, defaultNonBotUserName, defaultTeamID)
0 package slacktest
1
2 import (
3 "github.com/nlopes/slack/internal/errorsx"
4 )
5
6 const (
7 // ErrEmptyServerToHub is the error when attempting an empty server address to the hub
8 ErrEmptyServerToHub = errorsx.String("Unable to add an empty server address to hub")
9 // ErrPassedEmptyServerAddr is the error when being passed an empty server address
10 ErrPassedEmptyServerAddr = errorsx.String("Passed an empty server address")
11 // ErrNoQueuesRegisteredForServer is the error when there are no queues for a server in the hub
12 ErrNoQueuesRegisteredForServer = errorsx.String("No queues registered for server")
13 )
0 package slacktest
1
2 import (
3 "context"
4 "fmt"
5 "log"
6 "time"
7
8 websocket "github.com/gorilla/websocket"
9 slack "github.com/nlopes/slack"
10 )
11
12 func (sts *Server) queueForWebsocket(s, hubname string) {
13 channel, err := getHubForServer(hubname)
14 if err != nil {
15 log.Printf("Unable to get server's channels: %s", err.Error())
16 }
17 sts.seenOutboundMessages.Lock()
18 sts.seenOutboundMessages.messages = append(sts.seenOutboundMessages.messages, s)
19 sts.seenOutboundMessages.Unlock()
20 channel.sent <- s
21 }
22
23 func handlePendingMessages(c *websocket.Conn, hubname string) {
24 channel, err := getHubForServer(hubname)
25 if err != nil {
26 log.Printf("Unable to get server's channels: %s", err.Error())
27 return
28 }
29 for m := range channel.sent {
30 err := c.WriteMessage(websocket.TextMessage, []byte(m))
31 if err != nil {
32 log.Printf("error writing message to websocket: %s", err.Error())
33 continue
34 }
35 }
36 }
37
38 func (sts *Server) postProcessMessage(m, hubname string) {
39 channel, err := getHubForServer(hubname)
40 if err != nil {
41 log.Printf("Unable to get server's channels: %s", err.Error())
42 return
43 }
44 sts.seenInboundMessages.Lock()
45 sts.seenInboundMessages.messages = append(sts.seenInboundMessages.messages, m)
46 sts.seenInboundMessages.Unlock()
47 // send to firehose
48 channel.seen <- m
49 }
50
51 func newHub() *hub {
52 h := &hub{}
53 c := make(map[string]*messageChannels)
54 h.serverChannels = c
55 return h
56 }
57
58 func addServerToHub(s *Server, channels *messageChannels) error {
59 if s.ServerAddr == "" {
60 return ErrEmptyServerToHub
61 }
62 masterHub.Lock()
63 masterHub.serverChannels[s.ServerAddr] = channels
64 masterHub.Unlock()
65 return nil
66 }
67
68 func getHubForServer(serverAddr string) (*messageChannels, error) {
69 if serverAddr == "" {
70 return &messageChannels{}, ErrPassedEmptyServerAddr
71 }
72 masterHub.RLock()
73 defer masterHub.RUnlock()
74 channels, ok := masterHub.serverChannels[serverAddr]
75 if !ok {
76 return &messageChannels{}, ErrNoQueuesRegisteredForServer
77 }
78 return channels, nil
79 }
80
81 // BotNameFromContext returns the botname from a provided context
82 func BotNameFromContext(ctx context.Context) string {
83 botname, ok := ctx.Value(ServerBotNameContextKey).(string)
84 if !ok {
85 return defaultBotName
86 }
87 return botname
88 }
89
90 // BotIDFromContext returns the bot userid from a provided context
91 func BotIDFromContext(ctx context.Context) string {
92 botname, ok := ctx.Value(ServerBotIDContextKey).(string)
93 if !ok {
94 return defaultBotID
95 }
96 return botname
97 }
98
99 // generate a full rtminfo response for initial rtm connections
100 func generateRTMInfo(ctx context.Context, wsurl string) *fullInfoSlackResponse {
101 rtmInfo := slack.Info{
102 URL: wsurl,
103 Team: defaultTeam,
104 User: defaultBotInfo,
105 }
106 rtmInfo.User.ID = BotIDFromContext(ctx)
107 rtmInfo.User.Name = BotNameFromContext(ctx)
108 return &fullInfoSlackResponse{
109 rtmInfo,
110 okWebResponse,
111 }
112 }
113
114 func nowAsJSONTime() slack.JSONTime {
115 return slack.JSONTime(time.Now().Unix())
116 }
117
118 func defaultBotInfoJSON(ctx context.Context) string {
119 botid := BotIDFromContext(ctx)
120 botname := BotNameFromContext(ctx)
121 return fmt.Sprintf(`
122 {
123 "ok":true,
124 "bot":{
125 "id": "%s",
126 "app_id": "A4H1JB4AZ",
127 "deleted": false,
128 "name": "%s",
129 "icons": {
130 "image_36": "https://localhost.localdomain/img36.png",
131 "image_48": "https://localhost.localdomain/img48.png",
132 "image_72": "https://localhost.localdomain/img72.png"
133 }
134 }
135 }
136 `, botid, botname)
137 }
0 package slacktest
1
2 import (
3 "context"
4 "testing"
5
6 "github.com/stretchr/testify/assert"
7 )
8
9 func TestGenerateDefaultRTMInfo(t *testing.T) {
10 wsurl := "ws://127.0.0.1:5555/ws"
11 ctx := context.TODO()
12 info := generateRTMInfo(ctx, wsurl)
13 assert.Equal(t, wsurl, info.URL)
14 assert.True(t, info.Ok)
15 assert.Equal(t, defaultBotID, info.User.ID)
16 assert.Equal(t, defaultBotName, info.User.Name)
17 assert.Equal(t, defaultTeamID, info.Team.ID)
18 assert.Equal(t, defaultTeamName, info.Team.Name)
19 assert.Equal(t, defaultTeamDomain, info.Team.Domain)
20 }
21
22 func TestCustomDefaultRTMInfo(t *testing.T) {
23 wsurl := "ws://127.0.0.1:5555/ws"
24 ctx := context.TODO()
25 ctx = context.WithValue(ctx, ServerBotIDContextKey, "U1234567890")
26 ctx = context.WithValue(ctx, ServerBotNameContextKey, "SomeTestBotThing")
27 info := generateRTMInfo(ctx, wsurl)
28 assert.Equal(t, wsurl, info.URL)
29 assert.True(t, info.Ok)
30 assert.Equal(t, "U1234567890", info.User.ID)
31 assert.Equal(t, "SomeTestBotThing", info.User.Name)
32 assert.Equal(t, defaultTeamID, info.Team.ID)
33 assert.Equal(t, defaultTeamName, info.Team.Name)
34 assert.Equal(t, defaultTeamDomain, info.Team.Domain)
35 }
36
37 func TestGetHubMissingServerAddr(t *testing.T) {
38 mc, err := getHubForServer("")
39 assert.Nil(t, mc.seen, "seen should be nil")
40 assert.Nil(t, mc.sent, "sent should be nil")
41 assert.Nil(t, mc.posted, "posted should be nil")
42 assert.Error(t, err, "should return an error")
43 assert.EqualError(t, err, ErrPassedEmptyServerAddr.Error())
44 }
45
46 func TestGetHubNoQueuesForServer(t *testing.T) {
47 mc, err := getHubForServer("foo")
48 assert.Nil(t, mc.seen, "seen should be nil")
49 assert.Nil(t, mc.sent, "sent should be nil")
50 assert.Nil(t, mc.posted, "posted should be nil")
51 assert.Error(t, err, "should return an error")
52 assert.EqualError(t, err, ErrNoQueuesRegisteredForServer.Error())
53 }
54
55 func TestUnableToAddToHub(t *testing.T) {
56 err := addServerToHub(&Server{}, &messageChannels{})
57 assert.Error(t, err, "should return and error")
58 assert.EqualError(t, err, ErrEmptyServerToHub.Error())
59 }
0 package slacktest
1
2 import (
3 "context"
4 "encoding/json"
5 "fmt"
6 "io/ioutil"
7 "log"
8 "net/http"
9 "net/url"
10 "time"
11
12 websocket "github.com/gorilla/websocket"
13 slack "github.com/nlopes/slack"
14 )
15
16 func contextHandler(server *Server, next http.HandlerFunc) http.Handler {
17 return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
18 ctx := context.WithValue(r.Context(), ServerURLContextKey, server.GetAPIURL())
19 ctx = context.WithValue(ctx, ServerWSContextKey, server.GetWSURL())
20 ctx = context.WithValue(ctx, ServerBotNameContextKey, server.BotName)
21 ctx = context.WithValue(ctx, ServerBotChannelsContextKey, server.GetChannels())
22 ctx = context.WithValue(ctx, ServerBotGroupsContextKey, server.GetGroups())
23 ctx = context.WithValue(ctx, ServerBotHubNameContextKey, server.ServerAddr)
24 next.ServeHTTP(w, r.WithContext(ctx))
25 })
26 }
27
28 // handle auth.test
29 func authTestHandler(w http.ResponseWriter, _ *http.Request) {
30 _, _ = w.Write([]byte(defaultAuthTestJSON))
31 }
32
33 func usersInfoHandler(w http.ResponseWriter, _ *http.Request) {
34 _, _ = w.Write([]byte(defaultUsersInfoJSON))
35 }
36
37 func botsInfoHandler(w http.ResponseWriter, r *http.Request) {
38 _, _ = w.Write([]byte(defaultBotInfoJSON(r.Context())))
39 }
40
41 // handle channels.list
42 func listChannelsHandler(w http.ResponseWriter, _ *http.Request) {
43 _, _ = w.Write([]byte(defaultChannelsListJSON))
44 }
45
46 // handle groups.list
47 func listGroupsHandler(w http.ResponseWriter, _ *http.Request) {
48 _, _ = w.Write([]byte(defaultGroupsListJSON))
49 }
50
51 // handle chat.postMessage
52 func (sts *Server) postMessageHandler(w http.ResponseWriter, r *http.Request) {
53 serverAddr := r.Context().Value(ServerBotHubNameContextKey).(string)
54 data, err := ioutil.ReadAll(r.Body)
55 if err != nil {
56 msg := fmt.Sprintf("error reading body: %s", err.Error())
57 log.Printf(msg)
58 http.Error(w, msg, http.StatusInternalServerError)
59 return
60 }
61 values, vErr := url.ParseQuery(string(data))
62 if vErr != nil {
63 msg := fmt.Sprintf("Unable to decode query params: %s", vErr.Error())
64 log.Printf(msg)
65 http.Error(w, msg, http.StatusInternalServerError)
66 return
67 }
68
69 ts := time.Now().Unix()
70 resp := fmt.Sprintf(`{"channel":"%s","ts":"%d", "text":"%s", "ok": true}`, values.Get("channel"), ts, values.Get("text"))
71 m := slack.Message{}
72 m.Type = "message"
73 m.Channel = values.Get("channel")
74 m.Timestamp = fmt.Sprintf("%d", ts)
75 m.Text = values.Get("text")
76 if values.Get("as_user") != "true" {
77 m.User = defaultNonBotUserID
78 m.Username = defaultNonBotUserName
79 } else {
80 m.User = BotIDFromContext(r.Context())
81 m.Username = BotNameFromContext(r.Context())
82 }
83 attachments := values.Get("attachments")
84 if attachments != "" {
85 decoded, err := url.QueryUnescape(attachments)
86 if err != nil {
87 msg := fmt.Sprintf("Unable to decode attachments: %s", err.Error())
88 log.Printf(msg)
89 http.Error(w, msg, http.StatusInternalServerError)
90 return
91 }
92 var attaches []slack.Attachment
93 aJErr := json.Unmarshal([]byte(decoded), &attaches)
94 if aJErr != nil {
95 msg := fmt.Sprintf("Unable to decode attachments string to json: %s", aJErr.Error())
96 log.Printf(msg)
97 http.Error(w, msg, http.StatusInternalServerError)
98 return
99 }
100 m.Attachments = attaches
101 }
102 jsonMessage, jsonErr := json.Marshal(m)
103 if jsonErr != nil {
104 msg := fmt.Sprintf("Unable to marshal message: %s", jsonErr.Error())
105 log.Printf(msg)
106 http.Error(w, msg, http.StatusInternalServerError)
107 return
108 }
109 go sts.queueForWebsocket(string(jsonMessage), serverAddr)
110 _, _ = w.Write([]byte(resp))
111 }
112
113 func rtmConnectHandler(w http.ResponseWriter, r *http.Request) {
114 _, err := ioutil.ReadAll(r.Body)
115 if err != nil {
116 msg := fmt.Sprintf("Error reading body: %s", err.Error())
117 log.Printf(msg)
118 http.Error(w, msg, http.StatusInternalServerError)
119 return
120 }
121 wsurl := r.Context().Value(ServerWSContextKey).(string)
122 if wsurl == "" {
123 msg := "missing webservice url from context"
124 log.Printf(msg)
125 http.Error(w, msg, http.StatusInternalServerError)
126 return
127 }
128
129 fullresponse := generateRTMInfo(r.Context(), wsurl)
130 j, jErr := json.Marshal(fullresponse)
131 if jErr != nil {
132 msg := fmt.Sprintf("Unable to marshal response: %s", jErr.Error())
133 log.Printf("Error: %s", msg)
134 http.Error(w, msg, http.StatusInternalServerError)
135 return
136 }
137 _, wErr := w.Write(j)
138 if wErr != nil {
139 log.Printf("Error writing response: %s", wErr.Error())
140 }
141 }
142
143 func rtmStartHandler(w http.ResponseWriter, r *http.Request) {
144 _, err := ioutil.ReadAll(r.Body)
145 if err != nil {
146 msg := fmt.Sprintf("Error reading body: %s", err.Error())
147 log.Printf(msg)
148 http.Error(w, msg, http.StatusInternalServerError)
149 return
150 }
151 wsurl := r.Context().Value(ServerWSContextKey).(string)
152 if wsurl == "" {
153 msg := "missing webservice url from context"
154 log.Printf(msg)
155 http.Error(w, msg, http.StatusInternalServerError)
156 return
157 }
158
159 fullresponse := generateRTMInfo(r.Context(), wsurl)
160 j, jErr := json.Marshal(fullresponse)
161 if jErr != nil {
162 msg := fmt.Sprintf("Unable to marshal response: %s", jErr.Error())
163 log.Printf("Error: %s", msg)
164 http.Error(w, msg, http.StatusInternalServerError)
165 return
166 }
167 _, wErr := w.Write(j)
168 if wErr != nil {
169 log.Printf("Error writing response: %s", wErr.Error())
170 }
171 }
172
173 func (sts *Server) wsHandler(w http.ResponseWriter, r *http.Request) {
174 upgrader := websocket.Upgrader{
175 ReadBufferSize: 1024,
176 WriteBufferSize: 1024,
177 CheckOrigin: func(r *http.Request) bool {
178 return true
179 },
180 }
181 c, err := upgrader.Upgrade(w, r, nil)
182 if err != nil {
183 msg := fmt.Sprintf("Unable to upgrade to ws connection: %s", err.Error())
184 log.Print(msg)
185 http.Error(w, msg, http.StatusInternalServerError)
186 return
187 }
188 defer func() { _ = c.Close() }()
189 serverAddr := r.Context().Value(ServerBotHubNameContextKey).(string)
190 go handlePendingMessages(c, serverAddr)
191 for {
192 mt, messageBytes, err := c.ReadMessage()
193 if err != nil {
194 log.Printf("read error: %s", err.Error())
195 continue
196 }
197 message := string(messageBytes)
198 evt := &slack.Event{}
199 if err := json.Unmarshal(messageBytes, evt); err != nil {
200 log.Printf("Error unmarshalling message: %s", err.Error())
201 log.Printf("failed message: %s", string(message))
202 continue
203 }
204 if evt.Type == "ping" {
205 p := &slack.Ping{}
206 jErr := json.Unmarshal(messageBytes, p)
207 if jErr != nil {
208 log.Printf("Unable to decode ping event: %s", jErr.Error())
209 continue
210 }
211 //log.Print("responding to slack ping")
212 pong := &slack.Pong{
213 ReplyTo: p.ID,
214 Type: "pong",
215 }
216 j, _ := json.Marshal(pong)
217 wErr := c.WriteMessage(mt, j)
218 if wErr != nil {
219 log.Printf("error writing pong back to socket: %s", wErr.Error())
220 continue
221 }
222 continue
223 } else {
224 go sts.postProcessMessage(message, serverAddr)
225 }
226 }
227 }
0 package slacktest
1
2 import (
3 "testing"
4
5 slack "github.com/nlopes/slack"
6 "github.com/stretchr/testify/assert"
7 )
8
9 func TestAuthTestHandler(t *testing.T) {
10 s := NewTestServer()
11 go s.Start()
12
13 client := slack.New("ABCDEFG", slack.OptionAPIURL(s.GetAPIURL()))
14 user, err := client.AuthTest()
15 assert.NoError(t, err, "should not error out")
16 assert.Equal(t, defaultTeamName, user.Team, "user ID should be correct")
17 assert.Equal(t, defaultTeamID, user.TeamID, "user ID should be correct")
18 assert.Equal(t, defaultNonBotUserID, user.UserID, "user ID should be correct")
19 assert.Equal(t, defaultNonBotUserName, user.User, "user ID should be correct")
20 }
21
22 func TestPostMessageHandler(t *testing.T) {
23 s := NewTestServer()
24 go s.Start()
25
26 client := slack.New("ABCDEFG", slack.OptionAPIURL(s.GetAPIURL()))
27 channel, tstamp, err := client.PostMessage("foo", slack.MsgOptionText("some text", false), slack.MsgOptionPostMessageParameters(slack.PostMessageParameters{}))
28 assert.NoError(t, err, "should not error out")
29 assert.Equal(t, "foo", channel, "channel should be correct")
30 assert.NotEmpty(t, tstamp, "timestamp should not be empty")
31 }
32
33 func TestServerListChannels(t *testing.T) {
34 s := NewTestServer()
35 go s.Start()
36
37 client := slack.New("ABCDEFG", slack.OptionAPIURL(s.GetAPIURL()))
38 channels, err := client.GetChannels(true)
39 assert.NoError(t, err)
40 assert.Len(t, channels, 2)
41 assert.Equal(t, "C024BE91L", channels[0].ID)
42 assert.Equal(t, "C024BE92L", channels[1].ID)
43 for _, channel := range channels {
44 assert.Equal(t, "W012A3CDE", channel.Creator)
45 }
46 }
47
48 func TestUserInfoHandler(t *testing.T) {
49 s := NewTestServer()
50 go s.Start()
51
52 client := slack.New("ABCDEFG", slack.OptionAPIURL(s.GetAPIURL()))
53 user, err := client.GetUserInfo("123456")
54 assert.NoError(t, err)
55 assert.Equal(t, "W012A3CDE", user.ID)
56 assert.Equal(t, "spengler", user.Name)
57 assert.True(t, user.IsAdmin)
58 }
59
60 func TestBotInfoHandler(t *testing.T) {
61 s := NewTestServer()
62 go s.Start()
63
64 client := slack.New("ABCDEFG", slack.OptionAPIURL(s.GetAPIURL()))
65 bot, err := client.GetBotInfo(s.BotID)
66 assert.NoError(t, err)
67 assert.Equal(t, s.BotID, bot.ID)
68 assert.Equal(t, s.BotName, bot.Name)
69 assert.False(t, bot.Deleted)
70 }
71
72 func TestListGroupsHandler(t *testing.T) {
73 s := NewTestServer()
74 go s.Start()
75
76 client := slack.New("ABCDEFG", slack.OptionAPIURL(s.GetAPIURL()))
77 groups, err := client.GetGroups(true)
78 assert.NoError(t, err)
79 if !assert.Len(t, groups, 1, "should have one group") {
80 t.FailNow()
81 }
82 mygroup := groups[0]
83 assert.Equal(t, "G024BE91L", mygroup.ID, "id should match")
84 assert.Equal(t, "secretplans", mygroup.Name, "name should match")
85 assert.True(t, mygroup.IsGroup, "should be a group")
86 }
87
88 func TestListChannelsHandler(t *testing.T) {
89 s := NewTestServer()
90 go s.Start()
91
92 client := slack.New("ABCDEFG", slack.OptionAPIURL(s.GetAPIURL()))
93 channels, err := client.GetChannels(true)
94 assert.NoError(t, err)
95 if !assert.Len(t, channels, 2, "should have two channels") {
96 t.FailNow()
97 }
98 generalChan := channels[0]
99 otherChan := channels[1]
100 assert.Equal(t, "C024BE91L", generalChan.ID, "id should match")
101 assert.Equal(t, "general", generalChan.Name, "name should match")
102 assert.Equal(t, "Fun times", generalChan.Topic.Value)
103 assert.True(t, generalChan.IsMember, "should be in channel")
104 assert.Equal(t, "C024BE92L", otherChan.ID, "id should match")
105 assert.Equal(t, "bot-playground", otherChan.Name, "name should match")
106 assert.Equal(t, "Fun times", otherChan.Topic.Value)
107 assert.True(t, otherChan.IsMember, "should be in channel")
108 }
0 package slacktest
1
2 import (
3 "testing"
4 "time"
5
6 "github.com/nlopes/slack"
7 "github.com/stretchr/testify/assert"
8 )
9
10 func TestRTMInfo(t *testing.T) {
11 maxWait := 10 * time.Millisecond
12 s := NewTestServer()
13 go s.Start()
14
15 api := slack.New("ABCDEFG", slack.OptionAPIURL(s.GetAPIURL()))
16 rtm := api.NewRTM()
17 go rtm.ManageConnection()
18 messageChan := make(chan (*slack.ConnectedEvent), 1)
19 go func() {
20 for msg := range rtm.IncomingEvents {
21 switch ev := msg.Data.(type) {
22 case *slack.ConnectedEvent:
23 messageChan <- ev
24 }
25 }
26 }()
27 select {
28 case m := <-messageChan:
29 assert.Equal(t, s.BotID, m.Info.User.ID, "bot id did not match")
30 assert.Equal(t, s.BotName, m.Info.User.Name, "bot name did not match")
31 break
32 case <-time.After(maxWait):
33 assert.FailNow(t, "did not get connected event in time")
34
35 }
36 }
37
38 func TestRTMPing(t *testing.T) {
39 if testing.Short() {
40 t.Skip("skipping timered test")
41 }
42 maxWait := 45 * time.Second
43 s := NewTestServer()
44 go s.Start()
45
46 api := slack.New("ABCDEFG", slack.OptionAPIURL(s.GetAPIURL()))
47 rtm := api.NewRTM()
48 go rtm.ManageConnection()
49 messageChan := make(chan (*slack.LatencyReport), 1)
50 go func() {
51 for msg := range rtm.IncomingEvents {
52 switch ev := msg.Data.(type) {
53 case *slack.LatencyReport:
54 messageChan <- ev
55 }
56 }
57 }()
58 select {
59 case m := <-messageChan:
60 assert.NotEmpty(t, m.Value, "latency report should value a value")
61 assert.True(t, m.Value > 0, "latency report should be greater than 0")
62 break
63 case <-time.After(maxWait):
64 assert.FailNow(t, "did not get latency report in time")
65
66 }
67 }
68
69 func TestRTMDirectMessage(t *testing.T) {
70 maxWait := 5 * time.Second
71 s := NewTestServer()
72 go s.Start()
73
74 api := slack.New("ABCDEFG", slack.OptionAPIURL(s.GetAPIURL()))
75 rtm := api.NewRTM()
76 go rtm.ManageConnection()
77 messageChan := make(chan (*slack.MessageEvent), 1)
78 go func() {
79 for msg := range rtm.IncomingEvents {
80 switch ev := msg.Data.(type) {
81 case *slack.MessageEvent:
82 messageChan <- ev
83 }
84 }
85 }()
86 s.SendDirectMessageToBot("some text")
87 select {
88 case m := <-messageChan:
89 assert.Equal(t, defaultNonBotUserID, m.User)
90 assert.Equal(t, "D024BE91L", m.Channel)
91 assert.Equal(t, "some text", m.Text)
92 break
93 case <-time.After(maxWait):
94 assert.FailNow(t, "did not get direct message in time")
95 }
96 }
97
98 func TestRTMChannelMessage(t *testing.T) {
99 maxWait := 5 * time.Second
100 s := NewTestServer()
101 go s.Start()
102
103 api := slack.New("ABCDEFG", slack.OptionAPIURL(s.GetAPIURL()))
104 rtm := api.NewRTM()
105 go rtm.ManageConnection()
106 messageChan := make(chan (*slack.MessageEvent), 1)
107 go func() {
108 for msg := range rtm.IncomingEvents {
109 switch ev := msg.Data.(type) {
110 case *slack.MessageEvent:
111 messageChan <- ev
112 }
113 }
114 }()
115 s.SendMessageToChannel("#foochan", "some text")
116 select {
117 case m := <-messageChan:
118 assert.Equal(t, "#foochan", m.Channel)
119 assert.Equal(t, "some text", m.Text)
120 break
121 case <-time.After(maxWait):
122 assert.FailNow(t, "did not get channel message in time")
123 }
124
125 }
0 package slacktest
1
2 import (
3 "encoding/json"
4 "fmt"
5 "log"
6 "net/http"
7 "net/http/httptest"
8 "time"
9
10 "github.com/nlopes/slack"
11 )
12
13 func newMessageChannels() *messageChannels {
14 sent := make(chan (string))
15 seen := make(chan (string))
16 mc := messageChannels{
17 seen: seen,
18 sent: sent,
19 }
20 return &mc
21 }
22
23 // Customize the server's responses.
24 type Customize interface {
25 Handle(pattern string, handler http.HandlerFunc)
26 }
27
28 type binder func(Customize)
29
30 // NewTestServer returns a slacktest.Server ready to be started
31 func NewTestServer(custom ...binder) *Server {
32 serverChans := newMessageChannels()
33
34 channels := &serverChannels{}
35 groups := &serverGroups{}
36 s := &Server{
37 registered: map[string]struct{}{},
38 mux: http.NewServeMux(),
39 seenInboundMessages: &messageCollection{},
40 seenOutboundMessages: &messageCollection{},
41 }
42
43 for _, c := range custom {
44 c(s)
45 }
46
47 s.Handle("/ws", s.wsHandler)
48 s.Handle("/rtm.start", rtmStartHandler)
49 s.Handle("/rtm.connect", rtmConnectHandler)
50 s.Handle("/chat.postMessage", s.postMessageHandler)
51 s.Handle("/channels.list", listChannelsHandler)
52 s.Handle("/groups.list", listGroupsHandler)
53 s.Handle("/users.info", usersInfoHandler)
54 s.Handle("/bots.info", botsInfoHandler)
55 s.Handle("/auth.test", authTestHandler)
56
57 httpserver := httptest.NewUnstartedServer(s.mux)
58 addr := httpserver.Listener.Addr().String()
59
60 s.ServerAddr = addr
61 s.server = httpserver
62 s.BotName = defaultBotName
63 s.BotID = defaultBotID
64 s.SeenFeed = serverChans.seen
65 s.channels = channels
66 s.groups = groups
67
68 addErr := addServerToHub(s, serverChans)
69 if addErr != nil {
70 log.Printf("Unable to add server to hub: %s", addErr.Error())
71 }
72
73 return s
74 }
75
76 // Handle allow for customizing endpoints
77 func (sts *Server) Handle(pattern string, handler http.HandlerFunc) {
78 if _, found := sts.registered[pattern]; found {
79 log.Printf("route already registered: %s\n", pattern)
80 return
81 }
82
83 sts.registered[pattern] = struct{}{}
84 sts.mux.Handle(pattern, contextHandler(sts, handler))
85 }
86
87 // GetChannels returns all the fake channels registered
88 func (sts *Server) GetChannels() []slack.Channel {
89 sts.channels.RLock()
90 defer sts.channels.RUnlock()
91 return sts.channels.channels
92 }
93
94 // GetGroups returns all the fake groups registered
95 func (sts *Server) GetGroups() []slack.Group {
96 return sts.groups.channels
97 }
98
99 // GetSeenInboundMessages returns all messages seen via websocket excluding pings
100 func (sts *Server) GetSeenInboundMessages() []string {
101 sts.seenInboundMessages.RLock()
102 m := sts.seenInboundMessages.messages
103 sts.seenInboundMessages.RUnlock()
104 return m
105 }
106
107 // GetSeenOutboundMessages returns all messages seen via websocket excluding pings
108 func (sts *Server) GetSeenOutboundMessages() []string {
109 sts.seenOutboundMessages.RLock()
110 m := sts.seenOutboundMessages.messages
111 sts.seenOutboundMessages.RUnlock()
112 return m
113 }
114
115 // SawOutgoingMessage checks if a message was sent to connected websocket clients
116 func (sts *Server) SawOutgoingMessage(msg string) bool {
117 sts.seenOutboundMessages.RLock()
118 defer sts.seenOutboundMessages.RUnlock()
119 for _, m := range sts.seenOutboundMessages.messages {
120 evt := &slack.MessageEvent{}
121 jErr := json.Unmarshal([]byte(m), evt)
122 if jErr != nil {
123 continue
124 }
125
126 if evt.Text == msg {
127 return true
128 }
129 }
130 return false
131 }
132
133 // SawMessage checks if an incoming message was seen
134 func (sts *Server) SawMessage(msg string) bool {
135 sts.seenInboundMessages.RLock()
136 defer sts.seenInboundMessages.RUnlock()
137 for _, m := range sts.seenInboundMessages.messages {
138 evt := &slack.MessageEvent{}
139 jErr := json.Unmarshal([]byte(m), evt)
140 if jErr != nil {
141 // This event isn't a message event so we'll skip it
142 continue
143 }
144 if evt.Text == msg {
145 return true
146 }
147 }
148 return false
149 }
150
151 // GetAPIURL returns the api url you can pass to slack.SLACK_API
152 func (sts *Server) GetAPIURL() string {
153 return "http://" + sts.ServerAddr + "/"
154 }
155
156 // GetWSURL returns the websocket url
157 func (sts *Server) GetWSURL() string {
158 return "ws://" + sts.ServerAddr + "/ws"
159 }
160
161 // Stop stops the test server
162 func (sts *Server) Stop() {
163 sts.server.Close()
164 }
165
166 // Start starts the test server
167 func (sts *Server) Start() {
168 log.Print("starting server")
169 sts.server.Start()
170 }
171
172 // SendMessageToBot sends a message addressed to the Bot
173 func (sts *Server) SendMessageToBot(channel, msg string) {
174 m := slack.Message{}
175 m.Type = slack.TYPE_MESSAGE
176 m.Channel = channel
177 m.User = defaultNonBotUserID
178 m.Text = fmt.Sprintf("<@%s> %s", sts.BotID, msg)
179 m.Timestamp = fmt.Sprintf("%d", time.Now().Unix())
180 j, jErr := json.Marshal(m)
181 if jErr != nil {
182 log.Printf("Unable to marshal message for bot: %s", jErr.Error())
183 return
184 }
185 go sts.queueForWebsocket(string(j), sts.ServerAddr)
186 }
187
188 // SendDirectMessageToBot sends a direct message to the bot
189 func (sts *Server) SendDirectMessageToBot(msg string) {
190 m := slack.Message{}
191 m.Type = slack.TYPE_MESSAGE
192 m.Channel = "D024BE91L"
193 m.User = defaultNonBotUserID
194 m.Text = msg
195 m.Timestamp = fmt.Sprintf("%d", time.Now().Unix())
196 j, jErr := json.Marshal(m)
197 if jErr != nil {
198 log.Printf("Unable to marshal private message for bot: %s", jErr.Error())
199 return
200 }
201 go sts.queueForWebsocket(string(j), sts.ServerAddr)
202 }
203
204 // SendMessageToChannel sends a message to a channel
205 func (sts *Server) SendMessageToChannel(channel, msg string) {
206 m := slack.Message{}
207 m.Type = slack.TYPE_MESSAGE
208 m.Channel = channel
209 m.Text = msg
210 m.User = defaultNonBotUserID
211 m.Timestamp = fmt.Sprintf("%d", time.Now().Unix())
212 j, jErr := json.Marshal(m)
213 if jErr != nil {
214 log.Printf("Unable to marshal message for channel: %s", jErr.Error())
215 return
216 }
217 stringMsg := string(j)
218 go sts.queueForWebsocket(stringMsg, sts.ServerAddr)
219 }
220
221 // SendToWebsocket send `s` as is to connected clients.
222 // This is useful for sending your own custom json to the websocket
223 func (sts *Server) SendToWebsocket(s string) {
224 go sts.queueForWebsocket(s, sts.ServerAddr)
225 }
226
227 // SetBotName sets a custom botname
228 func (sts *Server) SetBotName(b string) {
229 sts.BotName = b
230 }
231
232 // SendBotChannelInvite invites the bot to a channel
233 func (sts *Server) SendBotChannelInvite() {
234 joinMsg := `
235 {
236 "type":"channel_joined",
237 "channel":
238 {
239 "id": "C024BE92L",
240 "name": "bot-playground",
241 "is_channel": true,
242 "created": 1360782804,
243 "creator": "W012A3CDE",
244 "is_archived": false,
245 "is_general": true,
246 "members": [
247 "W012A3CDE"
248 ],
249 "topic": {
250 "value": "Fun times",
251 "creator": "W012A3CDE",
252 "last_set": 1360782804
253 },
254 "purpose": {
255 "value": "This channel is for fun",
256 "creator": "W012A3CDE",
257 "last_set": 1360782804
258 },
259 "is_member": true
260 }
261 }`
262 sts.SendToWebsocket(joinMsg)
263 }
264
265 // SendBotGroupInvite invites the bot to a channel
266 func (sts *Server) SendBotGroupInvite() {
267 joinMsg := `
268 {
269 "type":"group_joined",
270 "channel":
271 {
272 "id": "G024BE91L",
273 "name": "secretplans",
274 "is_group": true,
275 "created": 1360782804,
276 "creator": "W012A3CDE",
277 "is_archived": false,
278 "members": [
279 "W012A3CDE"
280 ],
281 "topic": {
282 "value": "Secret plans on hold",
283 "creator": "W012A3CDE",
284 "last_set": 1360782804
285 },
286 "purpose": {
287 "value": "Discuss secret plans that no-one else should know",
288 "creator": "W012A3CDE",
289 "last_set": 1360782804
290 }
291 }
292 }`
293 sts.SendToWebsocket(joinMsg)
294 }
295
296 // GetTestRTMInstance will give you an RTM instance in the context of the current fake server
297 func (sts *Server) GetTestRTMInstance() *slack.RTM {
298 api := slack.New("ABCEFG", slack.OptionAPIURL(sts.GetAPIURL()))
299 rtm := api.NewRTM()
300 return rtm
301 }
0 package slacktest
1
2 import (
3 "encoding/json"
4 "fmt"
5 "testing"
6 "time"
7
8 "github.com/nlopes/slack"
9 "github.com/stretchr/testify/assert"
10 )
11
12 func TestDefaultNewServer(t *testing.T) {
13 s := NewTestServer()
14 assert.Equal(t, defaultBotID, s.BotID)
15 assert.Equal(t, defaultBotName, s.BotName)
16 assert.NotEmpty(t, s.ServerAddr)
17 s.Stop()
18 }
19
20 func TestCustomNewServer(t *testing.T) {
21 s := NewTestServer()
22 s.SetBotName("BobsBot")
23 assert.Equal(t, "BobsBot", s.BotName)
24 }
25
26 func TestServerSendMessageToChannel(t *testing.T) {
27 s := NewTestServer()
28 go s.Start()
29 s.SendMessageToChannel("C123456789", "some text")
30 time.Sleep(2 * time.Second)
31 assert.True(t, s.SawOutgoingMessage("some text"))
32 s.Stop()
33 }
34
35 func TestServerSendMessageToBot(t *testing.T) {
36 s := NewTestServer()
37 go s.Start()
38 s.SendMessageToBot("C123456789", "some text")
39 expectedMsg := fmt.Sprintf("<@%s> %s", s.BotID, "some text")
40 time.Sleep(2 * time.Second)
41 assert.True(t, s.SawOutgoingMessage(expectedMsg))
42 s.Stop()
43 }
44
45 func TestBotDirectMessageBotHandler(t *testing.T) {
46 s := NewTestServer()
47 go s.Start()
48 s.SendDirectMessageToBot("some text")
49 expectedMsg := fmt.Sprintf("some text")
50 time.Sleep(2)
51 assert.True(t, s.SawOutgoingMessage(expectedMsg))
52 s.Stop()
53 }
54
55 func TestGetSeenOutboundMessages(t *testing.T) {
56 maxWait := 5 * time.Second
57 s := NewTestServer()
58 go s.Start()
59
60 s.SendMessageToChannel("foo", "should see this message")
61 time.Sleep(maxWait)
62 seenOutbound := s.GetSeenOutboundMessages()
63 assert.True(t, len(seenOutbound) > 0)
64 hadMessage := false
65 for _, msg := range seenOutbound {
66 var m = slack.Message{}
67 jerr := json.Unmarshal([]byte(msg), &m)
68 assert.NoError(t, jerr, "messages should decode as slack.Message")
69 if m.Text == "should see this message" {
70 hadMessage = true
71 break
72 }
73 }
74 assert.True(t, hadMessage, "did not see my sent message")
75 }
76
77 func TestGetSeenInboundMessages(t *testing.T) {
78 maxWait := 5 * time.Second
79 s := NewTestServer()
80 go s.Start()
81
82 api := slack.New("ABCDEFG", slack.OptionAPIURL(s.GetAPIURL()))
83 rtm := api.NewRTM()
84 go rtm.ManageConnection()
85 rtm.SendMessage(&slack.OutgoingMessage{
86 Channel: "foo",
87 Text: "should see this inbound message",
88 })
89 time.Sleep(maxWait)
90 seenInbound := s.GetSeenInboundMessages()
91 assert.True(t, len(seenInbound) > 0)
92 hadMessage := false
93 for _, msg := range seenInbound {
94 var m = slack.Message{}
95 jerr := json.Unmarshal([]byte(msg), &m)
96 assert.NoError(t, jerr, "messages should decode as slack.Message")
97 if m.Text == "should see this inbound message" {
98 hadMessage = true
99 break
100 }
101 }
102 assert.True(t, hadMessage, "did not see my sent message")
103 assert.True(t, s.SawMessage("should see this inbound message"))
104 }
105
106 func TestSendChannelInvite(t *testing.T) {
107 maxWait := 5 * time.Second
108 s := NewTestServer()
109 go s.Start()
110 rtm := s.GetTestRTMInstance()
111 go rtm.ManageConnection()
112 evChan := make(chan (slack.Channel), 1)
113 go func() {
114 for msg := range rtm.IncomingEvents {
115 switch ev := msg.Data.(type) {
116 case *slack.ChannelJoinedEvent:
117 evChan <- ev.Channel
118 }
119 }
120 }()
121 s.SendBotChannelInvite()
122 time.Sleep(maxWait)
123 select {
124 case m := <-evChan:
125 assert.Equal(t, "C024BE92L", m.ID, "channel id should match")
126 assert.Equal(t, "Fun times", m.Topic.Value, "topic should match")
127 s.Stop()
128 break
129 case <-time.After(maxWait):
130 assert.FailNow(t, "did not get channel joined event in time")
131 }
132
133 }
134
135 func TestSendGroupInvite(t *testing.T) {
136 maxWait := 5 * time.Second
137 s := NewTestServer()
138 go s.Start()
139 rtm := s.GetTestRTMInstance()
140 go rtm.ManageConnection()
141 evChan := make(chan (slack.Channel), 1)
142 go func() {
143 for msg := range rtm.IncomingEvents {
144 switch ev := msg.Data.(type) {
145 case *slack.GroupJoinedEvent:
146 evChan <- ev.Channel
147 }
148 }
149 }()
150 s.SendBotGroupInvite()
151 time.Sleep(maxWait)
152 select {
153 case m := <-evChan:
154 assert.Equal(t, "G024BE91L", m.ID, "channel id should match")
155 assert.Equal(t, "Secret plans on hold", m.Topic.Value, "topic should match")
156 s.Stop()
157 break
158 case <-time.After(maxWait):
159 assert.FailNow(t, "did not get group joined event in time")
160 }
161
162 }
163
164 func TestServerSawMessage(t *testing.T) {
165 s := NewTestServer()
166 go s.Start()
167 assert.False(t, s.SawMessage("foo"), "should not have seen any message")
168 }
169
170 func TestServerSawOutgoingMessage(t *testing.T) {
171 s := NewTestServer()
172 go s.Start()
173 assert.False(t, s.SawOutgoingMessage("foo"), "should not have seen any message")
174 }
0 package slacktest
1
2 import (
3 "log"
4 "net/http"
5 "net/http/httptest"
6 "sync"
7
8 "github.com/nlopes/slack"
9 )
10
11 type contextKey string
12
13 // ServerURLContextKey is the context key to store the server's url
14 const ServerURLContextKey contextKey = "__SERVER_URL__"
15
16 // ServerWSContextKey is the context key to store the server's ws url
17 const ServerWSContextKey contextKey = "__SERVER_WS_URL__"
18
19 // ServerBotNameContextKey is the bot name
20 const ServerBotNameContextKey contextKey = "__SERVER_BOTNAME__"
21
22 // ServerBotIDContextKey is the bot userid
23 const ServerBotIDContextKey contextKey = "__SERVER_BOTID__"
24
25 // ServerBotChannelsContextKey is the list of channels associated with the fake server
26 const ServerBotChannelsContextKey contextKey = "__SERVER_CHANNELS__"
27
28 // ServerBotGroupsContextKey is the list of channels associated with the fake server
29 const ServerBotGroupsContextKey contextKey = "__SERVER_GROUPS__"
30
31 // ServerBotHubNameContextKey is the context key for passing along the server name registered in the hub
32 const ServerBotHubNameContextKey contextKey = "__SERVER_HUBNAME__"
33
34 var masterHub = newHub()
35
36 type hub struct {
37 sync.RWMutex
38 serverChannels map[string]*messageChannels
39 }
40
41 type messageChannels struct {
42 seen chan (string)
43 sent chan (string)
44 posted chan (slack.Message)
45 }
46 type messageCollection struct {
47 sync.RWMutex
48 messages []string
49 }
50
51 type serverChannels struct {
52 sync.RWMutex
53 channels []slack.Channel
54 }
55
56 type serverGroups struct {
57 sync.RWMutex
58 channels []slack.Group
59 }
60
61 // Server represents a Slack Test server
62 type Server struct {
63 registered map[string]struct{}
64 server *httptest.Server
65 mux *http.ServeMux
66 Logger *log.Logger
67 BotName string
68 BotID string
69 ServerAddr string
70 SeenFeed chan (string)
71 channels *serverChannels
72 groups *serverGroups
73 seenInboundMessages *messageCollection
74 seenOutboundMessages *messageCollection
75 }
76
77 type fullInfoSlackResponse struct {
78 slack.Info
79 slack.SlackResponse
80 }
0 // Package slackutilsx is a utility package that doesn't promise API stability.
1 // its for experimental functionality and utilities.
2 package slackutilsx
3
4 import (
5 "strings"
6 "unicode/utf8"
7 )
8
9 // ChannelType the type of channel based on the channelID
10 type ChannelType int
11
12 func (t ChannelType) String() string {
13 switch t {
14 case CTypeDM:
15 return "Direct"
16 case CTypeGroup:
17 return "Group"
18 case CTypeChannel:
19 return "Channel"
20 default:
21 return "Unknown"
22 }
23 }
24
25 const (
26 // CTypeUnknown represents channels we cannot properly detect.
27 CTypeUnknown ChannelType = iota
28 // CTypeDM is a private channel between two slack users.
29 CTypeDM
30 // CTypeGroup is a group channel.
31 CTypeGroup
32 // CTypeChannel is a public channel.
33 CTypeChannel
34 )
35
36 // DetectChannelType converts a channelID to a ChannelType.
37 // channelID must not be empty. However, if it is empty, the channel type will default to Unknown.
38 func DetectChannelType(channelID string) ChannelType {
39 // intentionally ignore the error and just default to CTypeUnknown
40 switch r, _ := utf8.DecodeRuneInString(channelID); r {
41 case 'C':
42 return CTypeChannel
43 case 'G':
44 return CTypeGroup
45 case 'D':
46 return CTypeDM
47 default:
48 return CTypeUnknown
49 }
50 }
51
52 // EscapeMessage text
53 func EscapeMessage(message string) string {
54 replacer := strings.NewReplacer("&", "&amp;", "<", "&lt;", ">", "&gt;")
55 return replacer.Replace(message)
56 }
57
58 // Retryable errors return true.
59 type Retryable interface {
60 Retryable() bool
61 }
0 package slackutilsx
1
2 import (
3 "testing"
4 )
5
6 func TestDetectChannelType(t *testing.T) {
7 test := func(channelID string, expected ChannelType) {
8 if computed := DetectChannelType(channelID); computed != expected {
9 t.Errorf("expected channelID %s to have type %s, got: %s", channelID, expected, computed)
10 }
11 }
12
13 test("G11111111", CTypeGroup)
14 test("D11111111", CTypeDM)
15 test("C11111111", CTypeChannel)
16 test("", CTypeUnknown)
17 test("X11111111", CTypeUnknown)
18 }
0 package slack
1
2 import (
3 "net/http"
4 )
5
6 // SlashCommand contains information about a request of the slash command
7 type SlashCommand struct {
8 Token string `json:"token"`
9 TeamID string `json:"team_id"`
10 TeamDomain string `json:"team_domain"`
11 EnterpriseID string `json:"enterprise_id,omitempty"`
12 EnterpriseName string `json:"enterprise_name,omitempty"`
13 ChannelID string `json:"channel_id"`
14 ChannelName string `json:"channel_name"`
15 UserID string `json:"user_id"`
16 UserName string `json:"user_name"`
17 Command string `json:"command"`
18 Text string `json:"text"`
19 ResponseURL string `json:"response_url"`
20 TriggerID string `json:"trigger_id"`
21 }
22
23 // SlashCommandParse will parse the request of the slash command
24 func SlashCommandParse(r *http.Request) (s SlashCommand, err error) {
25 if err = r.ParseForm(); err != nil {
26 return s, err
27 }
28 s.Token = r.PostForm.Get("token")
29 s.TeamID = r.PostForm.Get("team_id")
30 s.TeamDomain = r.PostForm.Get("team_domain")
31 s.EnterpriseID = r.PostForm.Get("enterprise_id")
32 s.EnterpriseName = r.PostForm.Get("enterprise_name")
33 s.ChannelID = r.PostForm.Get("channel_id")
34 s.ChannelName = r.PostForm.Get("channel_name")
35 s.UserID = r.PostForm.Get("user_id")
36 s.UserName = r.PostForm.Get("user_name")
37 s.Command = r.PostForm.Get("command")
38 s.Text = r.PostForm.Get("text")
39 s.ResponseURL = r.PostForm.Get("response_url")
40 s.TriggerID = r.PostForm.Get("trigger_id")
41 return s, nil
42 }
43
44 // ValidateToken validates verificationTokens
45 func (s SlashCommand) ValidateToken(verificationTokens ...string) bool {
46 for _, token := range verificationTokens {
47 if s.Token == token {
48 return true
49 }
50 }
51 return false
52 }
0 package slack
1
2 import (
3 "fmt"
4 "net/http"
5 "net/url"
6 "reflect"
7 "strings"
8 "testing"
9 )
10
11 func TestSlash_ServeHTTP(t *testing.T) {
12 once.Do(startServer)
13 serverURL := fmt.Sprintf("http://%s/slash", serverAddr)
14
15 tests := []struct {
16 body url.Values
17 wantParams SlashCommand
18 wantStatusCode int
19 }{
20 {
21 body: url.Values{
22 "command": []string{"/command"},
23 "team_domain": []string{"team"},
24 "enterprise_id": []string{"E0001"},
25 "enterprise_name": []string{"Globular%20Construct%20Inc"},
26 "channel_id": []string{"C1234ABCD"},
27 "text": []string{"text"},
28 "team_id": []string{"T1234ABCD"},
29 "user_id": []string{"U1234ABCD"},
30 "user_name": []string{"username"},
31 "response_url": []string{"https://hooks.slack.com/commands/XXXXXXXX/00000000000/YYYYYYYYYYYYYY"},
32 "token": []string{"valid"},
33 "channel_name": []string{"channel"},
34 "trigger_id": []string{"0000000000.1111111111.222222222222aaaaaaaaaaaaaa"},
35 },
36 wantParams: SlashCommand{
37 Command: "/command",
38 TeamDomain: "team",
39 EnterpriseID: "E0001",
40 EnterpriseName: "Globular%20Construct%20Inc",
41 ChannelID: "C1234ABCD",
42 Text: "text",
43 TeamID: "T1234ABCD",
44 UserID: "U1234ABCD",
45 UserName: "username",
46 ResponseURL: "https://hooks.slack.com/commands/XXXXXXXX/00000000000/YYYYYYYYYYYYYY",
47 Token: "valid",
48 ChannelName: "channel",
49 TriggerID: "0000000000.1111111111.222222222222aaaaaaaaaaaaaa",
50 },
51 wantStatusCode: http.StatusOK,
52 },
53 {
54 body: url.Values{
55 "token": []string{"invalid"},
56 },
57 wantParams: SlashCommand{
58 Token: "invalid",
59 },
60 wantStatusCode: http.StatusUnauthorized,
61 },
62 }
63
64 var slashCommand SlashCommand
65 client := &http.Client{}
66 http.HandleFunc("/slash", func(w http.ResponseWriter, r *http.Request) {
67 var err error
68 slashCommand, err = SlashCommandParse(r)
69 if err != nil {
70 w.WriteHeader(http.StatusInternalServerError)
71 }
72 acceptableTokens := []string{"valid", "valid2"}
73 if !slashCommand.ValidateToken(acceptableTokens...) {
74 w.WriteHeader(http.StatusUnauthorized)
75 }
76 })
77
78 for i, test := range tests {
79 req, err := http.NewRequest(http.MethodPost, serverURL, strings.NewReader(test.body.Encode()))
80 if err != nil {
81 t.Fatalf("%d: Unexpected error: %s", i, err)
82 }
83 req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
84
85 resp, err := client.Do(req)
86 if err != nil {
87 t.Fatalf("%d: Unexpected error: %s", i, err)
88 }
89
90 if resp.StatusCode != test.wantStatusCode {
91 t.Errorf("%d: Got status code %d, want %d", i, resp.StatusCode, test.wantStatusCode)
92 }
93 if !reflect.DeepEqual(slashCommand, test.wantParams) {
94 t.Errorf("%d: Got params %#v, want %#v", i, slashCommand, test.wantParams)
95 }
96 resp.Body.Close()
97 }
98 }
00 package slack
11
22 import (
3 "errors"
3 "context"
44 "net/url"
55 "strconv"
66 )
3636
3737 // AddStar stars an item in a channel
3838 func (api *Client) AddStar(channel string, item ItemRef) error {
39 return api.AddStarContext(context.Background(), channel, item)
40 }
41
42 // AddStarContext stars an item in a channel with a custom context
43 func (api *Client) AddStarContext(ctx context.Context, channel string, item ItemRef) error {
3944 values := url.Values{
4045 "channel": {channel},
41 "token": {api.config.token},
46 "token": {api.token},
4247 }
4348 if item.Timestamp != "" {
44 values.Set("timestamp", string(item.Timestamp))
49 values.Set("timestamp", item.Timestamp)
4550 }
4651 if item.File != "" {
47 values.Set("file", string(item.File))
52 values.Set("file", item.File)
4853 }
4954 if item.Comment != "" {
50 values.Set("file_comment", string(item.Comment))
55 values.Set("file_comment", item.Comment)
5156 }
57
5258 response := &SlackResponse{}
53 if err := post("stars.add", values, response, api.debug); err != nil {
59 if err := api.postMethod(ctx, "stars.add", values, response); err != nil {
5460 return err
5561 }
56 if !response.Ok {
57 return errors.New(response.Error)
58 }
59 return nil
62
63 return response.Err()
6064 }
6165
6266 // RemoveStar removes a starred item from a channel
6367 func (api *Client) RemoveStar(channel string, item ItemRef) error {
68 return api.RemoveStarContext(context.Background(), channel, item)
69 }
70
71 // RemoveStarContext removes a starred item from a channel with a custom context
72 func (api *Client) RemoveStarContext(ctx context.Context, channel string, item ItemRef) error {
6473 values := url.Values{
6574 "channel": {channel},
66 "token": {api.config.token},
75 "token": {api.token},
6776 }
6877 if item.Timestamp != "" {
69 values.Set("timestamp", string(item.Timestamp))
78 values.Set("timestamp", item.Timestamp)
7079 }
7180 if item.File != "" {
72 values.Set("file", string(item.File))
81 values.Set("file", item.File)
7382 }
7483 if item.Comment != "" {
75 values.Set("file_comment", string(item.Comment))
84 values.Set("file_comment", item.Comment)
7685 }
86
7787 response := &SlackResponse{}
78 if err := post("stars.remove", values, response, api.debug); err != nil {
88 if err := api.postMethod(ctx, "stars.remove", values, response); err != nil {
7989 return err
8090 }
81 if !response.Ok {
82 return errors.New(response.Error)
83 }
84 return nil
91
92 return response.Err()
8593 }
8694
8795 // ListStars returns information about the stars a user added
8896 func (api *Client) ListStars(params StarsParameters) ([]Item, *Paging, error) {
97 return api.ListStarsContext(context.Background(), params)
98 }
99
100 // ListStarsContext returns information about the stars a user added with a custom context
101 func (api *Client) ListStarsContext(ctx context.Context, params StarsParameters) ([]Item, *Paging, error) {
89102 values := url.Values{
90 "token": {api.config.token},
103 "token": {api.token},
91104 }
92105 if params.User != DEFAULT_STARS_USER {
93106 values.Add("user", params.User)
98111 if params.Page != DEFAULT_STARS_PAGE {
99112 values.Add("page", strconv.Itoa(params.Page))
100113 }
114
101115 response := &listResponseFull{}
102 err := post("stars.list", values, response, api.debug)
116 err := api.postMethod(ctx, "stars.list", values, response)
103117 if err != nil {
104118 return nil, nil, err
105119 }
106 if !response.Ok {
107 return nil, nil, errors.New(response.Error)
120
121 if err := response.Err(); err != nil {
122 return nil, nil, err
108123 }
124
109125 return response.Items, &response.Paging, nil
110126 }
111127
112 // GetStarred returns a list of StarredItem items. The user then has to iterate over them and figure out what they should
128 // GetStarred returns a list of StarredItem items.
129 //
130 // The user then has to iterate over them and figure out what they should
113131 // be looking at according to what is in the Type.
114132 // for _, item := range items {
115133 // switch c.Type {
122140 // This function still exists to maintain backwards compatibility.
123141 // I exposed it as returning []StarredItem, so it shall stay as StarredItem
124142 func (api *Client) GetStarred(params StarsParameters) ([]StarredItem, *Paging, error) {
125 items, paging, err := api.ListStars(params)
143 return api.GetStarredContext(context.Background(), params)
144 }
145
146 // GetStarredContext returns a list of StarredItem items with a custom context
147 //
148 // For more details see GetStarred
149 func (api *Client) GetStarredContext(ctx context.Context, params StarsParameters) ([]StarredItem, *Paging, error) {
150 items, paging, err := api.ListStarsContext(ctx, params)
126151 if err != nil {
127152 return nil, nil, err
128153 }
3838
3939 func TestSlack_AddStar(t *testing.T) {
4040 once.Do(startServer)
41 SLACK_API = "http://" + serverAddr + "/"
42 api := New("testing-token")
41 api := New("testing-token", OptionAPIURL("http://"+serverAddr+"/"))
4342 tests := []struct {
4443 channel string
4544 ref ItemRef
8685
8786 func TestSlack_RemoveStar(t *testing.T) {
8887 once.Do(startServer)
89 SLACK_API = "http://" + serverAddr + "/"
90 api := New("testing-token")
88 api := New("testing-token", OptionAPIURL("http://"+serverAddr+"/"))
9189 tests := []struct {
9290 channel string
9391 ref ItemRef
134132
135133 func TestSlack_ListStars(t *testing.T) {
136134 once.Do(startServer)
137 SLACK_API = "http://" + serverAddr + "/"
138 api := New("testing-token")
135 api := New("testing-token", OptionAPIURL("http://"+serverAddr+"/"))
139136 rh := newStarsHandler()
140137 http.HandleFunc("/stars.list", func(w http.ResponseWriter, r *http.Request) { rh.handler(w, r) })
141138 rh.response = `{"ok": true,
199196 NewMessageItem("C1", &Message{Msg: Msg{
200197 Text: "hello",
201198 Reactions: []ItemReaction{
202 ItemReaction{Name: "astonished", Count: 3, Users: []string{"U1", "U2", "U3"}},
203 ItemReaction{Name: "clock1", Count: 3, Users: []string{"U1", "U2"}},
199 {Name: "astonished", Count: 3, Users: []string{"U1", "U2", "U3"}},
200 {Name: "clock1", Count: 3, Users: []string{"U1", "U2"}},
204201 },
205202 }}),
206203 NewFileItem(&File{Name: "toy"}),
00 package slack
11
22 import (
3 "errors"
3 "context"
44 "net/url"
55 "strconv"
66 )
77
88 const (
9 DEFAULT_LOGINS_COUNT = 100
10 DEFAULT_LOGINS_PAGE = 1
9 DEFAULT_LOGINS_COUNT = 100
10 DEFAULT_LOGINS_PAGE = 1
1111 )
1212
1313 type TeamResponse struct {
2525
2626 type LoginResponse struct {
2727 Logins []Login `json:"logins"`
28 Paging `json:"paging"`
28 Paging `json:"paging"`
2929 SlackResponse
3030 }
31
3231
3332 type Login struct {
3433 UserID string `json:"user_id"`
4645 type BillableInfoResponse struct {
4746 BillableInfo map[string]BillingActive `json:"billable_info"`
4847 SlackResponse
49
5048 }
5149
5250 type BillingActive struct {
5553
5654 // AccessLogParameters contains all the parameters necessary (including the optional ones) for a GetAccessLogs() request
5755 type AccessLogParameters struct {
58 Count int
59 Page int
56 Count int
57 Page int
6058 }
6159
6260 // NewAccessLogParameters provides an instance of AccessLogParameters with all the sane default values set
6765 }
6866 }
6967
70
71 func teamRequest(path string, values url.Values, debug bool) (*TeamResponse, error) {
68 func (api *Client) teamRequest(ctx context.Context, path string, values url.Values) (*TeamResponse, error) {
7269 response := &TeamResponse{}
73 err := post(path, values, response, debug)
70 err := api.postMethod(ctx, path, values, response)
7471 if err != nil {
7572 return nil, err
7673 }
7774
78 if !response.Ok {
79 return nil, errors.New(response.Error)
80 }
81
82 return response, nil
75 return response, response.Err()
8376 }
8477
85 func billableInfoRequest(path string, values url.Values, debug bool) (map[string]BillingActive, error) {
78 func (api *Client) billableInfoRequest(ctx context.Context, path string, values url.Values) (map[string]BillingActive, error) {
8679 response := &BillableInfoResponse{}
87 err := post(path, values, response, debug)
80 err := api.postMethod(ctx, path, values, response)
8881 if err != nil {
8982 return nil, err
9083 }
9184
92 if !response.Ok {
93 return nil, errors.New(response.Error)
94 }
95
96 return response.BillableInfo, nil
85 return response.BillableInfo, response.Err()
9786 }
9887
99 func accessLogsRequest(path string, values url.Values, debug bool) (*LoginResponse, error) {
88 func (api *Client) accessLogsRequest(ctx context.Context, path string, values url.Values) (*LoginResponse, error) {
10089 response := &LoginResponse{}
101 err := post(path, values, response, debug)
90 err := api.postMethod(ctx, path, values, response)
10291 if err != nil {
10392 return nil, err
10493 }
105 if !response.Ok {
106 return nil, errors.New(response.Error)
107 }
108 return response, nil
94 return response, response.Err()
10995 }
110
11196
11297 // GetTeamInfo gets the Team Information of the user
11398 func (api *Client) GetTeamInfo() (*TeamInfo, error) {
99 return api.GetTeamInfoContext(context.Background())
100 }
101
102 // GetTeamInfoContext gets the Team Information of the user with a custom context
103 func (api *Client) GetTeamInfoContext(ctx context.Context) (*TeamInfo, error) {
114104 values := url.Values{
115 "token": {api.config.token},
105 "token": {api.token},
116106 }
117107
118 response, err := teamRequest("team.info", values, api.debug)
108 response, err := api.teamRequest(ctx, "team.info", values)
119109 if err != nil {
120110 return nil, err
121111 }
124114
125115 // GetAccessLogs retrieves a page of logins according to the parameters given
126116 func (api *Client) GetAccessLogs(params AccessLogParameters) ([]Login, *Paging, error) {
117 return api.GetAccessLogsContext(context.Background(), params)
118 }
119
120 // GetAccessLogsContext retrieves a page of logins according to the parameters given with a custom context
121 func (api *Client) GetAccessLogsContext(ctx context.Context, params AccessLogParameters) ([]Login, *Paging, error) {
127122 values := url.Values{
128 "token": {api.config.token},
123 "token": {api.token},
129124 }
130125 if params.Count != DEFAULT_LOGINS_COUNT {
131126 values.Add("count", strconv.Itoa(params.Count))
133128 if params.Page != DEFAULT_LOGINS_PAGE {
134129 values.Add("page", strconv.Itoa(params.Page))
135130 }
136 response, err := accessLogsRequest("team.accessLogs", values, api.debug)
131
132 response, err := api.accessLogsRequest(ctx, "team.accessLogs", values)
137133 if err != nil {
138134 return nil, nil, err
139135 }
140136 return response.Logins, &response.Paging, nil
141137 }
142138
139 // GetBillableInfo ...
143140 func (api *Client) GetBillableInfo(user string) (map[string]BillingActive, error) {
141 return api.GetBillableInfoContext(context.Background(), user)
142 }
143
144 // GetBillableInfoContext ...
145 func (api *Client) GetBillableInfoContext(ctx context.Context, user string) (map[string]BillingActive, error) {
144146 values := url.Values{
145 "token": {api.config.token},
146 "user": {user},
147 "token": {api.token},
148 "user": {user},
147149 }
148150
149 return billableInfoRequest("team.billableInfo", values, api.debug)
151 return api.billableInfoRequest(ctx, "team.billableInfo", values)
150152 }
151153
152154 // GetBillableInfoForTeam returns the billing_active status of all users on the team.
153155 func (api *Client) GetBillableInfoForTeam() (map[string]BillingActive, error) {
156 return api.GetBillableInfoForTeamContext(context.Background())
157 }
158
159 // GetBillableInfoForTeamContext returns the billing_active status of all users on the team with a custom context
160 func (api *Client) GetBillableInfoForTeamContext(ctx context.Context) (map[string]BillingActive, error) {
154161 values := url.Values{
155 "token": {api.config.token},
162 "token": {api.token},
156163 }
157164
158 return billableInfoRequest("team.billableInfo", values, api.debug)
165 return api.billableInfoRequest(ctx, "team.billableInfo", values)
159166 }
22 import (
33 "errors"
44 "net/http"
5 "strings"
56 "testing"
6 "strings"
77 )
88
99 var (
3030 http.HandleFunc("/team.info", getTeamInfo)
3131
3232 once.Do(startServer)
33 SLACK_API = "http://" + serverAddr + "/"
34 api := New("testing-token")
33 api := New("testing-token", OptionAPIURL("http://"+serverAddr+"/"))
3534
3635 teamInfo, err := api.GetTeamInfo()
3736 if err != nil {
9493 http.HandleFunc("/team.accessLogs", getTeamAccessLogs)
9594
9695 once.Do(startServer)
97 SLACK_API = "http://" + serverAddr + "/"
98 api := New("testing-token")
96 api := New("testing-token", OptionAPIURL("http://"+serverAddr+"/"))
9997
10098 logins, paging, err := api.GetAccessLogs(NewAccessLogParameters())
10199 if err != nil {
111109 login1 := logins[0]
112110 login2 := logins[1]
113111
114 if (login1.UserID != "F0UWHUX") {
112 if login1.UserID != "F0UWHUX" {
115113 t.Fatal(ErrIncorrectResponse)
116114 }
117 if (login1.Username != "notalar") {
115 if login1.Username != "notalar" {
118116 t.Fatal(ErrIncorrectResponse)
119117 }
120 if (login1.DateFirst != 1475684477) {
118 if login1.DateFirst != 1475684477 {
121119 t.Fatal(ErrIncorrectResponse)
122120 }
123 if (login1.DateLast != 1475684645) {
121 if login1.DateLast != 1475684645 {
124122 t.Fatal(ErrIncorrectResponse)
125123 }
126 if (login1.Count != 8) {
124 if login1.Count != 8 {
127125 t.Fatal(ErrIncorrectResponse)
128126 }
129 if (login1.IP != "127.0.0.1") {
127 if login1.IP != "127.0.0.1" {
130128 t.Fatal(ErrIncorrectResponse)
131129 }
132 if (!strings.HasPrefix(login1.UserAgent, "SlackWeb")) {
130 if !strings.HasPrefix(login1.UserAgent, "SlackWeb") {
133131 t.Fatal(ErrIncorrectResponse)
134132 }
135 if (login1.ISP != "AT&T U-verse") {
133 if login1.ISP != "AT&T U-verse" {
136134 t.Fatal(ErrIncorrectResponse)
137135 }
138 if (login1.Country != "US") {
136 if login1.Country != "US" {
139137 t.Fatal(ErrIncorrectResponse)
140138 }
141 if (login1.Region != "IN") {
139 if login1.Region != "IN" {
142140 t.Fatal(ErrIncorrectResponse)
143141 }
144142
145143 // test that the null values from login2 are coming across correctly
146 if (login2.ISP != "") {
144 if login2.ISP != "" {
147145 t.Fatal(ErrIncorrectResponse)
148146 }
149 if (login2.Country != "") {
147 if login2.Country != "" {
150148 t.Fatal(ErrIncorrectResponse)
151149 }
152 if (login2.Region != "") {
150 if login2.Region != "" {
153151 t.Fatal(ErrIncorrectResponse)
154152 }
155153
156154 // test the paging
157 if (paging.Count != 2) {
155 if paging.Count != 2 {
158156 t.Fatal(ErrIncorrectResponse)
159157 }
160 if (paging.Total != 2) {
158 if paging.Total != 2 {
161159 t.Fatal(ErrIncorrectResponse)
162160 }
163 if (paging.Page != 1) {
161 if paging.Page != 1 {
164162 t.Fatal(ErrIncorrectResponse)
165163 }
166 if (paging.Pages != 1) {
164 if paging.Pages != 1 {
167165 t.Fatal(ErrIncorrectResponse)
168166 }
169167 }
170
00 package slack
11
22 import (
3 "errors"
3 "context"
44 "net/url"
55 "strings"
66 )
2323 DeletedBy string `json:"deleted_by"`
2424 Prefs UserGroupPrefs `json:"prefs"`
2525 UserCount int `json:"user_count"`
26 Users []string `json:"users"`
2627 }
2728
2829 // UserGroupPrefs contains default channels and groups (private channels)
3839 SlackResponse
3940 }
4041
41 func userGroupRequest(path string, values url.Values, debug bool) (*userGroupResponseFull, error) {
42 func (api *Client) userGroupRequest(ctx context.Context, path string, values url.Values) (*userGroupResponseFull, error) {
4243 response := &userGroupResponseFull{}
43 err := post(path, values, response, debug)
44 err := api.postMethod(ctx, path, values, response)
4445 if err != nil {
4546 return nil, err
4647 }
47 if !response.Ok {
48 return nil, errors.New(response.Error)
49 }
50 return response, nil
48
49 return response, response.Err()
5150 }
5251
5352 // CreateUserGroup creates a new user group
5453 func (api *Client) CreateUserGroup(userGroup UserGroup) (UserGroup, error) {
55 values := url.Values{
56 "token": {api.config.token},
54 return api.CreateUserGroupContext(context.Background(), userGroup)
55 }
56
57 // CreateUserGroupContext creates a new user group with a custom context
58 func (api *Client) CreateUserGroupContext(ctx context.Context, userGroup UserGroup) (UserGroup, error) {
59 values := url.Values{
60 "token": {api.token},
5761 "name": {userGroup.Name},
5862 }
5963
6973 values["channels"] = []string{strings.Join(userGroup.Prefs.Channels, ",")}
7074 }
7175
72 response, err := userGroupRequest("usergroups.create", values, api.debug)
76 response, err := api.userGroupRequest(ctx, "usergroups.create", values)
7377 if err != nil {
7478 return UserGroup{}, err
7579 }
7882
7983 // DisableUserGroup disables an existing user group
8084 func (api *Client) DisableUserGroup(userGroup string) (UserGroup, error) {
81 values := url.Values{
82 "token": {api.config.token},
83 "usergroup": {userGroup},
84 }
85
86 response, err := userGroupRequest("usergroups.disable", values, api.debug)
85 return api.DisableUserGroupContext(context.Background(), userGroup)
86 }
87
88 // DisableUserGroupContext disables an existing user group with a custom context
89 func (api *Client) DisableUserGroupContext(ctx context.Context, userGroup string) (UserGroup, error) {
90 values := url.Values{
91 "token": {api.token},
92 "usergroup": {userGroup},
93 }
94
95 response, err := api.userGroupRequest(ctx, "usergroups.disable", values)
8796 if err != nil {
8897 return UserGroup{}, err
8998 }
92101
93102 // EnableUserGroup enables an existing user group
94103 func (api *Client) EnableUserGroup(userGroup string) (UserGroup, error) {
95 values := url.Values{
96 "token": {api.config.token},
97 "usergroup": {userGroup},
98 }
99
100 response, err := userGroupRequest("usergroups.enable", values, api.debug)
101 if err != nil {
102 return UserGroup{}, err
103 }
104 return response.UserGroup, nil
104 return api.EnableUserGroupContext(context.Background(), userGroup)
105 }
106
107 // EnableUserGroupContext enables an existing user group with a custom context
108 func (api *Client) EnableUserGroupContext(ctx context.Context, userGroup string) (UserGroup, error) {
109 values := url.Values{
110 "token": {api.token},
111 "usergroup": {userGroup},
112 }
113
114 response, err := api.userGroupRequest(ctx, "usergroups.enable", values)
115 if err != nil {
116 return UserGroup{}, err
117 }
118 return response.UserGroup, nil
119 }
120
121 // GetUserGroupsOption options for the GetUserGroups method call.
122 type GetUserGroupsOption func(*GetUserGroupsParams)
123
124 // GetUserGroupsOptionIncludeCount include the number of users in each User Group (default: false)
125 func GetUserGroupsOptionIncludeCount(b bool) GetUserGroupsOption {
126 return func(params *GetUserGroupsParams) {
127 params.IncludeCount = b
128 }
129 }
130
131 // GetUserGroupsOptionIncludeDisabled include disabled User Groups (default: false)
132 func GetUserGroupsOptionIncludeDisabled(b bool) GetUserGroupsOption {
133 return func(params *GetUserGroupsParams) {
134 params.IncludeDisabled = b
135 }
136 }
137
138 // GetUserGroupsOptionIncludeUsers include the list of users for each User Group (default: false)
139 func GetUserGroupsOptionIncludeUsers(b bool) GetUserGroupsOption {
140 return func(params *GetUserGroupsParams) {
141 params.IncludeUsers = b
142 }
143 }
144
145 // GetUserGroupsParams contains arguments for GetUserGroups method call
146 type GetUserGroupsParams struct {
147 IncludeCount bool
148 IncludeDisabled bool
149 IncludeUsers bool
105150 }
106151
107152 // GetUserGroups returns a list of user groups for the team
108 func (api *Client) GetUserGroups() ([]UserGroup, error) {
109 values := url.Values{
110 "token": {api.config.token},
111 }
112
113 response, err := userGroupRequest("usergroups.list", values, api.debug)
153 func (api *Client) GetUserGroups(options ...GetUserGroupsOption) ([]UserGroup, error) {
154 return api.GetUserGroupsContext(context.Background(), options...)
155 }
156
157 // GetUserGroupsContext returns a list of user groups for the team with a custom context
158 func (api *Client) GetUserGroupsContext(ctx context.Context, options ...GetUserGroupsOption) ([]UserGroup, error) {
159 params := GetUserGroupsParams{}
160
161 for _, opt := range options {
162 opt(&params)
163 }
164
165 values := url.Values{
166 "token": {api.token},
167 }
168 if params.IncludeCount {
169 values.Add("include_count", "true")
170 }
171 if params.IncludeDisabled {
172 values.Add("include_disabled", "true")
173 }
174 if params.IncludeUsers {
175 values.Add("include_users", "true")
176 }
177
178 response, err := api.userGroupRequest(ctx, "usergroups.list", values)
114179 if err != nil {
115180 return nil, err
116181 }
119184
120185 // UpdateUserGroup will update an existing user group
121186 func (api *Client) UpdateUserGroup(userGroup UserGroup) (UserGroup, error) {
122 values := url.Values{
123 "token": {api.config.token},
187 return api.UpdateUserGroupContext(context.Background(), userGroup)
188 }
189
190 // UpdateUserGroupContext will update an existing user group with a custom context
191 func (api *Client) UpdateUserGroupContext(ctx context.Context, userGroup UserGroup) (UserGroup, error) {
192 values := url.Values{
193 "token": {api.token},
124194 "usergroup": {userGroup.ID},
125195 }
126196
135205 if userGroup.Description != "" {
136206 values["description"] = []string{userGroup.Description}
137207 }
138
139 response, err := userGroupRequest("usergroups.update", values, api.debug)
208
209 if len(userGroup.Prefs.Channels) > 0 {
210 values["channels"] = []string{strings.Join(userGroup.Prefs.Channels, ",")}
211 }
212
213 response, err := api.userGroupRequest(ctx, "usergroups.update", values)
140214 if err != nil {
141215 return UserGroup{}, err
142216 }
145219
146220 // GetUserGroupMembers will retrieve the current list of users in a group
147221 func (api *Client) GetUserGroupMembers(userGroup string) ([]string, error) {
148 values := url.Values{
149 "token": {api.config.token},
150 "usergroup": {userGroup},
151 }
152
153 response, err := userGroupRequest("usergroups.users.list", values, api.debug)
222 return api.GetUserGroupMembersContext(context.Background(), userGroup)
223 }
224
225 // GetUserGroupMembersContext will retrieve the current list of users in a group with a custom context
226 func (api *Client) GetUserGroupMembersContext(ctx context.Context, userGroup string) ([]string, error) {
227 values := url.Values{
228 "token": {api.token},
229 "usergroup": {userGroup},
230 }
231
232 response, err := api.userGroupRequest(ctx, "usergroups.users.list", values)
154233 if err != nil {
155234 return []string{}, err
156235 }
159238
160239 // UpdateUserGroupMembers will update the members of an existing user group
161240 func (api *Client) UpdateUserGroupMembers(userGroup string, members string) (UserGroup, error) {
162 values := url.Values{
163 "token": {api.config.token},
241 return api.UpdateUserGroupMembersContext(context.Background(), userGroup, members)
242 }
243
244 // UpdateUserGroupMembersContext will update the members of an existing user group with a custom context
245 func (api *Client) UpdateUserGroupMembersContext(ctx context.Context, userGroup string, members string) (UserGroup, error) {
246 values := url.Values{
247 "token": {api.token},
164248 "usergroup": {userGroup},
165249 "users": {members},
166250 }
167251
168 response, err := userGroupRequest("usergroups.users.update", values, api.debug)
169 if err != nil {
170 return UserGroup{}, err
171 }
172 return response.UserGroup, nil
173 }
252 response, err := api.userGroupRequest(ctx, "usergroups.users.update", values)
253 if err != nil {
254 return UserGroup{}, err
255 }
256 return response.UserGroup, nil
257 }
6060
6161 func TestCreateUserGroup(t *testing.T) {
6262 once.Do(startServer)
63 SLACK_API = "http://" + serverAddr + "/"
64 api := New("testing-token")
63 api := New("testing-token", OptionAPIURL("http://"+serverAddr+"/"))
6564
6665 tests := []struct {
6766 userGroup UserGroup
125124 "group2",
126125 "group3"
127126 ]
128 },
127 },
128 "users": [
129 "user1",
130 "user2"
131 ],
129132 "user_count": 2
130133 }
131134 ]
137140 http.HandleFunc("/usergroups.list", getUserGroups)
138141
139142 once.Do(startServer)
140 SLACK_API = "http://" + serverAddr + "/"
141 api := New("testing-token")
143 api := New("testing-token", OptionAPIURL("http://"+serverAddr+"/"))
142144
143 userGroups, err := api.GetUserGroups()
145 userGroups, err := api.GetUserGroups(GetUserGroupsOptionIncludeUsers(true))
144146 if err != nil {
145147 t.Errorf("Unexpected error: %s", err)
146148 return
170172 Channels: []string{"channel1", "channel2"},
171173 Groups: []string{"group1", "group2", "group3"},
172174 },
175 Users: []string{
176 "user1",
177 "user2",
178 },
173179 UserCount: 2,
174180 }
175181
+416
-128
users.go less more
00 package slack
11
22 import (
3 "context"
34 "encoding/json"
4 "errors"
55 "net/url"
6 "strconv"
7 "time"
68 )
79
810 const (
1315
1416 // UserProfile contains all the information details of a given user
1517 type UserProfile struct {
16 FirstName string `json:"first_name"`
17 LastName string `json:"last_name"`
18 RealName string `json:"real_name"`
19 RealNameNormalized string `json:"real_name_normalized"`
20 Email string `json:"email"`
21 Skype string `json:"skype"`
22 Phone string `json:"phone"`
23 Image24 string `json:"image_24"`
24 Image32 string `json:"image_32"`
25 Image48 string `json:"image_48"`
26 Image72 string `json:"image_72"`
27 Image192 string `json:"image_192"`
28 ImageOriginal string `json:"image_original"`
29 Title string `json:"title"`
30 BotID string `json:"bot_id,omitempty"`
31 ApiAppID string `json:"api_app_id,omitempty"`
32 StatusText string `json:"status_text,omitempty"`
33 StatusEmoji string `json:"status_emoji,omitempty"`
18 FirstName string `json:"first_name"`
19 LastName string `json:"last_name"`
20 RealName string `json:"real_name"`
21 RealNameNormalized string `json:"real_name_normalized"`
22 DisplayName string `json:"display_name"`
23 DisplayNameNormalized string `json:"display_name_normalized"`
24 Email string `json:"email"`
25 Skype string `json:"skype"`
26 Phone string `json:"phone"`
27 Image24 string `json:"image_24"`
28 Image32 string `json:"image_32"`
29 Image48 string `json:"image_48"`
30 Image72 string `json:"image_72"`
31 Image192 string `json:"image_192"`
32 ImageOriginal string `json:"image_original"`
33 Title string `json:"title"`
34 BotID string `json:"bot_id,omitempty"`
35 ApiAppID string `json:"api_app_id,omitempty"`
36 StatusText string `json:"status_text,omitempty"`
37 StatusEmoji string `json:"status_emoji,omitempty"`
38 StatusExpiration int `json:"status_expiration"`
39 Team string `json:"team"`
40 Fields UserProfileCustomFields `json:"fields"`
41 }
42
43 // UserProfileCustomFields represents user profile's custom fields.
44 // Slack API's response data type is inconsistent so we use the struct.
45 // For detail, please see below.
46 // https://github.com/nlopes/slack/pull/298#discussion_r185159233
47 type UserProfileCustomFields struct {
48 fields map[string]UserProfileCustomField
49 }
50
51 // UnmarshalJSON is the implementation of the json.Unmarshaler interface.
52 func (fields *UserProfileCustomFields) UnmarshalJSON(b []byte) error {
53 // https://github.com/nlopes/slack/pull/298#discussion_r185159233
54 if string(b) == "[]" {
55 return nil
56 }
57 return json.Unmarshal(b, &fields.fields)
58 }
59
60 // MarshalJSON is the implementation of the json.Marshaler interface.
61 func (fields UserProfileCustomFields) MarshalJSON() ([]byte, error) {
62 if len(fields.fields) == 0 {
63 return []byte("[]"), nil
64 }
65 return json.Marshal(fields.fields)
66 }
67
68 // ToMap returns a map of custom fields.
69 func (fields *UserProfileCustomFields) ToMap() map[string]UserProfileCustomField {
70 return fields.fields
71 }
72
73 // Len returns the number of custom fields.
74 func (fields *UserProfileCustomFields) Len() int {
75 return len(fields.fields)
76 }
77
78 // SetMap sets a map of custom fields.
79 func (fields *UserProfileCustomFields) SetMap(m map[string]UserProfileCustomField) {
80 fields.fields = m
81 }
82
83 // FieldsMap returns a map of custom fields.
84 func (profile *UserProfile) FieldsMap() map[string]UserProfileCustomField {
85 return profile.Fields.ToMap()
86 }
87
88 // SetFieldsMap sets a map of custom fields.
89 func (profile *UserProfile) SetFieldsMap(m map[string]UserProfileCustomField) {
90 profile.Fields.SetMap(m)
91 }
92
93 // UserProfileCustomField represents a custom user profile field
94 type UserProfileCustomField struct {
95 Value string `json:"value"`
96 Alt string `json:"alt"`
97 Label string `json:"label"`
3498 }
3599
36100 // User contains all the information of a user
37101 type User struct {
38 ID string `json:"id"`
39 Name string `json:"name"`
40 Deleted bool `json:"deleted"`
41 Color string `json:"color"`
42 RealName string `json:"real_name"`
43 TZ string `json:"tz,omitempty"`
44 TZLabel string `json:"tz_label"`
45 TZOffset int `json:"tz_offset"`
46 Profile UserProfile `json:"profile"`
47 IsBot bool `json:"is_bot"`
48 IsAdmin bool `json:"is_admin"`
49 IsOwner bool `json:"is_owner"`
50 IsPrimaryOwner bool `json:"is_primary_owner"`
51 IsRestricted bool `json:"is_restricted"`
52 IsUltraRestricted bool `json:"is_ultra_restricted"`
53 Has2FA bool `json:"has_2fa"`
54 HasFiles bool `json:"has_files"`
55 Presence string `json:"presence"`
102 ID string `json:"id"`
103 TeamID string `json:"team_id"`
104 Name string `json:"name"`
105 Deleted bool `json:"deleted"`
106 Color string `json:"color"`
107 RealName string `json:"real_name"`
108 TZ string `json:"tz,omitempty"`
109 TZLabel string `json:"tz_label"`
110 TZOffset int `json:"tz_offset"`
111 Profile UserProfile `json:"profile"`
112 IsBot bool `json:"is_bot"`
113 IsAdmin bool `json:"is_admin"`
114 IsOwner bool `json:"is_owner"`
115 IsPrimaryOwner bool `json:"is_primary_owner"`
116 IsRestricted bool `json:"is_restricted"`
117 IsUltraRestricted bool `json:"is_ultra_restricted"`
118 IsStranger bool `json:"is_stranger"`
119 IsAppUser bool `json:"is_app_user"`
120 IsInvitedUser bool `json:"is_invited_user"`
121 Has2FA bool `json:"has_2fa"`
122 HasFiles bool `json:"has_files"`
123 Presence string `json:"presence"`
124 Locale string `json:"locale"`
125 Updated JSONTime `json:"updated"`
126 Enterprise EnterpriseUser `json:"enterprise_user,omitempty"`
56127 }
57128
58129 // UserPresence contains details about a user online status
81152 Image72 string `json:"image_72"`
82153 Image192 string `json:"image_192"`
83154 Image512 string `json:"image_512"`
155 }
156
157 // EnterpriseUser is present when a user is part of Slack Enterprise Grid
158 // https://api.slack.com/types/user#enterprise_grid_user_objects
159 type EnterpriseUser struct {
160 ID string `json:"id"`
161 EnterpriseID string `json:"enterprise_id"`
162 EnterpriseName string `json:"enterprise_name"`
163 IsAdmin bool `json:"is_admin"`
164 IsOwner bool `json:"is_owner"`
165 Teams []string `json:"teams"`
84166 }
85167
86168 type TeamIdentity struct {
99181 }
100182
101183 type userResponseFull struct {
102 Members []User `json:"members,omitempty"` // ListUsers
103 User `json:"user,omitempty"` // GetUserInfo
104 UserPresence // GetUserPresence
184 Members []User `json:"members,omitempty"`
185 User `json:"user,omitempty"`
186 UserPresence
105187 SlackResponse
188 Metadata ResponseMetadata `json:"response_metadata"`
106189 }
107190
108191 type UserSetPhotoParams struct {
119202 }
120203 }
121204
122 func userRequest(path string, values url.Values, debug bool) (*userResponseFull, error) {
205 func (api *Client) userRequest(ctx context.Context, path string, values url.Values) (*userResponseFull, error) {
123206 response := &userResponseFull{}
124 err := post(path, values, response, debug)
125 if err != nil {
126 return nil, err
127 }
128 if !response.Ok {
129 return nil, errors.New(response.Error)
130 }
131 return response, nil
207 err := api.postMethod(ctx, path, values, response)
208 if err != nil {
209 return nil, err
210 }
211
212 return response, response.Err()
132213 }
133214
134215 // GetUserPresence will retrieve the current presence status of given user.
135216 func (api *Client) GetUserPresence(user string) (*UserPresence, error) {
136 values := url.Values{
137 "token": {api.config.token},
217 return api.GetUserPresenceContext(context.Background(), user)
218 }
219
220 // GetUserPresenceContext will retrieve the current presence status of given user with a custom context.
221 func (api *Client) GetUserPresenceContext(ctx context.Context, user string) (*UserPresence, error) {
222 values := url.Values{
223 "token": {api.token},
138224 "user": {user},
139225 }
140 response, err := userRequest("users.getPresence", values, api.debug)
226
227 response, err := api.userRequest(ctx, "users.getPresence", values)
141228 if err != nil {
142229 return nil, err
143230 }
146233
147234 // GetUserInfo will retrieve the complete user information
148235 func (api *Client) GetUserInfo(user string) (*User, error) {
149 values := url.Values{
150 "token": {api.config.token},
151 "user": {user},
152 }
153 response, err := userRequest("users.info", values, api.debug)
236 return api.GetUserInfoContext(context.Background(), user)
237 }
238
239 // GetUserInfoContext will retrieve the complete user information with a custom context
240 func (api *Client) GetUserInfoContext(ctx context.Context, user string) (*User, error) {
241 values := url.Values{
242 "token": {api.token},
243 "user": {user},
244 "include_locale": {strconv.FormatBool(true)},
245 }
246
247 response, err := api.userRequest(ctx, "users.info", values)
154248 if err != nil {
155249 return nil, err
156250 }
157251 return &response.User, nil
252 }
253
254 // GetUsersOption options for the GetUsers method call.
255 type GetUsersOption func(*UserPagination)
256
257 // GetUsersOptionLimit limit the number of users returned
258 func GetUsersOptionLimit(n int) GetUsersOption {
259 return func(p *UserPagination) {
260 p.limit = n
261 }
262 }
263
264 // GetUsersOptionPresence include user presence
265 func GetUsersOptionPresence(n bool) GetUsersOption {
266 return func(p *UserPagination) {
267 p.presence = n
268 }
269 }
270
271 func newUserPagination(c *Client, options ...GetUsersOption) (up UserPagination) {
272 up = UserPagination{
273 c: c,
274 limit: 200, // per slack api documentation.
275 }
276
277 for _, opt := range options {
278 opt(&up)
279 }
280
281 return up
282 }
283
284 // UserPagination allows for paginating over the users
285 type UserPagination struct {
286 Users []User
287 limit int
288 presence bool
289 previousResp *ResponseMetadata
290 c *Client
291 }
292
293 // Done checks if the pagination has completed
294 func (UserPagination) Done(err error) bool {
295 return err == errPaginationComplete
296 }
297
298 // Failure checks if pagination failed.
299 func (t UserPagination) Failure(err error) error {
300 if t.Done(err) {
301 return nil
302 }
303
304 return err
305 }
306
307 func (t UserPagination) Next(ctx context.Context) (_ UserPagination, err error) {
308 var (
309 resp *userResponseFull
310 )
311
312 if t.c == nil || (t.previousResp != nil && t.previousResp.Cursor == "") {
313 return t, errPaginationComplete
314 }
315
316 t.previousResp = t.previousResp.initialize()
317
318 values := url.Values{
319 "limit": {strconv.Itoa(t.limit)},
320 "presence": {strconv.FormatBool(t.presence)},
321 "token": {t.c.token},
322 "cursor": {t.previousResp.Cursor},
323 "include_locale": {strconv.FormatBool(true)},
324 }
325
326 if resp, err = t.c.userRequest(ctx, "users.list", values); err != nil {
327 return t, err
328 }
329
330 t.c.Debugf("GetUsersContext: got %d users; metadata %v", len(resp.Members), resp.Metadata)
331 t.Users = resp.Members
332 t.previousResp = &resp.Metadata
333
334 return t, nil
335 }
336
337 // GetUsersPaginated fetches users in a paginated fashion, see GetUsersContext for usage.
338 func (api *Client) GetUsersPaginated(options ...GetUsersOption) UserPagination {
339 return newUserPagination(api, options...)
158340 }
159341
160342 // GetUsers returns the list of users (with their detailed information)
161343 func (api *Client) GetUsers() ([]User, error) {
162 values := url.Values{
163 "token": {api.config.token},
164 "presence": {"1"},
165 }
166 response, err := userRequest("users.list", values, api.debug)
167 if err != nil {
168 return nil, err
169 }
170 return response.Members, nil
344 return api.GetUsersContext(context.Background())
345 }
346
347 // GetUsersContext returns the list of users (with their detailed information) with a custom context
348 func (api *Client) GetUsersContext(ctx context.Context) (results []User, err error) {
349 p := api.GetUsersPaginated()
350 for err == nil {
351 p, err = p.Next(ctx)
352 if err == nil {
353 results = append(results, p.Users...)
354 } else if rateLimitedError, ok := err.(*RateLimitedError); ok {
355 select {
356 case <-ctx.Done():
357 err = ctx.Err()
358 case <-time.After(rateLimitedError.RetryAfter):
359 err = nil
360 }
361 }
362 }
363
364 return results, p.Failure(err)
365 }
366
367 // GetUserByEmail will retrieve the complete user information by email
368 func (api *Client) GetUserByEmail(email string) (*User, error) {
369 return api.GetUserByEmailContext(context.Background(), email)
370 }
371
372 // GetUserByEmailContext will retrieve the complete user information by email with a custom context
373 func (api *Client) GetUserByEmailContext(ctx context.Context, email string) (*User, error) {
374 values := url.Values{
375 "token": {api.token},
376 "email": {email},
377 }
378 response, err := api.userRequest(ctx, "users.lookupByEmail", values)
379 if err != nil {
380 return nil, err
381 }
382 return &response.User, nil
171383 }
172384
173385 // SetUserAsActive marks the currently authenticated user as active
174386 func (api *Client) SetUserAsActive() error {
175 values := url.Values{
176 "token": {api.config.token},
177 }
178 _, err := userRequest("users.setActive", values, api.debug)
179 if err != nil {
180 return err
181 }
182 return nil
387 return api.SetUserAsActiveContext(context.Background())
388 }
389
390 // SetUserAsActiveContext marks the currently authenticated user as active with a custom context
391 func (api *Client) SetUserAsActiveContext(ctx context.Context) (err error) {
392 values := url.Values{
393 "token": {api.token},
394 }
395
396 _, err = api.userRequest(ctx, "users.setActive", values)
397 return err
183398 }
184399
185400 // SetUserPresence changes the currently authenticated user presence
186401 func (api *Client) SetUserPresence(presence string) error {
187 values := url.Values{
188 "token": {api.config.token},
402 return api.SetUserPresenceContext(context.Background(), presence)
403 }
404
405 // SetUserPresenceContext changes the currently authenticated user presence with a custom context
406 func (api *Client) SetUserPresenceContext(ctx context.Context, presence string) error {
407 values := url.Values{
408 "token": {api.token},
189409 "presence": {presence},
190410 }
191 _, err := userRequest("users.setPresence", values, api.debug)
192 if err != nil {
193 return err
194 }
195 return nil
196
411
412 _, err := api.userRequest(ctx, "users.setPresence", values)
413 return err
197414 }
198415
199416 // GetUserIdentity will retrieve user info available per identity scopes
200417 func (api *Client) GetUserIdentity() (*UserIdentityResponse, error) {
201 values := url.Values{
202 "token": {api.config.token},
203 }
204 response := &UserIdentityResponse{}
205 err := post("users.identity", values, response, api.debug)
206 if err != nil {
207 return nil, err
208 }
209 if !response.Ok {
210 return nil, errors.New(response.Error)
211 }
418 return api.GetUserIdentityContext(context.Background())
419 }
420
421 // GetUserIdentityContext will retrieve user info available per identity scopes with a custom context
422 func (api *Client) GetUserIdentityContext(ctx context.Context) (response *UserIdentityResponse, err error) {
423 values := url.Values{
424 "token": {api.token},
425 }
426 response = &UserIdentityResponse{}
427
428 err = api.postMethod(ctx, "users.identity", values, response)
429 if err != nil {
430 return nil, err
431 }
432
433 if err := response.Err(); err != nil {
434 return nil, err
435 }
436
212437 return response, nil
213438 }
214439
215440 // SetUserPhoto changes the currently authenticated user's profile image
216441 func (api *Client) SetUserPhoto(image string, params UserSetPhotoParams) error {
442 return api.SetUserPhotoContext(context.Background(), image, params)
443 }
444
445 // SetUserPhotoContext changes the currently authenticated user's profile image using a custom context
446 func (api *Client) SetUserPhotoContext(ctx context.Context, image string, params UserSetPhotoParams) (err error) {
217447 response := &SlackResponse{}
218448 values := url.Values{
219 "token": {api.config.token},
449 "token": {api.token},
220450 }
221451 if params.CropX != DEFAULT_USER_PHOTO_CROP_X {
222 values.Add("crop_x", string(params.CropX))
452 values.Add("crop_x", strconv.Itoa(params.CropX))
223453 }
224454 if params.CropY != DEFAULT_USER_PHOTO_CROP_Y {
225 values.Add("crop_y", string(params.CropY))
455 values.Add("crop_y", strconv.Itoa(params.CropX))
226456 }
227457 if params.CropW != DEFAULT_USER_PHOTO_CROP_W {
228 values.Add("crop_w", string(params.CropW))
229 }
230 err := postLocalWithMultipartResponse("users.setPhoto", image, "image", values, response, api.debug)
458 values.Add("crop_w", strconv.Itoa(params.CropW))
459 }
460
461 err = postLocalWithMultipartResponse(ctx, api.httpclient, api.endpoint+"users.setPhoto", image, "image", values, response, api)
231462 if err != nil {
232463 return err
233464 }
234 if !response.Ok {
235 return errors.New(response.Error)
236 }
237 return nil
465
466 return response.Err()
238467 }
239468
240469 // DeleteUserPhoto deletes the current authenticated user's profile image
241470 func (api *Client) DeleteUserPhoto() error {
471 return api.DeleteUserPhotoContext(context.Background())
472 }
473
474 // DeleteUserPhotoContext deletes the current authenticated user's profile image with a custom context
475 func (api *Client) DeleteUserPhotoContext(ctx context.Context) (err error) {
242476 response := &SlackResponse{}
243477 values := url.Values{
244 "token": {api.config.token},
245 }
246 err := post("users.deletePhoto", values, response, api.debug)
478 "token": {api.token},
479 }
480
481 err = api.postMethod(ctx, "users.deletePhoto", values, response)
247482 if err != nil {
248483 return err
249484 }
250 if !response.Ok {
251 return errors.New(response.Error)
252 }
253 return nil
485
486 return response.Err()
254487 }
255488
256489 // SetUserCustomStatus will set a custom status and emoji for the currently
257490 // authenticated user. If statusEmoji is "" and statusText is not, the Slack API
258491 // will automatically set it to ":speech_balloon:". Otherwise, if both are ""
259 // the Slack API will unset the custom status/emoji.
260 func (api *Client) SetUserCustomStatus(statusText, statusEmoji string) error {
492 // the Slack API will unset the custom status/emoji. If statusExpiration is set to 0
493 // the status will not expire.
494 func (api *Client) SetUserCustomStatus(statusText, statusEmoji string, statusExpiration int64) error {
495 return api.SetUserCustomStatusContextWithUser(context.Background(), "", statusText, statusEmoji, statusExpiration)
496 }
497
498 // SetUserCustomStatusContext will set a custom status and emoji for the currently authenticated user with a custom context
499 //
500 // For more information see SetUserCustomStatus
501 func (api *Client) SetUserCustomStatusContext(ctx context.Context, statusText, statusEmoji string, statusExpiration int64) error {
502 return api.SetUserCustomStatusContextWithUser(context.Background(), "", statusText, statusEmoji, statusExpiration)
503 }
504
505 // SetUserCustomStatusWithUser will set a custom status and emoji for the provided user.
506 //
507 // For more information see SetUserCustomStatus
508 func (api *Client) SetUserCustomStatusWithUser(user, statusText, statusEmoji string, statusExpiration int64) error {
509 return api.SetUserCustomStatusContextWithUser(context.Background(), user, statusText, statusEmoji, statusExpiration)
510 }
511
512 // SetUserCustomStatusContextWithUser will set a custom status and emoji for the provided user with a custom context
513 //
514 // For more information see SetUserCustomStatus
515 func (api *Client) SetUserCustomStatusContextWithUser(ctx context.Context, user, statusText, statusEmoji string, statusExpiration int64) error {
261516 // XXX(theckman): this anonymous struct is for making requests to the Slack
262517 // API for setting and unsetting a User's Custom Status/Emoji. To change
263518 // these values we must provide a JSON document as the profile POST field.
270525 // - https://api.slack.com/docs/presence-and-status#custom_status
271526 profile, err := json.Marshal(
272527 &struct {
273 StatusText string `json:"status_text"`
274 StatusEmoji string `json:"status_emoji"`
528 StatusText string `json:"status_text"`
529 StatusEmoji string `json:"status_emoji"`
530 StatusExpiration int64 `json:"status_expiration"`
275531 }{
276 StatusText: statusText,
277 StatusEmoji: statusEmoji,
532 StatusText: statusText,
533 StatusEmoji: statusEmoji,
534 StatusExpiration: statusExpiration,
278535 },
279536 )
280537
283540 }
284541
285542 values := url.Values{
286 "token": {api.config.token},
543 "user": {user},
544 "token": {api.token},
287545 "profile": {string(profile)},
288546 }
289547
290548 response := &userResponseFull{}
291
292 if err = post("users.profile.set", values, response, api.debug); err != nil {
549 if err = api.postMethod(ctx, "users.profile.set", values, response); err != nil {
293550 return err
294551 }
295552
296 if !response.Ok {
297 return errors.New(response.Error)
298 }
299
300 return nil
553 return response.Err()
301554 }
302555
303556 // UnsetUserCustomStatus removes the custom status message for the currently
304 // authenticated user. This is a convenience method that wraps
305 // (*Client).SetUserCustomStatus().
557 // authenticated user. This is a convenience method that wraps (*Client).SetUserCustomStatus().
306558 func (api *Client) UnsetUserCustomStatus() error {
307 return api.SetUserCustomStatus("", "")
308 }
559 return api.UnsetUserCustomStatusContext(context.Background())
560 }
561
562 // UnsetUserCustomStatusContext removes the custom status message for the currently authenticated user
563 // with a custom context. This is a convenience method that wraps (*Client).SetUserCustomStatus().
564 func (api *Client) UnsetUserCustomStatusContext(ctx context.Context) error {
565 return api.SetUserCustomStatusContext(ctx, "", "", 0)
566 }
567
568 // GetUserProfile retrieves a user's profile information.
569 func (api *Client) GetUserProfile(userID string, includeLabels bool) (*UserProfile, error) {
570 return api.GetUserProfileContext(context.Background(), userID, includeLabels)
571 }
572
573 type getUserProfileResponse struct {
574 SlackResponse
575 Profile *UserProfile `json:"profile"`
576 }
577
578 // GetUserProfileContext retrieves a user's profile information with a context.
579 func (api *Client) GetUserProfileContext(ctx context.Context, userID string, includeLabels bool) (*UserProfile, error) {
580 values := url.Values{"token": {api.token}, "user": {userID}}
581 if includeLabels {
582 values.Add("include_labels", "true")
583 }
584 resp := &getUserProfileResponse{}
585
586 err := api.postMethod(ctx, "users.profile.get", values, &resp)
587 if err != nil {
588 return nil, err
589 }
590
591 if err := resp.Err(); err != nil {
592 return nil, err
593 }
594
595 return resp.Profile, nil
596 }
00 package slack
11
22 import (
3 "bytes"
34 "encoding/json"
45 "fmt"
6 "image"
7 "image/draw"
8 "image/png"
9 "io"
10 "io/ioutil"
511 "net/http"
12 "os"
13 "reflect"
14 "strconv"
15 "sync/atomic"
616 "testing"
717 )
18
19 func getTestUserProfileCustomField() UserProfileCustomField {
20 return UserProfileCustomField{
21 Value: "test value",
22 Alt: "",
23 Label: "",
24 }
25 }
26
27 func getTestUserProfileCustomFields() UserProfileCustomFields {
28 return UserProfileCustomFields{
29 fields: map[string]UserProfileCustomField{
30 "Xxxxxx": getTestUserProfileCustomField(),
31 }}
32 }
33
34 func getTestUserProfile() UserProfile {
35 return UserProfile{
36 StatusText: "testStatus",
37 StatusEmoji: ":construction:",
38 RealName: "Test Real Name",
39 RealNameNormalized: "Test Real Name Normalized",
40 DisplayName: "Test Display Name",
41 DisplayNameNormalized: "Test Display Name Normalized",
42 Email: "test@test.com",
43 Image24: "https://s3-us-west-2.amazonaws.com/slack-files2/avatars/2016-10-18/92962080834_ef14c1469fc0741caea1_24.jpg",
44 Image32: "https://s3-us-west-2.amazonaws.com/slack-files2/avatars/2016-10-18/92962080834_ef14c1469fc0741caea1_32.jpg",
45 Image48: "https://s3-us-west-2.amazonaws.com/slack-files2/avatars/2016-10-18/92962080834_ef14c1469fc0741caea1_48.jpg",
46 Image72: "https://s3-us-west-2.amazonaws.com/slack-files2/avatars/2016-10-18/92962080834_ef14c1469fc0741caea1_72.jpg",
47 Image192: "https://s3-us-west-2.amazonaws.com/slack-files2/avatars/2016-10-18/92962080834_ef14c1469fc0741caea1_192.jpg",
48 Fields: getTestUserProfileCustomFields(),
49 }
50 }
51
52 func getTestUserWithId(id string) User {
53 return User{
54 ID: id,
55 Name: "Test User",
56 Deleted: false,
57 Color: "9f69e7",
58 RealName: "testuser",
59 TZ: "America/Los_Angeles",
60 TZLabel: "Pacific Daylight Time",
61 TZOffset: -25200,
62 Profile: getTestUserProfile(),
63 IsBot: false,
64 IsAdmin: false,
65 IsOwner: false,
66 IsPrimaryOwner: false,
67 IsRestricted: false,
68 IsUltraRestricted: false,
69 Updated: 1555425715,
70 Has2FA: false,
71 }
72 }
73
74 func getTestUser() User {
75 return getTestUserWithId("UXXXXXXXX")
76 }
877
978 func getUserIdentity(rw http.ResponseWriter, r *http.Request) {
1079 rw.Header().Set("Content-Type", "application/json")
38107 rw.Write(response)
39108 }
40109
110 func getUserInfo(rw http.ResponseWriter, r *http.Request) {
111 rw.Header().Set("Content-Type", "application/json")
112 response, _ := json.Marshal(struct {
113 Ok bool `json:"ok"`
114 User User `json:"user"`
115 }{
116 Ok: true,
117 User: getTestUser(),
118 })
119 rw.Write(response)
120 }
121
122 func getUserByEmail(rw http.ResponseWriter, r *http.Request) {
123 rw.Header().Set("Content-Type", "application/json")
124 response, _ := json.Marshal(struct {
125 Ok bool `json:"ok"`
126 User User `json:"user"`
127 }{
128 Ok: true,
129 User: getTestUser(),
130 })
131 rw.Write(response)
132 }
133
41134 func httpTestErrReply(w http.ResponseWriter, clientErr bool, msg string) {
42135 if clientErr {
43136 w.WriteHeader(http.StatusBadRequest)
93186 http.HandleFunc("/users.identity", getUserIdentity)
94187
95188 once.Do(startServer)
96 SLACK_API = "http://" + serverAddr + "/"
97 api := New("testing-token")
189 api := New("testing-token", OptionAPIURL("http://"+serverAddr+"/"))
98190
99191 identity, err := api.GetUserIdentity()
100192 if err != nil {
129221 }
130222 }
131223
224 func TestGetUserInfo(t *testing.T) {
225 http.HandleFunc("/users.info", getUserInfo)
226 expectedUser := getTestUser()
227
228 once.Do(startServer)
229 api := New("testing-token", OptionAPIURL("http://"+serverAddr+"/"))
230
231 user, err := api.GetUserInfo("UXXXXXXXX")
232 if err != nil {
233 t.Errorf("Unexpected error: %s", err)
234 return
235 }
236 if !reflect.DeepEqual(expectedUser, *user) {
237 t.Fatal(ErrIncorrectResponse)
238 }
239 }
240
241 func TestGetUserByEmail(t *testing.T) {
242 http.HandleFunc("/users.lookupByEmail", getUserByEmail)
243 expectedUser := getTestUser()
244
245 once.Do(startServer)
246 api := New("testing-token", OptionAPIURL("http://"+serverAddr+"/"))
247
248 user, err := api.GetUserByEmail("test@test.com")
249 if err != nil {
250 t.Errorf("Unexpected error: %s", err)
251 return
252 }
253 if !reflect.DeepEqual(expectedUser, *user) {
254 t.Fatal(ErrIncorrectResponse)
255 }
256 }
257
132258 func TestUserCustomStatus(t *testing.T) {
133259 up := &UserProfile{}
134260
137263 http.HandleFunc("/users.profile.set", setUserProfile)
138264
139265 once.Do(startServer)
140 SLACK_API = "http://" + serverAddr + "/"
141 api := New("testing-token")
266 api := New("testing-token", OptionAPIURL("http://"+serverAddr+"/"))
142267
143268 testSetUserCustomStatus(api, up, t)
144269 testUnsetUserCustomStatus(api, up, t)
146271
147272 func testSetUserCustomStatus(api *Client, up *UserProfile, t *testing.T) {
148273 const (
149 statusText = "testStatus"
150 statusEmoji = ":construction:"
274 statusText = "testStatus"
275 statusEmoji = ":construction:"
276 statusExpiration = 1551619082
151277 )
152
153 if err := api.SetUserCustomStatus(statusText, statusEmoji); err != nil {
154 t.Fatalf(`SetUserCustomStatus(%q, %q) = %#v, want <nil>`, statusText, statusEmoji, err)
278 if err := api.SetUserCustomStatus(statusText, statusEmoji, statusExpiration); err != nil {
279 t.Fatalf(`SetUserCustomStatus(%q, %q, %q) = %#v, want <nil>`, statusText, statusEmoji, statusExpiration, err)
155280 }
156281
157282 if up.StatusText != statusText {
160285
161286 if up.StatusEmoji != statusEmoji {
162287 t.Fatalf(`UserProfile.StatusEmoji = %q, want %q`, up.StatusEmoji, statusEmoji)
288 }
289 if up.StatusExpiration != statusExpiration {
290 t.Fatalf(`UserProfile.StatusExpiration = %q, want %q`, up.StatusExpiration, statusExpiration)
163291 }
164292 }
165293
176304 t.Fatalf(`UserProfile.StatusEmoji = %q, want %q`, up.StatusEmoji, "")
177305 }
178306 }
307
308 func TestGetUsers(t *testing.T) {
309 http.DefaultServeMux = new(http.ServeMux)
310 http.HandleFunc("/users.list", getUserPage(4))
311
312 once.Do(startServer)
313 api := New("testing-token", OptionAPIURL("http://"+serverAddr+"/"))
314
315 users, err := api.GetUsers()
316 if err != nil {
317 t.Errorf("Unexpected error: %s", err)
318 return
319 }
320
321 if !reflect.DeepEqual([]User{
322 getTestUserWithId("U000"),
323 getTestUserWithId("U001"),
324 getTestUserWithId("U002"),
325 getTestUserWithId("U003"),
326 }, users) {
327 t.Fatal(ErrIncorrectResponse)
328 }
329 }
330
331 // returns n pages users.
332 func getUserPage(max int64) func(rw http.ResponseWriter, r *http.Request) {
333 var n int64
334 return func(rw http.ResponseWriter, r *http.Request) {
335 var cpage int64
336 sresp := SlackResponse{
337 Ok: true,
338 }
339 members := []User{
340 getTestUserWithId(fmt.Sprintf("U%03d", n)),
341 }
342 rw.Header().Set("Content-Type", "application/json")
343 if cpage = atomic.AddInt64(&n, 1); cpage == max {
344 response, _ := json.Marshal(userResponseFull{
345 SlackResponse: sresp,
346 Members: members,
347 })
348 rw.Write(response)
349 return
350 }
351 response, _ := json.Marshal(userResponseFull{
352 SlackResponse: sresp,
353 Members: members,
354 Metadata: ResponseMetadata{Cursor: strconv.Itoa(int(cpage))},
355 })
356 rw.Write(response)
357 }
358 }
359
360 // returns n pages of users and sends rate limited errors in between successful pages.
361 func getUserPagesWithRateLimitErrors(max int64) func(rw http.ResponseWriter, r *http.Request) {
362 var n int64
363 doRateLimit := false
364 return func(rw http.ResponseWriter, r *http.Request) {
365 defer func() {
366 doRateLimit = !doRateLimit
367 }()
368 if doRateLimit {
369 rw.Header().Set("Retry-After", "1")
370 rw.WriteHeader(http.StatusTooManyRequests)
371 return
372 }
373 var cpage int64
374 sresp := SlackResponse{
375 Ok: true,
376 }
377 members := []User{
378 getTestUserWithId(fmt.Sprintf("U%03d", n)),
379 }
380 rw.Header().Set("Content-Type", "application/json")
381 if cpage = atomic.AddInt64(&n, 1); cpage == max {
382 response, _ := json.Marshal(userResponseFull{
383 SlackResponse: sresp,
384 Members: members,
385 })
386 rw.Write(response)
387 return
388 }
389 response, _ := json.Marshal(userResponseFull{
390 SlackResponse: sresp,
391 Members: members,
392 Metadata: ResponseMetadata{Cursor: strconv.Itoa(int(cpage))},
393 })
394 rw.Write(response)
395 }
396 }
397
398 func TestSetUserPhoto(t *testing.T) {
399 file, fileContent, teardown := createUserPhoto(t)
400 defer teardown()
401
402 params := UserSetPhotoParams{CropX: 0, CropY: 0, CropW: 32}
403
404 http.HandleFunc("/users.setPhoto", setUserPhotoHandler(fileContent, params))
405
406 once.Do(startServer)
407 api := New(validToken, OptionAPIURL("http://"+serverAddr+"/"))
408
409 err := api.SetUserPhoto(file.Name(), params)
410 if err != nil {
411 t.Fatalf("unexpected error: %+v\n", err)
412 }
413 }
414
415 func setUserPhotoHandler(wantBytes []byte, wantParams UserSetPhotoParams) http.HandlerFunc {
416 const maxMemory = 1 << 20 // 1 MB
417
418 return func(w http.ResponseWriter, r *http.Request) {
419 if err := r.ParseMultipartForm(maxMemory); err != nil {
420 httpTestErrReply(w, false, fmt.Sprintf("failed to parse multipart/form: %+v", err))
421 return
422 }
423
424 // Test for expected token
425 if v := r.Form.Get("token"); v != validToken {
426 httpTestErrReply(w, true, fmt.Sprintf("expected multipart form value token=%v", validToken))
427 return
428 }
429
430 // Test for expected crop params
431 if wantParams.CropX != DEFAULT_USER_PHOTO_CROP_X {
432 if cx, err := strconv.Atoi(r.Form.Get("crop_x")); err != nil || cx != wantParams.CropX {
433 httpTestErrReply(w, true, fmt.Sprintf("expected multipart form value crop_x=%d", wantParams.CropX))
434 return
435 }
436 }
437 if wantParams.CropY != DEFAULT_USER_PHOTO_CROP_Y {
438 if cy, err := strconv.Atoi(r.Form.Get("crop_y")); err != nil || cy != wantParams.CropY {
439 httpTestErrReply(w, true, fmt.Sprintf("expected multipart form value crop_y=%d", wantParams.CropY))
440 return
441 }
442 }
443 if wantParams.CropW != DEFAULT_USER_PHOTO_CROP_W {
444 if cw, err := strconv.Atoi(r.Form.Get("crop_w")); err != nil || cw != wantParams.CropW {
445 httpTestErrReply(w, true, fmt.Sprintf("expected multipart form value crop_w=%d", wantParams.CropW))
446 return
447 }
448 }
449
450 // Test for expected image
451 f, ok := r.MultipartForm.File["image"]
452 if !ok || len(f) == 0 {
453 httpTestErrReply(w, true, `expected multipart form file "image"`)
454 return
455 }
456 file, err := f[0].Open()
457 if err != nil {
458 httpTestErrReply(w, true, fmt.Sprintf("failed to open uploaded file: %+v", err))
459 return
460 }
461 gotBytes, err := ioutil.ReadAll(file)
462 if err != nil {
463 httpTestErrReply(w, true, fmt.Sprintf("failed to read uploaded file: %+v", err))
464 return
465 }
466 if !bytes.Equal(wantBytes, gotBytes) {
467 httpTestErrReply(w, true, "uploaded bytes did not match expected bytes")
468 return
469 }
470
471 w.Header().Set("Content-Type", "application/json")
472 fmt.Fprint(w, `{"ok":true}`)
473 }
474 }
475
476 // createUserPhoto generates a temp photo for testing. It returns the file handle, the file
477 // contents, and a function that can be called to remove the file.
478 func createUserPhoto(t *testing.T) (*os.File, []byte, func()) {
479 photo := image.NewRGBA(image.Rect(0, 0, 64, 64))
480 draw.Draw(photo, photo.Bounds(), image.Black, image.ZP, draw.Src)
481
482 f, err := ioutil.TempFile(os.TempDir(), "profile.png")
483 if err != nil {
484 t.Fatalf("failed to create test photo: %+v\n", err)
485 }
486
487 var buf bytes.Buffer
488 if err := png.Encode(io.MultiWriter(&buf, f), photo); err != nil {
489 t.Fatalf("failed to write test photo: %+v\n", err)
490 }
491
492 teardown := func() {
493 if err := os.Remove(f.Name()); err != nil {
494 t.Fatalf("failed to remove test photo: %+v\n", err)
495 }
496 }
497
498 return f, buf.Bytes(), teardown
499 }
500
501 func getUserProfileHandler(rw http.ResponseWriter, r *http.Request) {
502 rw.Header().Set("Content-Type", "application/json")
503 profile := getTestUserProfile()
504 resp, _ := json.Marshal(&getUserProfileResponse{
505 SlackResponse: SlackResponse{Ok: true},
506 Profile: &profile})
507 rw.Write(resp)
508 }
509
510 func TestGetUserProfile(t *testing.T) {
511 http.HandleFunc("/users.profile.get", getUserProfileHandler)
512 once.Do(startServer)
513 api := New("testing-token", OptionAPIURL("http://"+serverAddr+"/"))
514 profile, err := api.GetUserProfile("UXXXXXXXX", false)
515 if err != nil {
516 t.Fatalf("Unexpected error: %s", err)
517 }
518 exp := getTestUserProfile()
519 if profile.DisplayName != exp.DisplayName {
520 t.Fatalf(`profile.DisplayName = "%s", wanted "%s"`, profile.DisplayName, exp.DisplayName)
521 }
522 }
523
524 func TestSetFieldsMap(t *testing.T) {
525 p := &UserProfile{}
526 exp := map[string]UserProfileCustomField{
527 "Xxxxxx": getTestUserProfileCustomField(),
528 }
529 p.SetFieldsMap(exp)
530 act := p.FieldsMap()
531 if !reflect.DeepEqual(act, exp) {
532 t.Fatalf(`p.FieldsMap() = %v, wanted %v`, act, exp)
533 }
534 }
535
536 func TestUserProfileCustomFieldsUnmarshalJSON(t *testing.T) {
537 fields := &UserProfileCustomFields{}
538 if err := json.Unmarshal([]byte(`[]`), fields); err != nil {
539 t.Fatal(err)
540 }
541 if err := json.Unmarshal([]byte(`{
542 "Xxxxxx": {
543 "value": "test value",
544 "alt": ""
545 }
546 }`), fields); err != nil {
547 t.Fatal(err)
548 }
549 act := fields.ToMap()["Xxxxxx"].Value
550 exp := "test value"
551 if act != exp {
552 t.Fatalf(`fields.ToMap()["Xxxxxx"]["value"] = "%s", wanted "%s"`, act, exp)
553 }
554 }
555
556 func TestUserProfileCustomFieldsMarshalJSON(t *testing.T) {
557 fields := UserProfileCustomFields{}
558 b, err := json.Marshal(fields)
559 if err != nil {
560 t.Fatal(err)
561 }
562 if string(b) != "[]" {
563 t.Fatalf(`string(b) = "%s", wanted "[]"`, string(b))
564 }
565 fields = getTestUserProfileCustomFields()
566 if _, err := json.Marshal(fields); err != nil {
567 t.Fatal(err)
568 }
569 }
570
571 func TestUserProfileCustomFieldsToMap(t *testing.T) {
572 m := map[string]UserProfileCustomField{
573 "Xxxxxx": getTestUserProfileCustomField(),
574 }
575 fields := UserProfileCustomFields{fields: m}
576 act := fields.ToMap()
577 if !reflect.DeepEqual(act, m) {
578 t.Fatalf(`fields.ToMap() = %v, wanted %v`, act, m)
579 }
580 }
581
582 func TestUserProfileCustomFieldsLen(t *testing.T) {
583 fields := UserProfileCustomFields{
584 fields: map[string]UserProfileCustomField{
585 "Xxxxxx": getTestUserProfileCustomField(),
586 }}
587 if fields.Len() != 1 {
588 t.Fatalf(`fields.Len() = %d, wanted 1`, fields.Len())
589 }
590 }
591
592 func TestUserProfileCustomFieldsSetMap(t *testing.T) {
593 fields := UserProfileCustomFields{}
594 m := map[string]UserProfileCustomField{
595 "Xxxxxx": getTestUserProfileCustomField(),
596 }
597 fields.SetMap(m)
598 if !reflect.DeepEqual(fields.fields, m) {
599 t.Fatalf(`fields.fields = %v, wanted %v`, fields.fields, m)
600 }
601 }
602
603 func TestGetUsersHandlesRateLimit(t *testing.T) {
604 http.DefaultServeMux = new(http.ServeMux)
605 http.HandleFunc("/users.list", getUserPagesWithRateLimitErrors(4))
606
607 once.Do(startServer)
608 api := New("testing-token", OptionAPIURL("http://"+serverAddr+"/"))
609
610 users, err := api.GetUsers()
611 if err != nil {
612 t.Errorf("Unexpected error: %s", err)
613 return
614 }
615
616 if !reflect.DeepEqual([]User{
617 getTestUserWithId("U000"),
618 getTestUserWithId("U001"),
619 getTestUserWithId("U002"),
620 getTestUserWithId("U003"),
621 }, users) {
622 t.Fatal(ErrIncorrectResponse)
623 }
624 }
625
626 func TestGetUsersReturnsServerError(t *testing.T) {
627 http.DefaultServeMux = new(http.ServeMux)
628 http.HandleFunc("/users.list", func(w http.ResponseWriter, r *http.Request) {
629 w.WriteHeader(http.StatusInternalServerError)
630 })
631
632 once.Do(startServer)
633 api := New("testing-token", OptionAPIURL("http://"+serverAddr+"/"))
634
635 _, err := api.GetUsers()
636
637 if err == nil {
638 t.Errorf("Expected error but got nil")
639 return
640 }
641
642 expectedErr := "slack server error: 500 Internal Server Error"
643 if err.Error() != expectedErr {
644 t.Errorf("Expected: %s. Got: %s", expectedErr, err.Error())
645 }
646 }
0 package slack
1
2 import (
3 "bytes"
4 "encoding/json"
5 "net/http"
6
7 "github.com/pkg/errors"
8 )
9
10 type WebhookMessage struct {
11 Username string `json:"username,omitempty"`
12 IconEmoji string `json:"icon_emoji,omitempty"`
13 IconURL string `json:"icon_url,omitempty"`
14 Channel string `json:"channel,omitempty"`
15 ThreadTimestamp string `json:"thread_ts,omitempty"`
16 Text string `json:"text,omitempty"`
17 Attachments []Attachment `json:"attachments,omitempty"`
18 Parse string `json:"parse,omitempty"`
19 }
20
21 func PostWebhook(url string, msg *WebhookMessage) error {
22 return PostWebhookCustomHTTP(url, http.DefaultClient, msg)
23 }
24
25 func PostWebhookCustomHTTP(url string, httpClient *http.Client, msg *WebhookMessage) error {
26 raw, err := json.Marshal(msg)
27
28 if err != nil {
29 return errors.Wrap(err, "marshal failed")
30 }
31
32 response, err := httpClient.Post(url, "application/json", bytes.NewReader(raw))
33
34 if err != nil {
35 return errors.Wrap(err, "failed to post webhook")
36 }
37
38 return checkStatusCode(response, discard{})
39 }
0 package slack
1
2 import (
3 "encoding/json"
4 "net/http"
5 "reflect"
6 "testing"
7 )
8
9 func TestPostWebhook_OK(t *testing.T) {
10 once.Do(startServer)
11
12 var receivedPayload WebhookMessage
13
14 http.HandleFunc("/webhook", func(rw http.ResponseWriter, r *http.Request) {
15 rw.Header().Set("Content-Type", "application/json")
16
17 decoder := json.NewDecoder(r.Body)
18 err := decoder.Decode(&receivedPayload)
19 if err != nil {
20 t.Errorf("Request contained invalid JSON, %s", err)
21 }
22
23 response := []byte(`{}`)
24 rw.Write(response)
25 })
26
27 url := "http://" + serverAddr + "/webhook"
28
29 payload := &WebhookMessage{
30 Text: "Test Text",
31 Attachments: []Attachment{
32 {
33 Text: "Foo",
34 },
35 },
36 }
37
38 err := PostWebhook(url, payload)
39
40 if err != nil {
41 t.Errorf("Expected not to receive error: %s", err)
42 }
43
44 if !reflect.DeepEqual(payload, &receivedPayload) {
45 t.Errorf("Payload did not match\nwant: %#v\n got: %#v", payload, receivedPayload)
46 }
47 }
48
49 func TestPostWebhook_NotOK(t *testing.T) {
50 once.Do(startServer)
51
52 http.HandleFunc("/webhook2", func(rw http.ResponseWriter, r *http.Request) {
53 rw.WriteHeader(http.StatusInternalServerError)
54 rw.Write([]byte("500 - Something bad happened!"))
55 })
56
57 url := "http://" + serverAddr + "/webhook2"
58
59 err := PostWebhook(url, &WebhookMessage{})
60
61 if err == nil {
62 t.Errorf("Expected to receive error")
63 }
64 }
11
22 import (
33 "encoding/json"
4 "errors"
4 "net/url"
5 "sync"
56 "time"
67
7 "golang.org/x/net/websocket"
8 "github.com/gorilla/websocket"
89 )
910
1011 const (
1819 //
1920 // Create this element with Client's NewRTM() or NewRTMWithOptions(*RTMOptions)
2021 type RTM struct {
21 idGen IDGenerator
22 pings map[int]time.Time
22 // Client is the main API, embedded
23 Client
24
25 idGen IDGenerator
26 pingInterval time.Duration
27 pingDeadman *time.Timer
2328
2429 // Connection life-cycle
2530 conn *websocket.Conn
2631 IncomingEvents chan RTMEvent
2732 outgoingMessages chan OutgoingMessage
2833 killChannel chan bool
34 disconnected chan struct{}
35 disconnectedm *sync.Once
2936 forcePing chan bool
3037 rawEvents chan json.RawMessage
31 wasIntentional bool
32 isConnected bool
33
34 // Client is the main API, embedded
35 Client
36 websocketURL string
3738
3839 // UserDetails upon connection
3940 info *Info
4243 // rtm.start to connect to Slack, otherwise it will use
4344 // rtm.connect
4445 useRTMStart bool
46
47 // dialer is a gorilla/websocket Dialer. If nil, use the default
48 // Dialer.
49 dialer *websocket.Dialer
50
51 // mu is mutex used to prevent RTM connection race conditions
52 mu *sync.Mutex
53
54 // connParams is a map of flags for connection parameters.
55 connParams url.Values
4556 }
4657
47 // RTMOptions allows configuration of various options available for RTM messaging
48 //
49 // This structure will evolve in time so please make sure you are always using the
50 // named keys for every entry available as per Go 1 compatibility promise adding fields
51 // to this structure should not be considered a breaking change.
52 type RTMOptions struct {
53 // UseRTMStart set to true in order to use rtm.start or false to use rtm.connect
54 // As of 11th July 2017 you should prefer setting this to false, see:
55 // https://api.slack.com/changelog/2017-04-start-using-rtm-connect-and-stop-using-rtm-start
56 UseRTMStart bool
58 // signal that we are disconnected by closing the channel.
59 // protect it with a mutex to ensure it only happens once.
60 func (rtm *RTM) disconnect() {
61 rtm.disconnectedm.Do(func() {
62 close(rtm.disconnected)
63 })
5764 }
5865
5966 // Disconnect and wait, blocking until a successful disconnection.
6067 func (rtm *RTM) Disconnect() error {
61 if !rtm.isConnected {
62 return errors.New("Invalid call to Disconnect - Slack API is already disconnected")
68 // always push into the kill channel when invoked,
69 // this lets the ManagedConnection() function properly clean up.
70 // if the buffer is full then just continue on.
71 select {
72 case rtm.killChannel <- true:
73 return nil
74 case <-rtm.disconnected:
75 return ErrAlreadyDisconnected
6376 }
64 rtm.killChannel <- true
65 return nil
66 }
67
68 // Reconnect only makes sense if you've successfully disconnectd with Disconnect().
69 func (rtm *RTM) Reconnect() error {
70 logger.Println("RTM::Reconnect not implemented!")
71 return nil
7277 }
7378
7479 // GetInfo returns the info structure received when calling
75 // "startrtm", holding all channels, groups and other metadata needed
76 // to implement a full chat client. It will be non-nil after a call to
77 // StartRTM().
80 // "startrtm", holding metadata needed to implement a full
81 // chat client. It will be non-nil after a call to StartRTM().
7882 func (rtm *RTM) GetInfo() *Info {
7983 return rtm.info
8084 }
9094
9195 rtm.outgoingMessages <- *msg
9296 }
97
98 func (rtm *RTM) resetDeadman() {
99 rtm.pingDeadman.Reset(deadmanDuration(rtm.pingInterval))
100 }
101
102 func deadmanDuration(d time.Duration) time.Duration {
103 return d * 4
104 }
1717 // ConnectionErrorEvent contains information about a connection error
1818 type ConnectionErrorEvent struct {
1919 Attempt int
20 Backoff time.Duration // how long we'll wait before the next attempt
2021 ErrorObj error
2122 }
2223
3334 // DisconnectedEvent contains information about how we disconnected
3435 type DisconnectedEvent struct {
3536 Intentional bool
37 Cause error
3638 }
3739
3840 // LatencyReport contains information about connection latency
6264 return fmt.Sprintf("Message too long (max %d characters)", m.MaxLength)
6365 }
6466
67 // RateLimitEvent is used when Slack warns that rate-limits are being hit.
68 type RateLimitEvent struct{}
69
70 func (e *RateLimitEvent) Error() string {
71 return "Messages are being sent too fast."
72 }
73
6574 // OutgoingErrorEvent contains information in case there were errors sending messages
6675 type OutgoingErrorEvent struct {
6776 Message OutgoingMessage
33 "encoding/json"
44 "fmt"
55 "io"
6 "net/http"
7 stdurl "net/url"
68 "reflect"
79 "time"
810
9 "golang.org/x/net/websocket"
11 "github.com/gorilla/websocket"
12 "github.com/nlopes/slack/internal/errorsx"
13 "github.com/nlopes/slack/internal/timex"
1014 )
1115
1216 // ManageConnection can be called on a Slack RTM instance returned by the
2327 //
2428 // The defined error events are located in websocket_internals.go.
2529 func (rtm *RTM) ManageConnection() {
26 var connectionCount int
27 for {
28 connectionCount++
30 var (
31 err error
32 info *Info
33 conn *websocket.Conn
34 )
35
36 for connectionCount := 0; ; connectionCount++ {
2937 // start trying to connect
3038 // the returned err is already passed onto the IncomingEvents channel
31 info, conn, err := rtm.connect(connectionCount, rtm.useRTMStart)
32 // if err != nil then the connection is sucessful - otherwise it is
33 // fatal
34 if err != nil {
39 if info, conn, err = rtm.connect(connectionCount, rtm.useRTMStart); err != nil {
40 // when the connection is unsuccessful its fatal, and we need to bail out.
41 rtm.Debugf("Failed to connect with RTM on try %d: %s", connectionCount, err)
42 rtm.disconnect()
3543 return
3644 }
45
46 // lock to prevent data races with Disconnect particularly around isConnected
47 // and conn.
48 rtm.mu.Lock()
49 rtm.conn = conn
3750 rtm.info = info
51 rtm.mu.Unlock()
52
3853 rtm.IncomingEvents <- RTMEvent{"connected", &ConnectedEvent{
3954 ConnectionCount: connectionCount,
4055 Info: info,
4156 }}
4257
43 rtm.conn = conn
44 rtm.isConnected = true
45
46 keepRunning := make(chan bool)
47 // we're now connected (or have failed fatally) so we can set up
48 // listeners
49 go rtm.handleIncomingEvents(keepRunning)
58 rtm.Debugf("RTM connection succeeded on try %d", connectionCount)
59
60 // we're now connected so we can set up listeners
61 go rtm.handleIncomingEvents()
5062
5163 // this should be a blocking call until the connection has ended
52 rtm.handleEvents(keepRunning, 30*time.Second)
53
54 // after being disconnected we need to check if it was intentional
55 // if not then we should try to reconnect
56 if rtm.wasIntentional {
64 rtm.handleEvents()
65
66 select {
67 case <-rtm.disconnected:
68 // after handle events returns we need to check if we're disconnected
5769 return
58 }
59 // else continue and run the loop again to connect
70 default:
71 // otherwise continue and run the loop again to reconnect
72 }
6073 }
6174 }
6275
6679 // If useRTMStart is false then it uses rtm.connect to create the connection,
6780 // otherwise it uses rtm.start.
6881 func (rtm *RTM) connect(connectionCount int, useRTMStart bool) (*Info, *websocket.Conn, error) {
82 const (
83 errInvalidAuth = "invalid_auth"
84 errInactiveAccount = "account_inactive"
85 errMissingAuthToken = "not_authed"
86 )
87
6988 // used to provide exponential backoff wait time with jitter before trying
7089 // to connect to slack again
7190 boff := &backoff{
72 Min: 100 * time.Millisecond,
73 Max: 5 * time.Minute,
74 Factor: 2,
75 Jitter: true,
91 Max: 5 * time.Minute,
7692 }
7793
7894 for {
95 var (
96 backoff time.Duration
97 )
98
7999 // send connecting event
80100 rtm.IncomingEvents <- RTMEvent{"connecting", &ConnectingEvent{
81101 Attempt: boff.attempts + 1,
82102 ConnectionCount: connectionCount,
83103 }}
104
84105 // attempt to start the connection
85106 info, conn, err := rtm.startRTMAndDial(useRTMStart)
86107 if err == nil {
87108 return info, conn, nil
88109 }
89 // check for fatal errors - currently only invalid_auth
90 if sErr, ok := err.(*WebError); ok && (sErr.Error() == "invalid_auth" || sErr.Error() == "account_inactive") {
110
111 // check for fatal errors
112 switch err.Error() {
113 case errInvalidAuth, errInactiveAccount, errMissingAuthToken:
114 rtm.Debugf("invalid auth when connecting with RTM: %s", err)
91115 rtm.IncomingEvents <- RTMEvent{"invalid_auth", &InvalidAuthEvent{}}
92 return nil, nil, sErr
93 }
94
116 return nil, nil, err
117 default:
118 }
119
120 switch actual := err.(type) {
121 case statusCodeError:
122 if actual.Code == http.StatusNotFound {
123 rtm.Debugf("invalid auth when connecting with RTM: %s", err)
124 rtm.IncomingEvents <- RTMEvent{"invalid_auth", &InvalidAuthEvent{}}
125 return nil, nil, err
126 }
127 case *RateLimitedError:
128 backoff = actual.RetryAfter
129 default:
130 }
131
132 backoff = timex.Max(backoff, boff.Duration())
95133 // any other errors are treated as recoverable and we try again after
96134 // sending the event along the IncomingEvents channel
97135 rtm.IncomingEvents <- RTMEvent{"connection_error", &ConnectionErrorEvent{
98136 Attempt: boff.attempts,
137 Backoff: backoff,
99138 ErrorObj: err,
100139 }}
140
101141 // get time we should wait before attempting to connect again
102 dur := boff.Duration()
103 rtm.Debugf("reconnection %d failed: %s", boff.attempts+1, err)
104 rtm.Debugln(" -> reconnecting in", dur)
105 time.Sleep(dur)
142 rtm.Debugf("reconnection %d failed: %s reconnecting in %v\n", boff.attempts, err, backoff)
143
144 // wait for one of the following to occur,
145 // backoff duration has elapsed, killChannel is signalled, or
146 // the rtm finishes disconnecting.
147 select {
148 case <-time.After(backoff): // retry after the backoff.
149 case intentional := <-rtm.killChannel:
150 if intentional {
151 rtm.killConnection(intentional, ErrRTMDisconnected)
152 return nil, nil, ErrRTMDisconnected
153 }
154 case <-rtm.disconnected:
155 return nil, nil, ErrRTMDisconnected
156 }
106157 }
107158 }
108159
109160 // startRTMAndDial attempts to connect to the slack websocket. If useRTMStart is true,
110161 // then it returns the full information returned by the "rtm.start" method on the
111162 // slack API. Else it uses the "rtm.connect" method to connect
112 func (rtm *RTM) startRTMAndDial(useRTMStart bool) (*Info, *websocket.Conn, error) {
113 var info *Info
114 var url string
115 var err error
163 func (rtm *RTM) startRTMAndDial(useRTMStart bool) (info *Info, _ *websocket.Conn, err error) {
164 var (
165 url string
166 )
116167
117168 if useRTMStart {
169 rtm.Debugf("Starting RTM")
118170 info, url, err = rtm.StartRTM()
119171 } else {
172 rtm.Debugf("Connecting to RTM")
120173 info, url, err = rtm.ConnectRTM()
121174 }
175 if err != nil {
176 rtm.Debugf("Failed to start or connect to RTM: %s", err)
177 return nil, nil, err
178 }
179
180 // install connection parameters
181 u, err := stdurl.Parse(url)
122182 if err != nil {
123183 return nil, nil, err
124184 }
125
126 conn, err := websocketProxyDial(url, "http://api.slack.com")
185 u.RawQuery = rtm.connParams.Encode()
186 url = u.String()
187
188 rtm.Debugf("Dialing to websocket on url %s", url)
189 // Only use HTTPS for connections to prevent MITM attacks on the connection.
190 upgradeHeader := http.Header{}
191 upgradeHeader.Add("Origin", "https://api.slack.com")
192 dialer := websocket.DefaultDialer
193 if rtm.dialer != nil {
194 dialer = rtm.dialer
195 }
196 conn, _, err := dialer.Dial(url, upgradeHeader)
127197 if err != nil {
198 rtm.Debugf("Failed to dial to the websocket: %s", err)
128199 return nil, nil, err
129200 }
130201 return info, conn, err
135206 //
136207 // This should not be called directly! Instead a boolean value (true for
137208 // intentional, false otherwise) should be sent to the killChannel on the RTM.
138 func (rtm *RTM) killConnection(keepRunning chan bool, intentional bool) error {
209 func (rtm *RTM) killConnection(intentional bool, cause error) (err error) {
139210 rtm.Debugln("killing connection")
140 if rtm.isConnected {
141 close(keepRunning)
142 }
143 rtm.isConnected = false
144 rtm.wasIntentional = intentional
145 err := rtm.conn.Close()
146 rtm.IncomingEvents <- RTMEvent{"disconnected", &DisconnectedEvent{intentional}}
211
212 if rtm.conn != nil {
213 err = rtm.conn.Close()
214 }
215
216 rtm.IncomingEvents <- RTMEvent{"disconnected", &DisconnectedEvent{Intentional: intentional, Cause: cause}}
217
218 if intentional {
219 rtm.disconnect()
220 }
221
147222 return err
148223 }
149224
152227 // interval. This also sends outgoing messages that are received from the RTM's
153228 // outgoingMessages channel. This also handles incoming raw events from the RTM
154229 // rawEvents channel.
155 func (rtm *RTM) handleEvents(keepRunning chan bool, interval time.Duration) {
156 ticker := time.NewTicker(interval)
230 func (rtm *RTM) handleEvents() {
231 ticker := time.NewTicker(rtm.pingInterval)
157232 defer ticker.Stop()
158233 for {
159234 select {
160235 // catch "stop" signal on channel close
161236 case intentional := <-rtm.killChannel:
162 _ = rtm.killConnection(keepRunning, intentional)
237 _ = rtm.killConnection(intentional, errorsx.String("signaled"))
163238 return
164 // send pings on ticker interval
239 // detect when the connection is dead.
240 case <-rtm.pingDeadman.C:
241 _ = rtm.killConnection(false, errorsx.String("deadman switch triggered"))
242 return
243 // send pings on ticker interval
165244 case <-ticker.C:
166 err := rtm.ping()
167 if err != nil {
168 _ = rtm.killConnection(keepRunning, false)
245 if err := rtm.ping(); err != nil {
246 _ = rtm.killConnection(false, err)
169247 return
170248 }
171249 case <-rtm.forcePing:
172 err := rtm.ping()
173 if err != nil {
174 _ = rtm.killConnection(keepRunning, false)
250 if err := rtm.ping(); err != nil {
251 _ = rtm.killConnection(false, err)
175252 return
176253 }
177254 // listen for messages that need to be sent
179256 rtm.sendOutgoingMessage(msg)
180257 // listen for incoming messages that need to be parsed
181258 case rawEvent := <-rtm.rawEvents:
182 rtm.handleRawEvent(rawEvent)
259 switch rtm.handleRawEvent(rawEvent) {
260 case rtmEventTypeGoodbye:
261 _ = rtm.killConnection(false, errorsx.String("goodbye detected"))
262 return
263 default:
264 }
183265 }
184266 }
185267 }
189271 //
190272 // This will stop executing once the RTM's keepRunning channel has been closed
191273 // or has anything sent to it.
192 func (rtm *RTM) handleIncomingEvents(keepRunning <-chan bool) {
274 func (rtm *RTM) handleIncomingEvents() {
193275 for {
194 // non-blocking listen to see if channel is closed
195 select {
196 // catch "stop" signal on channel close
197 case <-keepRunning:
276 if err := rtm.receiveIncomingEvent(); err != nil {
198277 return
199 default:
200 rtm.receiveIncomingEvent()
201278 }
202279 }
203280 }
207284 if err := rtm.conn.SetWriteDeadline(time.Now().Add(10 * time.Second)); err != nil {
208285 return err
209286 }
210 if err := websocket.JSON.Send(rtm.conn, msg); err != nil {
287 if err := rtm.conn.WriteJSON(msg); err != nil {
211288 return err
212289 }
213290 // remove write deadline
220297 // and instead lets a future failed 'PING' detect the failed connection.
221298 func (rtm *RTM) sendOutgoingMessage(msg OutgoingMessage) {
222299 rtm.Debugln("Sending message:", msg)
223 if len(msg.Text) > MaxMessageTextLength {
300 if len([]rune(msg.Text)) > MaxMessageTextLength {
224301 rtm.IncomingEvents <- RTMEvent{"outgoing_error", &MessageTooLongEvent{
225302 Message: msg,
226303 MaxLength: MaxMessageTextLength,
233310 Message: msg,
234311 ErrorObj: err,
235312 }}
236 // TODO force ping?
237313 }
238314 }
239315
247323 func (rtm *RTM) ping() error {
248324 id := rtm.idGen.Next()
249325 rtm.Debugln("Sending PING ", id)
250 rtm.pings[id] = time.Now()
251
252 msg := &Ping{ID: id, Type: "ping"}
326 msg := &Ping{ID: id, Type: "ping", Timestamp: time.Now().Unix()}
253327
254328 if err := rtm.sendWithDeadline(msg); err != nil {
255329 rtm.Debugf("RTM Error sending 'PING %d': %s", id, err.Error())
260334
261335 // receiveIncomingEvent attempts to receive an event from the RTM's websocket.
262336 // This will block until a frame is available from the websocket.
263 func (rtm *RTM) receiveIncomingEvent() {
337 // If the read from the websocket results in a fatal error, this function will return non-nil.
338 func (rtm *RTM) receiveIncomingEvent() error {
264339 event := json.RawMessage{}
265 err := websocket.JSON.Receive(rtm.conn, &event)
266 if err == io.EOF {
340 err := rtm.conn.ReadJSON(&event)
341 switch {
342 case err == io.ErrUnexpectedEOF:
267343 // EOF's don't seem to signify a failed connection so instead we ignore
268344 // them here and detect a failed connection upon attempting to send a
269345 // 'PING' message
270346
271 // trigger a 'PING' to detect pontential websocket disconnect
272 rtm.forcePing <- true
273 return
274 } else if err != nil {
347 // trigger a 'PING' to detect potential websocket disconnect
348 select {
349 case rtm.forcePing <- true:
350 case <-rtm.disconnected:
351 }
352 case err != nil:
353 // All other errors from ReadJSON come from NextReader, and should
354 // kill the read loop and force a reconnect.
275355 rtm.IncomingEvents <- RTMEvent{"incoming_error", &IncomingEventError{
276356 ErrorObj: err,
277357 }}
278 // force a ping here too?
279 return
280 } else if len(event) == 0 {
358
359 select {
360 case rtm.killChannel <- false:
361 case <-rtm.disconnected:
362 }
363
364 return err
365 case len(event) == 0:
281366 rtm.Debugln("Received empty event")
282 return
283 }
284 rtm.Debugln("Incoming Event:", string(event[:]))
285 rtm.rawEvents <- event
367 default:
368 rtm.Debugln("Incoming Event:", string(event))
369 select {
370 case rtm.rawEvents <- event:
371 case <-rtm.disconnected:
372 rtm.Debugln("disonnected while attempting to send raw event")
373 }
374 }
375 return nil
286376 }
287377
288378 // handleRawEvent takes a raw JSON message received from the slack websocket
289379 // and handles the encoded event.
290 func (rtm *RTM) handleRawEvent(rawEvent json.RawMessage) {
380 // returns the event type of the message.
381 func (rtm *RTM) handleRawEvent(rawEvent json.RawMessage) string {
291382 event := &Event{}
292383 err := json.Unmarshal(rawEvent, event)
293384 if err != nil {
294385 rtm.IncomingEvents <- RTMEvent{"unmarshalling_error", &UnmarshallingErrorEvent{err}}
295 return
296 }
386 return ""
387 }
388
297389 switch event.Type {
298 case "":
390 case rtmEventTypeAck:
299391 rtm.handleAck(rawEvent)
300 case "hello":
392 case rtmEventTypeHello:
301393 rtm.IncomingEvents <- RTMEvent{"hello", &HelloEvent{}}
302 case "pong":
394 case rtmEventTypePong:
303395 rtm.handlePong(rawEvent)
396 case rtmEventTypeGoodbye:
397 // just return the event type up for goodbye, will be handled by caller.
398 case rtmEventTypeDesktopNotification:
399 rtm.Debugln("Received desktop notification, ignoring")
304400 default:
305401 rtm.handleEvent(event.Type, rawEvent)
306402 }
403
404 return event.Type
307405 }
308406
309407 // handleAck handles an incoming 'ACK' message.
314412 rtm.Debugln(" -> Erroneous 'ack' event:", string(event))
315413 return
316414 }
415
317416 if ack.Ok {
318417 rtm.IncomingEvents <- RTMEvent{"ack", ack}
418 } else if ack.RTMResponse.Error != nil {
419 // As there is no documentation for RTM error-codes, this
420 // identification of a rate-limit warning is very brittle.
421 if ack.RTMResponse.Error.Code == -1 && ack.RTMResponse.Error.Msg == "slow down, too many messages..." {
422 rtm.IncomingEvents <- RTMEvent{"ack_error", &RateLimitEvent{}}
423 } else {
424 rtm.IncomingEvents <- RTMEvent{"ack_error", &AckErrorEvent{ack.Error}}
425 }
319426 } else {
320 rtm.IncomingEvents <- RTMEvent{"ack_error", &AckErrorEvent{ack.Error}}
427 rtm.IncomingEvents <- RTMEvent{"ack_error", &AckErrorEvent{fmt.Errorf("ack decode failure")}}
321428 }
322429 }
323430
325432 // a previously sent 'PING' message. This is then used to compute the
326433 // connection's latency.
327434 func (rtm *RTM) handlePong(event json.RawMessage) {
328 pong := &Pong{}
329 if err := json.Unmarshal(event, pong); err != nil {
330 rtm.Debugln("RTM Error unmarshalling 'pong' event:", err)
331 rtm.Debugln(" -> Erroneous 'ping' event:", string(event))
435 var (
436 p Pong
437 )
438
439 rtm.resetDeadman()
440
441 if err := json.Unmarshal(event, &p); err != nil {
442 rtm.Client.log.Println("RTM Error unmarshalling 'pong' event:", err)
332443 return
333444 }
334 if pingTime, exists := rtm.pings[pong.ReplyTo]; exists {
335 latency := time.Since(pingTime)
336 rtm.IncomingEvents <- RTMEvent{"latency_report", &LatencyReport{Value: latency}}
337 delete(rtm.pings, pong.ReplyTo)
338 } else {
339 rtm.Debugln("RTM Error - unmatched 'pong' event:", string(event))
340 }
445
446 latency := time.Since(time.Unix(p.Timestamp, 0))
447 rtm.IncomingEvents <- RTMEvent{"latency_report", &LatencyReport{Value: latency}}
341448 }
342449
343450 // handleEvent is the "default" response to an event that does not have a
347454 // correct struct then this sends an UnmarshallingErrorEvent to the
348455 // IncomingEvents channel.
349456 func (rtm *RTM) handleEvent(typeStr string, event json.RawMessage) {
350 v, exists := eventMapping[typeStr]
457 v, exists := EventMapping[typeStr]
351458 if !exists {
352 rtm.Debugf("RTM Error, received unmapped event %q: %s\n", typeStr, string(event))
353 err := fmt.Errorf("RTM Error: Received unmapped event %q: %s\n", typeStr, string(event))
459 rtm.Debugf("RTM Error - received unmapped event %q: %s\n", typeStr, string(event))
460 err := fmt.Errorf("RTM Error: Received unmapped event %q: %s", typeStr, string(event))
354461 rtm.IncomingEvents <- RTMEvent{"unmarshalling_error", &UnmarshallingErrorEvent{err}}
355462 return
356463 }
359466 err := json.Unmarshal(event, recvEvent)
360467 if err != nil {
361468 rtm.Debugf("RTM Error, could not unmarshall event %q: %s\n", typeStr, string(event))
362 err := fmt.Errorf("RTM Error: Could not unmarshall event %q: %s\n", typeStr, string(event))
469 err := fmt.Errorf("RTM Error: Could not unmarshall event %q: %s", typeStr, string(event))
363470 rtm.IncomingEvents <- RTMEvent{"unmarshalling_error", &UnmarshallingErrorEvent{err}}
364471 return
365472 }
366473 rtm.IncomingEvents <- RTMEvent{typeStr, recvEvent}
367474 }
368475
369 // eventMapping holds a mapping of event names to their corresponding struct
476 // EventMapping holds a mapping of event names to their corresponding struct
370477 // implementations. The structs should be instances of the unmarshalling
371478 // target for the matching event type.
372 var eventMapping = map[string]interface{}{
479 var EventMapping = map[string]interface{}{
373480 "message": MessageEvent{},
374481 "presence_change": PresenceChangeEvent{},
375482 "user_typing": UserTypingEvent{},
447554 "accounts_changed": AccountsChangedEvent{},
448555
449556 "reconnect_url": ReconnectUrlEvent{},
450 }
557
558 "member_joined_channel": MemberJoinedChannelEvent{},
559 "member_left_channel": MemberLeftChannelEvent{},
560
561 "subteam_created": SubteamCreatedEvent{},
562 "subteam_self_added": SubteamSelfAddedEvent{},
563 "subteam_self_removed": SubteamSelfRemovedEvent{},
564 "subteam_updated": SubteamUpdatedEvent{},
565 }
0 package slack_test
1
2 import (
3 "fmt"
4 "net/http"
5 "testing"
6 "time"
7
8 "github.com/nlopes/slack"
9 "github.com/nlopes/slack/slacktest"
10 "github.com/stretchr/testify/assert"
11 )
12
13 const (
14 testMessage = "test message"
15 testToken = "TEST_TOKEN"
16 )
17
18 func TestRTMDisconnect(t *testing.T) {
19 // actually connect to slack here w/ an invalid token
20 api := slack.New(testToken)
21 rtm := api.NewRTM()
22 go rtm.ManageConnection()
23
24 // Observe incoming messages.
25 done := make(chan struct{})
26 connectingReceived := false
27 disconnectedReceived := false
28
29 go func() {
30 for msg := range rtm.IncomingEvents {
31 switch ev := msg.Data.(type) {
32 case *slack.InvalidAuthEvent:
33 t.Log("invalid auth event received")
34 disconnectedReceived = true
35 close(done)
36 case *slack.ConnectingEvent:
37 connectingReceived = true
38 case *slack.ConnectedEvent:
39 t.Error("received connected events on an invalid connection")
40 t.Fail()
41 default:
42 t.Logf("discarded event of type '%s' with content '%#v'", msg.Type, ev)
43 }
44 }
45 }()
46
47 select {
48 case <-done:
49 case <-time.After(5 * time.Second):
50 t.Error("timed out waiting for disconnect")
51 t.Fail()
52 }
53
54 // Verify that all expected events have been received by the RTM client.
55 assert.True(t, connectingReceived, "Should have received a connecting event from the RTM instance.")
56 assert.True(t, disconnectedReceived, "Should have received a disconnected event from the RTM instance.")
57 }
58
59 func TestRTMConnectRateLimit(t *testing.T) {
60 // Set up the test server.
61 testServer := slacktest.NewTestServer(
62 func(c slacktest.Customize) {
63 c.Handle("/rtm.connect", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
64 w.Header().Add("Retry-After", "1")
65 w.WriteHeader(http.StatusTooManyRequests)
66 }))
67 },
68 )
69 go testServer.Start()
70
71 // Setup and start the RTM.
72 api := slack.New(testToken, slack.OptionAPIURL(testServer.GetAPIURL()))
73 rtm := api.NewRTM()
74 go rtm.ManageConnection()
75
76 // Observe incoming failures
77 connectionFailure := make(chan *slack.ConnectionErrorEvent)
78 go func() {
79 for msg := range rtm.IncomingEvents {
80 switch ev := msg.Data.(type) {
81 case *slack.ConnectingEvent:
82 case *slack.ConnectionErrorEvent:
83 connectionFailure <- ev
84 if ev.Attempt > 5 {
85 rtm.Disconnect()
86 }
87 case *slack.DisconnectedEvent:
88 if ev.Intentional {
89 close(connectionFailure)
90 return
91 }
92 default:
93 t.Logf("Discarded event of type '%s' with content '%#v'", msg.Type, ev)
94 }
95 }
96 }()
97
98 previous := time.Duration(0)
99 for ev := range connectionFailure {
100 assert.True(t, previous <= ev.Backoff, fmt.Sprintf("backoff should increase during rate limits: %v <= %v", previous, ev.Backoff))
101 previous = ev.Backoff
102 }
103 testServer.Stop()
104 }
105
106 func TestRTMSingleConnect(t *testing.T) {
107 // Set up the test server.
108 testServer := slacktest.NewTestServer()
109 go testServer.Start()
110
111 // Setup and start the RTM.
112 api := slack.New(testToken, slack.OptionAPIURL(testServer.GetAPIURL()))
113 rtm := api.NewRTM()
114 go rtm.ManageConnection()
115
116 // Observe incoming messages.
117 done := make(chan struct{})
118 connectingReceived := false
119 connectedReceived := false
120 testMessageReceived := false
121 go func() {
122 for msg := range rtm.IncomingEvents {
123 switch ev := msg.Data.(type) {
124 case *slack.ConnectingEvent:
125 if connectingReceived {
126 t.Error("Received multiple connecting events.")
127 t.Fail()
128 }
129 connectingReceived = true
130 case *slack.ConnectedEvent:
131 if connectedReceived {
132 t.Error("Received multiple connected events.")
133 t.Fail()
134 }
135 connectedReceived = true
136 case *slack.MessageEvent:
137 if ev.Text == testMessage {
138 testMessageReceived = true
139 rtm.Disconnect()
140 }
141 t.Logf("Discarding message with content %+v", ev)
142 case *slack.DisconnectedEvent:
143 if ev.Intentional {
144 done <- struct{}{}
145 return
146 }
147 default:
148 t.Logf("Discarded event of type '%s' with content '%#v'", msg.Type, ev)
149 }
150 }
151 }()
152
153 // Send a message and sleep for some time to make sure the message can be processed client-side.
154 testServer.SendDirectMessageToBot(testMessage)
155 <-done
156 testServer.Stop()
157
158 // Verify that all expected events have been received by the RTM client.
159 assert.True(t, connectingReceived, "Should have received a connecting event from the RTM instance.")
160 assert.True(t, connectedReceived, "Should have received a connected event from the RTM instance.")
161 assert.True(t, testMessageReceived, "Should have received a test message from the server.")
162 }
4242
4343 // PresenceChangeEvent represents the presence change event
4444 type PresenceChangeEvent struct {
45 Type string `json:"type"`
46 Presence string `json:"presence"`
47 User string `json:"user"`
45 Type string `json:"type"`
46 Presence string `json:"presence"`
47 User string `json:"user"`
48 Users []string `json:"users"`
4849 }
4950
5051 // UserTypingEvent represents the user typing event
7980 SubType string `json:"subtype"`
8081 Name string `json:"name"`
8182 Names []string `json:"names"`
82 Value string `json:"value"`
83 Value string `json:"value"`
8384 EventTimestamp string `json:"event_ts"`
8485 }
8586
118119 Type string `json:"type"`
119120 URL string `json:"url"`
120121 }
122
123 // MemberJoinedChannelEvent, a user joined a public or private channel
124 type MemberJoinedChannelEvent struct {
125 Type string `json:"type"`
126 User string `json:"user"`
127 Channel string `json:"channel"`
128 ChannelType string `json:"channel_type"`
129 Team string `json:"team"`
130 Inviter string `json:"inviter"`
131 }
132
133 // MemberJoinedChannelEvent, a user left a public or private channel
134 type MemberLeftChannelEvent struct {
135 Type string `json:"type"`
136 User string `json:"user"`
137 Channel string `json:"channel"`
138 ChannelType string `json:"channel_type"`
139 Team string `json:"team"`
140 }
+0
-83
websocket_proxy.go less more
0 package slack
1
2 import (
3 "crypto/tls"
4 "errors"
5 "net"
6 "net/http"
7 "net/http/httputil"
8 "net/url"
9 "os"
10 "strings"
11
12 "golang.org/x/net/websocket"
13 )
14
15 // Taken and reworked from: https://gist.github.com/madmo/8548738
16 func websocketHTTPConnect(proxy, urlString string) (net.Conn, error) {
17 p, err := net.Dial("tcp", proxy)
18 if err != nil {
19 return nil, err
20 }
21
22 turl, err := url.Parse(urlString)
23 if err != nil {
24 return nil, err
25 }
26
27 req := http.Request{
28 Method: "CONNECT",
29 URL: &url.URL{},
30 Host: turl.Host,
31 }
32
33 cc := httputil.NewProxyClientConn(p, nil)
34 cc.Do(&req)
35 if err != nil && err != httputil.ErrPersistEOF {
36 return nil, err
37 }
38
39 rwc, _ := cc.Hijack()
40
41 return rwc, nil
42 }
43
44 func websocketProxyDial(urlString, origin string) (ws *websocket.Conn, err error) {
45 if os.Getenv("HTTP_PROXY") == "" {
46 return websocket.Dial(urlString, "", origin)
47 }
48
49 purl, err := url.Parse(os.Getenv("HTTP_PROXY"))
50 if err != nil {
51 return nil, err
52 }
53
54 config, err := websocket.NewConfig(urlString, origin)
55 if err != nil {
56 return nil, err
57 }
58
59 client, err := websocketHTTPConnect(purl.Host, urlString)
60 if err != nil {
61 return nil, err
62 }
63
64 switch config.Location.Scheme {
65 case "ws":
66 case "wss":
67 tlsClient := tls.Client(client, &tls.Config{
68 ServerName: strings.Split(config.Location.Host, ":")[0],
69 })
70 err := tlsClient.Handshake()
71 if err != nil {
72 tlsClient.Close()
73 return nil, err
74 }
75 client = tlsClient
76
77 default:
78 return nil, errors.New("invalid websocket schema")
79 }
80
81 return websocket.NewClient(config, client)
82 }
0 package slack
1
2 // SubteamCreatedEvent represents the Subteam created event
3 type SubteamCreatedEvent struct {
4 Type string `json:"type"`
5 Subteam UserGroup `json:"subteam"`
6 }
7
8 // SubteamCreatedEvent represents the membership of an existing User Group has changed event
9 type SubteamMembersChangedEvent struct {
10 Type string `json:"type"`
11 SubteamID string `json:"subteam_id"`
12 TeamID string `json:"team_id"`
13 DatePreviousUpdate JSONTime `json:"date_previous_update"`
14 DateUpdate JSONTime `json:"date_update"`
15 AddedUsers []string `json:"added_users"`
16 AddedUsersCount string `json:"added_users_count"`
17 RemovedUsers []string `json:"removed_users"`
18 RemovedUsersCount string `json:"removed_users_count"`
19 }
20
21 // SubteamSelfAddedEvent represents an event of you have been added to a User Group
22 type SubteamSelfAddedEvent struct {
23 Type string `json:"type"`
24 SubteamID string `json:"subteam_id"`
25 }
26
27 // SubteamSelfRemovedEvent represents an event of you have been removed from a User Group
28 type SubteamSelfRemovedEvent SubteamSelfAddedEvent
29
30 // SubteamUpdatedEvent represents an event of an existing User Group has been updated or its members changed
31 type SubteamUpdatedEvent struct {
32 Type string `json:"type"`
33 Subteam UserGroup `json:"subteam"`
34 }
+0
-20
websocket_utils.go less more
0 package slack
1
2 import (
3 "net"
4 "net/url"
5 )
6
7 var portMapping = map[string]string{"ws": "80", "wss": "443"}
8
9 func websocketizeURLPort(orig string) (string, error) {
10 urlObj, err := url.ParseRequestURI(orig)
11 if err != nil {
12 return "", err
13 }
14 _, _, err = net.SplitHostPort(urlObj.Host)
15 if err != nil {
16 return urlObj.Scheme + "://" + urlObj.Host + ":" + portMapping[urlObj.Scheme] + urlObj.Path, nil
17 }
18 return orig, nil
19 }