Codebase list golang-github-nlopes-slack / run/33fb859d-7349-4038-abd4-9c43e3922006/main
New upstream release. Debian Janitor 1 year, 2 months ago
178 changed file(s) with 21143 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 golang-github-nlopes-slack (0.6.0-1) UNRELEASED; urgency=low
1
2 * New upstream release.
3
4 -- Debian Janitor <janitor@jelmer.uk> Mon, 16 Jan 2023 17:01:00 -0000
5
06 golang-github-nlopes-slack (0.1.0-1) unstable; urgency=medium
17
28 * Initial release (Closes: #892804)
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 ISC License
1
2 Copyright (c) 2012-2016 Dave Collins <dave@davec.name>
3
4 Permission to use, copy, modify, and/or distribute this software for any
5 purpose with or without fee is hereby granted, provided that the above
6 copyright notice and this permission notice appear in all copies.
7
8 THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
9 WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
10 MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
11 ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
12 WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
13 ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
14 OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
0 // Copyright (c) 2015-2016 Dave Collins <dave@davec.name>
1 //
2 // Permission to use, copy, modify, and distribute this software for any
3 // purpose with or without fee is hereby granted, provided that the above
4 // copyright notice and this permission notice appear in all copies.
5 //
6 // THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
7 // WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
8 // MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
9 // ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
10 // WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
11 // ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
12 // OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
13
14 // NOTE: Due to the following build constraints, this file will only be compiled
15 // when the code is not running on Google App Engine, compiled by GopherJS, and
16 // "-tags safe" is not added to the go build command line. The "disableunsafe"
17 // tag is deprecated and thus should not be used.
18 // Go versions prior to 1.4 are disabled because they use a different layout
19 // for interfaces which make the implementation of unsafeReflectValue more complex.
20 // +build !js,!appengine,!safe,!disableunsafe,go1.4
21
22 package spew
23
24 import (
25 "reflect"
26 "unsafe"
27 )
28
29 const (
30 // UnsafeDisabled is a build-time constant which specifies whether or
31 // not access to the unsafe package is available.
32 UnsafeDisabled = false
33
34 // ptrSize is the size of a pointer on the current arch.
35 ptrSize = unsafe.Sizeof((*byte)(nil))
36 )
37
38 type flag uintptr
39
40 var (
41 // flagRO indicates whether the value field of a reflect.Value
42 // is read-only.
43 flagRO flag
44
45 // flagAddr indicates whether the address of the reflect.Value's
46 // value may be taken.
47 flagAddr flag
48 )
49
50 // flagKindMask holds the bits that make up the kind
51 // part of the flags field. In all the supported versions,
52 // it is in the lower 5 bits.
53 const flagKindMask = flag(0x1f)
54
55 // Different versions of Go have used different
56 // bit layouts for the flags type. This table
57 // records the known combinations.
58 var okFlags = []struct {
59 ro, addr flag
60 }{{
61 // From Go 1.4 to 1.5
62 ro: 1 << 5,
63 addr: 1 << 7,
64 }, {
65 // Up to Go tip.
66 ro: 1<<5 | 1<<6,
67 addr: 1 << 8,
68 }}
69
70 var flagValOffset = func() uintptr {
71 field, ok := reflect.TypeOf(reflect.Value{}).FieldByName("flag")
72 if !ok {
73 panic("reflect.Value has no flag field")
74 }
75 return field.Offset
76 }()
77
78 // flagField returns a pointer to the flag field of a reflect.Value.
79 func flagField(v *reflect.Value) *flag {
80 return (*flag)(unsafe.Pointer(uintptr(unsafe.Pointer(v)) + flagValOffset))
81 }
82
83 // unsafeReflectValue converts the passed reflect.Value into a one that bypasses
84 // the typical safety restrictions preventing access to unaddressable and
85 // unexported data. It works by digging the raw pointer to the underlying
86 // value out of the protected value and generating a new unprotected (unsafe)
87 // reflect.Value to it.
88 //
89 // This allows us to check for implementations of the Stringer and error
90 // interfaces to be used for pretty printing ordinarily unaddressable and
91 // inaccessible values such as unexported struct fields.
92 func unsafeReflectValue(v reflect.Value) reflect.Value {
93 if !v.IsValid() || (v.CanInterface() && v.CanAddr()) {
94 return v
95 }
96 flagFieldPtr := flagField(&v)
97 *flagFieldPtr &^= flagRO
98 *flagFieldPtr |= flagAddr
99 return v
100 }
101
102 // Sanity checks against future reflect package changes
103 // to the type or semantics of the Value.flag field.
104 func init() {
105 field, ok := reflect.TypeOf(reflect.Value{}).FieldByName("flag")
106 if !ok {
107 panic("reflect.Value has no flag field")
108 }
109 if field.Type.Kind() != reflect.TypeOf(flag(0)).Kind() {
110 panic("reflect.Value flag field has changed kind")
111 }
112 type t0 int
113 var t struct {
114 A t0
115 // t0 will have flagEmbedRO set.
116 t0
117 // a will have flagStickyRO set
118 a t0
119 }
120 vA := reflect.ValueOf(t).FieldByName("A")
121 va := reflect.ValueOf(t).FieldByName("a")
122 vt0 := reflect.ValueOf(t).FieldByName("t0")
123
124 // Infer flagRO from the difference between the flags
125 // for the (otherwise identical) fields in t.
126 flagPublic := *flagField(&vA)
127 flagWithRO := *flagField(&va) | *flagField(&vt0)
128 flagRO = flagPublic ^ flagWithRO
129
130 // Infer flagAddr from the difference between a value
131 // taken from a pointer and not.
132 vPtrA := reflect.ValueOf(&t).Elem().FieldByName("A")
133 flagNoPtr := *flagField(&vA)
134 flagPtr := *flagField(&vPtrA)
135 flagAddr = flagNoPtr ^ flagPtr
136
137 // Check that the inferred flags tally with one of the known versions.
138 for _, f := range okFlags {
139 if flagRO == f.ro && flagAddr == f.addr {
140 return
141 }
142 }
143 panic("reflect.Value read-only flag has changed semantics")
144 }
0 // Copyright (c) 2015-2016 Dave Collins <dave@davec.name>
1 //
2 // Permission to use, copy, modify, and distribute this software for any
3 // purpose with or without fee is hereby granted, provided that the above
4 // copyright notice and this permission notice appear in all copies.
5 //
6 // THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
7 // WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
8 // MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
9 // ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
10 // WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
11 // ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
12 // OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
13
14 // NOTE: Due to the following build constraints, this file will only be compiled
15 // when the code is running on Google App Engine, compiled by GopherJS, or
16 // "-tags safe" is added to the go build command line. The "disableunsafe"
17 // tag is deprecated and thus should not be used.
18 // +build js appengine safe disableunsafe !go1.4
19
20 package spew
21
22 import "reflect"
23
24 const (
25 // UnsafeDisabled is a build-time constant which specifies whether or
26 // not access to the unsafe package is available.
27 UnsafeDisabled = true
28 )
29
30 // unsafeReflectValue typically converts the passed reflect.Value into a one
31 // that bypasses the typical safety restrictions preventing access to
32 // unaddressable and unexported data. However, doing this relies on access to
33 // the unsafe package. This is a stub version which simply returns the passed
34 // reflect.Value when the unsafe package is not available.
35 func unsafeReflectValue(v reflect.Value) reflect.Value {
36 return v
37 }
0 /*
1 * Copyright (c) 2013-2016 Dave Collins <dave@davec.name>
2 *
3 * Permission to use, copy, modify, and distribute this software for any
4 * purpose with or without fee is hereby granted, provided that the above
5 * copyright notice and this permission notice appear in all copies.
6 *
7 * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
8 * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
9 * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
10 * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
11 * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
12 * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
13 * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
14 */
15
16 package spew
17
18 import (
19 "bytes"
20 "fmt"
21 "io"
22 "reflect"
23 "sort"
24 "strconv"
25 )
26
27 // Some constants in the form of bytes to avoid string overhead. This mirrors
28 // the technique used in the fmt package.
29 var (
30 panicBytes = []byte("(PANIC=")
31 plusBytes = []byte("+")
32 iBytes = []byte("i")
33 trueBytes = []byte("true")
34 falseBytes = []byte("false")
35 interfaceBytes = []byte("(interface {})")
36 commaNewlineBytes = []byte(",\n")
37 newlineBytes = []byte("\n")
38 openBraceBytes = []byte("{")
39 openBraceNewlineBytes = []byte("{\n")
40 closeBraceBytes = []byte("}")
41 asteriskBytes = []byte("*")
42 colonBytes = []byte(":")
43 colonSpaceBytes = []byte(": ")
44 openParenBytes = []byte("(")
45 closeParenBytes = []byte(")")
46 spaceBytes = []byte(" ")
47 pointerChainBytes = []byte("->")
48 nilAngleBytes = []byte("<nil>")
49 maxNewlineBytes = []byte("<max depth reached>\n")
50 maxShortBytes = []byte("<max>")
51 circularBytes = []byte("<already shown>")
52 circularShortBytes = []byte("<shown>")
53 invalidAngleBytes = []byte("<invalid>")
54 openBracketBytes = []byte("[")
55 closeBracketBytes = []byte("]")
56 percentBytes = []byte("%")
57 precisionBytes = []byte(".")
58 openAngleBytes = []byte("<")
59 closeAngleBytes = []byte(">")
60 openMapBytes = []byte("map[")
61 closeMapBytes = []byte("]")
62 lenEqualsBytes = []byte("len=")
63 capEqualsBytes = []byte("cap=")
64 )
65
66 // hexDigits is used to map a decimal value to a hex digit.
67 var hexDigits = "0123456789abcdef"
68
69 // catchPanic handles any panics that might occur during the handleMethods
70 // calls.
71 func catchPanic(w io.Writer, v reflect.Value) {
72 if err := recover(); err != nil {
73 w.Write(panicBytes)
74 fmt.Fprintf(w, "%v", err)
75 w.Write(closeParenBytes)
76 }
77 }
78
79 // handleMethods attempts to call the Error and String methods on the underlying
80 // type the passed reflect.Value represents and outputes the result to Writer w.
81 //
82 // It handles panics in any called methods by catching and displaying the error
83 // as the formatted value.
84 func handleMethods(cs *ConfigState, w io.Writer, v reflect.Value) (handled bool) {
85 // We need an interface to check if the type implements the error or
86 // Stringer interface. However, the reflect package won't give us an
87 // interface on certain things like unexported struct fields in order
88 // to enforce visibility rules. We use unsafe, when it's available,
89 // to bypass these restrictions since this package does not mutate the
90 // values.
91 if !v.CanInterface() {
92 if UnsafeDisabled {
93 return false
94 }
95
96 v = unsafeReflectValue(v)
97 }
98
99 // Choose whether or not to do error and Stringer interface lookups against
100 // the base type or a pointer to the base type depending on settings.
101 // Technically calling one of these methods with a pointer receiver can
102 // mutate the value, however, types which choose to satisify an error or
103 // Stringer interface with a pointer receiver should not be mutating their
104 // state inside these interface methods.
105 if !cs.DisablePointerMethods && !UnsafeDisabled && !v.CanAddr() {
106 v = unsafeReflectValue(v)
107 }
108 if v.CanAddr() {
109 v = v.Addr()
110 }
111
112 // Is it an error or Stringer?
113 switch iface := v.Interface().(type) {
114 case error:
115 defer catchPanic(w, v)
116 if cs.ContinueOnMethod {
117 w.Write(openParenBytes)
118 w.Write([]byte(iface.Error()))
119 w.Write(closeParenBytes)
120 w.Write(spaceBytes)
121 return false
122 }
123
124 w.Write([]byte(iface.Error()))
125 return true
126
127 case fmt.Stringer:
128 defer catchPanic(w, v)
129 if cs.ContinueOnMethod {
130 w.Write(openParenBytes)
131 w.Write([]byte(iface.String()))
132 w.Write(closeParenBytes)
133 w.Write(spaceBytes)
134 return false
135 }
136 w.Write([]byte(iface.String()))
137 return true
138 }
139 return false
140 }
141
142 // printBool outputs a boolean value as true or false to Writer w.
143 func printBool(w io.Writer, val bool) {
144 if val {
145 w.Write(trueBytes)
146 } else {
147 w.Write(falseBytes)
148 }
149 }
150
151 // printInt outputs a signed integer value to Writer w.
152 func printInt(w io.Writer, val int64, base int) {
153 w.Write([]byte(strconv.FormatInt(val, base)))
154 }
155
156 // printUint outputs an unsigned integer value to Writer w.
157 func printUint(w io.Writer, val uint64, base int) {
158 w.Write([]byte(strconv.FormatUint(val, base)))
159 }
160
161 // printFloat outputs a floating point value using the specified precision,
162 // which is expected to be 32 or 64bit, to Writer w.
163 func printFloat(w io.Writer, val float64, precision int) {
164 w.Write([]byte(strconv.FormatFloat(val, 'g', -1, precision)))
165 }
166
167 // printComplex outputs a complex value using the specified float precision
168 // for the real and imaginary parts to Writer w.
169 func printComplex(w io.Writer, c complex128, floatPrecision int) {
170 r := real(c)
171 w.Write(openParenBytes)
172 w.Write([]byte(strconv.FormatFloat(r, 'g', -1, floatPrecision)))
173 i := imag(c)
174 if i >= 0 {
175 w.Write(plusBytes)
176 }
177 w.Write([]byte(strconv.FormatFloat(i, 'g', -1, floatPrecision)))
178 w.Write(iBytes)
179 w.Write(closeParenBytes)
180 }
181
182 // printHexPtr outputs a uintptr formatted as hexadecimal with a leading '0x'
183 // prefix to Writer w.
184 func printHexPtr(w io.Writer, p uintptr) {
185 // Null pointer.
186 num := uint64(p)
187 if num == 0 {
188 w.Write(nilAngleBytes)
189 return
190 }
191
192 // Max uint64 is 16 bytes in hex + 2 bytes for '0x' prefix
193 buf := make([]byte, 18)
194
195 // It's simpler to construct the hex string right to left.
196 base := uint64(16)
197 i := len(buf) - 1
198 for num >= base {
199 buf[i] = hexDigits[num%base]
200 num /= base
201 i--
202 }
203 buf[i] = hexDigits[num]
204
205 // Add '0x' prefix.
206 i--
207 buf[i] = 'x'
208 i--
209 buf[i] = '0'
210
211 // Strip unused leading bytes.
212 buf = buf[i:]
213 w.Write(buf)
214 }
215
216 // valuesSorter implements sort.Interface to allow a slice of reflect.Value
217 // elements to be sorted.
218 type valuesSorter struct {
219 values []reflect.Value
220 strings []string // either nil or same len and values
221 cs *ConfigState
222 }
223
224 // newValuesSorter initializes a valuesSorter instance, which holds a set of
225 // surrogate keys on which the data should be sorted. It uses flags in
226 // ConfigState to decide if and how to populate those surrogate keys.
227 func newValuesSorter(values []reflect.Value, cs *ConfigState) sort.Interface {
228 vs := &valuesSorter{values: values, cs: cs}
229 if canSortSimply(vs.values[0].Kind()) {
230 return vs
231 }
232 if !cs.DisableMethods {
233 vs.strings = make([]string, len(values))
234 for i := range vs.values {
235 b := bytes.Buffer{}
236 if !handleMethods(cs, &b, vs.values[i]) {
237 vs.strings = nil
238 break
239 }
240 vs.strings[i] = b.String()
241 }
242 }
243 if vs.strings == nil && cs.SpewKeys {
244 vs.strings = make([]string, len(values))
245 for i := range vs.values {
246 vs.strings[i] = Sprintf("%#v", vs.values[i].Interface())
247 }
248 }
249 return vs
250 }
251
252 // canSortSimply tests whether a reflect.Kind is a primitive that can be sorted
253 // directly, or whether it should be considered for sorting by surrogate keys
254 // (if the ConfigState allows it).
255 func canSortSimply(kind reflect.Kind) bool {
256 // This switch parallels valueSortLess, except for the default case.
257 switch kind {
258 case reflect.Bool:
259 return true
260 case reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64, reflect.Int:
261 return true
262 case reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uint:
263 return true
264 case reflect.Float32, reflect.Float64:
265 return true
266 case reflect.String:
267 return true
268 case reflect.Uintptr:
269 return true
270 case reflect.Array:
271 return true
272 }
273 return false
274 }
275
276 // Len returns the number of values in the slice. It is part of the
277 // sort.Interface implementation.
278 func (s *valuesSorter) Len() int {
279 return len(s.values)
280 }
281
282 // Swap swaps the values at the passed indices. It is part of the
283 // sort.Interface implementation.
284 func (s *valuesSorter) Swap(i, j int) {
285 s.values[i], s.values[j] = s.values[j], s.values[i]
286 if s.strings != nil {
287 s.strings[i], s.strings[j] = s.strings[j], s.strings[i]
288 }
289 }
290
291 // valueSortLess returns whether the first value should sort before the second
292 // value. It is used by valueSorter.Less as part of the sort.Interface
293 // implementation.
294 func valueSortLess(a, b reflect.Value) bool {
295 switch a.Kind() {
296 case reflect.Bool:
297 return !a.Bool() && b.Bool()
298 case reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64, reflect.Int:
299 return a.Int() < b.Int()
300 case reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uint:
301 return a.Uint() < b.Uint()
302 case reflect.Float32, reflect.Float64:
303 return a.Float() < b.Float()
304 case reflect.String:
305 return a.String() < b.String()
306 case reflect.Uintptr:
307 return a.Uint() < b.Uint()
308 case reflect.Array:
309 // Compare the contents of both arrays.
310 l := a.Len()
311 for i := 0; i < l; i++ {
312 av := a.Index(i)
313 bv := b.Index(i)
314 if av.Interface() == bv.Interface() {
315 continue
316 }
317 return valueSortLess(av, bv)
318 }
319 }
320 return a.String() < b.String()
321 }
322
323 // Less returns whether the value at index i should sort before the
324 // value at index j. It is part of the sort.Interface implementation.
325 func (s *valuesSorter) Less(i, j int) bool {
326 if s.strings == nil {
327 return valueSortLess(s.values[i], s.values[j])
328 }
329 return s.strings[i] < s.strings[j]
330 }
331
332 // sortValues is a sort function that handles both native types and any type that
333 // can be converted to error or Stringer. Other inputs are sorted according to
334 // their Value.String() value to ensure display stability.
335 func sortValues(values []reflect.Value, cs *ConfigState) {
336 if len(values) == 0 {
337 return
338 }
339 sort.Sort(newValuesSorter(values, cs))
340 }
0 /*
1 * Copyright (c) 2013-2016 Dave Collins <dave@davec.name>
2 *
3 * Permission to use, copy, modify, and distribute this software for any
4 * purpose with or without fee is hereby granted, provided that the above
5 * copyright notice and this permission notice appear in all copies.
6 *
7 * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
8 * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
9 * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
10 * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
11 * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
12 * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
13 * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
14 */
15
16 package spew
17
18 import (
19 "bytes"
20 "fmt"
21 "io"
22 "os"
23 )
24
25 // ConfigState houses the configuration options used by spew to format and
26 // display values. There is a global instance, Config, that is used to control
27 // all top-level Formatter and Dump functionality. Each ConfigState instance
28 // provides methods equivalent to the top-level functions.
29 //
30 // The zero value for ConfigState provides no indentation. You would typically
31 // want to set it to a space or a tab.
32 //
33 // Alternatively, you can use NewDefaultConfig to get a ConfigState instance
34 // with default settings. See the documentation of NewDefaultConfig for default
35 // values.
36 type ConfigState struct {
37 // Indent specifies the string to use for each indentation level. The
38 // global config instance that all top-level functions use set this to a
39 // single space by default. If you would like more indentation, you might
40 // set this to a tab with "\t" or perhaps two spaces with " ".
41 Indent string
42
43 // MaxDepth controls the maximum number of levels to descend into nested
44 // data structures. The default, 0, means there is no limit.
45 //
46 // NOTE: Circular data structures are properly detected, so it is not
47 // necessary to set this value unless you specifically want to limit deeply
48 // nested data structures.
49 MaxDepth int
50
51 // DisableMethods specifies whether or not error and Stringer interfaces are
52 // invoked for types that implement them.
53 DisableMethods bool
54
55 // DisablePointerMethods specifies whether or not to check for and invoke
56 // error and Stringer interfaces on types which only accept a pointer
57 // receiver when the current type is not a pointer.
58 //
59 // NOTE: This might be an unsafe action since calling one of these methods
60 // with a pointer receiver could technically mutate the value, however,
61 // in practice, types which choose to satisify an error or Stringer
62 // interface with a pointer receiver should not be mutating their state
63 // inside these interface methods. As a result, this option relies on
64 // access to the unsafe package, so it will not have any effect when
65 // running in environments without access to the unsafe package such as
66 // Google App Engine or with the "safe" build tag specified.
67 DisablePointerMethods bool
68
69 // DisablePointerAddresses specifies whether to disable the printing of
70 // pointer addresses. This is useful when diffing data structures in tests.
71 DisablePointerAddresses bool
72
73 // DisableCapacities specifies whether to disable the printing of capacities
74 // for arrays, slices, maps and channels. This is useful when diffing
75 // data structures in tests.
76 DisableCapacities bool
77
78 // ContinueOnMethod specifies whether or not recursion should continue once
79 // a custom error or Stringer interface is invoked. The default, false,
80 // means it will print the results of invoking the custom error or Stringer
81 // interface and return immediately instead of continuing to recurse into
82 // the internals of the data type.
83 //
84 // NOTE: This flag does not have any effect if method invocation is disabled
85 // via the DisableMethods or DisablePointerMethods options.
86 ContinueOnMethod bool
87
88 // SortKeys specifies map keys should be sorted before being printed. Use
89 // this to have a more deterministic, diffable output. Note that only
90 // native types (bool, int, uint, floats, uintptr and string) and types
91 // that support the error or Stringer interfaces (if methods are
92 // enabled) are supported, with other types sorted according to the
93 // reflect.Value.String() output which guarantees display stability.
94 SortKeys bool
95
96 // SpewKeys specifies that, as a last resort attempt, map keys should
97 // be spewed to strings and sorted by those strings. This is only
98 // considered if SortKeys is true.
99 SpewKeys bool
100 }
101
102 // Config is the active configuration of the top-level functions.
103 // The configuration can be changed by modifying the contents of spew.Config.
104 var Config = ConfigState{Indent: " "}
105
106 // Errorf is a wrapper for fmt.Errorf that treats each argument as if it were
107 // passed with a Formatter interface returned by c.NewFormatter. It returns
108 // the formatted string as a value that satisfies error. See NewFormatter
109 // for formatting details.
110 //
111 // This function is shorthand for the following syntax:
112 //
113 // fmt.Errorf(format, c.NewFormatter(a), c.NewFormatter(b))
114 func (c *ConfigState) Errorf(format string, a ...interface{}) (err error) {
115 return fmt.Errorf(format, c.convertArgs(a)...)
116 }
117
118 // Fprint is a wrapper for fmt.Fprint that treats each argument as if it were
119 // passed with a Formatter interface returned by c.NewFormatter. It returns
120 // the number of bytes written and any write error encountered. See
121 // NewFormatter for formatting details.
122 //
123 // This function is shorthand for the following syntax:
124 //
125 // fmt.Fprint(w, c.NewFormatter(a), c.NewFormatter(b))
126 func (c *ConfigState) Fprint(w io.Writer, a ...interface{}) (n int, err error) {
127 return fmt.Fprint(w, c.convertArgs(a)...)
128 }
129
130 // Fprintf is a wrapper for fmt.Fprintf that treats each argument as if it were
131 // passed with a Formatter interface returned by c.NewFormatter. It returns
132 // the number of bytes written and any write error encountered. See
133 // NewFormatter for formatting details.
134 //
135 // This function is shorthand for the following syntax:
136 //
137 // fmt.Fprintf(w, format, c.NewFormatter(a), c.NewFormatter(b))
138 func (c *ConfigState) Fprintf(w io.Writer, format string, a ...interface{}) (n int, err error) {
139 return fmt.Fprintf(w, format, c.convertArgs(a)...)
140 }
141
142 // Fprintln is a wrapper for fmt.Fprintln that treats each argument as if it
143 // passed with a Formatter interface returned by c.NewFormatter. See
144 // NewFormatter for formatting details.
145 //
146 // This function is shorthand for the following syntax:
147 //
148 // fmt.Fprintln(w, c.NewFormatter(a), c.NewFormatter(b))
149 func (c *ConfigState) Fprintln(w io.Writer, a ...interface{}) (n int, err error) {
150 return fmt.Fprintln(w, c.convertArgs(a)...)
151 }
152
153 // Print is a wrapper for fmt.Print that treats each argument as if it were
154 // passed with a Formatter interface returned by c.NewFormatter. It returns
155 // the number of bytes written and any write error encountered. See
156 // NewFormatter for formatting details.
157 //
158 // This function is shorthand for the following syntax:
159 //
160 // fmt.Print(c.NewFormatter(a), c.NewFormatter(b))
161 func (c *ConfigState) Print(a ...interface{}) (n int, err error) {
162 return fmt.Print(c.convertArgs(a)...)
163 }
164
165 // Printf is a wrapper for fmt.Printf that treats each argument as if it were
166 // passed with a Formatter interface returned by c.NewFormatter. It returns
167 // the number of bytes written and any write error encountered. See
168 // NewFormatter for formatting details.
169 //
170 // This function is shorthand for the following syntax:
171 //
172 // fmt.Printf(format, c.NewFormatter(a), c.NewFormatter(b))
173 func (c *ConfigState) Printf(format string, a ...interface{}) (n int, err error) {
174 return fmt.Printf(format, c.convertArgs(a)...)
175 }
176
177 // Println is a wrapper for fmt.Println that treats each argument as if it were
178 // passed with a Formatter interface returned by c.NewFormatter. It returns
179 // the number of bytes written and any write error encountered. See
180 // NewFormatter for formatting details.
181 //
182 // This function is shorthand for the following syntax:
183 //
184 // fmt.Println(c.NewFormatter(a), c.NewFormatter(b))
185 func (c *ConfigState) Println(a ...interface{}) (n int, err error) {
186 return fmt.Println(c.convertArgs(a)...)
187 }
188
189 // Sprint is a wrapper for fmt.Sprint that treats each argument as if it were
190 // passed with a Formatter interface returned by c.NewFormatter. It returns
191 // the resulting string. See NewFormatter for formatting details.
192 //
193 // This function is shorthand for the following syntax:
194 //
195 // fmt.Sprint(c.NewFormatter(a), c.NewFormatter(b))
196 func (c *ConfigState) Sprint(a ...interface{}) string {
197 return fmt.Sprint(c.convertArgs(a)...)
198 }
199
200 // Sprintf is a wrapper for fmt.Sprintf that treats each argument as if it were
201 // passed with a Formatter interface returned by c.NewFormatter. It returns
202 // the resulting string. See NewFormatter for formatting details.
203 //
204 // This function is shorthand for the following syntax:
205 //
206 // fmt.Sprintf(format, c.NewFormatter(a), c.NewFormatter(b))
207 func (c *ConfigState) Sprintf(format string, a ...interface{}) string {
208 return fmt.Sprintf(format, c.convertArgs(a)...)
209 }
210
211 // Sprintln is a wrapper for fmt.Sprintln that treats each argument as if it
212 // were passed with a Formatter interface returned by c.NewFormatter. It
213 // returns the resulting string. See NewFormatter for formatting details.
214 //
215 // This function is shorthand for the following syntax:
216 //
217 // fmt.Sprintln(c.NewFormatter(a), c.NewFormatter(b))
218 func (c *ConfigState) Sprintln(a ...interface{}) string {
219 return fmt.Sprintln(c.convertArgs(a)...)
220 }
221
222 /*
223 NewFormatter returns a custom formatter that satisfies the fmt.Formatter
224 interface. As a result, it integrates cleanly with standard fmt package
225 printing functions. The formatter is useful for inline printing of smaller data
226 types similar to the standard %v format specifier.
227
228 The custom formatter only responds to the %v (most compact), %+v (adds pointer
229 addresses), %#v (adds types), and %#+v (adds types and pointer addresses) verb
230 combinations. Any other verbs such as %x and %q will be sent to the the
231 standard fmt package for formatting. In addition, the custom formatter ignores
232 the width and precision arguments (however they will still work on the format
233 specifiers not handled by the custom formatter).
234
235 Typically this function shouldn't be called directly. It is much easier to make
236 use of the custom formatter by calling one of the convenience functions such as
237 c.Printf, c.Println, or c.Printf.
238 */
239 func (c *ConfigState) NewFormatter(v interface{}) fmt.Formatter {
240 return newFormatter(c, v)
241 }
242
243 // Fdump formats and displays the passed arguments to io.Writer w. It formats
244 // exactly the same as Dump.
245 func (c *ConfigState) Fdump(w io.Writer, a ...interface{}) {
246 fdump(c, w, a...)
247 }
248
249 /*
250 Dump displays the passed parameters to standard out with newlines, customizable
251 indentation, and additional debug information such as complete types and all
252 pointer addresses used to indirect to the final value. It provides the
253 following features over the built-in printing facilities provided by the fmt
254 package:
255
256 * Pointers are dereferenced and followed
257 * Circular data structures are detected and handled properly
258 * Custom Stringer/error interfaces are optionally invoked, including
259 on unexported types
260 * Custom types which only implement the Stringer/error interfaces via
261 a pointer receiver are optionally invoked when passing non-pointer
262 variables
263 * Byte arrays and slices are dumped like the hexdump -C command which
264 includes offsets, byte values in hex, and ASCII output
265
266 The configuration options are controlled by modifying the public members
267 of c. See ConfigState for options documentation.
268
269 See Fdump if you would prefer dumping to an arbitrary io.Writer or Sdump to
270 get the formatted result as a string.
271 */
272 func (c *ConfigState) Dump(a ...interface{}) {
273 fdump(c, os.Stdout, a...)
274 }
275
276 // Sdump returns a string with the passed arguments formatted exactly the same
277 // as Dump.
278 func (c *ConfigState) Sdump(a ...interface{}) string {
279 var buf bytes.Buffer
280 fdump(c, &buf, a...)
281 return buf.String()
282 }
283
284 // convertArgs accepts a slice of arguments and returns a slice of the same
285 // length with each argument converted to a spew Formatter interface using
286 // the ConfigState associated with s.
287 func (c *ConfigState) convertArgs(args []interface{}) (formatters []interface{}) {
288 formatters = make([]interface{}, len(args))
289 for index, arg := range args {
290 formatters[index] = newFormatter(c, arg)
291 }
292 return formatters
293 }
294
295 // NewDefaultConfig returns a ConfigState with the following default settings.
296 //
297 // Indent: " "
298 // MaxDepth: 0
299 // DisableMethods: false
300 // DisablePointerMethods: false
301 // ContinueOnMethod: false
302 // SortKeys: false
303 func NewDefaultConfig() *ConfigState {
304 return &ConfigState{Indent: " "}
305 }
0 /*
1 * Copyright (c) 2013-2016 Dave Collins <dave@davec.name>
2 *
3 * Permission to use, copy, modify, and distribute this software for any
4 * purpose with or without fee is hereby granted, provided that the above
5 * copyright notice and this permission notice appear in all copies.
6 *
7 * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
8 * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
9 * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
10 * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
11 * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
12 * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
13 * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
14 */
15
16 /*
17 Package spew implements a deep pretty printer for Go data structures to aid in
18 debugging.
19
20 A quick overview of the additional features spew provides over the built-in
21 printing facilities for Go data types are as follows:
22
23 * Pointers are dereferenced and followed
24 * Circular data structures are detected and handled properly
25 * Custom Stringer/error interfaces are optionally invoked, including
26 on unexported types
27 * Custom types which only implement the Stringer/error interfaces via
28 a pointer receiver are optionally invoked when passing non-pointer
29 variables
30 * Byte arrays and slices are dumped like the hexdump -C command which
31 includes offsets, byte values in hex, and ASCII output (only when using
32 Dump style)
33
34 There are two different approaches spew allows for dumping Go data structures:
35
36 * Dump style which prints with newlines, customizable indentation,
37 and additional debug information such as types and all pointer addresses
38 used to indirect to the final value
39 * A custom Formatter interface that integrates cleanly with the standard fmt
40 package and replaces %v, %+v, %#v, and %#+v to provide inline printing
41 similar to the default %v while providing the additional functionality
42 outlined above and passing unsupported format verbs such as %x and %q
43 along to fmt
44
45 Quick Start
46
47 This section demonstrates how to quickly get started with spew. See the
48 sections below for further details on formatting and configuration options.
49
50 To dump a variable with full newlines, indentation, type, and pointer
51 information use Dump, Fdump, or Sdump:
52 spew.Dump(myVar1, myVar2, ...)
53 spew.Fdump(someWriter, myVar1, myVar2, ...)
54 str := spew.Sdump(myVar1, myVar2, ...)
55
56 Alternatively, if you would prefer to use format strings with a compacted inline
57 printing style, use the convenience wrappers Printf, Fprintf, etc with
58 %v (most compact), %+v (adds pointer addresses), %#v (adds types), or
59 %#+v (adds types and pointer addresses):
60 spew.Printf("myVar1: %v -- myVar2: %+v", myVar1, myVar2)
61 spew.Printf("myVar3: %#v -- myVar4: %#+v", myVar3, myVar4)
62 spew.Fprintf(someWriter, "myVar1: %v -- myVar2: %+v", myVar1, myVar2)
63 spew.Fprintf(someWriter, "myVar3: %#v -- myVar4: %#+v", myVar3, myVar4)
64
65 Configuration Options
66
67 Configuration of spew is handled by fields in the ConfigState type. For
68 convenience, all of the top-level functions use a global state available
69 via the spew.Config global.
70
71 It is also possible to create a ConfigState instance that provides methods
72 equivalent to the top-level functions. This allows concurrent configuration
73 options. See the ConfigState documentation for more details.
74
75 The following configuration options are available:
76 * Indent
77 String to use for each indentation level for Dump functions.
78 It is a single space by default. A popular alternative is "\t".
79
80 * MaxDepth
81 Maximum number of levels to descend into nested data structures.
82 There is no limit by default.
83
84 * DisableMethods
85 Disables invocation of error and Stringer interface methods.
86 Method invocation is enabled by default.
87
88 * DisablePointerMethods
89 Disables invocation of error and Stringer interface methods on types
90 which only accept pointer receivers from non-pointer variables.
91 Pointer method invocation is enabled by default.
92
93 * DisablePointerAddresses
94 DisablePointerAddresses specifies whether to disable the printing of
95 pointer addresses. This is useful when diffing data structures in tests.
96
97 * DisableCapacities
98 DisableCapacities specifies whether to disable the printing of
99 capacities for arrays, slices, maps and channels. This is useful when
100 diffing data structures in tests.
101
102 * ContinueOnMethod
103 Enables recursion into types after invoking error and Stringer interface
104 methods. Recursion after method invocation is disabled by default.
105
106 * SortKeys
107 Specifies map keys should be sorted before being printed. Use
108 this to have a more deterministic, diffable output. Note that
109 only native types (bool, int, uint, floats, uintptr and string)
110 and types which implement error or Stringer interfaces are
111 supported with other types sorted according to the
112 reflect.Value.String() output which guarantees display
113 stability. Natural map order is used by default.
114
115 * SpewKeys
116 Specifies that, as a last resort attempt, map keys should be
117 spewed to strings and sorted by those strings. This is only
118 considered if SortKeys is true.
119
120 Dump Usage
121
122 Simply call spew.Dump with a list of variables you want to dump:
123
124 spew.Dump(myVar1, myVar2, ...)
125
126 You may also call spew.Fdump if you would prefer to output to an arbitrary
127 io.Writer. For example, to dump to standard error:
128
129 spew.Fdump(os.Stderr, myVar1, myVar2, ...)
130
131 A third option is to call spew.Sdump to get the formatted output as a string:
132
133 str := spew.Sdump(myVar1, myVar2, ...)
134
135 Sample Dump Output
136
137 See the Dump example for details on the setup of the types and variables being
138 shown here.
139
140 (main.Foo) {
141 unexportedField: (*main.Bar)(0xf84002e210)({
142 flag: (main.Flag) flagTwo,
143 data: (uintptr) <nil>
144 }),
145 ExportedField: (map[interface {}]interface {}) (len=1) {
146 (string) (len=3) "one": (bool) true
147 }
148 }
149
150 Byte (and uint8) arrays and slices are displayed uniquely like the hexdump -C
151 command as shown.
152 ([]uint8) (len=32 cap=32) {
153 00000000 11 12 13 14 15 16 17 18 19 1a 1b 1c 1d 1e 1f 20 |............... |
154 00000010 21 22 23 24 25 26 27 28 29 2a 2b 2c 2d 2e 2f 30 |!"#$%&'()*+,-./0|
155 00000020 31 32 |12|
156 }
157
158 Custom Formatter
159
160 Spew provides a custom formatter that implements the fmt.Formatter interface
161 so that it integrates cleanly with standard fmt package printing functions. The
162 formatter is useful for inline printing of smaller data types similar to the
163 standard %v format specifier.
164
165 The custom formatter only responds to the %v (most compact), %+v (adds pointer
166 addresses), %#v (adds types), or %#+v (adds types and pointer addresses) verb
167 combinations. Any other verbs such as %x and %q will be sent to the the
168 standard fmt package for formatting. In addition, the custom formatter ignores
169 the width and precision arguments (however they will still work on the format
170 specifiers not handled by the custom formatter).
171
172 Custom Formatter Usage
173
174 The simplest way to make use of the spew custom formatter is to call one of the
175 convenience functions such as spew.Printf, spew.Println, or spew.Printf. The
176 functions have syntax you are most likely already familiar with:
177
178 spew.Printf("myVar1: %v -- myVar2: %+v", myVar1, myVar2)
179 spew.Printf("myVar3: %#v -- myVar4: %#+v", myVar3, myVar4)
180 spew.Println(myVar, myVar2)
181 spew.Fprintf(os.Stderr, "myVar1: %v -- myVar2: %+v", myVar1, myVar2)
182 spew.Fprintf(os.Stderr, "myVar3: %#v -- myVar4: %#+v", myVar3, myVar4)
183
184 See the Index for the full list convenience functions.
185
186 Sample Formatter Output
187
188 Double pointer to a uint8:
189 %v: <**>5
190 %+v: <**>(0xf8400420d0->0xf8400420c8)5
191 %#v: (**uint8)5
192 %#+v: (**uint8)(0xf8400420d0->0xf8400420c8)5
193
194 Pointer to circular struct with a uint8 field and a pointer to itself:
195 %v: <*>{1 <*><shown>}
196 %+v: <*>(0xf84003e260){ui8:1 c:<*>(0xf84003e260)<shown>}
197 %#v: (*main.circular){ui8:(uint8)1 c:(*main.circular)<shown>}
198 %#+v: (*main.circular)(0xf84003e260){ui8:(uint8)1 c:(*main.circular)(0xf84003e260)<shown>}
199
200 See the Printf example for details on the setup of variables being shown
201 here.
202
203 Errors
204
205 Since it is possible for custom Stringer/error interfaces to panic, spew
206 detects them and handles them internally by printing the panic information
207 inline with the output. Since spew is intended to provide deep pretty printing
208 capabilities on structures, it intentionally does not return any errors.
209 */
210 package spew
0 /*
1 * Copyright (c) 2013-2016 Dave Collins <dave@davec.name>
2 *
3 * Permission to use, copy, modify, and distribute this software for any
4 * purpose with or without fee is hereby granted, provided that the above
5 * copyright notice and this permission notice appear in all copies.
6 *
7 * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
8 * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
9 * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
10 * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
11 * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
12 * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
13 * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
14 */
15
16 package spew
17
18 import (
19 "bytes"
20 "encoding/hex"
21 "fmt"
22 "io"
23 "os"
24 "reflect"
25 "regexp"
26 "strconv"
27 "strings"
28 )
29
30 var (
31 // uint8Type is a reflect.Type representing a uint8. It is used to
32 // convert cgo types to uint8 slices for hexdumping.
33 uint8Type = reflect.TypeOf(uint8(0))
34
35 // cCharRE is a regular expression that matches a cgo char.
36 // It is used to detect character arrays to hexdump them.
37 cCharRE = regexp.MustCompile(`^.*\._Ctype_char$`)
38
39 // cUnsignedCharRE is a regular expression that matches a cgo unsigned
40 // char. It is used to detect unsigned character arrays to hexdump
41 // them.
42 cUnsignedCharRE = regexp.MustCompile(`^.*\._Ctype_unsignedchar$`)
43
44 // cUint8tCharRE is a regular expression that matches a cgo uint8_t.
45 // It is used to detect uint8_t arrays to hexdump them.
46 cUint8tCharRE = regexp.MustCompile(`^.*\._Ctype_uint8_t$`)
47 )
48
49 // dumpState contains information about the state of a dump operation.
50 type dumpState struct {
51 w io.Writer
52 depth int
53 pointers map[uintptr]int
54 ignoreNextType bool
55 ignoreNextIndent bool
56 cs *ConfigState
57 }
58
59 // indent performs indentation according to the depth level and cs.Indent
60 // option.
61 func (d *dumpState) indent() {
62 if d.ignoreNextIndent {
63 d.ignoreNextIndent = false
64 return
65 }
66 d.w.Write(bytes.Repeat([]byte(d.cs.Indent), d.depth))
67 }
68
69 // unpackValue returns values inside of non-nil interfaces when possible.
70 // This is useful for data types like structs, arrays, slices, and maps which
71 // can contain varying types packed inside an interface.
72 func (d *dumpState) unpackValue(v reflect.Value) reflect.Value {
73 if v.Kind() == reflect.Interface && !v.IsNil() {
74 v = v.Elem()
75 }
76 return v
77 }
78
79 // dumpPtr handles formatting of pointers by indirecting them as necessary.
80 func (d *dumpState) dumpPtr(v reflect.Value) {
81 // Remove pointers at or below the current depth from map used to detect
82 // circular refs.
83 for k, depth := range d.pointers {
84 if depth >= d.depth {
85 delete(d.pointers, k)
86 }
87 }
88
89 // Keep list of all dereferenced pointers to show later.
90 pointerChain := make([]uintptr, 0)
91
92 // Figure out how many levels of indirection there are by dereferencing
93 // pointers and unpacking interfaces down the chain while detecting circular
94 // references.
95 nilFound := false
96 cycleFound := false
97 indirects := 0
98 ve := v
99 for ve.Kind() == reflect.Ptr {
100 if ve.IsNil() {
101 nilFound = true
102 break
103 }
104 indirects++
105 addr := ve.Pointer()
106 pointerChain = append(pointerChain, addr)
107 if pd, ok := d.pointers[addr]; ok && pd < d.depth {
108 cycleFound = true
109 indirects--
110 break
111 }
112 d.pointers[addr] = d.depth
113
114 ve = ve.Elem()
115 if ve.Kind() == reflect.Interface {
116 if ve.IsNil() {
117 nilFound = true
118 break
119 }
120 ve = ve.Elem()
121 }
122 }
123
124 // Display type information.
125 d.w.Write(openParenBytes)
126 d.w.Write(bytes.Repeat(asteriskBytes, indirects))
127 d.w.Write([]byte(ve.Type().String()))
128 d.w.Write(closeParenBytes)
129
130 // Display pointer information.
131 if !d.cs.DisablePointerAddresses && len(pointerChain) > 0 {
132 d.w.Write(openParenBytes)
133 for i, addr := range pointerChain {
134 if i > 0 {
135 d.w.Write(pointerChainBytes)
136 }
137 printHexPtr(d.w, addr)
138 }
139 d.w.Write(closeParenBytes)
140 }
141
142 // Display dereferenced value.
143 d.w.Write(openParenBytes)
144 switch {
145 case nilFound:
146 d.w.Write(nilAngleBytes)
147
148 case cycleFound:
149 d.w.Write(circularBytes)
150
151 default:
152 d.ignoreNextType = true
153 d.dump(ve)
154 }
155 d.w.Write(closeParenBytes)
156 }
157
158 // dumpSlice handles formatting of arrays and slices. Byte (uint8 under
159 // reflection) arrays and slices are dumped in hexdump -C fashion.
160 func (d *dumpState) dumpSlice(v reflect.Value) {
161 // Determine whether this type should be hex dumped or not. Also,
162 // for types which should be hexdumped, try to use the underlying data
163 // first, then fall back to trying to convert them to a uint8 slice.
164 var buf []uint8
165 doConvert := false
166 doHexDump := false
167 numEntries := v.Len()
168 if numEntries > 0 {
169 vt := v.Index(0).Type()
170 vts := vt.String()
171 switch {
172 // C types that need to be converted.
173 case cCharRE.MatchString(vts):
174 fallthrough
175 case cUnsignedCharRE.MatchString(vts):
176 fallthrough
177 case cUint8tCharRE.MatchString(vts):
178 doConvert = true
179
180 // Try to use existing uint8 slices and fall back to converting
181 // and copying if that fails.
182 case vt.Kind() == reflect.Uint8:
183 // We need an addressable interface to convert the type
184 // to a byte slice. However, the reflect package won't
185 // give us an interface on certain things like
186 // unexported struct fields in order to enforce
187 // visibility rules. We use unsafe, when available, to
188 // bypass these restrictions since this package does not
189 // mutate the values.
190 vs := v
191 if !vs.CanInterface() || !vs.CanAddr() {
192 vs = unsafeReflectValue(vs)
193 }
194 if !UnsafeDisabled {
195 vs = vs.Slice(0, numEntries)
196
197 // Use the existing uint8 slice if it can be
198 // type asserted.
199 iface := vs.Interface()
200 if slice, ok := iface.([]uint8); ok {
201 buf = slice
202 doHexDump = true
203 break
204 }
205 }
206
207 // The underlying data needs to be converted if it can't
208 // be type asserted to a uint8 slice.
209 doConvert = true
210 }
211
212 // Copy and convert the underlying type if needed.
213 if doConvert && vt.ConvertibleTo(uint8Type) {
214 // Convert and copy each element into a uint8 byte
215 // slice.
216 buf = make([]uint8, numEntries)
217 for i := 0; i < numEntries; i++ {
218 vv := v.Index(i)
219 buf[i] = uint8(vv.Convert(uint8Type).Uint())
220 }
221 doHexDump = true
222 }
223 }
224
225 // Hexdump the entire slice as needed.
226 if doHexDump {
227 indent := strings.Repeat(d.cs.Indent, d.depth)
228 str := indent + hex.Dump(buf)
229 str = strings.Replace(str, "\n", "\n"+indent, -1)
230 str = strings.TrimRight(str, d.cs.Indent)
231 d.w.Write([]byte(str))
232 return
233 }
234
235 // Recursively call dump for each item.
236 for i := 0; i < numEntries; i++ {
237 d.dump(d.unpackValue(v.Index(i)))
238 if i < (numEntries - 1) {
239 d.w.Write(commaNewlineBytes)
240 } else {
241 d.w.Write(newlineBytes)
242 }
243 }
244 }
245
246 // dump is the main workhorse for dumping a value. It uses the passed reflect
247 // value to figure out what kind of object we are dealing with and formats it
248 // appropriately. It is a recursive function, however circular data structures
249 // are detected and handled properly.
250 func (d *dumpState) dump(v reflect.Value) {
251 // Handle invalid reflect values immediately.
252 kind := v.Kind()
253 if kind == reflect.Invalid {
254 d.w.Write(invalidAngleBytes)
255 return
256 }
257
258 // Handle pointers specially.
259 if kind == reflect.Ptr {
260 d.indent()
261 d.dumpPtr(v)
262 return
263 }
264
265 // Print type information unless already handled elsewhere.
266 if !d.ignoreNextType {
267 d.indent()
268 d.w.Write(openParenBytes)
269 d.w.Write([]byte(v.Type().String()))
270 d.w.Write(closeParenBytes)
271 d.w.Write(spaceBytes)
272 }
273 d.ignoreNextType = false
274
275 // Display length and capacity if the built-in len and cap functions
276 // work with the value's kind and the len/cap itself is non-zero.
277 valueLen, valueCap := 0, 0
278 switch v.Kind() {
279 case reflect.Array, reflect.Slice, reflect.Chan:
280 valueLen, valueCap = v.Len(), v.Cap()
281 case reflect.Map, reflect.String:
282 valueLen = v.Len()
283 }
284 if valueLen != 0 || !d.cs.DisableCapacities && valueCap != 0 {
285 d.w.Write(openParenBytes)
286 if valueLen != 0 {
287 d.w.Write(lenEqualsBytes)
288 printInt(d.w, int64(valueLen), 10)
289 }
290 if !d.cs.DisableCapacities && valueCap != 0 {
291 if valueLen != 0 {
292 d.w.Write(spaceBytes)
293 }
294 d.w.Write(capEqualsBytes)
295 printInt(d.w, int64(valueCap), 10)
296 }
297 d.w.Write(closeParenBytes)
298 d.w.Write(spaceBytes)
299 }
300
301 // Call Stringer/error interfaces if they exist and the handle methods flag
302 // is enabled
303 if !d.cs.DisableMethods {
304 if (kind != reflect.Invalid) && (kind != reflect.Interface) {
305 if handled := handleMethods(d.cs, d.w, v); handled {
306 return
307 }
308 }
309 }
310
311 switch kind {
312 case reflect.Invalid:
313 // Do nothing. We should never get here since invalid has already
314 // been handled above.
315
316 case reflect.Bool:
317 printBool(d.w, v.Bool())
318
319 case reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64, reflect.Int:
320 printInt(d.w, v.Int(), 10)
321
322 case reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uint:
323 printUint(d.w, v.Uint(), 10)
324
325 case reflect.Float32:
326 printFloat(d.w, v.Float(), 32)
327
328 case reflect.Float64:
329 printFloat(d.w, v.Float(), 64)
330
331 case reflect.Complex64:
332 printComplex(d.w, v.Complex(), 32)
333
334 case reflect.Complex128:
335 printComplex(d.w, v.Complex(), 64)
336
337 case reflect.Slice:
338 if v.IsNil() {
339 d.w.Write(nilAngleBytes)
340 break
341 }
342 fallthrough
343
344 case reflect.Array:
345 d.w.Write(openBraceNewlineBytes)
346 d.depth++
347 if (d.cs.MaxDepth != 0) && (d.depth > d.cs.MaxDepth) {
348 d.indent()
349 d.w.Write(maxNewlineBytes)
350 } else {
351 d.dumpSlice(v)
352 }
353 d.depth--
354 d.indent()
355 d.w.Write(closeBraceBytes)
356
357 case reflect.String:
358 d.w.Write([]byte(strconv.Quote(v.String())))
359
360 case reflect.Interface:
361 // The only time we should get here is for nil interfaces due to
362 // unpackValue calls.
363 if v.IsNil() {
364 d.w.Write(nilAngleBytes)
365 }
366
367 case reflect.Ptr:
368 // Do nothing. We should never get here since pointers have already
369 // been handled above.
370
371 case reflect.Map:
372 // nil maps should be indicated as different than empty maps
373 if v.IsNil() {
374 d.w.Write(nilAngleBytes)
375 break
376 }
377
378 d.w.Write(openBraceNewlineBytes)
379 d.depth++
380 if (d.cs.MaxDepth != 0) && (d.depth > d.cs.MaxDepth) {
381 d.indent()
382 d.w.Write(maxNewlineBytes)
383 } else {
384 numEntries := v.Len()
385 keys := v.MapKeys()
386 if d.cs.SortKeys {
387 sortValues(keys, d.cs)
388 }
389 for i, key := range keys {
390 d.dump(d.unpackValue(key))
391 d.w.Write(colonSpaceBytes)
392 d.ignoreNextIndent = true
393 d.dump(d.unpackValue(v.MapIndex(key)))
394 if i < (numEntries - 1) {
395 d.w.Write(commaNewlineBytes)
396 } else {
397 d.w.Write(newlineBytes)
398 }
399 }
400 }
401 d.depth--
402 d.indent()
403 d.w.Write(closeBraceBytes)
404
405 case reflect.Struct:
406 d.w.Write(openBraceNewlineBytes)
407 d.depth++
408 if (d.cs.MaxDepth != 0) && (d.depth > d.cs.MaxDepth) {
409 d.indent()
410 d.w.Write(maxNewlineBytes)
411 } else {
412 vt := v.Type()
413 numFields := v.NumField()
414 for i := 0; i < numFields; i++ {
415 d.indent()
416 vtf := vt.Field(i)
417 d.w.Write([]byte(vtf.Name))
418 d.w.Write(colonSpaceBytes)
419 d.ignoreNextIndent = true
420 d.dump(d.unpackValue(v.Field(i)))
421 if i < (numFields - 1) {
422 d.w.Write(commaNewlineBytes)
423 } else {
424 d.w.Write(newlineBytes)
425 }
426 }
427 }
428 d.depth--
429 d.indent()
430 d.w.Write(closeBraceBytes)
431
432 case reflect.Uintptr:
433 printHexPtr(d.w, uintptr(v.Uint()))
434
435 case reflect.UnsafePointer, reflect.Chan, reflect.Func:
436 printHexPtr(d.w, v.Pointer())
437
438 // There were not any other types at the time this code was written, but
439 // fall back to letting the default fmt package handle it in case any new
440 // types are added.
441 default:
442 if v.CanInterface() {
443 fmt.Fprintf(d.w, "%v", v.Interface())
444 } else {
445 fmt.Fprintf(d.w, "%v", v.String())
446 }
447 }
448 }
449
450 // fdump is a helper function to consolidate the logic from the various public
451 // methods which take varying writers and config states.
452 func fdump(cs *ConfigState, w io.Writer, a ...interface{}) {
453 for _, arg := range a {
454 if arg == nil {
455 w.Write(interfaceBytes)
456 w.Write(spaceBytes)
457 w.Write(nilAngleBytes)
458 w.Write(newlineBytes)
459 continue
460 }
461
462 d := dumpState{w: w, cs: cs}
463 d.pointers = make(map[uintptr]int)
464 d.dump(reflect.ValueOf(arg))
465 d.w.Write(newlineBytes)
466 }
467 }
468
469 // Fdump formats and displays the passed arguments to io.Writer w. It formats
470 // exactly the same as Dump.
471 func Fdump(w io.Writer, a ...interface{}) {
472 fdump(&Config, w, a...)
473 }
474
475 // Sdump returns a string with the passed arguments formatted exactly the same
476 // as Dump.
477 func Sdump(a ...interface{}) string {
478 var buf bytes.Buffer
479 fdump(&Config, &buf, a...)
480 return buf.String()
481 }
482
483 /*
484 Dump displays the passed parameters to standard out with newlines, customizable
485 indentation, and additional debug information such as complete types and all
486 pointer addresses used to indirect to the final value. It provides the
487 following features over the built-in printing facilities provided by the fmt
488 package:
489
490 * Pointers are dereferenced and followed
491 * Circular data structures are detected and handled properly
492 * Custom Stringer/error interfaces are optionally invoked, including
493 on unexported types
494 * Custom types which only implement the Stringer/error interfaces via
495 a pointer receiver are optionally invoked when passing non-pointer
496 variables
497 * Byte arrays and slices are dumped like the hexdump -C command which
498 includes offsets, byte values in hex, and ASCII output
499
500 The configuration options are controlled by an exported package global,
501 spew.Config. See ConfigState for options documentation.
502
503 See Fdump if you would prefer dumping to an arbitrary io.Writer or Sdump to
504 get the formatted result as a string.
505 */
506 func Dump(a ...interface{}) {
507 fdump(&Config, os.Stdout, a...)
508 }
0 /*
1 * Copyright (c) 2013-2016 Dave Collins <dave@davec.name>
2 *
3 * Permission to use, copy, modify, and distribute this software for any
4 * purpose with or without fee is hereby granted, provided that the above
5 * copyright notice and this permission notice appear in all copies.
6 *
7 * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
8 * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
9 * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
10 * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
11 * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
12 * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
13 * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
14 */
15
16 package spew
17
18 import (
19 "bytes"
20 "fmt"
21 "reflect"
22 "strconv"
23 "strings"
24 )
25
26 // supportedFlags is a list of all the character flags supported by fmt package.
27 const supportedFlags = "0-+# "
28
29 // formatState implements the fmt.Formatter interface and contains information
30 // about the state of a formatting operation. The NewFormatter function can
31 // be used to get a new Formatter which can be used directly as arguments
32 // in standard fmt package printing calls.
33 type formatState struct {
34 value interface{}
35 fs fmt.State
36 depth int
37 pointers map[uintptr]int
38 ignoreNextType bool
39 cs *ConfigState
40 }
41
42 // buildDefaultFormat recreates the original format string without precision
43 // and width information to pass in to fmt.Sprintf in the case of an
44 // unrecognized type. Unless new types are added to the language, this
45 // function won't ever be called.
46 func (f *formatState) buildDefaultFormat() (format string) {
47 buf := bytes.NewBuffer(percentBytes)
48
49 for _, flag := range supportedFlags {
50 if f.fs.Flag(int(flag)) {
51 buf.WriteRune(flag)
52 }
53 }
54
55 buf.WriteRune('v')
56
57 format = buf.String()
58 return format
59 }
60
61 // constructOrigFormat recreates the original format string including precision
62 // and width information to pass along to the standard fmt package. This allows
63 // automatic deferral of all format strings this package doesn't support.
64 func (f *formatState) constructOrigFormat(verb rune) (format string) {
65 buf := bytes.NewBuffer(percentBytes)
66
67 for _, flag := range supportedFlags {
68 if f.fs.Flag(int(flag)) {
69 buf.WriteRune(flag)
70 }
71 }
72
73 if width, ok := f.fs.Width(); ok {
74 buf.WriteString(strconv.Itoa(width))
75 }
76
77 if precision, ok := f.fs.Precision(); ok {
78 buf.Write(precisionBytes)
79 buf.WriteString(strconv.Itoa(precision))
80 }
81
82 buf.WriteRune(verb)
83
84 format = buf.String()
85 return format
86 }
87
88 // unpackValue returns values inside of non-nil interfaces when possible and
89 // ensures that types for values which have been unpacked from an interface
90 // are displayed when the show types flag is also set.
91 // This is useful for data types like structs, arrays, slices, and maps which
92 // can contain varying types packed inside an interface.
93 func (f *formatState) unpackValue(v reflect.Value) reflect.Value {
94 if v.Kind() == reflect.Interface {
95 f.ignoreNextType = false
96 if !v.IsNil() {
97 v = v.Elem()
98 }
99 }
100 return v
101 }
102
103 // formatPtr handles formatting of pointers by indirecting them as necessary.
104 func (f *formatState) formatPtr(v reflect.Value) {
105 // Display nil if top level pointer is nil.
106 showTypes := f.fs.Flag('#')
107 if v.IsNil() && (!showTypes || f.ignoreNextType) {
108 f.fs.Write(nilAngleBytes)
109 return
110 }
111
112 // Remove pointers at or below the current depth from map used to detect
113 // circular refs.
114 for k, depth := range f.pointers {
115 if depth >= f.depth {
116 delete(f.pointers, k)
117 }
118 }
119
120 // Keep list of all dereferenced pointers to possibly show later.
121 pointerChain := make([]uintptr, 0)
122
123 // Figure out how many levels of indirection there are by derferencing
124 // pointers and unpacking interfaces down the chain while detecting circular
125 // references.
126 nilFound := false
127 cycleFound := false
128 indirects := 0
129 ve := v
130 for ve.Kind() == reflect.Ptr {
131 if ve.IsNil() {
132 nilFound = true
133 break
134 }
135 indirects++
136 addr := ve.Pointer()
137 pointerChain = append(pointerChain, addr)
138 if pd, ok := f.pointers[addr]; ok && pd < f.depth {
139 cycleFound = true
140 indirects--
141 break
142 }
143 f.pointers[addr] = f.depth
144
145 ve = ve.Elem()
146 if ve.Kind() == reflect.Interface {
147 if ve.IsNil() {
148 nilFound = true
149 break
150 }
151 ve = ve.Elem()
152 }
153 }
154
155 // Display type or indirection level depending on flags.
156 if showTypes && !f.ignoreNextType {
157 f.fs.Write(openParenBytes)
158 f.fs.Write(bytes.Repeat(asteriskBytes, indirects))
159 f.fs.Write([]byte(ve.Type().String()))
160 f.fs.Write(closeParenBytes)
161 } else {
162 if nilFound || cycleFound {
163 indirects += strings.Count(ve.Type().String(), "*")
164 }
165 f.fs.Write(openAngleBytes)
166 f.fs.Write([]byte(strings.Repeat("*", indirects)))
167 f.fs.Write(closeAngleBytes)
168 }
169
170 // Display pointer information depending on flags.
171 if f.fs.Flag('+') && (len(pointerChain) > 0) {
172 f.fs.Write(openParenBytes)
173 for i, addr := range pointerChain {
174 if i > 0 {
175 f.fs.Write(pointerChainBytes)
176 }
177 printHexPtr(f.fs, addr)
178 }
179 f.fs.Write(closeParenBytes)
180 }
181
182 // Display dereferenced value.
183 switch {
184 case nilFound:
185 f.fs.Write(nilAngleBytes)
186
187 case cycleFound:
188 f.fs.Write(circularShortBytes)
189
190 default:
191 f.ignoreNextType = true
192 f.format(ve)
193 }
194 }
195
196 // format is the main workhorse for providing the Formatter interface. It
197 // uses the passed reflect value to figure out what kind of object we are
198 // dealing with and formats it appropriately. It is a recursive function,
199 // however circular data structures are detected and handled properly.
200 func (f *formatState) format(v reflect.Value) {
201 // Handle invalid reflect values immediately.
202 kind := v.Kind()
203 if kind == reflect.Invalid {
204 f.fs.Write(invalidAngleBytes)
205 return
206 }
207
208 // Handle pointers specially.
209 if kind == reflect.Ptr {
210 f.formatPtr(v)
211 return
212 }
213
214 // Print type information unless already handled elsewhere.
215 if !f.ignoreNextType && f.fs.Flag('#') {
216 f.fs.Write(openParenBytes)
217 f.fs.Write([]byte(v.Type().String()))
218 f.fs.Write(closeParenBytes)
219 }
220 f.ignoreNextType = false
221
222 // Call Stringer/error interfaces if they exist and the handle methods
223 // flag is enabled.
224 if !f.cs.DisableMethods {
225 if (kind != reflect.Invalid) && (kind != reflect.Interface) {
226 if handled := handleMethods(f.cs, f.fs, v); handled {
227 return
228 }
229 }
230 }
231
232 switch kind {
233 case reflect.Invalid:
234 // Do nothing. We should never get here since invalid has already
235 // been handled above.
236
237 case reflect.Bool:
238 printBool(f.fs, v.Bool())
239
240 case reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64, reflect.Int:
241 printInt(f.fs, v.Int(), 10)
242
243 case reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uint:
244 printUint(f.fs, v.Uint(), 10)
245
246 case reflect.Float32:
247 printFloat(f.fs, v.Float(), 32)
248
249 case reflect.Float64:
250 printFloat(f.fs, v.Float(), 64)
251
252 case reflect.Complex64:
253 printComplex(f.fs, v.Complex(), 32)
254
255 case reflect.Complex128:
256 printComplex(f.fs, v.Complex(), 64)
257
258 case reflect.Slice:
259 if v.IsNil() {
260 f.fs.Write(nilAngleBytes)
261 break
262 }
263 fallthrough
264
265 case reflect.Array:
266 f.fs.Write(openBracketBytes)
267 f.depth++
268 if (f.cs.MaxDepth != 0) && (f.depth > f.cs.MaxDepth) {
269 f.fs.Write(maxShortBytes)
270 } else {
271 numEntries := v.Len()
272 for i := 0; i < numEntries; i++ {
273 if i > 0 {
274 f.fs.Write(spaceBytes)
275 }
276 f.ignoreNextType = true
277 f.format(f.unpackValue(v.Index(i)))
278 }
279 }
280 f.depth--
281 f.fs.Write(closeBracketBytes)
282
283 case reflect.String:
284 f.fs.Write([]byte(v.String()))
285
286 case reflect.Interface:
287 // The only time we should get here is for nil interfaces due to
288 // unpackValue calls.
289 if v.IsNil() {
290 f.fs.Write(nilAngleBytes)
291 }
292
293 case reflect.Ptr:
294 // Do nothing. We should never get here since pointers have already
295 // been handled above.
296
297 case reflect.Map:
298 // nil maps should be indicated as different than empty maps
299 if v.IsNil() {
300 f.fs.Write(nilAngleBytes)
301 break
302 }
303
304 f.fs.Write(openMapBytes)
305 f.depth++
306 if (f.cs.MaxDepth != 0) && (f.depth > f.cs.MaxDepth) {
307 f.fs.Write(maxShortBytes)
308 } else {
309 keys := v.MapKeys()
310 if f.cs.SortKeys {
311 sortValues(keys, f.cs)
312 }
313 for i, key := range keys {
314 if i > 0 {
315 f.fs.Write(spaceBytes)
316 }
317 f.ignoreNextType = true
318 f.format(f.unpackValue(key))
319 f.fs.Write(colonBytes)
320 f.ignoreNextType = true
321 f.format(f.unpackValue(v.MapIndex(key)))
322 }
323 }
324 f.depth--
325 f.fs.Write(closeMapBytes)
326
327 case reflect.Struct:
328 numFields := v.NumField()
329 f.fs.Write(openBraceBytes)
330 f.depth++
331 if (f.cs.MaxDepth != 0) && (f.depth > f.cs.MaxDepth) {
332 f.fs.Write(maxShortBytes)
333 } else {
334 vt := v.Type()
335 for i := 0; i < numFields; i++ {
336 if i > 0 {
337 f.fs.Write(spaceBytes)
338 }
339 vtf := vt.Field(i)
340 if f.fs.Flag('+') || f.fs.Flag('#') {
341 f.fs.Write([]byte(vtf.Name))
342 f.fs.Write(colonBytes)
343 }
344 f.format(f.unpackValue(v.Field(i)))
345 }
346 }
347 f.depth--
348 f.fs.Write(closeBraceBytes)
349
350 case reflect.Uintptr:
351 printHexPtr(f.fs, uintptr(v.Uint()))
352
353 case reflect.UnsafePointer, reflect.Chan, reflect.Func:
354 printHexPtr(f.fs, v.Pointer())
355
356 // There were not any other types at the time this code was written, but
357 // fall back to letting the default fmt package handle it if any get added.
358 default:
359 format := f.buildDefaultFormat()
360 if v.CanInterface() {
361 fmt.Fprintf(f.fs, format, v.Interface())
362 } else {
363 fmt.Fprintf(f.fs, format, v.String())
364 }
365 }
366 }
367
368 // Format satisfies the fmt.Formatter interface. See NewFormatter for usage
369 // details.
370 func (f *formatState) Format(fs fmt.State, verb rune) {
371 f.fs = fs
372
373 // Use standard formatting for verbs that are not v.
374 if verb != 'v' {
375 format := f.constructOrigFormat(verb)
376 fmt.Fprintf(fs, format, f.value)
377 return
378 }
379
380 if f.value == nil {
381 if fs.Flag('#') {
382 fs.Write(interfaceBytes)
383 }
384 fs.Write(nilAngleBytes)
385 return
386 }
387
388 f.format(reflect.ValueOf(f.value))
389 }
390
391 // newFormatter is a helper function to consolidate the logic from the various
392 // public methods which take varying config states.
393 func newFormatter(cs *ConfigState, v interface{}) fmt.Formatter {
394 fs := &formatState{value: v, cs: cs}
395 fs.pointers = make(map[uintptr]int)
396 return fs
397 }
398
399 /*
400 NewFormatter returns a custom formatter that satisfies the fmt.Formatter
401 interface. As a result, it integrates cleanly with standard fmt package
402 printing functions. The formatter is useful for inline printing of smaller data
403 types similar to the standard %v format specifier.
404
405 The custom formatter only responds to the %v (most compact), %+v (adds pointer
406 addresses), %#v (adds types), or %#+v (adds types and pointer addresses) verb
407 combinations. Any other verbs such as %x and %q will be sent to the the
408 standard fmt package for formatting. In addition, the custom formatter ignores
409 the width and precision arguments (however they will still work on the format
410 specifiers not handled by the custom formatter).
411
412 Typically this function shouldn't be called directly. It is much easier to make
413 use of the custom formatter by calling one of the convenience functions such as
414 Printf, Println, or Fprintf.
415 */
416 func NewFormatter(v interface{}) fmt.Formatter {
417 return newFormatter(&Config, v)
418 }
0 /*
1 * Copyright (c) 2013-2016 Dave Collins <dave@davec.name>
2 *
3 * Permission to use, copy, modify, and distribute this software for any
4 * purpose with or without fee is hereby granted, provided that the above
5 * copyright notice and this permission notice appear in all copies.
6 *
7 * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
8 * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
9 * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
10 * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
11 * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
12 * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
13 * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
14 */
15
16 package spew
17
18 import (
19 "fmt"
20 "io"
21 )
22
23 // Errorf is a wrapper for fmt.Errorf that treats each argument as if it were
24 // passed with a default Formatter interface returned by NewFormatter. It
25 // returns the formatted string as a value that satisfies error. See
26 // NewFormatter for formatting details.
27 //
28 // This function is shorthand for the following syntax:
29 //
30 // fmt.Errorf(format, spew.NewFormatter(a), spew.NewFormatter(b))
31 func Errorf(format string, a ...interface{}) (err error) {
32 return fmt.Errorf(format, convertArgs(a)...)
33 }
34
35 // Fprint is a wrapper for fmt.Fprint that treats each argument as if it were
36 // passed with a default Formatter interface returned by NewFormatter. It
37 // returns the number of bytes written and any write error encountered. See
38 // NewFormatter for formatting details.
39 //
40 // This function is shorthand for the following syntax:
41 //
42 // fmt.Fprint(w, spew.NewFormatter(a), spew.NewFormatter(b))
43 func Fprint(w io.Writer, a ...interface{}) (n int, err error) {
44 return fmt.Fprint(w, convertArgs(a)...)
45 }
46
47 // Fprintf is a wrapper for fmt.Fprintf that treats each argument as if it were
48 // passed with a default Formatter interface returned by NewFormatter. It
49 // returns the number of bytes written and any write error encountered. See
50 // NewFormatter for formatting details.
51 //
52 // This function is shorthand for the following syntax:
53 //
54 // fmt.Fprintf(w, format, spew.NewFormatter(a), spew.NewFormatter(b))
55 func Fprintf(w io.Writer, format string, a ...interface{}) (n int, err error) {
56 return fmt.Fprintf(w, format, convertArgs(a)...)
57 }
58
59 // Fprintln is a wrapper for fmt.Fprintln that treats each argument as if it
60 // passed with a default Formatter interface returned by NewFormatter. See
61 // NewFormatter for formatting details.
62 //
63 // This function is shorthand for the following syntax:
64 //
65 // fmt.Fprintln(w, spew.NewFormatter(a), spew.NewFormatter(b))
66 func Fprintln(w io.Writer, a ...interface{}) (n int, err error) {
67 return fmt.Fprintln(w, convertArgs(a)...)
68 }
69
70 // Print is a wrapper for fmt.Print that treats each argument as if it were
71 // passed with a default Formatter interface returned by NewFormatter. It
72 // returns the number of bytes written and any write error encountered. See
73 // NewFormatter for formatting details.
74 //
75 // This function is shorthand for the following syntax:
76 //
77 // fmt.Print(spew.NewFormatter(a), spew.NewFormatter(b))
78 func Print(a ...interface{}) (n int, err error) {
79 return fmt.Print(convertArgs(a)...)
80 }
81
82 // Printf is a wrapper for fmt.Printf that treats each argument as if it were
83 // passed with a default Formatter interface returned by NewFormatter. It
84 // returns the number of bytes written and any write error encountered. See
85 // NewFormatter for formatting details.
86 //
87 // This function is shorthand for the following syntax:
88 //
89 // fmt.Printf(format, spew.NewFormatter(a), spew.NewFormatter(b))
90 func Printf(format string, a ...interface{}) (n int, err error) {
91 return fmt.Printf(format, convertArgs(a)...)
92 }
93
94 // Println is a wrapper for fmt.Println that treats each argument as if it were
95 // passed with a default Formatter interface returned by NewFormatter. It
96 // returns the number of bytes written and any write error encountered. See
97 // NewFormatter for formatting details.
98 //
99 // This function is shorthand for the following syntax:
100 //
101 // fmt.Println(spew.NewFormatter(a), spew.NewFormatter(b))
102 func Println(a ...interface{}) (n int, err error) {
103 return fmt.Println(convertArgs(a)...)
104 }
105
106 // Sprint is a wrapper for fmt.Sprint that treats each argument as if it were
107 // passed with a default Formatter interface returned by NewFormatter. It
108 // returns the resulting string. See NewFormatter for formatting details.
109 //
110 // This function is shorthand for the following syntax:
111 //
112 // fmt.Sprint(spew.NewFormatter(a), spew.NewFormatter(b))
113 func Sprint(a ...interface{}) string {
114 return fmt.Sprint(convertArgs(a)...)
115 }
116
117 // Sprintf is a wrapper for fmt.Sprintf that treats each argument as if it were
118 // passed with a default Formatter interface returned by NewFormatter. It
119 // returns the resulting string. See NewFormatter for formatting details.
120 //
121 // This function is shorthand for the following syntax:
122 //
123 // fmt.Sprintf(format, spew.NewFormatter(a), spew.NewFormatter(b))
124 func Sprintf(format string, a ...interface{}) string {
125 return fmt.Sprintf(format, convertArgs(a)...)
126 }
127
128 // Sprintln is a wrapper for fmt.Sprintln that treats each argument as if it
129 // were passed with a default Formatter interface returned by NewFormatter. It
130 // returns the resulting string. See NewFormatter for formatting details.
131 //
132 // This function is shorthand for the following syntax:
133 //
134 // fmt.Sprintln(spew.NewFormatter(a), spew.NewFormatter(b))
135 func Sprintln(a ...interface{}) string {
136 return fmt.Sprintln(convertArgs(a)...)
137 }
138
139 // convertArgs accepts a slice of arguments and returns a slice of the same
140 // length with each argument converted to a default spew Formatter interface.
141 func convertArgs(args []interface{}) (formatters []interface{}) {
142 formatters = make([]interface{}, len(args))
143 for index, arg := range args {
144 formatters[index] = NewFormatter(arg)
145 }
146 return formatters
147 }
0 # Compiled Object files, Static and Dynamic libs (Shared Objects)
1 *.o
2 *.a
3 *.so
4
5 # Folders
6 _obj
7 _test
8
9 # Architecture specific extensions/prefixes
10 *.[568vq]
11 [568vq].out
12
13 *.cgo1.go
14 *.cgo2.c
15 _cgo_defun.c
16 _cgo_gotypes.go
17 _cgo_export.*
18
19 _testmain.go
20
21 *.exe
22
23 .idea/
24 *.iml
0 language: go
1 sudo: false
2
3 matrix:
4 include:
5 - go: 1.4
6 - go: 1.5
7 - go: 1.6
8 - go: 1.7
9 - go: 1.8
10 - go: tip
11 allow_failures:
12 - go: tip
13
14 script:
15 - go get -t -v ./...
16 - diff -u <(echo -n) <(gofmt -d .)
17 - go vet $(go list ./... | grep -v /vendor/)
18 - go test -v -race ./...
0 # This is the official list of Gorilla WebSocket authors for copyright
1 # purposes.
2 #
3 # Please keep the list sorted.
4
5 Gary Burd <gary@beagledreams.com>
6 Joachim Bauch <mail@joachim-bauch.de>
7
0 Copyright (c) 2013 The Gorilla WebSocket Authors. All rights reserved.
1
2 Redistribution and use in source and binary forms, with or without
3 modification, are permitted provided that the following conditions are met:
4
5 Redistributions of source code must retain the above copyright notice, this
6 list of conditions and the following disclaimer.
7
8 Redistributions in binary form must reproduce the above copyright notice,
9 this list of conditions and the following disclaimer in the documentation
10 and/or other materials provided with the distribution.
11
12 THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
13 ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
14 WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
15 DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
16 FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
17 DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
18 SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
19 CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
20 OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
21 OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
0 # Gorilla WebSocket
1
2 Gorilla WebSocket is a [Go](http://golang.org/) implementation of the
3 [WebSocket](http://www.rfc-editor.org/rfc/rfc6455.txt) protocol.
4
5 [![Build Status](https://travis-ci.org/gorilla/websocket.svg?branch=master)](https://travis-ci.org/gorilla/websocket)
6 [![GoDoc](https://godoc.org/github.com/gorilla/websocket?status.svg)](https://godoc.org/github.com/gorilla/websocket)
7
8 ### Documentation
9
10 * [API Reference](http://godoc.org/github.com/gorilla/websocket)
11 * [Chat example](https://github.com/gorilla/websocket/tree/master/examples/chat)
12 * [Command example](https://github.com/gorilla/websocket/tree/master/examples/command)
13 * [Client and server example](https://github.com/gorilla/websocket/tree/master/examples/echo)
14 * [File watch example](https://github.com/gorilla/websocket/tree/master/examples/filewatch)
15
16 ### Status
17
18 The Gorilla WebSocket package provides a complete and tested implementation of
19 the [WebSocket](http://www.rfc-editor.org/rfc/rfc6455.txt) protocol. The
20 package API is stable.
21
22 ### Installation
23
24 go get github.com/gorilla/websocket
25
26 ### Protocol Compliance
27
28 The Gorilla WebSocket package passes the server tests in the [Autobahn Test
29 Suite](http://autobahn.ws/testsuite) using the application in the [examples/autobahn
30 subdirectory](https://github.com/gorilla/websocket/tree/master/examples/autobahn).
31
32 ### Gorilla WebSocket compared with other packages
33
34 <table>
35 <tr>
36 <th></th>
37 <th><a href="http://godoc.org/github.com/gorilla/websocket">github.com/gorilla</a></th>
38 <th><a href="http://godoc.org/golang.org/x/net/websocket">golang.org/x/net</a></th>
39 </tr>
40 <tr>
41 <tr><td colspan="3"><a href="http://tools.ietf.org/html/rfc6455">RFC 6455</a> Features</td></tr>
42 <tr><td>Passes <a href="http://autobahn.ws/testsuite/">Autobahn Test Suite</a></td><td><a href="https://github.com/gorilla/websocket/tree/master/examples/autobahn">Yes</a></td><td>No</td></tr>
43 <tr><td>Receive <a href="https://tools.ietf.org/html/rfc6455#section-5.4">fragmented</a> message<td>Yes</td><td><a href="https://code.google.com/p/go/issues/detail?id=7632">No</a>, see note 1</td></tr>
44 <tr><td>Send <a href="https://tools.ietf.org/html/rfc6455#section-5.5.1">close</a> message</td><td><a href="http://godoc.org/github.com/gorilla/websocket#hdr-Control_Messages">Yes</a></td><td><a href="https://code.google.com/p/go/issues/detail?id=4588">No</a></td></tr>
45 <tr><td>Send <a href="https://tools.ietf.org/html/rfc6455#section-5.5.2">pings</a> and receive <a href="https://tools.ietf.org/html/rfc6455#section-5.5.3">pongs</a></td><td><a href="http://godoc.org/github.com/gorilla/websocket#hdr-Control_Messages">Yes</a></td><td>No</td></tr>
46 <tr><td>Get the <a href="https://tools.ietf.org/html/rfc6455#section-5.6">type</a> of a received data message</td><td>Yes</td><td>Yes, see note 2</td></tr>
47 <tr><td colspan="3">Other Features</tr></td>
48 <tr><td><a href="https://tools.ietf.org/html/rfc7692">Compression Extensions</a></td><td>Experimental</td><td>No</td></tr>
49 <tr><td>Read message using io.Reader</td><td><a href="http://godoc.org/github.com/gorilla/websocket#Conn.NextReader">Yes</a></td><td>No, see note 3</td></tr>
50 <tr><td>Write message using io.WriteCloser</td><td><a href="http://godoc.org/github.com/gorilla/websocket#Conn.NextWriter">Yes</a></td><td>No, see note 3</td></tr>
51 </table>
52
53 Notes:
54
55 1. Large messages are fragmented in [Chrome's new WebSocket implementation](http://www.ietf.org/mail-archive/web/hybi/current/msg10503.html).
56 2. The application can get the type of a received data message by implementing
57 a [Codec marshal](http://godoc.org/golang.org/x/net/websocket#Codec.Marshal)
58 function.
59 3. The go.net io.Reader and io.Writer operate across WebSocket frame boundaries.
60 Read returns when the input buffer is full or a frame boundary is
61 encountered. Each call to Write sends a single frame message. The Gorilla
62 io.Reader and io.WriteCloser operate on a single WebSocket message.
63
0 // Copyright 2013 The Gorilla WebSocket Authors. All rights reserved.
1 // Use of this source code is governed by a BSD-style
2 // license that can be found in the LICENSE file.
3
4 package websocket
5
6 import (
7 "bufio"
8 "bytes"
9 "crypto/tls"
10 "encoding/base64"
11 "errors"
12 "io"
13 "io/ioutil"
14 "net"
15 "net/http"
16 "net/url"
17 "strings"
18 "time"
19 )
20
21 // ErrBadHandshake is returned when the server response to opening handshake is
22 // invalid.
23 var ErrBadHandshake = errors.New("websocket: bad handshake")
24
25 var errInvalidCompression = errors.New("websocket: invalid compression negotiation")
26
27 // NewClient creates a new client connection using the given net connection.
28 // The URL u specifies the host and request URI. Use requestHeader to specify
29 // the origin (Origin), subprotocols (Sec-WebSocket-Protocol) and cookies
30 // (Cookie). Use the response.Header to get the selected subprotocol
31 // (Sec-WebSocket-Protocol) and cookies (Set-Cookie).
32 //
33 // If the WebSocket handshake fails, ErrBadHandshake is returned along with a
34 // non-nil *http.Response so that callers can handle redirects, authentication,
35 // etc.
36 //
37 // Deprecated: Use Dialer instead.
38 func NewClient(netConn net.Conn, u *url.URL, requestHeader http.Header, readBufSize, writeBufSize int) (c *Conn, response *http.Response, err error) {
39 d := Dialer{
40 ReadBufferSize: readBufSize,
41 WriteBufferSize: writeBufSize,
42 NetDial: func(net, addr string) (net.Conn, error) {
43 return netConn, nil
44 },
45 }
46 return d.Dial(u.String(), requestHeader)
47 }
48
49 // A Dialer contains options for connecting to WebSocket server.
50 type Dialer struct {
51 // NetDial specifies the dial function for creating TCP connections. If
52 // NetDial is nil, net.Dial is used.
53 NetDial func(network, addr string) (net.Conn, error)
54
55 // Proxy specifies a function to return a proxy for a given
56 // Request. If the function returns a non-nil error, the
57 // request is aborted with the provided error.
58 // If Proxy is nil or returns a nil *URL, no proxy is used.
59 Proxy func(*http.Request) (*url.URL, error)
60
61 // TLSClientConfig specifies the TLS configuration to use with tls.Client.
62 // If nil, the default configuration is used.
63 TLSClientConfig *tls.Config
64
65 // HandshakeTimeout specifies the duration for the handshake to complete.
66 HandshakeTimeout time.Duration
67
68 // ReadBufferSize and WriteBufferSize specify I/O buffer sizes. If a buffer
69 // size is zero, then a useful default size is used. The I/O buffer sizes
70 // do not limit the size of the messages that can be sent or received.
71 ReadBufferSize, WriteBufferSize int
72
73 // Subprotocols specifies the client's requested subprotocols.
74 Subprotocols []string
75
76 // EnableCompression specifies if the client should attempt to negotiate
77 // per message compression (RFC 7692). Setting this value to true does not
78 // guarantee that compression will be supported. Currently only "no context
79 // takeover" modes are supported.
80 EnableCompression bool
81
82 // Jar specifies the cookie jar.
83 // If Jar is nil, cookies are not sent in requests and ignored
84 // in responses.
85 Jar http.CookieJar
86 }
87
88 var errMalformedURL = errors.New("malformed ws or wss URL")
89
90 // parseURL parses the URL.
91 //
92 // This function is a replacement for the standard library url.Parse function.
93 // In Go 1.4 and earlier, url.Parse loses information from the path.
94 func parseURL(s string) (*url.URL, error) {
95 // From the RFC:
96 //
97 // ws-URI = "ws:" "//" host [ ":" port ] path [ "?" query ]
98 // wss-URI = "wss:" "//" host [ ":" port ] path [ "?" query ]
99 var u url.URL
100 switch {
101 case strings.HasPrefix(s, "ws://"):
102 u.Scheme = "ws"
103 s = s[len("ws://"):]
104 case strings.HasPrefix(s, "wss://"):
105 u.Scheme = "wss"
106 s = s[len("wss://"):]
107 default:
108 return nil, errMalformedURL
109 }
110
111 if i := strings.Index(s, "?"); i >= 0 {
112 u.RawQuery = s[i+1:]
113 s = s[:i]
114 }
115
116 if i := strings.Index(s, "/"); i >= 0 {
117 u.Opaque = s[i:]
118 s = s[:i]
119 } else {
120 u.Opaque = "/"
121 }
122
123 u.Host = s
124
125 if strings.Contains(u.Host, "@") {
126 // Don't bother parsing user information because user information is
127 // not allowed in websocket URIs.
128 return nil, errMalformedURL
129 }
130
131 return &u, nil
132 }
133
134 func hostPortNoPort(u *url.URL) (hostPort, hostNoPort string) {
135 hostPort = u.Host
136 hostNoPort = u.Host
137 if i := strings.LastIndex(u.Host, ":"); i > strings.LastIndex(u.Host, "]") {
138 hostNoPort = hostNoPort[:i]
139 } else {
140 switch u.Scheme {
141 case "wss":
142 hostPort += ":443"
143 case "https":
144 hostPort += ":443"
145 default:
146 hostPort += ":80"
147 }
148 }
149 return hostPort, hostNoPort
150 }
151
152 // DefaultDialer is a dialer with all fields set to the default zero values.
153 var DefaultDialer = &Dialer{
154 Proxy: http.ProxyFromEnvironment,
155 }
156
157 // Dial creates a new client connection. Use requestHeader to specify the
158 // origin (Origin), subprotocols (Sec-WebSocket-Protocol) and cookies (Cookie).
159 // Use the response.Header to get the selected subprotocol
160 // (Sec-WebSocket-Protocol) and cookies (Set-Cookie).
161 //
162 // If the WebSocket handshake fails, ErrBadHandshake is returned along with a
163 // non-nil *http.Response so that callers can handle redirects, authentication,
164 // etcetera. The response body may not contain the entire response and does not
165 // need to be closed by the application.
166 func (d *Dialer) Dial(urlStr string, requestHeader http.Header) (*Conn, *http.Response, error) {
167
168 if d == nil {
169 d = &Dialer{
170 Proxy: http.ProxyFromEnvironment,
171 }
172 }
173
174 challengeKey, err := generateChallengeKey()
175 if err != nil {
176 return nil, nil, err
177 }
178
179 u, err := parseURL(urlStr)
180 if err != nil {
181 return nil, nil, err
182 }
183
184 switch u.Scheme {
185 case "ws":
186 u.Scheme = "http"
187 case "wss":
188 u.Scheme = "https"
189 default:
190 return nil, nil, errMalformedURL
191 }
192
193 if u.User != nil {
194 // User name and password are not allowed in websocket URIs.
195 return nil, nil, errMalformedURL
196 }
197
198 req := &http.Request{
199 Method: "GET",
200 URL: u,
201 Proto: "HTTP/1.1",
202 ProtoMajor: 1,
203 ProtoMinor: 1,
204 Header: make(http.Header),
205 Host: u.Host,
206 }
207
208 // Set the cookies present in the cookie jar of the dialer
209 if d.Jar != nil {
210 for _, cookie := range d.Jar.Cookies(u) {
211 req.AddCookie(cookie)
212 }
213 }
214
215 // Set the request headers using the capitalization for names and values in
216 // RFC examples. Although the capitalization shouldn't matter, there are
217 // servers that depend on it. The Header.Set method is not used because the
218 // method canonicalizes the header names.
219 req.Header["Upgrade"] = []string{"websocket"}
220 req.Header["Connection"] = []string{"Upgrade"}
221 req.Header["Sec-WebSocket-Key"] = []string{challengeKey}
222 req.Header["Sec-WebSocket-Version"] = []string{"13"}
223 if len(d.Subprotocols) > 0 {
224 req.Header["Sec-WebSocket-Protocol"] = []string{strings.Join(d.Subprotocols, ", ")}
225 }
226 for k, vs := range requestHeader {
227 switch {
228 case k == "Host":
229 if len(vs) > 0 {
230 req.Host = vs[0]
231 }
232 case k == "Upgrade" ||
233 k == "Connection" ||
234 k == "Sec-Websocket-Key" ||
235 k == "Sec-Websocket-Version" ||
236 k == "Sec-Websocket-Extensions" ||
237 (k == "Sec-Websocket-Protocol" && len(d.Subprotocols) > 0):
238 return nil, nil, errors.New("websocket: duplicate header not allowed: " + k)
239 default:
240 req.Header[k] = vs
241 }
242 }
243
244 if d.EnableCompression {
245 req.Header.Set("Sec-Websocket-Extensions", "permessage-deflate; server_no_context_takeover; client_no_context_takeover")
246 }
247
248 hostPort, hostNoPort := hostPortNoPort(u)
249
250 var proxyURL *url.URL
251 // Check wether the proxy method has been configured
252 if d.Proxy != nil {
253 proxyURL, err = d.Proxy(req)
254 }
255 if err != nil {
256 return nil, nil, err
257 }
258
259 var targetHostPort string
260 if proxyURL != nil {
261 targetHostPort, _ = hostPortNoPort(proxyURL)
262 } else {
263 targetHostPort = hostPort
264 }
265
266 var deadline time.Time
267 if d.HandshakeTimeout != 0 {
268 deadline = time.Now().Add(d.HandshakeTimeout)
269 }
270
271 netDial := d.NetDial
272 if netDial == nil {
273 netDialer := &net.Dialer{Deadline: deadline}
274 netDial = netDialer.Dial
275 }
276
277 netConn, err := netDial("tcp", targetHostPort)
278 if err != nil {
279 return nil, nil, err
280 }
281
282 defer func() {
283 if netConn != nil {
284 netConn.Close()
285 }
286 }()
287
288 if err := netConn.SetDeadline(deadline); err != nil {
289 return nil, nil, err
290 }
291
292 if proxyURL != nil {
293 connectHeader := make(http.Header)
294 if user := proxyURL.User; user != nil {
295 proxyUser := user.Username()
296 if proxyPassword, passwordSet := user.Password(); passwordSet {
297 credential := base64.StdEncoding.EncodeToString([]byte(proxyUser + ":" + proxyPassword))
298 connectHeader.Set("Proxy-Authorization", "Basic "+credential)
299 }
300 }
301 connectReq := &http.Request{
302 Method: "CONNECT",
303 URL: &url.URL{Opaque: hostPort},
304 Host: hostPort,
305 Header: connectHeader,
306 }
307
308 connectReq.Write(netConn)
309
310 // Read response.
311 // Okay to use and discard buffered reader here, because
312 // TLS server will not speak until spoken to.
313 br := bufio.NewReader(netConn)
314 resp, err := http.ReadResponse(br, connectReq)
315 if err != nil {
316 return nil, nil, err
317 }
318 if resp.StatusCode != 200 {
319 f := strings.SplitN(resp.Status, " ", 2)
320 return nil, nil, errors.New(f[1])
321 }
322 }
323
324 if u.Scheme == "https" {
325 cfg := cloneTLSConfig(d.TLSClientConfig)
326 if cfg.ServerName == "" {
327 cfg.ServerName = hostNoPort
328 }
329 tlsConn := tls.Client(netConn, cfg)
330 netConn = tlsConn
331 if err := tlsConn.Handshake(); err != nil {
332 return nil, nil, err
333 }
334 if !cfg.InsecureSkipVerify {
335 if err := tlsConn.VerifyHostname(cfg.ServerName); err != nil {
336 return nil, nil, err
337 }
338 }
339 }
340
341 conn := newConn(netConn, false, d.ReadBufferSize, d.WriteBufferSize)
342
343 if err := req.Write(netConn); err != nil {
344 return nil, nil, err
345 }
346
347 resp, err := http.ReadResponse(conn.br, req)
348 if err != nil {
349 return nil, nil, err
350 }
351
352 if d.Jar != nil {
353 if rc := resp.Cookies(); len(rc) > 0 {
354 d.Jar.SetCookies(u, rc)
355 }
356 }
357
358 if resp.StatusCode != 101 ||
359 !strings.EqualFold(resp.Header.Get("Upgrade"), "websocket") ||
360 !strings.EqualFold(resp.Header.Get("Connection"), "upgrade") ||
361 resp.Header.Get("Sec-Websocket-Accept") != computeAcceptKey(challengeKey) {
362 // Before closing the network connection on return from this
363 // function, slurp up some of the response to aid application
364 // debugging.
365 buf := make([]byte, 1024)
366 n, _ := io.ReadFull(resp.Body, buf)
367 resp.Body = ioutil.NopCloser(bytes.NewReader(buf[:n]))
368 return nil, resp, ErrBadHandshake
369 }
370
371 for _, ext := range parseExtensions(resp.Header) {
372 if ext[""] != "permessage-deflate" {
373 continue
374 }
375 _, snct := ext["server_no_context_takeover"]
376 _, cnct := ext["client_no_context_takeover"]
377 if !snct || !cnct {
378 return nil, resp, errInvalidCompression
379 }
380 conn.newCompressionWriter = compressNoContextTakeover
381 conn.newDecompressionReader = decompressNoContextTakeover
382 break
383 }
384
385 resp.Body = ioutil.NopCloser(bytes.NewReader([]byte{}))
386 conn.subprotocol = resp.Header.Get("Sec-Websocket-Protocol")
387
388 netConn.SetDeadline(time.Time{})
389 netConn = nil // to avoid close in defer.
390 return conn, resp, nil
391 }
0 // Copyright 2013 The Gorilla WebSocket Authors. All rights reserved.
1 // Use of this source code is governed by a BSD-style
2 // license that can be found in the LICENSE file.
3
4 // +build go1.8
5
6 package websocket
7
8 import "crypto/tls"
9
10 func cloneTLSConfig(cfg *tls.Config) *tls.Config {
11 if cfg == nil {
12 return &tls.Config{}
13 }
14 return cfg.Clone()
15 }
0 // Copyright 2013 The Gorilla WebSocket Authors. All rights reserved.
1 // Use of this source code is governed by a BSD-style
2 // license that can be found in the LICENSE file.
3
4 // +build !go1.8
5
6 package websocket
7
8 import "crypto/tls"
9
10 // cloneTLSConfig clones all public fields except the fields
11 // SessionTicketsDisabled and SessionTicketKey. This avoids copying the
12 // sync.Mutex in the sync.Once and makes it safe to call cloneTLSConfig on a
13 // config in active use.
14 func cloneTLSConfig(cfg *tls.Config) *tls.Config {
15 if cfg == nil {
16 return &tls.Config{}
17 }
18 return &tls.Config{
19 Rand: cfg.Rand,
20 Time: cfg.Time,
21 Certificates: cfg.Certificates,
22 NameToCertificate: cfg.NameToCertificate,
23 GetCertificate: cfg.GetCertificate,
24 RootCAs: cfg.RootCAs,
25 NextProtos: cfg.NextProtos,
26 ServerName: cfg.ServerName,
27 ClientAuth: cfg.ClientAuth,
28 ClientCAs: cfg.ClientCAs,
29 InsecureSkipVerify: cfg.InsecureSkipVerify,
30 CipherSuites: cfg.CipherSuites,
31 PreferServerCipherSuites: cfg.PreferServerCipherSuites,
32 ClientSessionCache: cfg.ClientSessionCache,
33 MinVersion: cfg.MinVersion,
34 MaxVersion: cfg.MaxVersion,
35 CurvePreferences: cfg.CurvePreferences,
36 }
37 }
0 // Copyright 2017 The Gorilla WebSocket Authors. All rights reserved.
1 // Use of this source code is governed by a BSD-style
2 // license that can be found in the LICENSE file.
3
4 package websocket
5
6 import (
7 "compress/flate"
8 "errors"
9 "io"
10 "strings"
11 "sync"
12 )
13
14 const (
15 minCompressionLevel = -2 // flate.HuffmanOnly not defined in Go < 1.6
16 maxCompressionLevel = flate.BestCompression
17 defaultCompressionLevel = 1
18 )
19
20 var (
21 flateWriterPools [maxCompressionLevel - minCompressionLevel + 1]sync.Pool
22 flateReaderPool = sync.Pool{New: func() interface{} {
23 return flate.NewReader(nil)
24 }}
25 )
26
27 func decompressNoContextTakeover(r io.Reader) io.ReadCloser {
28 const tail =
29 // Add four bytes as specified in RFC
30 "\x00\x00\xff\xff" +
31 // Add final block to squelch unexpected EOF error from flate reader.
32 "\x01\x00\x00\xff\xff"
33
34 fr, _ := flateReaderPool.Get().(io.ReadCloser)
35 fr.(flate.Resetter).Reset(io.MultiReader(r, strings.NewReader(tail)), nil)
36 return &flateReadWrapper{fr}
37 }
38
39 func isValidCompressionLevel(level int) bool {
40 return minCompressionLevel <= level && level <= maxCompressionLevel
41 }
42
43 func compressNoContextTakeover(w io.WriteCloser, level int) io.WriteCloser {
44 p := &flateWriterPools[level-minCompressionLevel]
45 tw := &truncWriter{w: w}
46 fw, _ := p.Get().(*flate.Writer)
47 if fw == nil {
48 fw, _ = flate.NewWriter(tw, level)
49 } else {
50 fw.Reset(tw)
51 }
52 return &flateWriteWrapper{fw: fw, tw: tw, p: p}
53 }
54
55 // truncWriter is an io.Writer that writes all but the last four bytes of the
56 // stream to another io.Writer.
57 type truncWriter struct {
58 w io.WriteCloser
59 n int
60 p [4]byte
61 }
62
63 func (w *truncWriter) Write(p []byte) (int, error) {
64 n := 0
65
66 // fill buffer first for simplicity.
67 if w.n < len(w.p) {
68 n = copy(w.p[w.n:], p)
69 p = p[n:]
70 w.n += n
71 if len(p) == 0 {
72 return n, nil
73 }
74 }
75
76 m := len(p)
77 if m > len(w.p) {
78 m = len(w.p)
79 }
80
81 if nn, err := w.w.Write(w.p[:m]); err != nil {
82 return n + nn, err
83 }
84
85 copy(w.p[:], w.p[m:])
86 copy(w.p[len(w.p)-m:], p[len(p)-m:])
87 nn, err := w.w.Write(p[:len(p)-m])
88 return n + nn, err
89 }
90
91 type flateWriteWrapper struct {
92 fw *flate.Writer
93 tw *truncWriter
94 p *sync.Pool
95 }
96
97 func (w *flateWriteWrapper) Write(p []byte) (int, error) {
98 if w.fw == nil {
99 return 0, errWriteClosed
100 }
101 return w.fw.Write(p)
102 }
103
104 func (w *flateWriteWrapper) Close() error {
105 if w.fw == nil {
106 return errWriteClosed
107 }
108 err1 := w.fw.Flush()
109 w.p.Put(w.fw)
110 w.fw = nil
111 if w.tw.p != [4]byte{0, 0, 0xff, 0xff} {
112 return errors.New("websocket: internal error, unexpected bytes at end of flate stream")
113 }
114 err2 := w.tw.w.Close()
115 if err1 != nil {
116 return err1
117 }
118 return err2
119 }
120
121 type flateReadWrapper struct {
122 fr io.ReadCloser
123 }
124
125 func (r *flateReadWrapper) Read(p []byte) (int, error) {
126 if r.fr == nil {
127 return 0, io.ErrClosedPipe
128 }
129 n, err := r.fr.Read(p)
130 if err == io.EOF {
131 // Preemptively place the reader back in the pool. This helps with
132 // scenarios where the application does not call NextReader() soon after
133 // this final read.
134 r.Close()
135 }
136 return n, err
137 }
138
139 func (r *flateReadWrapper) Close() error {
140 if r.fr == nil {
141 return io.ErrClosedPipe
142 }
143 err := r.fr.Close()
144 flateReaderPool.Put(r.fr)
145 r.fr = nil
146 return err
147 }
0 // Copyright 2013 The Gorilla WebSocket Authors. All rights reserved.
1 // Use of this source code is governed by a BSD-style
2 // license that can be found in the LICENSE file.
3
4 package websocket
5
6 import (
7 "bufio"
8 "encoding/binary"
9 "errors"
10 "io"
11 "io/ioutil"
12 "math/rand"
13 "net"
14 "strconv"
15 "sync"
16 "time"
17 "unicode/utf8"
18 )
19
20 const (
21 // Frame header byte 0 bits from Section 5.2 of RFC 6455
22 finalBit = 1 << 7
23 rsv1Bit = 1 << 6
24 rsv2Bit = 1 << 5
25 rsv3Bit = 1 << 4
26
27 // Frame header byte 1 bits from Section 5.2 of RFC 6455
28 maskBit = 1 << 7
29
30 maxFrameHeaderSize = 2 + 8 + 4 // Fixed header + length + mask
31 maxControlFramePayloadSize = 125
32
33 writeWait = time.Second
34
35 defaultReadBufferSize = 4096
36 defaultWriteBufferSize = 4096
37
38 continuationFrame = 0
39 noFrame = -1
40 )
41
42 // Close codes defined in RFC 6455, section 11.7.
43 const (
44 CloseNormalClosure = 1000
45 CloseGoingAway = 1001
46 CloseProtocolError = 1002
47 CloseUnsupportedData = 1003
48 CloseNoStatusReceived = 1005
49 CloseAbnormalClosure = 1006
50 CloseInvalidFramePayloadData = 1007
51 ClosePolicyViolation = 1008
52 CloseMessageTooBig = 1009
53 CloseMandatoryExtension = 1010
54 CloseInternalServerErr = 1011
55 CloseServiceRestart = 1012
56 CloseTryAgainLater = 1013
57 CloseTLSHandshake = 1015
58 )
59
60 // The message types are defined in RFC 6455, section 11.8.
61 const (
62 // TextMessage denotes a text data message. The text message payload is
63 // interpreted as UTF-8 encoded text data.
64 TextMessage = 1
65
66 // BinaryMessage denotes a binary data message.
67 BinaryMessage = 2
68
69 // CloseMessage denotes a close control message. The optional message
70 // payload contains a numeric code and text. Use the FormatCloseMessage
71 // function to format a close message payload.
72 CloseMessage = 8
73
74 // PingMessage denotes a ping control message. The optional message payload
75 // is UTF-8 encoded text.
76 PingMessage = 9
77
78 // PongMessage denotes a ping control message. The optional message payload
79 // is UTF-8 encoded text.
80 PongMessage = 10
81 )
82
83 // ErrCloseSent is returned when the application writes a message to the
84 // connection after sending a close message.
85 var ErrCloseSent = errors.New("websocket: close sent")
86
87 // ErrReadLimit is returned when reading a message that is larger than the
88 // read limit set for the connection.
89 var ErrReadLimit = errors.New("websocket: read limit exceeded")
90
91 // netError satisfies the net Error interface.
92 type netError struct {
93 msg string
94 temporary bool
95 timeout bool
96 }
97
98 func (e *netError) Error() string { return e.msg }
99 func (e *netError) Temporary() bool { return e.temporary }
100 func (e *netError) Timeout() bool { return e.timeout }
101
102 // CloseError represents close frame.
103 type CloseError struct {
104
105 // Code is defined in RFC 6455, section 11.7.
106 Code int
107
108 // Text is the optional text payload.
109 Text string
110 }
111
112 func (e *CloseError) Error() string {
113 s := []byte("websocket: close ")
114 s = strconv.AppendInt(s, int64(e.Code), 10)
115 switch e.Code {
116 case CloseNormalClosure:
117 s = append(s, " (normal)"...)
118 case CloseGoingAway:
119 s = append(s, " (going away)"...)
120 case CloseProtocolError:
121 s = append(s, " (protocol error)"...)
122 case CloseUnsupportedData:
123 s = append(s, " (unsupported data)"...)
124 case CloseNoStatusReceived:
125 s = append(s, " (no status)"...)
126 case CloseAbnormalClosure:
127 s = append(s, " (abnormal closure)"...)
128 case CloseInvalidFramePayloadData:
129 s = append(s, " (invalid payload data)"...)
130 case ClosePolicyViolation:
131 s = append(s, " (policy violation)"...)
132 case CloseMessageTooBig:
133 s = append(s, " (message too big)"...)
134 case CloseMandatoryExtension:
135 s = append(s, " (mandatory extension missing)"...)
136 case CloseInternalServerErr:
137 s = append(s, " (internal server error)"...)
138 case CloseTLSHandshake:
139 s = append(s, " (TLS handshake error)"...)
140 }
141 if e.Text != "" {
142 s = append(s, ": "...)
143 s = append(s, e.Text...)
144 }
145 return string(s)
146 }
147
148 // IsCloseError returns boolean indicating whether the error is a *CloseError
149 // with one of the specified codes.
150 func IsCloseError(err error, codes ...int) bool {
151 if e, ok := err.(*CloseError); ok {
152 for _, code := range codes {
153 if e.Code == code {
154 return true
155 }
156 }
157 }
158 return false
159 }
160
161 // IsUnexpectedCloseError returns boolean indicating whether the error is a
162 // *CloseError with a code not in the list of expected codes.
163 func IsUnexpectedCloseError(err error, expectedCodes ...int) bool {
164 if e, ok := err.(*CloseError); ok {
165 for _, code := range expectedCodes {
166 if e.Code == code {
167 return false
168 }
169 }
170 return true
171 }
172 return false
173 }
174
175 var (
176 errWriteTimeout = &netError{msg: "websocket: write timeout", timeout: true, temporary: true}
177 errUnexpectedEOF = &CloseError{Code: CloseAbnormalClosure, Text: io.ErrUnexpectedEOF.Error()}
178 errBadWriteOpCode = errors.New("websocket: bad write message type")
179 errWriteClosed = errors.New("websocket: write closed")
180 errInvalidControlFrame = errors.New("websocket: invalid control frame")
181 )
182
183 func newMaskKey() [4]byte {
184 n := rand.Uint32()
185 return [4]byte{byte(n), byte(n >> 8), byte(n >> 16), byte(n >> 24)}
186 }
187
188 func hideTempErr(err error) error {
189 if e, ok := err.(net.Error); ok && e.Temporary() {
190 err = &netError{msg: e.Error(), timeout: e.Timeout()}
191 }
192 return err
193 }
194
195 func isControl(frameType int) bool {
196 return frameType == CloseMessage || frameType == PingMessage || frameType == PongMessage
197 }
198
199 func isData(frameType int) bool {
200 return frameType == TextMessage || frameType == BinaryMessage
201 }
202
203 var validReceivedCloseCodes = map[int]bool{
204 // see http://www.iana.org/assignments/websocket/websocket.xhtml#close-code-number
205
206 CloseNormalClosure: true,
207 CloseGoingAway: true,
208 CloseProtocolError: true,
209 CloseUnsupportedData: true,
210 CloseNoStatusReceived: false,
211 CloseAbnormalClosure: false,
212 CloseInvalidFramePayloadData: true,
213 ClosePolicyViolation: true,
214 CloseMessageTooBig: true,
215 CloseMandatoryExtension: true,
216 CloseInternalServerErr: true,
217 CloseServiceRestart: true,
218 CloseTryAgainLater: true,
219 CloseTLSHandshake: false,
220 }
221
222 func isValidReceivedCloseCode(code int) bool {
223 return validReceivedCloseCodes[code] || (code >= 3000 && code <= 4999)
224 }
225
226 // The Conn type represents a WebSocket connection.
227 type Conn struct {
228 conn net.Conn
229 isServer bool
230 subprotocol string
231
232 // Write fields
233 mu chan bool // used as mutex to protect write to conn
234 writeBuf []byte // frame is constructed in this buffer.
235 writeDeadline time.Time
236 writer io.WriteCloser // the current writer returned to the application
237 isWriting bool // for best-effort concurrent write detection
238
239 writeErrMu sync.Mutex
240 writeErr error
241
242 enableWriteCompression bool
243 compressionLevel int
244 newCompressionWriter func(io.WriteCloser, int) io.WriteCloser
245
246 // Read fields
247 reader io.ReadCloser // the current reader returned to the application
248 readErr error
249 br *bufio.Reader
250 readRemaining int64 // bytes remaining in current frame.
251 readFinal bool // true the current message has more frames.
252 readLength int64 // Message size.
253 readLimit int64 // Maximum message size.
254 readMaskPos int
255 readMaskKey [4]byte
256 handlePong func(string) error
257 handlePing func(string) error
258 handleClose func(int, string) error
259 readErrCount int
260 messageReader *messageReader // the current low-level reader
261
262 readDecompress bool // whether last read frame had RSV1 set
263 newDecompressionReader func(io.Reader) io.ReadCloser
264 }
265
266 func newConn(conn net.Conn, isServer bool, readBufferSize, writeBufferSize int) *Conn {
267 return newConnBRW(conn, isServer, readBufferSize, writeBufferSize, nil)
268 }
269
270 type writeHook struct {
271 p []byte
272 }
273
274 func (wh *writeHook) Write(p []byte) (int, error) {
275 wh.p = p
276 return len(p), nil
277 }
278
279 func newConnBRW(conn net.Conn, isServer bool, readBufferSize, writeBufferSize int, brw *bufio.ReadWriter) *Conn {
280 mu := make(chan bool, 1)
281 mu <- true
282
283 var br *bufio.Reader
284 if readBufferSize == 0 && brw != nil && brw.Reader != nil {
285 // Reuse the supplied bufio.Reader if the buffer has a useful size.
286 // This code assumes that peek on a reader returns
287 // bufio.Reader.buf[:0].
288 brw.Reader.Reset(conn)
289 if p, err := brw.Reader.Peek(0); err == nil && cap(p) >= 256 {
290 br = brw.Reader
291 }
292 }
293 if br == nil {
294 if readBufferSize == 0 {
295 readBufferSize = defaultReadBufferSize
296 }
297 if readBufferSize < maxControlFramePayloadSize {
298 readBufferSize = maxControlFramePayloadSize
299 }
300 br = bufio.NewReaderSize(conn, readBufferSize)
301 }
302
303 var writeBuf []byte
304 if writeBufferSize == 0 && brw != nil && brw.Writer != nil {
305 // Use the bufio.Writer's buffer if the buffer has a useful size. This
306 // code assumes that bufio.Writer.buf[:1] is passed to the
307 // bufio.Writer's underlying writer.
308 var wh writeHook
309 brw.Writer.Reset(&wh)
310 brw.Writer.WriteByte(0)
311 brw.Flush()
312 if cap(wh.p) >= maxFrameHeaderSize+256 {
313 writeBuf = wh.p[:cap(wh.p)]
314 }
315 }
316
317 if writeBuf == nil {
318 if writeBufferSize == 0 {
319 writeBufferSize = defaultWriteBufferSize
320 }
321 writeBuf = make([]byte, writeBufferSize+maxFrameHeaderSize)
322 }
323
324 c := &Conn{
325 isServer: isServer,
326 br: br,
327 conn: conn,
328 mu: mu,
329 readFinal: true,
330 writeBuf: writeBuf,
331 enableWriteCompression: true,
332 compressionLevel: defaultCompressionLevel,
333 }
334 c.SetCloseHandler(nil)
335 c.SetPingHandler(nil)
336 c.SetPongHandler(nil)
337 return c
338 }
339
340 // Subprotocol returns the negotiated protocol for the connection.
341 func (c *Conn) Subprotocol() string {
342 return c.subprotocol
343 }
344
345 // Close closes the underlying network connection without sending or waiting for a close frame.
346 func (c *Conn) Close() error {
347 return c.conn.Close()
348 }
349
350 // LocalAddr returns the local network address.
351 func (c *Conn) LocalAddr() net.Addr {
352 return c.conn.LocalAddr()
353 }
354
355 // RemoteAddr returns the remote network address.
356 func (c *Conn) RemoteAddr() net.Addr {
357 return c.conn.RemoteAddr()
358 }
359
360 // Write methods
361
362 func (c *Conn) writeFatal(err error) error {
363 err = hideTempErr(err)
364 c.writeErrMu.Lock()
365 if c.writeErr == nil {
366 c.writeErr = err
367 }
368 c.writeErrMu.Unlock()
369 return err
370 }
371
372 func (c *Conn) write(frameType int, deadline time.Time, bufs ...[]byte) error {
373 <-c.mu
374 defer func() { c.mu <- true }()
375
376 c.writeErrMu.Lock()
377 err := c.writeErr
378 c.writeErrMu.Unlock()
379 if err != nil {
380 return err
381 }
382
383 c.conn.SetWriteDeadline(deadline)
384 for _, buf := range bufs {
385 if len(buf) > 0 {
386 _, err := c.conn.Write(buf)
387 if err != nil {
388 return c.writeFatal(err)
389 }
390 }
391 }
392
393 if frameType == CloseMessage {
394 c.writeFatal(ErrCloseSent)
395 }
396 return nil
397 }
398
399 // WriteControl writes a control message with the given deadline. The allowed
400 // message types are CloseMessage, PingMessage and PongMessage.
401 func (c *Conn) WriteControl(messageType int, data []byte, deadline time.Time) error {
402 if !isControl(messageType) {
403 return errBadWriteOpCode
404 }
405 if len(data) > maxControlFramePayloadSize {
406 return errInvalidControlFrame
407 }
408
409 b0 := byte(messageType) | finalBit
410 b1 := byte(len(data))
411 if !c.isServer {
412 b1 |= maskBit
413 }
414
415 buf := make([]byte, 0, maxFrameHeaderSize+maxControlFramePayloadSize)
416 buf = append(buf, b0, b1)
417
418 if c.isServer {
419 buf = append(buf, data...)
420 } else {
421 key := newMaskKey()
422 buf = append(buf, key[:]...)
423 buf = append(buf, data...)
424 maskBytes(key, 0, buf[6:])
425 }
426
427 d := time.Hour * 1000
428 if !deadline.IsZero() {
429 d = deadline.Sub(time.Now())
430 if d < 0 {
431 return errWriteTimeout
432 }
433 }
434
435 timer := time.NewTimer(d)
436 select {
437 case <-c.mu:
438 timer.Stop()
439 case <-timer.C:
440 return errWriteTimeout
441 }
442 defer func() { c.mu <- true }()
443
444 c.writeErrMu.Lock()
445 err := c.writeErr
446 c.writeErrMu.Unlock()
447 if err != nil {
448 return err
449 }
450
451 c.conn.SetWriteDeadline(deadline)
452 _, err = c.conn.Write(buf)
453 if err != nil {
454 return c.writeFatal(err)
455 }
456 if messageType == CloseMessage {
457 c.writeFatal(ErrCloseSent)
458 }
459 return err
460 }
461
462 func (c *Conn) prepWrite(messageType int) error {
463 // Close previous writer if not already closed by the application. It's
464 // probably better to return an error in this situation, but we cannot
465 // change this without breaking existing applications.
466 if c.writer != nil {
467 c.writer.Close()
468 c.writer = nil
469 }
470
471 if !isControl(messageType) && !isData(messageType) {
472 return errBadWriteOpCode
473 }
474
475 c.writeErrMu.Lock()
476 err := c.writeErr
477 c.writeErrMu.Unlock()
478 return err
479 }
480
481 // NextWriter returns a writer for the next message to send. The writer's Close
482 // method flushes the complete message to the network.
483 //
484 // There can be at most one open writer on a connection. NextWriter closes the
485 // previous writer if the application has not already done so.
486 func (c *Conn) NextWriter(messageType int) (io.WriteCloser, error) {
487 if err := c.prepWrite(messageType); err != nil {
488 return nil, err
489 }
490
491 mw := &messageWriter{
492 c: c,
493 frameType: messageType,
494 pos: maxFrameHeaderSize,
495 }
496 c.writer = mw
497 if c.newCompressionWriter != nil && c.enableWriteCompression && isData(messageType) {
498 w := c.newCompressionWriter(c.writer, c.compressionLevel)
499 mw.compress = true
500 c.writer = w
501 }
502 return c.writer, nil
503 }
504
505 type messageWriter struct {
506 c *Conn
507 compress bool // whether next call to flushFrame should set RSV1
508 pos int // end of data in writeBuf.
509 frameType int // type of the current frame.
510 err error
511 }
512
513 func (w *messageWriter) fatal(err error) error {
514 if w.err != nil {
515 w.err = err
516 w.c.writer = nil
517 }
518 return err
519 }
520
521 // flushFrame writes buffered data and extra as a frame to the network. The
522 // final argument indicates that this is the last frame in the message.
523 func (w *messageWriter) flushFrame(final bool, extra []byte) error {
524 c := w.c
525 length := w.pos - maxFrameHeaderSize + len(extra)
526
527 // Check for invalid control frames.
528 if isControl(w.frameType) &&
529 (!final || length > maxControlFramePayloadSize) {
530 return w.fatal(errInvalidControlFrame)
531 }
532
533 b0 := byte(w.frameType)
534 if final {
535 b0 |= finalBit
536 }
537 if w.compress {
538 b0 |= rsv1Bit
539 }
540 w.compress = false
541
542 b1 := byte(0)
543 if !c.isServer {
544 b1 |= maskBit
545 }
546
547 // Assume that the frame starts at beginning of c.writeBuf.
548 framePos := 0
549 if c.isServer {
550 // Adjust up if mask not included in the header.
551 framePos = 4
552 }
553
554 switch {
555 case length >= 65536:
556 c.writeBuf[framePos] = b0
557 c.writeBuf[framePos+1] = b1 | 127
558 binary.BigEndian.PutUint64(c.writeBuf[framePos+2:], uint64(length))
559 case length > 125:
560 framePos += 6
561 c.writeBuf[framePos] = b0
562 c.writeBuf[framePos+1] = b1 | 126
563 binary.BigEndian.PutUint16(c.writeBuf[framePos+2:], uint16(length))
564 default:
565 framePos += 8
566 c.writeBuf[framePos] = b0
567 c.writeBuf[framePos+1] = b1 | byte(length)
568 }
569
570 if !c.isServer {
571 key := newMaskKey()
572 copy(c.writeBuf[maxFrameHeaderSize-4:], key[:])
573 maskBytes(key, 0, c.writeBuf[maxFrameHeaderSize:w.pos])
574 if len(extra) > 0 {
575 return c.writeFatal(errors.New("websocket: internal error, extra used in client mode"))
576 }
577 }
578
579 // Write the buffers to the connection with best-effort detection of
580 // concurrent writes. See the concurrency section in the package
581 // documentation for more info.
582
583 if c.isWriting {
584 panic("concurrent write to websocket connection")
585 }
586 c.isWriting = true
587
588 err := c.write(w.frameType, c.writeDeadline, c.writeBuf[framePos:w.pos], extra)
589
590 if !c.isWriting {
591 panic("concurrent write to websocket connection")
592 }
593 c.isWriting = false
594
595 if err != nil {
596 return w.fatal(err)
597 }
598
599 if final {
600 c.writer = nil
601 return nil
602 }
603
604 // Setup for next frame.
605 w.pos = maxFrameHeaderSize
606 w.frameType = continuationFrame
607 return nil
608 }
609
610 func (w *messageWriter) ncopy(max int) (int, error) {
611 n := len(w.c.writeBuf) - w.pos
612 if n <= 0 {
613 if err := w.flushFrame(false, nil); err != nil {
614 return 0, err
615 }
616 n = len(w.c.writeBuf) - w.pos
617 }
618 if n > max {
619 n = max
620 }
621 return n, nil
622 }
623
624 func (w *messageWriter) Write(p []byte) (int, error) {
625 if w.err != nil {
626 return 0, w.err
627 }
628
629 if len(p) > 2*len(w.c.writeBuf) && w.c.isServer {
630 // Don't buffer large messages.
631 err := w.flushFrame(false, p)
632 if err != nil {
633 return 0, err
634 }
635 return len(p), nil
636 }
637
638 nn := len(p)
639 for len(p) > 0 {
640 n, err := w.ncopy(len(p))
641 if err != nil {
642 return 0, err
643 }
644 copy(w.c.writeBuf[w.pos:], p[:n])
645 w.pos += n
646 p = p[n:]
647 }
648 return nn, nil
649 }
650
651 func (w *messageWriter) WriteString(p string) (int, error) {
652 if w.err != nil {
653 return 0, w.err
654 }
655
656 nn := len(p)
657 for len(p) > 0 {
658 n, err := w.ncopy(len(p))
659 if err != nil {
660 return 0, err
661 }
662 copy(w.c.writeBuf[w.pos:], p[:n])
663 w.pos += n
664 p = p[n:]
665 }
666 return nn, nil
667 }
668
669 func (w *messageWriter) ReadFrom(r io.Reader) (nn int64, err error) {
670 if w.err != nil {
671 return 0, w.err
672 }
673 for {
674 if w.pos == len(w.c.writeBuf) {
675 err = w.flushFrame(false, nil)
676 if err != nil {
677 break
678 }
679 }
680 var n int
681 n, err = r.Read(w.c.writeBuf[w.pos:])
682 w.pos += n
683 nn += int64(n)
684 if err != nil {
685 if err == io.EOF {
686 err = nil
687 }
688 break
689 }
690 }
691 return nn, err
692 }
693
694 func (w *messageWriter) Close() error {
695 if w.err != nil {
696 return w.err
697 }
698 if err := w.flushFrame(true, nil); err != nil {
699 return err
700 }
701 w.err = errWriteClosed
702 return nil
703 }
704
705 // WritePreparedMessage writes prepared message into connection.
706 func (c *Conn) WritePreparedMessage(pm *PreparedMessage) error {
707 frameType, frameData, err := pm.frame(prepareKey{
708 isServer: c.isServer,
709 compress: c.newCompressionWriter != nil && c.enableWriteCompression && isData(pm.messageType),
710 compressionLevel: c.compressionLevel,
711 })
712 if err != nil {
713 return err
714 }
715 if c.isWriting {
716 panic("concurrent write to websocket connection")
717 }
718 c.isWriting = true
719 err = c.write(frameType, c.writeDeadline, frameData, nil)
720 if !c.isWriting {
721 panic("concurrent write to websocket connection")
722 }
723 c.isWriting = false
724 return err
725 }
726
727 // WriteMessage is a helper method for getting a writer using NextWriter,
728 // writing the message and closing the writer.
729 func (c *Conn) WriteMessage(messageType int, data []byte) error {
730
731 if c.isServer && (c.newCompressionWriter == nil || !c.enableWriteCompression) {
732 // Fast path with no allocations and single frame.
733
734 if err := c.prepWrite(messageType); err != nil {
735 return err
736 }
737 mw := messageWriter{c: c, frameType: messageType, pos: maxFrameHeaderSize}
738 n := copy(c.writeBuf[mw.pos:], data)
739 mw.pos += n
740 data = data[n:]
741 return mw.flushFrame(true, data)
742 }
743
744 w, err := c.NextWriter(messageType)
745 if err != nil {
746 return err
747 }
748 if _, err = w.Write(data); err != nil {
749 return err
750 }
751 return w.Close()
752 }
753
754 // SetWriteDeadline sets the write deadline on the underlying network
755 // connection. After a write has timed out, the websocket state is corrupt and
756 // all future writes will return an error. A zero value for t means writes will
757 // not time out.
758 func (c *Conn) SetWriteDeadline(t time.Time) error {
759 c.writeDeadline = t
760 return nil
761 }
762
763 // Read methods
764
765 func (c *Conn) advanceFrame() (int, error) {
766
767 // 1. Skip remainder of previous frame.
768
769 if c.readRemaining > 0 {
770 if _, err := io.CopyN(ioutil.Discard, c.br, c.readRemaining); err != nil {
771 return noFrame, err
772 }
773 }
774
775 // 2. Read and parse first two bytes of frame header.
776
777 p, err := c.read(2)
778 if err != nil {
779 return noFrame, err
780 }
781
782 final := p[0]&finalBit != 0
783 frameType := int(p[0] & 0xf)
784 mask := p[1]&maskBit != 0
785 c.readRemaining = int64(p[1] & 0x7f)
786
787 c.readDecompress = false
788 if c.newDecompressionReader != nil && (p[0]&rsv1Bit) != 0 {
789 c.readDecompress = true
790 p[0] &^= rsv1Bit
791 }
792
793 if rsv := p[0] & (rsv1Bit | rsv2Bit | rsv3Bit); rsv != 0 {
794 return noFrame, c.handleProtocolError("unexpected reserved bits 0x" + strconv.FormatInt(int64(rsv), 16))
795 }
796
797 switch frameType {
798 case CloseMessage, PingMessage, PongMessage:
799 if c.readRemaining > maxControlFramePayloadSize {
800 return noFrame, c.handleProtocolError("control frame length > 125")
801 }
802 if !final {
803 return noFrame, c.handleProtocolError("control frame not final")
804 }
805 case TextMessage, BinaryMessage:
806 if !c.readFinal {
807 return noFrame, c.handleProtocolError("message start before final message frame")
808 }
809 c.readFinal = final
810 case continuationFrame:
811 if c.readFinal {
812 return noFrame, c.handleProtocolError("continuation after final message frame")
813 }
814 c.readFinal = final
815 default:
816 return noFrame, c.handleProtocolError("unknown opcode " + strconv.Itoa(frameType))
817 }
818
819 // 3. Read and parse frame length.
820
821 switch c.readRemaining {
822 case 126:
823 p, err := c.read(2)
824 if err != nil {
825 return noFrame, err
826 }
827 c.readRemaining = int64(binary.BigEndian.Uint16(p))
828 case 127:
829 p, err := c.read(8)
830 if err != nil {
831 return noFrame, err
832 }
833 c.readRemaining = int64(binary.BigEndian.Uint64(p))
834 }
835
836 // 4. Handle frame masking.
837
838 if mask != c.isServer {
839 return noFrame, c.handleProtocolError("incorrect mask flag")
840 }
841
842 if mask {
843 c.readMaskPos = 0
844 p, err := c.read(len(c.readMaskKey))
845 if err != nil {
846 return noFrame, err
847 }
848 copy(c.readMaskKey[:], p)
849 }
850
851 // 5. For text and binary messages, enforce read limit and return.
852
853 if frameType == continuationFrame || frameType == TextMessage || frameType == BinaryMessage {
854
855 c.readLength += c.readRemaining
856 if c.readLimit > 0 && c.readLength > c.readLimit {
857 c.WriteControl(CloseMessage, FormatCloseMessage(CloseMessageTooBig, ""), time.Now().Add(writeWait))
858 return noFrame, ErrReadLimit
859 }
860
861 return frameType, nil
862 }
863
864 // 6. Read control frame payload.
865
866 var payload []byte
867 if c.readRemaining > 0 {
868 payload, err = c.read(int(c.readRemaining))
869 c.readRemaining = 0
870 if err != nil {
871 return noFrame, err
872 }
873 if c.isServer {
874 maskBytes(c.readMaskKey, 0, payload)
875 }
876 }
877
878 // 7. Process control frame payload.
879
880 switch frameType {
881 case PongMessage:
882 if err := c.handlePong(string(payload)); err != nil {
883 return noFrame, err
884 }
885 case PingMessage:
886 if err := c.handlePing(string(payload)); err != nil {
887 return noFrame, err
888 }
889 case CloseMessage:
890 closeCode := CloseNoStatusReceived
891 closeText := ""
892 if len(payload) >= 2 {
893 closeCode = int(binary.BigEndian.Uint16(payload))
894 if !isValidReceivedCloseCode(closeCode) {
895 return noFrame, c.handleProtocolError("invalid close code")
896 }
897 closeText = string(payload[2:])
898 if !utf8.ValidString(closeText) {
899 return noFrame, c.handleProtocolError("invalid utf8 payload in close frame")
900 }
901 }
902 if err := c.handleClose(closeCode, closeText); err != nil {
903 return noFrame, err
904 }
905 return noFrame, &CloseError{Code: closeCode, Text: closeText}
906 }
907
908 return frameType, nil
909 }
910
911 func (c *Conn) handleProtocolError(message string) error {
912 c.WriteControl(CloseMessage, FormatCloseMessage(CloseProtocolError, message), time.Now().Add(writeWait))
913 return errors.New("websocket: " + message)
914 }
915
916 // NextReader returns the next data message received from the peer. The
917 // returned messageType is either TextMessage or BinaryMessage.
918 //
919 // There can be at most one open reader on a connection. NextReader discards
920 // the previous message if the application has not already consumed it.
921 //
922 // Applications must break out of the application's read loop when this method
923 // returns a non-nil error value. Errors returned from this method are
924 // permanent. Once this method returns a non-nil error, all subsequent calls to
925 // this method return the same error.
926 func (c *Conn) NextReader() (messageType int, r io.Reader, err error) {
927 // Close previous reader, only relevant for decompression.
928 if c.reader != nil {
929 c.reader.Close()
930 c.reader = nil
931 }
932
933 c.messageReader = nil
934 c.readLength = 0
935
936 for c.readErr == nil {
937 frameType, err := c.advanceFrame()
938 if err != nil {
939 c.readErr = hideTempErr(err)
940 break
941 }
942 if frameType == TextMessage || frameType == BinaryMessage {
943 c.messageReader = &messageReader{c}
944 c.reader = c.messageReader
945 if c.readDecompress {
946 c.reader = c.newDecompressionReader(c.reader)
947 }
948 return frameType, c.reader, nil
949 }
950 }
951
952 // Applications that do handle the error returned from this method spin in
953 // tight loop on connection failure. To help application developers detect
954 // this error, panic on repeated reads to the failed connection.
955 c.readErrCount++
956 if c.readErrCount >= 1000 {
957 panic("repeated read on failed websocket connection")
958 }
959
960 return noFrame, nil, c.readErr
961 }
962
963 type messageReader struct{ c *Conn }
964
965 func (r *messageReader) Read(b []byte) (int, error) {
966 c := r.c
967 if c.messageReader != r {
968 return 0, io.EOF
969 }
970
971 for c.readErr == nil {
972
973 if c.readRemaining > 0 {
974 if int64(len(b)) > c.readRemaining {
975 b = b[:c.readRemaining]
976 }
977 n, err := c.br.Read(b)
978 c.readErr = hideTempErr(err)
979 if c.isServer {
980 c.readMaskPos = maskBytes(c.readMaskKey, c.readMaskPos, b[:n])
981 }
982 c.readRemaining -= int64(n)
983 if c.readRemaining > 0 && c.readErr == io.EOF {
984 c.readErr = errUnexpectedEOF
985 }
986 return n, c.readErr
987 }
988
989 if c.readFinal {
990 c.messageReader = nil
991 return 0, io.EOF
992 }
993
994 frameType, err := c.advanceFrame()
995 switch {
996 case err != nil:
997 c.readErr = hideTempErr(err)
998 case frameType == TextMessage || frameType == BinaryMessage:
999 c.readErr = errors.New("websocket: internal error, unexpected text or binary in Reader")
1000 }
1001 }
1002
1003 err := c.readErr
1004 if err == io.EOF && c.messageReader == r {
1005 err = errUnexpectedEOF
1006 }
1007 return 0, err
1008 }
1009
1010 func (r *messageReader) Close() error {
1011 return nil
1012 }
1013
1014 // ReadMessage is a helper method for getting a reader using NextReader and
1015 // reading from that reader to a buffer.
1016 func (c *Conn) ReadMessage() (messageType int, p []byte, err error) {
1017 var r io.Reader
1018 messageType, r, err = c.NextReader()
1019 if err != nil {
1020 return messageType, nil, err
1021 }
1022 p, err = ioutil.ReadAll(r)
1023 return messageType, p, err
1024 }
1025
1026 // SetReadDeadline sets the read deadline on the underlying network connection.
1027 // After a read has timed out, the websocket connection state is corrupt and
1028 // all future reads will return an error. A zero value for t means reads will
1029 // not time out.
1030 func (c *Conn) SetReadDeadline(t time.Time) error {
1031 return c.conn.SetReadDeadline(t)
1032 }
1033
1034 // SetReadLimit sets the maximum size for a message read from the peer. If a
1035 // message exceeds the limit, the connection sends a close frame to the peer
1036 // and returns ErrReadLimit to the application.
1037 func (c *Conn) SetReadLimit(limit int64) {
1038 c.readLimit = limit
1039 }
1040
1041 // CloseHandler returns the current close handler
1042 func (c *Conn) CloseHandler() func(code int, text string) error {
1043 return c.handleClose
1044 }
1045
1046 // SetCloseHandler sets the handler for close messages received from the peer.
1047 // The code argument to h is the received close code or CloseNoStatusReceived
1048 // if the close message is empty. The default close handler sends a close frame
1049 // back to the peer.
1050 //
1051 // The application must read the connection to process close messages as
1052 // described in the section on Control Frames above.
1053 //
1054 // The connection read methods return a CloseError when a close frame is
1055 // received. Most applications should handle close messages as part of their
1056 // normal error handling. Applications should only set a close handler when the
1057 // application must perform some action before sending a close frame back to
1058 // the peer.
1059 func (c *Conn) SetCloseHandler(h func(code int, text string) error) {
1060 if h == nil {
1061 h = func(code int, text string) error {
1062 message := []byte{}
1063 if code != CloseNoStatusReceived {
1064 message = FormatCloseMessage(code, "")
1065 }
1066 c.WriteControl(CloseMessage, message, time.Now().Add(writeWait))
1067 return nil
1068 }
1069 }
1070 c.handleClose = h
1071 }
1072
1073 // PingHandler returns the current ping handler
1074 func (c *Conn) PingHandler() func(appData string) error {
1075 return c.handlePing
1076 }
1077
1078 // SetPingHandler sets the handler for ping messages received from the peer.
1079 // The appData argument to h is the PING frame application data. The default
1080 // ping handler sends a pong to the peer.
1081 //
1082 // The application must read the connection to process ping messages as
1083 // described in the section on Control Frames above.
1084 func (c *Conn) SetPingHandler(h func(appData string) error) {
1085 if h == nil {
1086 h = func(message string) error {
1087 err := c.WriteControl(PongMessage, []byte(message), time.Now().Add(writeWait))
1088 if err == ErrCloseSent {
1089 return nil
1090 } else if e, ok := err.(net.Error); ok && e.Temporary() {
1091 return nil
1092 }
1093 return err
1094 }
1095 }
1096 c.handlePing = h
1097 }
1098
1099 // PongHandler returns the current pong handler
1100 func (c *Conn) PongHandler() func(appData string) error {
1101 return c.handlePong
1102 }
1103
1104 // SetPongHandler sets the handler for pong messages received from the peer.
1105 // The appData argument to h is the PONG frame application data. The default
1106 // pong handler does nothing.
1107 //
1108 // The application must read the connection to process ping messages as
1109 // described in the section on Control Frames above.
1110 func (c *Conn) SetPongHandler(h func(appData string) error) {
1111 if h == nil {
1112 h = func(string) error { return nil }
1113 }
1114 c.handlePong = h
1115 }
1116
1117 // UnderlyingConn returns the internal net.Conn. This can be used to further
1118 // modifications to connection specific flags.
1119 func (c *Conn) UnderlyingConn() net.Conn {
1120 return c.conn
1121 }
1122
1123 // EnableWriteCompression enables and disables write compression of
1124 // subsequent text and binary messages. This function is a noop if
1125 // compression was not negotiated with the peer.
1126 func (c *Conn) EnableWriteCompression(enable bool) {
1127 c.enableWriteCompression = enable
1128 }
1129
1130 // SetCompressionLevel sets the flate compression level for subsequent text and
1131 // binary messages. This function is a noop if compression was not negotiated
1132 // with the peer. See the compress/flate package for a description of
1133 // compression levels.
1134 func (c *Conn) SetCompressionLevel(level int) error {
1135 if !isValidCompressionLevel(level) {
1136 return errors.New("websocket: invalid compression level")
1137 }
1138 c.compressionLevel = level
1139 return nil
1140 }
1141
1142 // FormatCloseMessage formats closeCode and text as a WebSocket close message.
1143 func FormatCloseMessage(closeCode int, text string) []byte {
1144 buf := make([]byte, 2+len(text))
1145 binary.BigEndian.PutUint16(buf, uint16(closeCode))
1146 copy(buf[2:], text)
1147 return buf
1148 }
0 // Copyright 2016 The Gorilla WebSocket Authors. All rights reserved.
1 // Use of this source code is governed by a BSD-style
2 // license that can be found in the LICENSE file.
3
4 // +build go1.5
5
6 package websocket
7
8 import "io"
9
10 func (c *Conn) read(n int) ([]byte, error) {
11 p, err := c.br.Peek(n)
12 if err == io.EOF {
13 err = errUnexpectedEOF
14 }
15 c.br.Discard(len(p))
16 return p, err
17 }
0 // Copyright 2016 The Gorilla WebSocket Authors. All rights reserved.
1 // Use of this source code is governed by a BSD-style
2 // license that can be found in the LICENSE file.
3
4 // +build !go1.5
5
6 package websocket
7
8 import "io"
9
10 func (c *Conn) read(n int) ([]byte, error) {
11 p, err := c.br.Peek(n)
12 if err == io.EOF {
13 err = errUnexpectedEOF
14 }
15 if len(p) > 0 {
16 // advance over the bytes just read
17 io.ReadFull(c.br, p)
18 }
19 return p, err
20 }
0 // Copyright 2013 The Gorilla WebSocket Authors. All rights reserved.
1 // Use of this source code is governed by a BSD-style
2 // license that can be found in the LICENSE file.
3
4 // Package websocket implements the WebSocket protocol defined in RFC 6455.
5 //
6 // Overview
7 //
8 // The Conn type represents a WebSocket connection. A server application uses
9 // the Upgrade function from an Upgrader object with a HTTP request handler
10 // to get a pointer to a Conn:
11 //
12 // var upgrader = websocket.Upgrader{
13 // ReadBufferSize: 1024,
14 // WriteBufferSize: 1024,
15 // }
16 //
17 // func handler(w http.ResponseWriter, r *http.Request) {
18 // conn, err := upgrader.Upgrade(w, r, nil)
19 // if err != nil {
20 // log.Println(err)
21 // return
22 // }
23 // ... Use conn to send and receive messages.
24 // }
25 //
26 // Call the connection's WriteMessage and ReadMessage methods to send and
27 // receive messages as a slice of bytes. This snippet of code shows how to echo
28 // messages using these methods:
29 //
30 // for {
31 // messageType, p, err := conn.ReadMessage()
32 // if err != nil {
33 // return
34 // }
35 // if err = conn.WriteMessage(messageType, p); err != nil {
36 // return err
37 // }
38 // }
39 //
40 // In above snippet of code, p is a []byte and messageType is an int with value
41 // websocket.BinaryMessage or websocket.TextMessage.
42 //
43 // An application can also send and receive messages using the io.WriteCloser
44 // and io.Reader interfaces. To send a message, call the connection NextWriter
45 // method to get an io.WriteCloser, write the message to the writer and close
46 // the writer when done. To receive a message, call the connection NextReader
47 // method to get an io.Reader and read until io.EOF is returned. This snippet
48 // shows how to echo messages using the NextWriter and NextReader methods:
49 //
50 // for {
51 // messageType, r, err := conn.NextReader()
52 // if err != nil {
53 // return
54 // }
55 // w, err := conn.NextWriter(messageType)
56 // if err != nil {
57 // return err
58 // }
59 // if _, err := io.Copy(w, r); err != nil {
60 // return err
61 // }
62 // if err := w.Close(); err != nil {
63 // return err
64 // }
65 // }
66 //
67 // Data Messages
68 //
69 // The WebSocket protocol distinguishes between text and binary data messages.
70 // Text messages are interpreted as UTF-8 encoded text. The interpretation of
71 // binary messages is left to the application.
72 //
73 // This package uses the TextMessage and BinaryMessage integer constants to
74 // identify the two data message types. The ReadMessage and NextReader methods
75 // return the type of the received message. The messageType argument to the
76 // WriteMessage and NextWriter methods specifies the type of a sent message.
77 //
78 // It is the application's responsibility to ensure that text messages are
79 // valid UTF-8 encoded text.
80 //
81 // Control Messages
82 //
83 // The WebSocket protocol defines three types of control messages: close, ping
84 // and pong. Call the connection WriteControl, WriteMessage or NextWriter
85 // methods to send a control message to the peer.
86 //
87 // Connections handle received close messages by sending a close message to the
88 // peer and returning a *CloseError from the the NextReader, ReadMessage or the
89 // message Read method.
90 //
91 // Connections handle received ping and pong messages by invoking callback
92 // functions set with SetPingHandler and SetPongHandler methods. The callback
93 // functions are called from the NextReader, ReadMessage and the message Read
94 // methods.
95 //
96 // The default ping handler sends a pong to the peer. The application's reading
97 // goroutine can block for a short time while the handler writes the pong data
98 // to the connection.
99 //
100 // The application must read the connection to process ping, pong and close
101 // messages sent from the peer. If the application is not otherwise interested
102 // in messages from the peer, then the application should start a goroutine to
103 // read and discard messages from the peer. A simple example is:
104 //
105 // func readLoop(c *websocket.Conn) {
106 // for {
107 // if _, _, err := c.NextReader(); err != nil {
108 // c.Close()
109 // break
110 // }
111 // }
112 // }
113 //
114 // Concurrency
115 //
116 // Connections support one concurrent reader and one concurrent writer.
117 //
118 // Applications are responsible for ensuring that no more than one goroutine
119 // calls the write methods (NextWriter, SetWriteDeadline, WriteMessage,
120 // WriteJSON, EnableWriteCompression, SetCompressionLevel) concurrently and
121 // that no more than one goroutine calls the read methods (NextReader,
122 // SetReadDeadline, ReadMessage, ReadJSON, SetPongHandler, SetPingHandler)
123 // concurrently.
124 //
125 // The Close and WriteControl methods can be called concurrently with all other
126 // methods.
127 //
128 // Origin Considerations
129 //
130 // Web browsers allow Javascript applications to open a WebSocket connection to
131 // any host. It's up to the server to enforce an origin policy using the Origin
132 // request header sent by the browser.
133 //
134 // The Upgrader calls the function specified in the CheckOrigin field to check
135 // the origin. If the CheckOrigin function returns false, then the Upgrade
136 // method fails the WebSocket handshake with HTTP status 403.
137 //
138 // If the CheckOrigin field is nil, then the Upgrader uses a safe default: fail
139 // the handshake if the Origin request header is present and not equal to the
140 // Host request header.
141 //
142 // An application can allow connections from any origin by specifying a
143 // function that always returns true:
144 //
145 // var upgrader = websocket.Upgrader{
146 // CheckOrigin: func(r *http.Request) bool { return true },
147 // }
148 //
149 // The deprecated Upgrade function does not enforce an origin policy. It's the
150 // application's responsibility to check the Origin header before calling
151 // Upgrade.
152 //
153 // Compression EXPERIMENTAL
154 //
155 // Per message compression extensions (RFC 7692) are experimentally supported
156 // by this package in a limited capacity. Setting the EnableCompression option
157 // to true in Dialer or Upgrader will attempt to negotiate per message deflate
158 // support.
159 //
160 // var upgrader = websocket.Upgrader{
161 // EnableCompression: true,
162 // }
163 //
164 // If compression was successfully negotiated with the connection's peer, any
165 // message received in compressed form will be automatically decompressed.
166 // All Read methods will return uncompressed bytes.
167 //
168 // Per message compression of messages written to a connection can be enabled
169 // or disabled by calling the corresponding Conn method:
170 //
171 // conn.EnableWriteCompression(false)
172 //
173 // Currently this package does not support compression with "context takeover".
174 // This means that messages must be compressed and decompressed in isolation,
175 // without retaining sliding window or dictionary state across messages. For
176 // more details refer to RFC 7692.
177 //
178 // Use of compression is experimental and may result in decreased performance.
179 package websocket
0 // Copyright 2013 The Gorilla WebSocket Authors. All rights reserved.
1 // Use of this source code is governed by a BSD-style
2 // license that can be found in the LICENSE file.
3
4 package websocket
5
6 import (
7 "encoding/json"
8 "io"
9 )
10
11 // WriteJSON is deprecated, use c.WriteJSON instead.
12 func WriteJSON(c *Conn, v interface{}) error {
13 return c.WriteJSON(v)
14 }
15
16 // WriteJSON writes the JSON encoding of v to the connection.
17 //
18 // See the documentation for encoding/json Marshal for details about the
19 // conversion of Go values to JSON.
20 func (c *Conn) WriteJSON(v interface{}) error {
21 w, err := c.NextWriter(TextMessage)
22 if err != nil {
23 return err
24 }
25 err1 := json.NewEncoder(w).Encode(v)
26 err2 := w.Close()
27 if err1 != nil {
28 return err1
29 }
30 return err2
31 }
32
33 // ReadJSON is deprecated, use c.ReadJSON instead.
34 func ReadJSON(c *Conn, v interface{}) error {
35 return c.ReadJSON(v)
36 }
37
38 // ReadJSON reads the next JSON-encoded message from the connection and stores
39 // it in the value pointed to by v.
40 //
41 // See the documentation for the encoding/json Unmarshal function for details
42 // about the conversion of JSON to a Go value.
43 func (c *Conn) ReadJSON(v interface{}) error {
44 _, r, err := c.NextReader()
45 if err != nil {
46 return err
47 }
48 err = json.NewDecoder(r).Decode(v)
49 if err == io.EOF {
50 // One value is expected in the message.
51 err = io.ErrUnexpectedEOF
52 }
53 return err
54 }
0 // Copyright 2016 The Gorilla WebSocket Authors. All rights reserved. Use of
1 // this source code is governed by a BSD-style license that can be found in the
2 // LICENSE file.
3
4 // +build !appengine
5
6 package websocket
7
8 import "unsafe"
9
10 const wordSize = int(unsafe.Sizeof(uintptr(0)))
11
12 func maskBytes(key [4]byte, pos int, b []byte) int {
13
14 // Mask one byte at a time for small buffers.
15 if len(b) < 2*wordSize {
16 for i := range b {
17 b[i] ^= key[pos&3]
18 pos++
19 }
20 return pos & 3
21 }
22
23 // Mask one byte at a time to word boundary.
24 if n := int(uintptr(unsafe.Pointer(&b[0]))) % wordSize; n != 0 {
25 n = wordSize - n
26 for i := range b[:n] {
27 b[i] ^= key[pos&3]
28 pos++
29 }
30 b = b[n:]
31 }
32
33 // Create aligned word size key.
34 var k [wordSize]byte
35 for i := range k {
36 k[i] = key[(pos+i)&3]
37 }
38 kw := *(*uintptr)(unsafe.Pointer(&k))
39
40 // Mask one word at a time.
41 n := (len(b) / wordSize) * wordSize
42 for i := 0; i < n; i += wordSize {
43 *(*uintptr)(unsafe.Pointer(uintptr(unsafe.Pointer(&b[0])) + uintptr(i))) ^= kw
44 }
45
46 // Mask one byte at a time for remaining bytes.
47 b = b[n:]
48 for i := range b {
49 b[i] ^= key[pos&3]
50 pos++
51 }
52
53 return pos & 3
54 }
0 // Copyright 2016 The Gorilla WebSocket Authors. All rights reserved. Use of
1 // this source code is governed by a BSD-style license that can be found in the
2 // LICENSE file.
3
4 // +build appengine
5
6 package websocket
7
8 func maskBytes(key [4]byte, pos int, b []byte) int {
9 for i := range b {
10 b[i] ^= key[pos&3]
11 pos++
12 }
13 return pos & 3
14 }
0 // Copyright 2017 The Gorilla WebSocket Authors. All rights reserved.
1 // Use of this source code is governed by a BSD-style
2 // license that can be found in the LICENSE file.
3
4 package websocket
5
6 import (
7 "bytes"
8 "net"
9 "sync"
10 "time"
11 )
12
13 // PreparedMessage caches on the wire representations of a message payload.
14 // Use PreparedMessage to efficiently send a message payload to multiple
15 // connections. PreparedMessage is especially useful when compression is used
16 // because the CPU and memory expensive compression operation can be executed
17 // once for a given set of compression options.
18 type PreparedMessage struct {
19 messageType int
20 data []byte
21 err error
22 mu sync.Mutex
23 frames map[prepareKey]*preparedFrame
24 }
25
26 // prepareKey defines a unique set of options to cache prepared frames in PreparedMessage.
27 type prepareKey struct {
28 isServer bool
29 compress bool
30 compressionLevel int
31 }
32
33 // preparedFrame contains data in wire representation.
34 type preparedFrame struct {
35 once sync.Once
36 data []byte
37 }
38
39 // NewPreparedMessage returns an initialized PreparedMessage. You can then send
40 // it to connection using WritePreparedMessage method. Valid wire
41 // representation will be calculated lazily only once for a set of current
42 // connection options.
43 func NewPreparedMessage(messageType int, data []byte) (*PreparedMessage, error) {
44 pm := &PreparedMessage{
45 messageType: messageType,
46 frames: make(map[prepareKey]*preparedFrame),
47 data: data,
48 }
49
50 // Prepare a plain server frame.
51 _, frameData, err := pm.frame(prepareKey{isServer: true, compress: false})
52 if err != nil {
53 return nil, err
54 }
55
56 // To protect against caller modifying the data argument, remember the data
57 // copied to the plain server frame.
58 pm.data = frameData[len(frameData)-len(data):]
59 return pm, nil
60 }
61
62 func (pm *PreparedMessage) frame(key prepareKey) (int, []byte, error) {
63 pm.mu.Lock()
64 frame, ok := pm.frames[key]
65 if !ok {
66 frame = &preparedFrame{}
67 pm.frames[key] = frame
68 }
69 pm.mu.Unlock()
70
71 var err error
72 frame.once.Do(func() {
73 // Prepare a frame using a 'fake' connection.
74 // TODO: Refactor code in conn.go to allow more direct construction of
75 // the frame.
76 mu := make(chan bool, 1)
77 mu <- true
78 var nc prepareConn
79 c := &Conn{
80 conn: &nc,
81 mu: mu,
82 isServer: key.isServer,
83 compressionLevel: key.compressionLevel,
84 enableWriteCompression: true,
85 writeBuf: make([]byte, defaultWriteBufferSize+maxFrameHeaderSize),
86 }
87 if key.compress {
88 c.newCompressionWriter = compressNoContextTakeover
89 }
90 err = c.WriteMessage(pm.messageType, pm.data)
91 frame.data = nc.buf.Bytes()
92 })
93 return pm.messageType, frame.data, err
94 }
95
96 type prepareConn struct {
97 buf bytes.Buffer
98 net.Conn
99 }
100
101 func (pc *prepareConn) Write(p []byte) (int, error) { return pc.buf.Write(p) }
102 func (pc *prepareConn) SetWriteDeadline(t time.Time) error { return nil }
0 // Copyright 2013 The Gorilla WebSocket Authors. All rights reserved.
1 // Use of this source code is governed by a BSD-style
2 // license that can be found in the LICENSE file.
3
4 package websocket
5
6 import (
7 "bufio"
8 "errors"
9 "net"
10 "net/http"
11 "net/url"
12 "strings"
13 "time"
14 )
15
16 // HandshakeError describes an error with the handshake from the peer.
17 type HandshakeError struct {
18 message string
19 }
20
21 func (e HandshakeError) Error() string { return e.message }
22
23 // Upgrader specifies parameters for upgrading an HTTP connection to a
24 // WebSocket connection.
25 type Upgrader struct {
26 // HandshakeTimeout specifies the duration for the handshake to complete.
27 HandshakeTimeout time.Duration
28
29 // ReadBufferSize and WriteBufferSize specify I/O buffer sizes. If a buffer
30 // size is zero, then buffers allocated by the HTTP server are used. The
31 // I/O buffer sizes do not limit the size of the messages that can be sent
32 // or received.
33 ReadBufferSize, WriteBufferSize int
34
35 // Subprotocols specifies the server's supported protocols in order of
36 // preference. If this field is set, then the Upgrade method negotiates a
37 // subprotocol by selecting the first match in this list with a protocol
38 // requested by the client.
39 Subprotocols []string
40
41 // Error specifies the function for generating HTTP error responses. If Error
42 // is nil, then http.Error is used to generate the HTTP response.
43 Error func(w http.ResponseWriter, r *http.Request, status int, reason error)
44
45 // CheckOrigin returns true if the request Origin header is acceptable. If
46 // CheckOrigin is nil, the host in the Origin header must not be set or
47 // must match the host of the request.
48 CheckOrigin func(r *http.Request) bool
49
50 // EnableCompression specify if the server should attempt to negotiate per
51 // message compression (RFC 7692). Setting this value to true does not
52 // guarantee that compression will be supported. Currently only "no context
53 // takeover" modes are supported.
54 EnableCompression bool
55 }
56
57 func (u *Upgrader) returnError(w http.ResponseWriter, r *http.Request, status int, reason string) (*Conn, error) {
58 err := HandshakeError{reason}
59 if u.Error != nil {
60 u.Error(w, r, status, err)
61 } else {
62 w.Header().Set("Sec-Websocket-Version", "13")
63 http.Error(w, http.StatusText(status), status)
64 }
65 return nil, err
66 }
67
68 // checkSameOrigin returns true if the origin is not set or is equal to the request host.
69 func checkSameOrigin(r *http.Request) bool {
70 origin := r.Header["Origin"]
71 if len(origin) == 0 {
72 return true
73 }
74 u, err := url.Parse(origin[0])
75 if err != nil {
76 return false
77 }
78 return u.Host == r.Host
79 }
80
81 func (u *Upgrader) selectSubprotocol(r *http.Request, responseHeader http.Header) string {
82 if u.Subprotocols != nil {
83 clientProtocols := Subprotocols(r)
84 for _, serverProtocol := range u.Subprotocols {
85 for _, clientProtocol := range clientProtocols {
86 if clientProtocol == serverProtocol {
87 return clientProtocol
88 }
89 }
90 }
91 } else if responseHeader != nil {
92 return responseHeader.Get("Sec-Websocket-Protocol")
93 }
94 return ""
95 }
96
97 // Upgrade upgrades the HTTP server connection to the WebSocket protocol.
98 //
99 // The responseHeader is included in the response to the client's upgrade
100 // request. Use the responseHeader to specify cookies (Set-Cookie) and the
101 // application negotiated subprotocol (Sec-Websocket-Protocol).
102 //
103 // If the upgrade fails, then Upgrade replies to the client with an HTTP error
104 // response.
105 func (u *Upgrader) Upgrade(w http.ResponseWriter, r *http.Request, responseHeader http.Header) (*Conn, error) {
106 if r.Method != "GET" {
107 return u.returnError(w, r, http.StatusMethodNotAllowed, "websocket: not a websocket handshake: request method is not GET")
108 }
109
110 if _, ok := responseHeader["Sec-Websocket-Extensions"]; ok {
111 return u.returnError(w, r, http.StatusInternalServerError, "websocket: application specific 'Sec-Websocket-Extensions' headers are unsupported")
112 }
113
114 if !tokenListContainsValue(r.Header, "Connection", "upgrade") {
115 return u.returnError(w, r, http.StatusBadRequest, "websocket: not a websocket handshake: 'upgrade' token not found in 'Connection' header")
116 }
117
118 if !tokenListContainsValue(r.Header, "Upgrade", "websocket") {
119 return u.returnError(w, r, http.StatusBadRequest, "websocket: not a websocket handshake: 'websocket' token not found in 'Upgrade' header")
120 }
121
122 if !tokenListContainsValue(r.Header, "Sec-Websocket-Version", "13") {
123 return u.returnError(w, r, http.StatusBadRequest, "websocket: unsupported version: 13 not found in 'Sec-Websocket-Version' header")
124 }
125
126 checkOrigin := u.CheckOrigin
127 if checkOrigin == nil {
128 checkOrigin = checkSameOrigin
129 }
130 if !checkOrigin(r) {
131 return u.returnError(w, r, http.StatusForbidden, "websocket: 'Origin' header value not allowed")
132 }
133
134 challengeKey := r.Header.Get("Sec-Websocket-Key")
135 if challengeKey == "" {
136 return u.returnError(w, r, http.StatusBadRequest, "websocket: not a websocket handshake: `Sec-Websocket-Key' header is missing or blank")
137 }
138
139 subprotocol := u.selectSubprotocol(r, responseHeader)
140
141 // Negotiate PMCE
142 var compress bool
143 if u.EnableCompression {
144 for _, ext := range parseExtensions(r.Header) {
145 if ext[""] != "permessage-deflate" {
146 continue
147 }
148 compress = true
149 break
150 }
151 }
152
153 var (
154 netConn net.Conn
155 err error
156 )
157
158 h, ok := w.(http.Hijacker)
159 if !ok {
160 return u.returnError(w, r, http.StatusInternalServerError, "websocket: response does not implement http.Hijacker")
161 }
162 var brw *bufio.ReadWriter
163 netConn, brw, err = h.Hijack()
164 if err != nil {
165 return u.returnError(w, r, http.StatusInternalServerError, err.Error())
166 }
167
168 if brw.Reader.Buffered() > 0 {
169 netConn.Close()
170 return nil, errors.New("websocket: client sent data before handshake is complete")
171 }
172
173 c := newConnBRW(netConn, true, u.ReadBufferSize, u.WriteBufferSize, brw)
174 c.subprotocol = subprotocol
175
176 if compress {
177 c.newCompressionWriter = compressNoContextTakeover
178 c.newDecompressionReader = decompressNoContextTakeover
179 }
180
181 p := c.writeBuf[:0]
182 p = append(p, "HTTP/1.1 101 Switching Protocols\r\nUpgrade: websocket\r\nConnection: Upgrade\r\nSec-WebSocket-Accept: "...)
183 p = append(p, computeAcceptKey(challengeKey)...)
184 p = append(p, "\r\n"...)
185 if c.subprotocol != "" {
186 p = append(p, "Sec-Websocket-Protocol: "...)
187 p = append(p, c.subprotocol...)
188 p = append(p, "\r\n"...)
189 }
190 if compress {
191 p = append(p, "Sec-Websocket-Extensions: permessage-deflate; server_no_context_takeover; client_no_context_takeover\r\n"...)
192 }
193 for k, vs := range responseHeader {
194 if k == "Sec-Websocket-Protocol" {
195 continue
196 }
197 for _, v := range vs {
198 p = append(p, k...)
199 p = append(p, ": "...)
200 for i := 0; i < len(v); i++ {
201 b := v[i]
202 if b <= 31 {
203 // prevent response splitting.
204 b = ' '
205 }
206 p = append(p, b)
207 }
208 p = append(p, "\r\n"...)
209 }
210 }
211 p = append(p, "\r\n"...)
212
213 // Clear deadlines set by HTTP server.
214 netConn.SetDeadline(time.Time{})
215
216 if u.HandshakeTimeout > 0 {
217 netConn.SetWriteDeadline(time.Now().Add(u.HandshakeTimeout))
218 }
219 if _, err = netConn.Write(p); err != nil {
220 netConn.Close()
221 return nil, err
222 }
223 if u.HandshakeTimeout > 0 {
224 netConn.SetWriteDeadline(time.Time{})
225 }
226
227 return c, nil
228 }
229
230 // Upgrade upgrades the HTTP server connection to the WebSocket protocol.
231 //
232 // This function is deprecated, use websocket.Upgrader instead.
233 //
234 // The application is responsible for checking the request origin before
235 // calling Upgrade. An example implementation of the same origin policy is:
236 //
237 // if req.Header.Get("Origin") != "http://"+req.Host {
238 // http.Error(w, "Origin not allowed", 403)
239 // return
240 // }
241 //
242 // If the endpoint supports subprotocols, then the application is responsible
243 // for negotiating the protocol used on the connection. Use the Subprotocols()
244 // function to get the subprotocols requested by the client. Use the
245 // Sec-Websocket-Protocol response header to specify the subprotocol selected
246 // by the application.
247 //
248 // The responseHeader is included in the response to the client's upgrade
249 // request. Use the responseHeader to specify cookies (Set-Cookie) and the
250 // negotiated subprotocol (Sec-Websocket-Protocol).
251 //
252 // The connection buffers IO to the underlying network connection. The
253 // readBufSize and writeBufSize parameters specify the size of the buffers to
254 // use. Messages can be larger than the buffers.
255 //
256 // If the request is not a valid WebSocket handshake, then Upgrade returns an
257 // error of type HandshakeError. Applications should handle this error by
258 // replying to the client with an HTTP error response.
259 func Upgrade(w http.ResponseWriter, r *http.Request, responseHeader http.Header, readBufSize, writeBufSize int) (*Conn, error) {
260 u := Upgrader{ReadBufferSize: readBufSize, WriteBufferSize: writeBufSize}
261 u.Error = func(w http.ResponseWriter, r *http.Request, status int, reason error) {
262 // don't return errors to maintain backwards compatibility
263 }
264 u.CheckOrigin = func(r *http.Request) bool {
265 // allow all connections by default
266 return true
267 }
268 return u.Upgrade(w, r, responseHeader)
269 }
270
271 // Subprotocols returns the subprotocols requested by the client in the
272 // Sec-Websocket-Protocol header.
273 func Subprotocols(r *http.Request) []string {
274 h := strings.TrimSpace(r.Header.Get("Sec-Websocket-Protocol"))
275 if h == "" {
276 return nil
277 }
278 protocols := strings.Split(h, ",")
279 for i := range protocols {
280 protocols[i] = strings.TrimSpace(protocols[i])
281 }
282 return protocols
283 }
284
285 // IsWebSocketUpgrade returns true if the client requested upgrade to the
286 // WebSocket protocol.
287 func IsWebSocketUpgrade(r *http.Request) bool {
288 return tokenListContainsValue(r.Header, "Connection", "upgrade") &&
289 tokenListContainsValue(r.Header, "Upgrade", "websocket")
290 }
0 // Copyright 2013 The Gorilla WebSocket Authors. All rights reserved.
1 // Use of this source code is governed by a BSD-style
2 // license that can be found in the LICENSE file.
3
4 package websocket
5
6 import (
7 "crypto/rand"
8 "crypto/sha1"
9 "encoding/base64"
10 "io"
11 "net/http"
12 "strings"
13 )
14
15 var keyGUID = []byte("258EAFA5-E914-47DA-95CA-C5AB0DC85B11")
16
17 func computeAcceptKey(challengeKey string) string {
18 h := sha1.New()
19 h.Write([]byte(challengeKey))
20 h.Write(keyGUID)
21 return base64.StdEncoding.EncodeToString(h.Sum(nil))
22 }
23
24 func generateChallengeKey() (string, error) {
25 p := make([]byte, 16)
26 if _, err := io.ReadFull(rand.Reader, p); err != nil {
27 return "", err
28 }
29 return base64.StdEncoding.EncodeToString(p), nil
30 }
31
32 // Octet types from RFC 2616.
33 var octetTypes [256]byte
34
35 const (
36 isTokenOctet = 1 << iota
37 isSpaceOctet
38 )
39
40 func init() {
41 // From RFC 2616
42 //
43 // OCTET = <any 8-bit sequence of data>
44 // CHAR = <any US-ASCII character (octets 0 - 127)>
45 // CTL = <any US-ASCII control character (octets 0 - 31) and DEL (127)>
46 // CR = <US-ASCII CR, carriage return (13)>
47 // LF = <US-ASCII LF, linefeed (10)>
48 // SP = <US-ASCII SP, space (32)>
49 // HT = <US-ASCII HT, horizontal-tab (9)>
50 // <"> = <US-ASCII double-quote mark (34)>
51 // CRLF = CR LF
52 // LWS = [CRLF] 1*( SP | HT )
53 // TEXT = <any OCTET except CTLs, but including LWS>
54 // separators = "(" | ")" | "<" | ">" | "@" | "," | ";" | ":" | "\" | <">
55 // | "/" | "[" | "]" | "?" | "=" | "{" | "}" | SP | HT
56 // token = 1*<any CHAR except CTLs or separators>
57 // qdtext = <any TEXT except <">>
58
59 for c := 0; c < 256; c++ {
60 var t byte
61 isCtl := c <= 31 || c == 127
62 isChar := 0 <= c && c <= 127
63 isSeparator := strings.IndexRune(" \t\"(),/:;<=>?@[]\\{}", rune(c)) >= 0
64 if strings.IndexRune(" \t\r\n", rune(c)) >= 0 {
65 t |= isSpaceOctet
66 }
67 if isChar && !isCtl && !isSeparator {
68 t |= isTokenOctet
69 }
70 octetTypes[c] = t
71 }
72 }
73
74 func skipSpace(s string) (rest string) {
75 i := 0
76 for ; i < len(s); i++ {
77 if octetTypes[s[i]]&isSpaceOctet == 0 {
78 break
79 }
80 }
81 return s[i:]
82 }
83
84 func nextToken(s string) (token, rest string) {
85 i := 0
86 for ; i < len(s); i++ {
87 if octetTypes[s[i]]&isTokenOctet == 0 {
88 break
89 }
90 }
91 return s[:i], s[i:]
92 }
93
94 func nextTokenOrQuoted(s string) (value string, rest string) {
95 if !strings.HasPrefix(s, "\"") {
96 return nextToken(s)
97 }
98 s = s[1:]
99 for i := 0; i < len(s); i++ {
100 switch s[i] {
101 case '"':
102 return s[:i], s[i+1:]
103 case '\\':
104 p := make([]byte, len(s)-1)
105 j := copy(p, s[:i])
106 escape := true
107 for i = i + 1; i < len(s); i++ {
108 b := s[i]
109 switch {
110 case escape:
111 escape = false
112 p[j] = b
113 j += 1
114 case b == '\\':
115 escape = true
116 case b == '"':
117 return string(p[:j]), s[i+1:]
118 default:
119 p[j] = b
120 j += 1
121 }
122 }
123 return "", ""
124 }
125 }
126 return "", ""
127 }
128
129 // tokenListContainsValue returns true if the 1#token header with the given
130 // name contains token.
131 func tokenListContainsValue(header http.Header, name string, value string) bool {
132 headers:
133 for _, s := range header[name] {
134 for {
135 var t string
136 t, s = nextToken(skipSpace(s))
137 if t == "" {
138 continue headers
139 }
140 s = skipSpace(s)
141 if s != "" && s[0] != ',' {
142 continue headers
143 }
144 if strings.EqualFold(t, value) {
145 return true
146 }
147 if s == "" {
148 continue headers
149 }
150 s = s[1:]
151 }
152 }
153 return false
154 }
155
156 // parseExtensiosn parses WebSocket extensions from a header.
157 func parseExtensions(header http.Header) []map[string]string {
158
159 // From RFC 6455:
160 //
161 // Sec-WebSocket-Extensions = extension-list
162 // extension-list = 1#extension
163 // extension = extension-token *( ";" extension-param )
164 // extension-token = registered-token
165 // registered-token = token
166 // extension-param = token [ "=" (token | quoted-string) ]
167 // ;When using the quoted-string syntax variant, the value
168 // ;after quoted-string unescaping MUST conform to the
169 // ;'token' ABNF.
170
171 var result []map[string]string
172 headers:
173 for _, s := range header["Sec-Websocket-Extensions"] {
174 for {
175 var t string
176 t, s = nextToken(skipSpace(s))
177 if t == "" {
178 continue headers
179 }
180 ext := map[string]string{"": t}
181 for {
182 s = skipSpace(s)
183 if !strings.HasPrefix(s, ";") {
184 break
185 }
186 var k string
187 k, s = nextToken(skipSpace(s[1:]))
188 if k == "" {
189 continue headers
190 }
191 s = skipSpace(s)
192 var v string
193 if strings.HasPrefix(s, "=") {
194 v, s = nextTokenOrQuoted(skipSpace(s[1:]))
195 s = skipSpace(s)
196 }
197 if s != "" && s[0] != ',' && s[0] != ';' {
198 continue headers
199 }
200 ext[k] = v
201 }
202 if s != "" && s[0] != ',' {
203 continue headers
204 }
205 result = append(result, ext)
206 if s == "" {
207 continue headers
208 }
209 s = s[1:]
210 }
211 }
212 return result
213 }
0 # Compiled Object files, Static and Dynamic libs (Shared Objects)
1 *.o
2 *.a
3 *.so
4
5 # Folders
6 _obj
7 _test
8
9 # Architecture specific extensions/prefixes
10 *.[568vq]
11 [568vq].out
12
13 *.cgo1.go
14 *.cgo2.c
15 _cgo_defun.c
16 _cgo_gotypes.go
17 _cgo_export.*
18
19 _testmain.go
20
21 *.exe
22 *.test
23 *.prof
0 language: go
1 go_import_path: github.com/pkg/errors
2 go:
3 - 1.4.3
4 - 1.5.4
5 - 1.6.2
6 - 1.7.1
7 - tip
8
9 script:
10 - go test -v ./...
0 Copyright (c) 2015, Dave Cheney <dave@cheney.net>
1 All rights reserved.
2
3 Redistribution and use in source and binary forms, with or without
4 modification, are permitted provided that the following conditions are met:
5
6 * Redistributions of source code must retain the above copyright notice, this
7 list of conditions and the following disclaimer.
8
9 * Redistributions in binary form must reproduce the above copyright notice,
10 this list of conditions and the following disclaimer in the documentation
11 and/or other materials provided with the distribution.
12
13 THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
14 AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
15 IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
16 DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
17 FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
18 DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
19 SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
20 CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
21 OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
22 OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
0 # errors [![Travis-CI](https://travis-ci.org/pkg/errors.svg)](https://travis-ci.org/pkg/errors) [![AppVeyor](https://ci.appveyor.com/api/projects/status/b98mptawhudj53ep/branch/master?svg=true)](https://ci.appveyor.com/project/davecheney/errors/branch/master) [![GoDoc](https://godoc.org/github.com/pkg/errors?status.svg)](http://godoc.org/github.com/pkg/errors) [![Report card](https://goreportcard.com/badge/github.com/pkg/errors)](https://goreportcard.com/report/github.com/pkg/errors)
1
2 Package errors provides simple error handling primitives.
3
4 `go get github.com/pkg/errors`
5
6 The traditional error handling idiom in Go is roughly akin to
7 ```go
8 if err != nil {
9 return err
10 }
11 ```
12 which applied recursively up the call stack results in error reports without context or debugging information. The errors package allows programmers to add context to the failure path in their code in a way that does not destroy the original value of the error.
13
14 ## Adding context to an error
15
16 The errors.Wrap function returns a new error that adds context to the original error. For example
17 ```go
18 _, err := ioutil.ReadAll(r)
19 if err != nil {
20 return errors.Wrap(err, "read failed")
21 }
22 ```
23 ## Retrieving the cause of an error
24
25 Using `errors.Wrap` constructs a stack of errors, adding context to the preceding error. Depending on the nature of the error it may be necessary to reverse the operation of errors.Wrap to retrieve the original error for inspection. Any error value which implements this interface can be inspected by `errors.Cause`.
26 ```go
27 type causer interface {
28 Cause() error
29 }
30 ```
31 `errors.Cause` will recursively retrieve the topmost error which does not implement `causer`, which is assumed to be the original cause. For example:
32 ```go
33 switch err := errors.Cause(err).(type) {
34 case *MyError:
35 // handle specifically
36 default:
37 // unknown error
38 }
39 ```
40
41 [Read the package documentation for more information](https://godoc.org/github.com/pkg/errors).
42
43 ## Contributing
44
45 We welcome pull requests, bug fixes and issue reports. With that said, the bar for adding new symbols to this package is intentionally set high.
46
47 Before proposing a change, please discuss your change by raising an issue.
48
49 ## Licence
50
51 BSD-2-Clause
0 version: build-{build}.{branch}
1
2 clone_folder: C:\gopath\src\github.com\pkg\errors
3 shallow_clone: true # for startup speed
4
5 environment:
6 GOPATH: C:\gopath
7
8 platform:
9 - x64
10
11 # http://www.appveyor.com/docs/installed-software
12 install:
13 # some helpful output for debugging builds
14 - go version
15 - go env
16 # pre-installed MinGW at C:\MinGW is 32bit only
17 # but MSYS2 at C:\msys64 has mingw64
18 - set PATH=C:\msys64\mingw64\bin;%PATH%
19 - gcc --version
20 - g++ --version
21
22 build_script:
23 - go install -v ./...
24
25 test_script:
26 - set PATH=C:\gopath\bin;%PATH%
27 - go test -v ./...
28
29 #artifacts:
30 # - path: '%GOPATH%\bin\*.exe'
31 deploy: off
0 // Package errors provides simple error handling primitives.
1 //
2 // The traditional error handling idiom in Go is roughly akin to
3 //
4 // if err != nil {
5 // return err
6 // }
7 //
8 // which applied recursively up the call stack results in error reports
9 // without context or debugging information. The errors package allows
10 // programmers to add context to the failure path in their code in a way
11 // that does not destroy the original value of the error.
12 //
13 // Adding context to an error
14 //
15 // The errors.Wrap function returns a new error that adds context to the
16 // original error by recording a stack trace at the point Wrap is called,
17 // and the supplied message. For example
18 //
19 // _, err := ioutil.ReadAll(r)
20 // if err != nil {
21 // return errors.Wrap(err, "read failed")
22 // }
23 //
24 // If additional control is required the errors.WithStack and errors.WithMessage
25 // functions destructure errors.Wrap into its component operations of annotating
26 // an error with a stack trace and an a message, respectively.
27 //
28 // Retrieving the cause of an error
29 //
30 // Using errors.Wrap constructs a stack of errors, adding context to the
31 // preceding error. Depending on the nature of the error it may be necessary
32 // to reverse the operation of errors.Wrap to retrieve the original error
33 // for inspection. Any error value which implements this interface
34 //
35 // type causer interface {
36 // Cause() error
37 // }
38 //
39 // can be inspected by errors.Cause. errors.Cause will recursively retrieve
40 // the topmost error which does not implement causer, which is assumed to be
41 // the original cause. For example:
42 //
43 // switch err := errors.Cause(err).(type) {
44 // case *MyError:
45 // // handle specifically
46 // default:
47 // // unknown error
48 // }
49 //
50 // causer interface is not exported by this package, but is considered a part
51 // of stable public API.
52 //
53 // Formatted printing of errors
54 //
55 // All error values returned from this package implement fmt.Formatter and can
56 // be formatted by the fmt package. The following verbs are supported
57 //
58 // %s print the error. If the error has a Cause it will be
59 // printed recursively
60 // %v see %s
61 // %+v extended format. Each Frame of the error's StackTrace will
62 // be printed in detail.
63 //
64 // Retrieving the stack trace of an error or wrapper
65 //
66 // New, Errorf, Wrap, and Wrapf record a stack trace at the point they are
67 // invoked. This information can be retrieved with the following interface.
68 //
69 // type stackTracer interface {
70 // StackTrace() errors.StackTrace
71 // }
72 //
73 // Where errors.StackTrace is defined as
74 //
75 // type StackTrace []Frame
76 //
77 // The Frame type represents a call site in the stack trace. Frame supports
78 // the fmt.Formatter interface that can be used for printing information about
79 // the stack trace of this error. For example:
80 //
81 // if err, ok := err.(stackTracer); ok {
82 // for _, f := range err.StackTrace() {
83 // fmt.Printf("%+s:%d", f)
84 // }
85 // }
86 //
87 // stackTracer interface is not exported by this package, but is considered a part
88 // of stable public API.
89 //
90 // See the documentation for Frame.Format for more details.
91 package errors
92
93 import (
94 "fmt"
95 "io"
96 )
97
98 // New returns an error with the supplied message.
99 // New also records the stack trace at the point it was called.
100 func New(message string) error {
101 return &fundamental{
102 msg: message,
103 stack: callers(),
104 }
105 }
106
107 // Errorf formats according to a format specifier and returns the string
108 // as a value that satisfies error.
109 // Errorf also records the stack trace at the point it was called.
110 func Errorf(format string, args ...interface{}) error {
111 return &fundamental{
112 msg: fmt.Sprintf(format, args...),
113 stack: callers(),
114 }
115 }
116
117 // fundamental is an error that has a message and a stack, but no caller.
118 type fundamental struct {
119 msg string
120 *stack
121 }
122
123 func (f *fundamental) Error() string { return f.msg }
124
125 func (f *fundamental) Format(s fmt.State, verb rune) {
126 switch verb {
127 case 'v':
128 if s.Flag('+') {
129 io.WriteString(s, f.msg)
130 f.stack.Format(s, verb)
131 return
132 }
133 fallthrough
134 case 's':
135 io.WriteString(s, f.msg)
136 case 'q':
137 fmt.Fprintf(s, "%q", f.msg)
138 }
139 }
140
141 // WithStack annotates err with a stack trace at the point WithStack was called.
142 // If err is nil, WithStack returns nil.
143 func WithStack(err error) error {
144 if err == nil {
145 return nil
146 }
147 return &withStack{
148 err,
149 callers(),
150 }
151 }
152
153 type withStack struct {
154 error
155 *stack
156 }
157
158 func (w *withStack) Cause() error { return w.error }
159
160 func (w *withStack) Format(s fmt.State, verb rune) {
161 switch verb {
162 case 'v':
163 if s.Flag('+') {
164 fmt.Fprintf(s, "%+v", w.Cause())
165 w.stack.Format(s, verb)
166 return
167 }
168 fallthrough
169 case 's':
170 io.WriteString(s, w.Error())
171 case 'q':
172 fmt.Fprintf(s, "%q", w.Error())
173 }
174 }
175
176 // Wrap returns an error annotating err with a stack trace
177 // at the point Wrap is called, and the supplied message.
178 // If err is nil, Wrap returns nil.
179 func Wrap(err error, message string) error {
180 if err == nil {
181 return nil
182 }
183 err = &withMessage{
184 cause: err,
185 msg: message,
186 }
187 return &withStack{
188 err,
189 callers(),
190 }
191 }
192
193 // Wrapf returns an error annotating err with a stack trace
194 // at the point Wrapf is call, and the format specifier.
195 // If err is nil, Wrapf returns nil.
196 func Wrapf(err error, format string, args ...interface{}) error {
197 if err == nil {
198 return nil
199 }
200 err = &withMessage{
201 cause: err,
202 msg: fmt.Sprintf(format, args...),
203 }
204 return &withStack{
205 err,
206 callers(),
207 }
208 }
209
210 // WithMessage annotates err with a new message.
211 // If err is nil, WithMessage returns nil.
212 func WithMessage(err error, message string) error {
213 if err == nil {
214 return nil
215 }
216 return &withMessage{
217 cause: err,
218 msg: message,
219 }
220 }
221
222 type withMessage struct {
223 cause error
224 msg string
225 }
226
227 func (w *withMessage) Error() string { return w.msg + ": " + w.cause.Error() }
228 func (w *withMessage) Cause() error { return w.cause }
229
230 func (w *withMessage) Format(s fmt.State, verb rune) {
231 switch verb {
232 case 'v':
233 if s.Flag('+') {
234 fmt.Fprintf(s, "%+v\n", w.Cause())
235 io.WriteString(s, w.msg)
236 return
237 }
238 fallthrough
239 case 's', 'q':
240 io.WriteString(s, w.Error())
241 }
242 }
243
244 // Cause returns the underlying cause of the error, if possible.
245 // An error value has a cause if it implements the following
246 // interface:
247 //
248 // type causer interface {
249 // Cause() error
250 // }
251 //
252 // If the error does not implement Cause, the original error will
253 // be returned. If the error is nil, nil will be returned without further
254 // investigation.
255 func Cause(err error) error {
256 type causer interface {
257 Cause() error
258 }
259
260 for err != nil {
261 cause, ok := err.(causer)
262 if !ok {
263 break
264 }
265 err = cause.Cause()
266 }
267 return err
268 }
0 package errors
1
2 import (
3 "fmt"
4 "io"
5 "path"
6 "runtime"
7 "strings"
8 )
9
10 // Frame represents a program counter inside a stack frame.
11 type Frame uintptr
12
13 // pc returns the program counter for this frame;
14 // multiple frames may have the same PC value.
15 func (f Frame) pc() uintptr { return uintptr(f) - 1 }
16
17 // file returns the full path to the file that contains the
18 // function for this Frame's pc.
19 func (f Frame) file() string {
20 fn := runtime.FuncForPC(f.pc())
21 if fn == nil {
22 return "unknown"
23 }
24 file, _ := fn.FileLine(f.pc())
25 return file
26 }
27
28 // line returns the line number of source code of the
29 // function for this Frame's pc.
30 func (f Frame) line() int {
31 fn := runtime.FuncForPC(f.pc())
32 if fn == nil {
33 return 0
34 }
35 _, line := fn.FileLine(f.pc())
36 return line
37 }
38
39 // Format formats the frame according to the fmt.Formatter interface.
40 //
41 // %s source file
42 // %d source line
43 // %n function name
44 // %v equivalent to %s:%d
45 //
46 // Format accepts flags that alter the printing of some verbs, as follows:
47 //
48 // %+s path of source file relative to the compile time GOPATH
49 // %+v equivalent to %+s:%d
50 func (f Frame) Format(s fmt.State, verb rune) {
51 switch verb {
52 case 's':
53 switch {
54 case s.Flag('+'):
55 pc := f.pc()
56 fn := runtime.FuncForPC(pc)
57 if fn == nil {
58 io.WriteString(s, "unknown")
59 } else {
60 file, _ := fn.FileLine(pc)
61 fmt.Fprintf(s, "%s\n\t%s", fn.Name(), file)
62 }
63 default:
64 io.WriteString(s, path.Base(f.file()))
65 }
66 case 'd':
67 fmt.Fprintf(s, "%d", f.line())
68 case 'n':
69 name := runtime.FuncForPC(f.pc()).Name()
70 io.WriteString(s, funcname(name))
71 case 'v':
72 f.Format(s, 's')
73 io.WriteString(s, ":")
74 f.Format(s, 'd')
75 }
76 }
77
78 // StackTrace is stack of Frames from innermost (newest) to outermost (oldest).
79 type StackTrace []Frame
80
81 func (st StackTrace) Format(s fmt.State, verb rune) {
82 switch verb {
83 case 'v':
84 switch {
85 case s.Flag('+'):
86 for _, f := range st {
87 fmt.Fprintf(s, "\n%+v", f)
88 }
89 case s.Flag('#'):
90 fmt.Fprintf(s, "%#v", []Frame(st))
91 default:
92 fmt.Fprintf(s, "%v", []Frame(st))
93 }
94 case 's':
95 fmt.Fprintf(s, "%s", []Frame(st))
96 }
97 }
98
99 // stack represents a stack of program counters.
100 type stack []uintptr
101
102 func (s *stack) Format(st fmt.State, verb rune) {
103 switch verb {
104 case 'v':
105 switch {
106 case st.Flag('+'):
107 for _, pc := range *s {
108 f := Frame(pc)
109 fmt.Fprintf(st, "\n%+v", f)
110 }
111 }
112 }
113 }
114
115 func (s *stack) StackTrace() StackTrace {
116 f := make([]Frame, len(*s))
117 for i := 0; i < len(f); i++ {
118 f[i] = Frame((*s)[i])
119 }
120 return f
121 }
122
123 func callers() *stack {
124 const depth = 32
125 var pcs [depth]uintptr
126 n := runtime.Callers(3, pcs[:])
127 var st stack = pcs[0:n]
128 return &st
129 }
130
131 // funcname removes the path prefix component of a function's name reported by func.Name().
132 func funcname(name string) string {
133 i := strings.LastIndex(name, "/")
134 name = name[i+1:]
135 i = strings.Index(name, ".")
136 return name[i+1:]
137 }
138
139 func trimGOPATH(name, file string) string {
140 // Here we want to get the source file path relative to the compile time
141 // GOPATH. As of Go 1.6.x there is no direct way to know the compiled
142 // GOPATH at runtime, but we can infer the number of path segments in the
143 // GOPATH. We note that fn.Name() returns the function name qualified by
144 // the import path, which does not include the GOPATH. Thus we can trim
145 // segments from the beginning of the file path until the number of path
146 // separators remaining is one more than the number of path separators in
147 // the function name. For example, given:
148 //
149 // GOPATH /home/user
150 // file /home/user/src/pkg/sub/file.go
151 // fn.Name() pkg/sub.Type.Method
152 //
153 // We want to produce:
154 //
155 // pkg/sub/file.go
156 //
157 // From this we can easily see that fn.Name() has one less path separator
158 // than our desired output. We count separators from the end of the file
159 // path until it finds two more than in the function name and then move
160 // one character forward to preserve the initial path segment without a
161 // leading separator.
162 const sep = "/"
163 goal := strings.Count(name, sep) + 2
164 i := len(file)
165 for n := 0; n < goal; n++ {
166 i = strings.LastIndex(file[:i], sep)
167 if i == -1 {
168 // not enough separators found, set i so that the slice expression
169 // below leaves file unmodified
170 i = -len(sep)
171 break
172 }
173 }
174 // get back to 0 or trim the leading separator
175 file = file[i+len(sep):]
176 return file
177 }
0 Copyright (c) 2013, Patrick Mezard
1 All rights reserved.
2
3 Redistribution and use in source and binary forms, with or without
4 modification, are permitted provided that the following conditions are
5 met:
6
7 Redistributions of source code must retain the above copyright
8 notice, this list of conditions and the following disclaimer.
9 Redistributions in binary form must reproduce the above copyright
10 notice, this list of conditions and the following disclaimer in the
11 documentation and/or other materials provided with the distribution.
12 The names of its contributors may not be used to endorse or promote
13 products derived from this software without specific prior written
14 permission.
15
16 THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS
17 IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED
18 TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A
19 PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
20 HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
21 SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED
22 TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
23 PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
24 LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
25 NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
26 SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
0 // Package difflib is a partial port of Python difflib module.
1 //
2 // It provides tools to compare sequences of strings and generate textual diffs.
3 //
4 // The following class and functions have been ported:
5 //
6 // - SequenceMatcher
7 //
8 // - unified_diff
9 //
10 // - context_diff
11 //
12 // Getting unified diffs was the main goal of the port. Keep in mind this code
13 // is mostly suitable to output text differences in a human friendly way, there
14 // are no guarantees generated diffs are consumable by patch(1).
15 package difflib
16
17 import (
18 "bufio"
19 "bytes"
20 "fmt"
21 "io"
22 "strings"
23 )
24
25 func min(a, b int) int {
26 if a < b {
27 return a
28 }
29 return b
30 }
31
32 func max(a, b int) int {
33 if a > b {
34 return a
35 }
36 return b
37 }
38
39 func calculateRatio(matches, length int) float64 {
40 if length > 0 {
41 return 2.0 * float64(matches) / float64(length)
42 }
43 return 1.0
44 }
45
46 type Match struct {
47 A int
48 B int
49 Size int
50 }
51
52 type OpCode struct {
53 Tag byte
54 I1 int
55 I2 int
56 J1 int
57 J2 int
58 }
59
60 // SequenceMatcher compares sequence of strings. The basic
61 // algorithm predates, and is a little fancier than, an algorithm
62 // published in the late 1980's by Ratcliff and Obershelp under the
63 // hyperbolic name "gestalt pattern matching". The basic idea is to find
64 // the longest contiguous matching subsequence that contains no "junk"
65 // elements (R-O doesn't address junk). The same idea is then applied
66 // recursively to the pieces of the sequences to the left and to the right
67 // of the matching subsequence. This does not yield minimal edit
68 // sequences, but does tend to yield matches that "look right" to people.
69 //
70 // SequenceMatcher tries to compute a "human-friendly diff" between two
71 // sequences. Unlike e.g. UNIX(tm) diff, the fundamental notion is the
72 // longest *contiguous* & junk-free matching subsequence. That's what
73 // catches peoples' eyes. The Windows(tm) windiff has another interesting
74 // notion, pairing up elements that appear uniquely in each sequence.
75 // That, and the method here, appear to yield more intuitive difference
76 // reports than does diff. This method appears to be the least vulnerable
77 // to synching up on blocks of "junk lines", though (like blank lines in
78 // ordinary text files, or maybe "<P>" lines in HTML files). That may be
79 // because this is the only method of the 3 that has a *concept* of
80 // "junk" <wink>.
81 //
82 // Timing: Basic R-O is cubic time worst case and quadratic time expected
83 // case. SequenceMatcher is quadratic time for the worst case and has
84 // expected-case behavior dependent in a complicated way on how many
85 // elements the sequences have in common; best case time is linear.
86 type SequenceMatcher struct {
87 a []string
88 b []string
89 b2j map[string][]int
90 IsJunk func(string) bool
91 autoJunk bool
92 bJunk map[string]struct{}
93 matchingBlocks []Match
94 fullBCount map[string]int
95 bPopular map[string]struct{}
96 opCodes []OpCode
97 }
98
99 func NewMatcher(a, b []string) *SequenceMatcher {
100 m := SequenceMatcher{autoJunk: true}
101 m.SetSeqs(a, b)
102 return &m
103 }
104
105 func NewMatcherWithJunk(a, b []string, autoJunk bool,
106 isJunk func(string) bool) *SequenceMatcher {
107
108 m := SequenceMatcher{IsJunk: isJunk, autoJunk: autoJunk}
109 m.SetSeqs(a, b)
110 return &m
111 }
112
113 // Set two sequences to be compared.
114 func (m *SequenceMatcher) SetSeqs(a, b []string) {
115 m.SetSeq1(a)
116 m.SetSeq2(b)
117 }
118
119 // Set the first sequence to be compared. The second sequence to be compared is
120 // not changed.
121 //
122 // SequenceMatcher computes and caches detailed information about the second
123 // sequence, so if you want to compare one sequence S against many sequences,
124 // use .SetSeq2(s) once and call .SetSeq1(x) repeatedly for each of the other
125 // sequences.
126 //
127 // See also SetSeqs() and SetSeq2().
128 func (m *SequenceMatcher) SetSeq1(a []string) {
129 if &a == &m.a {
130 return
131 }
132 m.a = a
133 m.matchingBlocks = nil
134 m.opCodes = nil
135 }
136
137 // Set the second sequence to be compared. The first sequence to be compared is
138 // not changed.
139 func (m *SequenceMatcher) SetSeq2(b []string) {
140 if &b == &m.b {
141 return
142 }
143 m.b = b
144 m.matchingBlocks = nil
145 m.opCodes = nil
146 m.fullBCount = nil
147 m.chainB()
148 }
149
150 func (m *SequenceMatcher) chainB() {
151 // Populate line -> index mapping
152 b2j := map[string][]int{}
153 for i, s := range m.b {
154 indices := b2j[s]
155 indices = append(indices, i)
156 b2j[s] = indices
157 }
158
159 // Purge junk elements
160 m.bJunk = map[string]struct{}{}
161 if m.IsJunk != nil {
162 junk := m.bJunk
163 for s, _ := range b2j {
164 if m.IsJunk(s) {
165 junk[s] = struct{}{}
166 }
167 }
168 for s, _ := range junk {
169 delete(b2j, s)
170 }
171 }
172
173 // Purge remaining popular elements
174 popular := map[string]struct{}{}
175 n := len(m.b)
176 if m.autoJunk && n >= 200 {
177 ntest := n/100 + 1
178 for s, indices := range b2j {
179 if len(indices) > ntest {
180 popular[s] = struct{}{}
181 }
182 }
183 for s, _ := range popular {
184 delete(b2j, s)
185 }
186 }
187 m.bPopular = popular
188 m.b2j = b2j
189 }
190
191 func (m *SequenceMatcher) isBJunk(s string) bool {
192 _, ok := m.bJunk[s]
193 return ok
194 }
195
196 // Find longest matching block in a[alo:ahi] and b[blo:bhi].
197 //
198 // If IsJunk is not defined:
199 //
200 // Return (i,j,k) such that a[i:i+k] is equal to b[j:j+k], where
201 // alo <= i <= i+k <= ahi
202 // blo <= j <= j+k <= bhi
203 // and for all (i',j',k') meeting those conditions,
204 // k >= k'
205 // i <= i'
206 // and if i == i', j <= j'
207 //
208 // In other words, of all maximal matching blocks, return one that
209 // starts earliest in a, and of all those maximal matching blocks that
210 // start earliest in a, return the one that starts earliest in b.
211 //
212 // If IsJunk is defined, first the longest matching block is
213 // determined as above, but with the additional restriction that no
214 // junk element appears in the block. Then that block is extended as
215 // far as possible by matching (only) junk elements on both sides. So
216 // the resulting block never matches on junk except as identical junk
217 // happens to be adjacent to an "interesting" match.
218 //
219 // If no blocks match, return (alo, blo, 0).
220 func (m *SequenceMatcher) findLongestMatch(alo, ahi, blo, bhi int) Match {
221 // CAUTION: stripping common prefix or suffix would be incorrect.
222 // E.g.,
223 // ab
224 // acab
225 // Longest matching block is "ab", but if common prefix is
226 // stripped, it's "a" (tied with "b"). UNIX(tm) diff does so
227 // strip, so ends up claiming that ab is changed to acab by
228 // inserting "ca" in the middle. That's minimal but unintuitive:
229 // "it's obvious" that someone inserted "ac" at the front.
230 // Windiff ends up at the same place as diff, but by pairing up
231 // the unique 'b's and then matching the first two 'a's.
232 besti, bestj, bestsize := alo, blo, 0
233
234 // find longest junk-free match
235 // during an iteration of the loop, j2len[j] = length of longest
236 // junk-free match ending with a[i-1] and b[j]
237 j2len := map[int]int{}
238 for i := alo; i != ahi; i++ {
239 // look at all instances of a[i] in b; note that because
240 // b2j has no junk keys, the loop is skipped if a[i] is junk
241 newj2len := map[int]int{}
242 for _, j := range m.b2j[m.a[i]] {
243 // a[i] matches b[j]
244 if j < blo {
245 continue
246 }
247 if j >= bhi {
248 break
249 }
250 k := j2len[j-1] + 1
251 newj2len[j] = k
252 if k > bestsize {
253 besti, bestj, bestsize = i-k+1, j-k+1, k
254 }
255 }
256 j2len = newj2len
257 }
258
259 // Extend the best by non-junk elements on each end. In particular,
260 // "popular" non-junk elements aren't in b2j, which greatly speeds
261 // the inner loop above, but also means "the best" match so far
262 // doesn't contain any junk *or* popular non-junk elements.
263 for besti > alo && bestj > blo && !m.isBJunk(m.b[bestj-1]) &&
264 m.a[besti-1] == m.b[bestj-1] {
265 besti, bestj, bestsize = besti-1, bestj-1, bestsize+1
266 }
267 for besti+bestsize < ahi && bestj+bestsize < bhi &&
268 !m.isBJunk(m.b[bestj+bestsize]) &&
269 m.a[besti+bestsize] == m.b[bestj+bestsize] {
270 bestsize += 1
271 }
272
273 // Now that we have a wholly interesting match (albeit possibly
274 // empty!), we may as well suck up the matching junk on each
275 // side of it too. Can't think of a good reason not to, and it
276 // saves post-processing the (possibly considerable) expense of
277 // figuring out what to do with it. In the case of an empty
278 // interesting match, this is clearly the right thing to do,
279 // because no other kind of match is possible in the regions.
280 for besti > alo && bestj > blo && m.isBJunk(m.b[bestj-1]) &&
281 m.a[besti-1] == m.b[bestj-1] {
282 besti, bestj, bestsize = besti-1, bestj-1, bestsize+1
283 }
284 for besti+bestsize < ahi && bestj+bestsize < bhi &&
285 m.isBJunk(m.b[bestj+bestsize]) &&
286 m.a[besti+bestsize] == m.b[bestj+bestsize] {
287 bestsize += 1
288 }
289
290 return Match{A: besti, B: bestj, Size: bestsize}
291 }
292
293 // Return list of triples describing matching subsequences.
294 //
295 // Each triple is of the form (i, j, n), and means that
296 // a[i:i+n] == b[j:j+n]. The triples are monotonically increasing in
297 // i and in j. It's also guaranteed that if (i, j, n) and (i', j', n') are
298 // adjacent triples in the list, and the second is not the last triple in the
299 // list, then i+n != i' or j+n != j'. IOW, adjacent triples never describe
300 // adjacent equal blocks.
301 //
302 // The last triple is a dummy, (len(a), len(b), 0), and is the only
303 // triple with n==0.
304 func (m *SequenceMatcher) GetMatchingBlocks() []Match {
305 if m.matchingBlocks != nil {
306 return m.matchingBlocks
307 }
308
309 var matchBlocks func(alo, ahi, blo, bhi int, matched []Match) []Match
310 matchBlocks = func(alo, ahi, blo, bhi int, matched []Match) []Match {
311 match := m.findLongestMatch(alo, ahi, blo, bhi)
312 i, j, k := match.A, match.B, match.Size
313 if match.Size > 0 {
314 if alo < i && blo < j {
315 matched = matchBlocks(alo, i, blo, j, matched)
316 }
317 matched = append(matched, match)
318 if i+k < ahi && j+k < bhi {
319 matched = matchBlocks(i+k, ahi, j+k, bhi, matched)
320 }
321 }
322 return matched
323 }
324 matched := matchBlocks(0, len(m.a), 0, len(m.b), nil)
325
326 // It's possible that we have adjacent equal blocks in the
327 // matching_blocks list now.
328 nonAdjacent := []Match{}
329 i1, j1, k1 := 0, 0, 0
330 for _, b := range matched {
331 // Is this block adjacent to i1, j1, k1?
332 i2, j2, k2 := b.A, b.B, b.Size
333 if i1+k1 == i2 && j1+k1 == j2 {
334 // Yes, so collapse them -- this just increases the length of
335 // the first block by the length of the second, and the first
336 // block so lengthened remains the block to compare against.
337 k1 += k2
338 } else {
339 // Not adjacent. Remember the first block (k1==0 means it's
340 // the dummy we started with), and make the second block the
341 // new block to compare against.
342 if k1 > 0 {
343 nonAdjacent = append(nonAdjacent, Match{i1, j1, k1})
344 }
345 i1, j1, k1 = i2, j2, k2
346 }
347 }
348 if k1 > 0 {
349 nonAdjacent = append(nonAdjacent, Match{i1, j1, k1})
350 }
351
352 nonAdjacent = append(nonAdjacent, Match{len(m.a), len(m.b), 0})
353 m.matchingBlocks = nonAdjacent
354 return m.matchingBlocks
355 }
356
357 // Return list of 5-tuples describing how to turn a into b.
358 //
359 // Each tuple is of the form (tag, i1, i2, j1, j2). The first tuple
360 // has i1 == j1 == 0, and remaining tuples have i1 == the i2 from the
361 // tuple preceding it, and likewise for j1 == the previous j2.
362 //
363 // The tags are characters, with these meanings:
364 //
365 // 'r' (replace): a[i1:i2] should be replaced by b[j1:j2]
366 //
367 // 'd' (delete): a[i1:i2] should be deleted, j1==j2 in this case.
368 //
369 // 'i' (insert): b[j1:j2] should be inserted at a[i1:i1], i1==i2 in this case.
370 //
371 // 'e' (equal): a[i1:i2] == b[j1:j2]
372 func (m *SequenceMatcher) GetOpCodes() []OpCode {
373 if m.opCodes != nil {
374 return m.opCodes
375 }
376 i, j := 0, 0
377 matching := m.GetMatchingBlocks()
378 opCodes := make([]OpCode, 0, len(matching))
379 for _, m := range matching {
380 // invariant: we've pumped out correct diffs to change
381 // a[:i] into b[:j], and the next matching block is
382 // a[ai:ai+size] == b[bj:bj+size]. So we need to pump
383 // out a diff to change a[i:ai] into b[j:bj], pump out
384 // the matching block, and move (i,j) beyond the match
385 ai, bj, size := m.A, m.B, m.Size
386 tag := byte(0)
387 if i < ai && j < bj {
388 tag = 'r'
389 } else if i < ai {
390 tag = 'd'
391 } else if j < bj {
392 tag = 'i'
393 }
394 if tag > 0 {
395 opCodes = append(opCodes, OpCode{tag, i, ai, j, bj})
396 }
397 i, j = ai+size, bj+size
398 // the list of matching blocks is terminated by a
399 // sentinel with size 0
400 if size > 0 {
401 opCodes = append(opCodes, OpCode{'e', ai, i, bj, j})
402 }
403 }
404 m.opCodes = opCodes
405 return m.opCodes
406 }
407
408 // Isolate change clusters by eliminating ranges with no changes.
409 //
410 // Return a generator of groups with up to n lines of context.
411 // Each group is in the same format as returned by GetOpCodes().
412 func (m *SequenceMatcher) GetGroupedOpCodes(n int) [][]OpCode {
413 if n < 0 {
414 n = 3
415 }
416 codes := m.GetOpCodes()
417 if len(codes) == 0 {
418 codes = []OpCode{OpCode{'e', 0, 1, 0, 1}}
419 }
420 // Fixup leading and trailing groups if they show no changes.
421 if codes[0].Tag == 'e' {
422 c := codes[0]
423 i1, i2, j1, j2 := c.I1, c.I2, c.J1, c.J2
424 codes[0] = OpCode{c.Tag, max(i1, i2-n), i2, max(j1, j2-n), j2}
425 }
426 if codes[len(codes)-1].Tag == 'e' {
427 c := codes[len(codes)-1]
428 i1, i2, j1, j2 := c.I1, c.I2, c.J1, c.J2
429 codes[len(codes)-1] = OpCode{c.Tag, i1, min(i2, i1+n), j1, min(j2, j1+n)}
430 }
431 nn := n + n
432 groups := [][]OpCode{}
433 group := []OpCode{}
434 for _, c := range codes {
435 i1, i2, j1, j2 := c.I1, c.I2, c.J1, c.J2
436 // End the current group and start a new one whenever
437 // there is a large range with no changes.
438 if c.Tag == 'e' && i2-i1 > nn {
439 group = append(group, OpCode{c.Tag, i1, min(i2, i1+n),
440 j1, min(j2, j1+n)})
441 groups = append(groups, group)
442 group = []OpCode{}
443 i1, j1 = max(i1, i2-n), max(j1, j2-n)
444 }
445 group = append(group, OpCode{c.Tag, i1, i2, j1, j2})
446 }
447 if len(group) > 0 && !(len(group) == 1 && group[0].Tag == 'e') {
448 groups = append(groups, group)
449 }
450 return groups
451 }
452
453 // Return a measure of the sequences' similarity (float in [0,1]).
454 //
455 // Where T is the total number of elements in both sequences, and
456 // M is the number of matches, this is 2.0*M / T.
457 // Note that this is 1 if the sequences are identical, and 0 if
458 // they have nothing in common.
459 //
460 // .Ratio() is expensive to compute if you haven't already computed
461 // .GetMatchingBlocks() or .GetOpCodes(), in which case you may
462 // want to try .QuickRatio() or .RealQuickRation() first to get an
463 // upper bound.
464 func (m *SequenceMatcher) Ratio() float64 {
465 matches := 0
466 for _, m := range m.GetMatchingBlocks() {
467 matches += m.Size
468 }
469 return calculateRatio(matches, len(m.a)+len(m.b))
470 }
471
472 // Return an upper bound on ratio() relatively quickly.
473 //
474 // This isn't defined beyond that it is an upper bound on .Ratio(), and
475 // is faster to compute.
476 func (m *SequenceMatcher) QuickRatio() float64 {
477 // viewing a and b as multisets, set matches to the cardinality
478 // of their intersection; this counts the number of matches
479 // without regard to order, so is clearly an upper bound
480 if m.fullBCount == nil {
481 m.fullBCount = map[string]int{}
482 for _, s := range m.b {
483 m.fullBCount[s] = m.fullBCount[s] + 1
484 }
485 }
486
487 // avail[x] is the number of times x appears in 'b' less the
488 // number of times we've seen it in 'a' so far ... kinda
489 avail := map[string]int{}
490 matches := 0
491 for _, s := range m.a {
492 n, ok := avail[s]
493 if !ok {
494 n = m.fullBCount[s]
495 }
496 avail[s] = n - 1
497 if n > 0 {
498 matches += 1
499 }
500 }
501 return calculateRatio(matches, len(m.a)+len(m.b))
502 }
503
504 // Return an upper bound on ratio() very quickly.
505 //
506 // This isn't defined beyond that it is an upper bound on .Ratio(), and
507 // is faster to compute than either .Ratio() or .QuickRatio().
508 func (m *SequenceMatcher) RealQuickRatio() float64 {
509 la, lb := len(m.a), len(m.b)
510 return calculateRatio(min(la, lb), la+lb)
511 }
512
513 // Convert range to the "ed" format
514 func formatRangeUnified(start, stop int) string {
515 // Per the diff spec at http://www.unix.org/single_unix_specification/
516 beginning := start + 1 // lines start numbering with one
517 length := stop - start
518 if length == 1 {
519 return fmt.Sprintf("%d", beginning)
520 }
521 if length == 0 {
522 beginning -= 1 // empty ranges begin at line just before the range
523 }
524 return fmt.Sprintf("%d,%d", beginning, length)
525 }
526
527 // Unified diff parameters
528 type UnifiedDiff struct {
529 A []string // First sequence lines
530 FromFile string // First file name
531 FromDate string // First file time
532 B []string // Second sequence lines
533 ToFile string // Second file name
534 ToDate string // Second file time
535 Eol string // Headers end of line, defaults to LF
536 Context int // Number of context lines
537 }
538
539 // Compare two sequences of lines; generate the delta as a unified diff.
540 //
541 // Unified diffs are a compact way of showing line changes and a few
542 // lines of context. The number of context lines is set by 'n' which
543 // defaults to three.
544 //
545 // By default, the diff control lines (those with ---, +++, or @@) are
546 // created with a trailing newline. This is helpful so that inputs
547 // created from file.readlines() result in diffs that are suitable for
548 // file.writelines() since both the inputs and outputs have trailing
549 // newlines.
550 //
551 // For inputs that do not have trailing newlines, set the lineterm
552 // argument to "" so that the output will be uniformly newline free.
553 //
554 // The unidiff format normally has a header for filenames and modification
555 // times. Any or all of these may be specified using strings for
556 // 'fromfile', 'tofile', 'fromfiledate', and 'tofiledate'.
557 // The modification times are normally expressed in the ISO 8601 format.
558 func WriteUnifiedDiff(writer io.Writer, diff UnifiedDiff) error {
559 buf := bufio.NewWriter(writer)
560 defer buf.Flush()
561 wf := func(format string, args ...interface{}) error {
562 _, err := buf.WriteString(fmt.Sprintf(format, args...))
563 return err
564 }
565 ws := func(s string) error {
566 _, err := buf.WriteString(s)
567 return err
568 }
569
570 if len(diff.Eol) == 0 {
571 diff.Eol = "\n"
572 }
573
574 started := false
575 m := NewMatcher(diff.A, diff.B)
576 for _, g := range m.GetGroupedOpCodes(diff.Context) {
577 if !started {
578 started = true
579 fromDate := ""
580 if len(diff.FromDate) > 0 {
581 fromDate = "\t" + diff.FromDate
582 }
583 toDate := ""
584 if len(diff.ToDate) > 0 {
585 toDate = "\t" + diff.ToDate
586 }
587 if diff.FromFile != "" || diff.ToFile != "" {
588 err := wf("--- %s%s%s", diff.FromFile, fromDate, diff.Eol)
589 if err != nil {
590 return err
591 }
592 err = wf("+++ %s%s%s", diff.ToFile, toDate, diff.Eol)
593 if err != nil {
594 return err
595 }
596 }
597 }
598 first, last := g[0], g[len(g)-1]
599 range1 := formatRangeUnified(first.I1, last.I2)
600 range2 := formatRangeUnified(first.J1, last.J2)
601 if err := wf("@@ -%s +%s @@%s", range1, range2, diff.Eol); err != nil {
602 return err
603 }
604 for _, c := range g {
605 i1, i2, j1, j2 := c.I1, c.I2, c.J1, c.J2
606 if c.Tag == 'e' {
607 for _, line := range diff.A[i1:i2] {
608 if err := ws(" " + line); err != nil {
609 return err
610 }
611 }
612 continue
613 }
614 if c.Tag == 'r' || c.Tag == 'd' {
615 for _, line := range diff.A[i1:i2] {
616 if err := ws("-" + line); err != nil {
617 return err
618 }
619 }
620 }
621 if c.Tag == 'r' || c.Tag == 'i' {
622 for _, line := range diff.B[j1:j2] {
623 if err := ws("+" + line); err != nil {
624 return err
625 }
626 }
627 }
628 }
629 }
630 return nil
631 }
632
633 // Like WriteUnifiedDiff but returns the diff a string.
634 func GetUnifiedDiffString(diff UnifiedDiff) (string, error) {
635 w := &bytes.Buffer{}
636 err := WriteUnifiedDiff(w, diff)
637 return string(w.Bytes()), err
638 }
639
640 // Convert range to the "ed" format.
641 func formatRangeContext(start, stop int) string {
642 // Per the diff spec at http://www.unix.org/single_unix_specification/
643 beginning := start + 1 // lines start numbering with one
644 length := stop - start
645 if length == 0 {
646 beginning -= 1 // empty ranges begin at line just before the range
647 }
648 if length <= 1 {
649 return fmt.Sprintf("%d", beginning)
650 }
651 return fmt.Sprintf("%d,%d", beginning, beginning+length-1)
652 }
653
654 type ContextDiff UnifiedDiff
655
656 // Compare two sequences of lines; generate the delta as a context diff.
657 //
658 // Context diffs are a compact way of showing line changes and a few
659 // lines of context. The number of context lines is set by diff.Context
660 // which defaults to three.
661 //
662 // By default, the diff control lines (those with *** or ---) are
663 // created with a trailing newline.
664 //
665 // For inputs that do not have trailing newlines, set the diff.Eol
666 // argument to "" so that the output will be uniformly newline free.
667 //
668 // The context diff format normally has a header for filenames and
669 // modification times. Any or all of these may be specified using
670 // strings for diff.FromFile, diff.ToFile, diff.FromDate, diff.ToDate.
671 // The modification times are normally expressed in the ISO 8601 format.
672 // If not specified, the strings default to blanks.
673 func WriteContextDiff(writer io.Writer, diff ContextDiff) error {
674 buf := bufio.NewWriter(writer)
675 defer buf.Flush()
676 var diffErr error
677 wf := func(format string, args ...interface{}) {
678 _, err := buf.WriteString(fmt.Sprintf(format, args...))
679 if diffErr == nil && err != nil {
680 diffErr = err
681 }
682 }
683 ws := func(s string) {
684 _, err := buf.WriteString(s)
685 if diffErr == nil && err != nil {
686 diffErr = err
687 }
688 }
689
690 if len(diff.Eol) == 0 {
691 diff.Eol = "\n"
692 }
693
694 prefix := map[byte]string{
695 'i': "+ ",
696 'd': "- ",
697 'r': "! ",
698 'e': " ",
699 }
700
701 started := false
702 m := NewMatcher(diff.A, diff.B)
703 for _, g := range m.GetGroupedOpCodes(diff.Context) {
704 if !started {
705 started = true
706 fromDate := ""
707 if len(diff.FromDate) > 0 {
708 fromDate = "\t" + diff.FromDate
709 }
710 toDate := ""
711 if len(diff.ToDate) > 0 {
712 toDate = "\t" + diff.ToDate
713 }
714 if diff.FromFile != "" || diff.ToFile != "" {
715 wf("*** %s%s%s", diff.FromFile, fromDate, diff.Eol)
716 wf("--- %s%s%s", diff.ToFile, toDate, diff.Eol)
717 }
718 }
719
720 first, last := g[0], g[len(g)-1]
721 ws("***************" + diff.Eol)
722
723 range1 := formatRangeContext(first.I1, last.I2)
724 wf("*** %s ****%s", range1, diff.Eol)
725 for _, c := range g {
726 if c.Tag == 'r' || c.Tag == 'd' {
727 for _, cc := range g {
728 if cc.Tag == 'i' {
729 continue
730 }
731 for _, line := range diff.A[cc.I1:cc.I2] {
732 ws(prefix[cc.Tag] + line)
733 }
734 }
735 break
736 }
737 }
738
739 range2 := formatRangeContext(first.J1, last.J2)
740 wf("--- %s ----%s", range2, diff.Eol)
741 for _, c := range g {
742 if c.Tag == 'r' || c.Tag == 'i' {
743 for _, cc := range g {
744 if cc.Tag == 'd' {
745 continue
746 }
747 for _, line := range diff.B[cc.J1:cc.J2] {
748 ws(prefix[cc.Tag] + line)
749 }
750 }
751 break
752 }
753 }
754 }
755 return diffErr
756 }
757
758 // Like WriteContextDiff but returns the diff a string.
759 func GetContextDiffString(diff ContextDiff) (string, error) {
760 w := &bytes.Buffer{}
761 err := WriteContextDiff(w, diff)
762 return string(w.Bytes()), err
763 }
764
765 // Split a string on "\n" while preserving them. The output can be used
766 // as input for UnifiedDiff and ContextDiff structures.
767 func SplitLines(s string) []string {
768 lines := strings.SplitAfter(s, "\n")
769 lines[len(lines)-1] += "\n"
770 return lines
771 }
0 Copyright (c) 2012 - 2013 Mat Ryer and Tyler Bunnell
1
2 Please consider promoting this project if you find it useful.
3
4 Permission is hereby granted, free of charge, to any person
5 obtaining a copy of this software and associated documentation
6 files (the "Software"), to deal in the Software without restriction,
7 including without limitation the rights to use, copy, modify, merge,
8 publish, distribute, sublicense, and/or sell copies of the Software,
9 and to permit persons to whom the Software is furnished to do so,
10 subject to the following conditions:
11
12 The above copyright notice and this permission notice shall be included
13 in all copies or substantial portions of the Software.
14
15 THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
16 EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
17 OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
18 IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
19 DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT
20 OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE
21 OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
0 /*
1 * CODE GENERATED AUTOMATICALLY WITH github.com/stretchr/testify/_codegen
2 * THIS FILE MUST NOT BE EDITED BY HAND
3 */
4
5 package assert
6
7 import (
8 http "net/http"
9 url "net/url"
10 time "time"
11 )
12
13 // Conditionf uses a Comparison to assert a complex condition.
14 func Conditionf(t TestingT, comp Comparison, msg string, args ...interface{}) bool {
15 if h, ok := t.(tHelper); ok {
16 h.Helper()
17 }
18 return Condition(t, comp, append([]interface{}{msg}, args...)...)
19 }
20
21 // Containsf asserts that the specified string, list(array, slice...) or map contains the
22 // specified substring or element.
23 //
24 // assert.Containsf(t, "Hello World", "World", "error message %s", "formatted")
25 // assert.Containsf(t, ["Hello", "World"], "World", "error message %s", "formatted")
26 // assert.Containsf(t, {"Hello": "World"}, "Hello", "error message %s", "formatted")
27 func Containsf(t TestingT, s interface{}, contains interface{}, msg string, args ...interface{}) bool {
28 if h, ok := t.(tHelper); ok {
29 h.Helper()
30 }
31 return Contains(t, s, contains, append([]interface{}{msg}, args...)...)
32 }
33
34 // DirExistsf checks whether a directory exists in the given path. It also fails if the path is a file rather a directory or there is an error checking whether it exists.
35 func DirExistsf(t TestingT, path string, msg string, args ...interface{}) bool {
36 if h, ok := t.(tHelper); ok {
37 h.Helper()
38 }
39 return DirExists(t, path, append([]interface{}{msg}, args...)...)
40 }
41
42 // ElementsMatchf asserts that the specified listA(array, slice...) is equal to specified
43 // listB(array, slice...) ignoring the order of the elements. If there are duplicate elements,
44 // the number of appearances of each of them in both lists should match.
45 //
46 // assert.ElementsMatchf(t, [1, 3, 2, 3], [1, 3, 3, 2], "error message %s", "formatted")
47 func ElementsMatchf(t TestingT, listA interface{}, listB interface{}, msg string, args ...interface{}) bool {
48 if h, ok := t.(tHelper); ok {
49 h.Helper()
50 }
51 return ElementsMatch(t, listA, listB, append([]interface{}{msg}, args...)...)
52 }
53
54 // Emptyf asserts that the specified object is empty. I.e. nil, "", false, 0 or either
55 // a slice or a channel with len == 0.
56 //
57 // assert.Emptyf(t, obj, "error message %s", "formatted")
58 func Emptyf(t TestingT, object interface{}, msg string, args ...interface{}) bool {
59 if h, ok := t.(tHelper); ok {
60 h.Helper()
61 }
62 return Empty(t, object, append([]interface{}{msg}, args...)...)
63 }
64
65 // Equalf asserts that two objects are equal.
66 //
67 // assert.Equalf(t, 123, 123, "error message %s", "formatted")
68 //
69 // Pointer variable equality is determined based on the equality of the
70 // referenced values (as opposed to the memory addresses). Function equality
71 // cannot be determined and will always fail.
72 func Equalf(t TestingT, expected interface{}, actual interface{}, msg string, args ...interface{}) bool {
73 if h, ok := t.(tHelper); ok {
74 h.Helper()
75 }
76 return Equal(t, expected, actual, append([]interface{}{msg}, args...)...)
77 }
78
79 // EqualErrorf asserts that a function returned an error (i.e. not `nil`)
80 // and that it is equal to the provided error.
81 //
82 // actualObj, err := SomeFunction()
83 // assert.EqualErrorf(t, err, expectedErrorString, "error message %s", "formatted")
84 func EqualErrorf(t TestingT, theError error, errString string, msg string, args ...interface{}) bool {
85 if h, ok := t.(tHelper); ok {
86 h.Helper()
87 }
88 return EqualError(t, theError, errString, append([]interface{}{msg}, args...)...)
89 }
90
91 // EqualValuesf asserts that two objects are equal or convertable to the same types
92 // and equal.
93 //
94 // assert.EqualValuesf(t, uint32(123, "error message %s", "formatted"), int32(123))
95 func EqualValuesf(t TestingT, expected interface{}, actual interface{}, msg string, args ...interface{}) bool {
96 if h, ok := t.(tHelper); ok {
97 h.Helper()
98 }
99 return EqualValues(t, expected, actual, append([]interface{}{msg}, args...)...)
100 }
101
102 // Errorf asserts that a function returned an error (i.e. not `nil`).
103 //
104 // actualObj, err := SomeFunction()
105 // if assert.Errorf(t, err, "error message %s", "formatted") {
106 // assert.Equal(t, expectedErrorf, err)
107 // }
108 func Errorf(t TestingT, err error, msg string, args ...interface{}) bool {
109 if h, ok := t.(tHelper); ok {
110 h.Helper()
111 }
112 return Error(t, err, append([]interface{}{msg}, args...)...)
113 }
114
115 // Exactlyf asserts that two objects are equal in value and type.
116 //
117 // assert.Exactlyf(t, int32(123, "error message %s", "formatted"), int64(123))
118 func Exactlyf(t TestingT, expected interface{}, actual interface{}, msg string, args ...interface{}) bool {
119 if h, ok := t.(tHelper); ok {
120 h.Helper()
121 }
122 return Exactly(t, expected, actual, append([]interface{}{msg}, args...)...)
123 }
124
125 // Failf reports a failure through
126 func Failf(t TestingT, failureMessage string, msg string, args ...interface{}) bool {
127 if h, ok := t.(tHelper); ok {
128 h.Helper()
129 }
130 return Fail(t, failureMessage, append([]interface{}{msg}, args...)...)
131 }
132
133 // FailNowf fails test
134 func FailNowf(t TestingT, failureMessage string, msg string, args ...interface{}) bool {
135 if h, ok := t.(tHelper); ok {
136 h.Helper()
137 }
138 return FailNow(t, failureMessage, append([]interface{}{msg}, args...)...)
139 }
140
141 // Falsef asserts that the specified value is false.
142 //
143 // assert.Falsef(t, myBool, "error message %s", "formatted")
144 func Falsef(t TestingT, value bool, msg string, args ...interface{}) bool {
145 if h, ok := t.(tHelper); ok {
146 h.Helper()
147 }
148 return False(t, value, append([]interface{}{msg}, args...)...)
149 }
150
151 // FileExistsf checks whether a file exists in the given path. It also fails if the path points to a directory or there is an error when trying to check the file.
152 func FileExistsf(t TestingT, path string, msg string, args ...interface{}) bool {
153 if h, ok := t.(tHelper); ok {
154 h.Helper()
155 }
156 return FileExists(t, path, append([]interface{}{msg}, args...)...)
157 }
158
159 // HTTPBodyContainsf asserts that a specified handler returns a
160 // body that contains a string.
161 //
162 // assert.HTTPBodyContainsf(t, myHandler, "GET", "www.google.com", nil, "I'm Feeling Lucky", "error message %s", "formatted")
163 //
164 // Returns whether the assertion was successful (true) or not (false).
165 func HTTPBodyContainsf(t TestingT, handler http.HandlerFunc, method string, url string, values url.Values, str interface{}, msg string, args ...interface{}) bool {
166 if h, ok := t.(tHelper); ok {
167 h.Helper()
168 }
169 return HTTPBodyContains(t, handler, method, url, values, str, append([]interface{}{msg}, args...)...)
170 }
171
172 // HTTPBodyNotContainsf asserts that a specified handler returns a
173 // body that does not contain a string.
174 //
175 // assert.HTTPBodyNotContainsf(t, myHandler, "GET", "www.google.com", nil, "I'm Feeling Lucky", "error message %s", "formatted")
176 //
177 // Returns whether the assertion was successful (true) or not (false).
178 func HTTPBodyNotContainsf(t TestingT, handler http.HandlerFunc, method string, url string, values url.Values, str interface{}, msg string, args ...interface{}) bool {
179 if h, ok := t.(tHelper); ok {
180 h.Helper()
181 }
182 return HTTPBodyNotContains(t, handler, method, url, values, str, append([]interface{}{msg}, args...)...)
183 }
184
185 // HTTPErrorf asserts that a specified handler returns an error status code.
186 //
187 // assert.HTTPErrorf(t, myHandler, "POST", "/a/b/c", url.Values{"a": []string{"b", "c"}}
188 //
189 // Returns whether the assertion was successful (true, "error message %s", "formatted") or not (false).
190 func HTTPErrorf(t TestingT, handler http.HandlerFunc, method string, url string, values url.Values, msg string, args ...interface{}) bool {
191 if h, ok := t.(tHelper); ok {
192 h.Helper()
193 }
194 return HTTPError(t, handler, method, url, values, append([]interface{}{msg}, args...)...)
195 }
196
197 // HTTPRedirectf asserts that a specified handler returns a redirect status code.
198 //
199 // assert.HTTPRedirectf(t, myHandler, "GET", "/a/b/c", url.Values{"a": []string{"b", "c"}}
200 //
201 // Returns whether the assertion was successful (true, "error message %s", "formatted") or not (false).
202 func HTTPRedirectf(t TestingT, handler http.HandlerFunc, method string, url string, values url.Values, msg string, args ...interface{}) bool {
203 if h, ok := t.(tHelper); ok {
204 h.Helper()
205 }
206 return HTTPRedirect(t, handler, method, url, values, append([]interface{}{msg}, args...)...)
207 }
208
209 // HTTPSuccessf asserts that a specified handler returns a success status code.
210 //
211 // assert.HTTPSuccessf(t, myHandler, "POST", "http://www.google.com", nil, "error message %s", "formatted")
212 //
213 // Returns whether the assertion was successful (true) or not (false).
214 func HTTPSuccessf(t TestingT, handler http.HandlerFunc, method string, url string, values url.Values, msg string, args ...interface{}) bool {
215 if h, ok := t.(tHelper); ok {
216 h.Helper()
217 }
218 return HTTPSuccess(t, handler, method, url, values, append([]interface{}{msg}, args...)...)
219 }
220
221 // Implementsf asserts that an object is implemented by the specified interface.
222 //
223 // assert.Implementsf(t, (*MyInterface, "error message %s", "formatted")(nil), new(MyObject))
224 func Implementsf(t TestingT, interfaceObject interface{}, object interface{}, msg string, args ...interface{}) bool {
225 if h, ok := t.(tHelper); ok {
226 h.Helper()
227 }
228 return Implements(t, interfaceObject, object, append([]interface{}{msg}, args...)...)
229 }
230
231 // InDeltaf asserts that the two numerals are within delta of each other.
232 //
233 // assert.InDeltaf(t, math.Pi, (22 / 7.0, "error message %s", "formatted"), 0.01)
234 func InDeltaf(t TestingT, expected interface{}, actual interface{}, delta float64, msg string, args ...interface{}) bool {
235 if h, ok := t.(tHelper); ok {
236 h.Helper()
237 }
238 return InDelta(t, expected, actual, delta, append([]interface{}{msg}, args...)...)
239 }
240
241 // InDeltaMapValuesf is the same as InDelta, but it compares all values between two maps. Both maps must have exactly the same keys.
242 func InDeltaMapValuesf(t TestingT, expected interface{}, actual interface{}, delta float64, msg string, args ...interface{}) bool {
243 if h, ok := t.(tHelper); ok {
244 h.Helper()
245 }
246 return InDeltaMapValues(t, expected, actual, delta, append([]interface{}{msg}, args...)...)
247 }
248
249 // InDeltaSlicef is the same as InDelta, except it compares two slices.
250 func InDeltaSlicef(t TestingT, expected interface{}, actual interface{}, delta float64, msg string, args ...interface{}) bool {
251 if h, ok := t.(tHelper); ok {
252 h.Helper()
253 }
254 return InDeltaSlice(t, expected, actual, delta, append([]interface{}{msg}, args...)...)
255 }
256
257 // InEpsilonf asserts that expected and actual have a relative error less than epsilon
258 func InEpsilonf(t TestingT, expected interface{}, actual interface{}, epsilon float64, msg string, args ...interface{}) bool {
259 if h, ok := t.(tHelper); ok {
260 h.Helper()
261 }
262 return InEpsilon(t, expected, actual, epsilon, append([]interface{}{msg}, args...)...)
263 }
264
265 // InEpsilonSlicef is the same as InEpsilon, except it compares each value from two slices.
266 func InEpsilonSlicef(t TestingT, expected interface{}, actual interface{}, epsilon float64, msg string, args ...interface{}) bool {
267 if h, ok := t.(tHelper); ok {
268 h.Helper()
269 }
270 return InEpsilonSlice(t, expected, actual, epsilon, append([]interface{}{msg}, args...)...)
271 }
272
273 // IsTypef asserts that the specified objects are of the same type.
274 func IsTypef(t TestingT, expectedType interface{}, object interface{}, msg string, args ...interface{}) bool {
275 if h, ok := t.(tHelper); ok {
276 h.Helper()
277 }
278 return IsType(t, expectedType, object, append([]interface{}{msg}, args...)...)
279 }
280
281 // JSONEqf asserts that two JSON strings are equivalent.
282 //
283 // assert.JSONEqf(t, `{"hello": "world", "foo": "bar"}`, `{"foo": "bar", "hello": "world"}`, "error message %s", "formatted")
284 func JSONEqf(t TestingT, expected string, actual string, msg string, args ...interface{}) bool {
285 if h, ok := t.(tHelper); ok {
286 h.Helper()
287 }
288 return JSONEq(t, expected, actual, append([]interface{}{msg}, args...)...)
289 }
290
291 // Lenf asserts that the specified object has specific length.
292 // Lenf also fails if the object has a type that len() not accept.
293 //
294 // assert.Lenf(t, mySlice, 3, "error message %s", "formatted")
295 func Lenf(t TestingT, object interface{}, length int, msg string, args ...interface{}) bool {
296 if h, ok := t.(tHelper); ok {
297 h.Helper()
298 }
299 return Len(t, object, length, append([]interface{}{msg}, args...)...)
300 }
301
302 // Nilf asserts that the specified object is nil.
303 //
304 // assert.Nilf(t, err, "error message %s", "formatted")
305 func Nilf(t TestingT, object interface{}, msg string, args ...interface{}) bool {
306 if h, ok := t.(tHelper); ok {
307 h.Helper()
308 }
309 return Nil(t, object, append([]interface{}{msg}, args...)...)
310 }
311
312 // NoErrorf asserts that a function returned no error (i.e. `nil`).
313 //
314 // actualObj, err := SomeFunction()
315 // if assert.NoErrorf(t, err, "error message %s", "formatted") {
316 // assert.Equal(t, expectedObj, actualObj)
317 // }
318 func NoErrorf(t TestingT, err error, msg string, args ...interface{}) bool {
319 if h, ok := t.(tHelper); ok {
320 h.Helper()
321 }
322 return NoError(t, err, append([]interface{}{msg}, args...)...)
323 }
324
325 // NotContainsf asserts that the specified string, list(array, slice...) or map does NOT contain the
326 // specified substring or element.
327 //
328 // assert.NotContainsf(t, "Hello World", "Earth", "error message %s", "formatted")
329 // assert.NotContainsf(t, ["Hello", "World"], "Earth", "error message %s", "formatted")
330 // assert.NotContainsf(t, {"Hello": "World"}, "Earth", "error message %s", "formatted")
331 func NotContainsf(t TestingT, s interface{}, contains interface{}, msg string, args ...interface{}) bool {
332 if h, ok := t.(tHelper); ok {
333 h.Helper()
334 }
335 return NotContains(t, s, contains, append([]interface{}{msg}, args...)...)
336 }
337
338 // NotEmptyf asserts that the specified object is NOT empty. I.e. not nil, "", false, 0 or either
339 // a slice or a channel with len == 0.
340 //
341 // if assert.NotEmptyf(t, obj, "error message %s", "formatted") {
342 // assert.Equal(t, "two", obj[1])
343 // }
344 func NotEmptyf(t TestingT, object interface{}, msg string, args ...interface{}) bool {
345 if h, ok := t.(tHelper); ok {
346 h.Helper()
347 }
348 return NotEmpty(t, object, append([]interface{}{msg}, args...)...)
349 }
350
351 // NotEqualf asserts that the specified values are NOT equal.
352 //
353 // assert.NotEqualf(t, obj1, obj2, "error message %s", "formatted")
354 //
355 // Pointer variable equality is determined based on the equality of the
356 // referenced values (as opposed to the memory addresses).
357 func NotEqualf(t TestingT, expected interface{}, actual interface{}, msg string, args ...interface{}) bool {
358 if h, ok := t.(tHelper); ok {
359 h.Helper()
360 }
361 return NotEqual(t, expected, actual, append([]interface{}{msg}, args...)...)
362 }
363
364 // NotNilf asserts that the specified object is not nil.
365 //
366 // assert.NotNilf(t, err, "error message %s", "formatted")
367 func NotNilf(t TestingT, object interface{}, msg string, args ...interface{}) bool {
368 if h, ok := t.(tHelper); ok {
369 h.Helper()
370 }
371 return NotNil(t, object, append([]interface{}{msg}, args...)...)
372 }
373
374 // NotPanicsf asserts that the code inside the specified PanicTestFunc does NOT panic.
375 //
376 // assert.NotPanicsf(t, func(){ RemainCalm() }, "error message %s", "formatted")
377 func NotPanicsf(t TestingT, f PanicTestFunc, msg string, args ...interface{}) bool {
378 if h, ok := t.(tHelper); ok {
379 h.Helper()
380 }
381 return NotPanics(t, f, append([]interface{}{msg}, args...)...)
382 }
383
384 // NotRegexpf asserts that a specified regexp does not match a string.
385 //
386 // assert.NotRegexpf(t, regexp.MustCompile("starts", "error message %s", "formatted"), "it's starting")
387 // assert.NotRegexpf(t, "^start", "it's not starting", "error message %s", "formatted")
388 func NotRegexpf(t TestingT, rx interface{}, str interface{}, msg string, args ...interface{}) bool {
389 if h, ok := t.(tHelper); ok {
390 h.Helper()
391 }
392 return NotRegexp(t, rx, str, append([]interface{}{msg}, args...)...)
393 }
394
395 // NotSubsetf asserts that the specified list(array, slice...) contains not all
396 // elements given in the specified subset(array, slice...).
397 //
398 // assert.NotSubsetf(t, [1, 3, 4], [1, 2], "But [1, 3, 4] does not contain [1, 2]", "error message %s", "formatted")
399 func NotSubsetf(t TestingT, list interface{}, subset interface{}, msg string, args ...interface{}) bool {
400 if h, ok := t.(tHelper); ok {
401 h.Helper()
402 }
403 return NotSubset(t, list, subset, append([]interface{}{msg}, args...)...)
404 }
405
406 // NotZerof asserts that i is not the zero value for its type.
407 func NotZerof(t TestingT, i interface{}, msg string, args ...interface{}) bool {
408 if h, ok := t.(tHelper); ok {
409 h.Helper()
410 }
411 return NotZero(t, i, append([]interface{}{msg}, args...)...)
412 }
413
414 // Panicsf asserts that the code inside the specified PanicTestFunc panics.
415 //
416 // assert.Panicsf(t, func(){ GoCrazy() }, "error message %s", "formatted")
417 func Panicsf(t TestingT, f PanicTestFunc, msg string, args ...interface{}) bool {
418 if h, ok := t.(tHelper); ok {
419 h.Helper()
420 }
421 return Panics(t, f, append([]interface{}{msg}, args...)...)
422 }
423
424 // PanicsWithValuef asserts that the code inside the specified PanicTestFunc panics, and that
425 // the recovered panic value equals the expected panic value.
426 //
427 // assert.PanicsWithValuef(t, "crazy error", func(){ GoCrazy() }, "error message %s", "formatted")
428 func PanicsWithValuef(t TestingT, expected interface{}, f PanicTestFunc, msg string, args ...interface{}) bool {
429 if h, ok := t.(tHelper); ok {
430 h.Helper()
431 }
432 return PanicsWithValue(t, expected, f, append([]interface{}{msg}, args...)...)
433 }
434
435 // Regexpf asserts that a specified regexp matches a string.
436 //
437 // assert.Regexpf(t, regexp.MustCompile("start", "error message %s", "formatted"), "it's starting")
438 // assert.Regexpf(t, "start...$", "it's not starting", "error message %s", "formatted")
439 func Regexpf(t TestingT, rx interface{}, str interface{}, msg string, args ...interface{}) bool {
440 if h, ok := t.(tHelper); ok {
441 h.Helper()
442 }
443 return Regexp(t, rx, str, append([]interface{}{msg}, args...)...)
444 }
445
446 // Subsetf asserts that the specified list(array, slice...) contains all
447 // elements given in the specified subset(array, slice...).
448 //
449 // assert.Subsetf(t, [1, 2, 3], [1, 2], "But [1, 2, 3] does contain [1, 2]", "error message %s", "formatted")
450 func Subsetf(t TestingT, list interface{}, subset interface{}, msg string, args ...interface{}) bool {
451 if h, ok := t.(tHelper); ok {
452 h.Helper()
453 }
454 return Subset(t, list, subset, append([]interface{}{msg}, args...)...)
455 }
456
457 // Truef asserts that the specified value is true.
458 //
459 // assert.Truef(t, myBool, "error message %s", "formatted")
460 func Truef(t TestingT, value bool, msg string, args ...interface{}) bool {
461 if h, ok := t.(tHelper); ok {
462 h.Helper()
463 }
464 return True(t, value, append([]interface{}{msg}, args...)...)
465 }
466
467 // WithinDurationf asserts that the two times are within duration delta of each other.
468 //
469 // assert.WithinDurationf(t, time.Now(), time.Now(), 10*time.Second, "error message %s", "formatted")
470 func WithinDurationf(t TestingT, expected time.Time, actual time.Time, delta time.Duration, msg string, args ...interface{}) bool {
471 if h, ok := t.(tHelper); ok {
472 h.Helper()
473 }
474 return WithinDuration(t, expected, actual, delta, append([]interface{}{msg}, args...)...)
475 }
476
477 // Zerof asserts that i is the zero value for its type.
478 func Zerof(t TestingT, i interface{}, msg string, args ...interface{}) bool {
479 if h, ok := t.(tHelper); ok {
480 h.Helper()
481 }
482 return Zero(t, i, append([]interface{}{msg}, args...)...)
483 }
0 {{.CommentFormat}}
1 func {{.DocInfo.Name}}f(t TestingT, {{.ParamsFormat}}) bool {
2 if h, ok := t.(tHelper); ok { h.Helper() }
3 return {{.DocInfo.Name}}(t, {{.ForwardedParamsFormat}})
4 }
0 /*
1 * CODE GENERATED AUTOMATICALLY WITH github.com/stretchr/testify/_codegen
2 * THIS FILE MUST NOT BE EDITED BY HAND
3 */
4
5 package assert
6
7 import (
8 http "net/http"
9 url "net/url"
10 time "time"
11 )
12
13 // Condition uses a Comparison to assert a complex condition.
14 func (a *Assertions) Condition(comp Comparison, msgAndArgs ...interface{}) bool {
15 if h, ok := a.t.(tHelper); ok {
16 h.Helper()
17 }
18 return Condition(a.t, comp, msgAndArgs...)
19 }
20
21 // Conditionf uses a Comparison to assert a complex condition.
22 func (a *Assertions) Conditionf(comp Comparison, msg string, args ...interface{}) bool {
23 if h, ok := a.t.(tHelper); ok {
24 h.Helper()
25 }
26 return Conditionf(a.t, comp, msg, args...)
27 }
28
29 // Contains asserts that the specified string, list(array, slice...) or map contains the
30 // specified substring or element.
31 //
32 // a.Contains("Hello World", "World")
33 // a.Contains(["Hello", "World"], "World")
34 // a.Contains({"Hello": "World"}, "Hello")
35 func (a *Assertions) Contains(s interface{}, contains interface{}, msgAndArgs ...interface{}) bool {
36 if h, ok := a.t.(tHelper); ok {
37 h.Helper()
38 }
39 return Contains(a.t, s, contains, msgAndArgs...)
40 }
41
42 // Containsf asserts that the specified string, list(array, slice...) or map contains the
43 // specified substring or element.
44 //
45 // a.Containsf("Hello World", "World", "error message %s", "formatted")
46 // a.Containsf(["Hello", "World"], "World", "error message %s", "formatted")
47 // a.Containsf({"Hello": "World"}, "Hello", "error message %s", "formatted")
48 func (a *Assertions) Containsf(s interface{}, contains interface{}, msg string, args ...interface{}) bool {
49 if h, ok := a.t.(tHelper); ok {
50 h.Helper()
51 }
52 return Containsf(a.t, s, contains, msg, args...)
53 }
54
55 // DirExists checks whether a directory exists in the given path. It also fails if the path is a file rather a directory or there is an error checking whether it exists.
56 func (a *Assertions) DirExists(path string, msgAndArgs ...interface{}) bool {
57 if h, ok := a.t.(tHelper); ok {
58 h.Helper()
59 }
60 return DirExists(a.t, path, msgAndArgs...)
61 }
62
63 // DirExistsf checks whether a directory exists in the given path. It also fails if the path is a file rather a directory or there is an error checking whether it exists.
64 func (a *Assertions) DirExistsf(path string, msg string, args ...interface{}) bool {
65 if h, ok := a.t.(tHelper); ok {
66 h.Helper()
67 }
68 return DirExistsf(a.t, path, msg, args...)
69 }
70
71 // ElementsMatch asserts that the specified listA(array, slice...) is equal to specified
72 // listB(array, slice...) ignoring the order of the elements. If there are duplicate elements,
73 // the number of appearances of each of them in both lists should match.
74 //
75 // a.ElementsMatch([1, 3, 2, 3], [1, 3, 3, 2])
76 func (a *Assertions) ElementsMatch(listA interface{}, listB interface{}, msgAndArgs ...interface{}) bool {
77 if h, ok := a.t.(tHelper); ok {
78 h.Helper()
79 }
80 return ElementsMatch(a.t, listA, listB, msgAndArgs...)
81 }
82
83 // ElementsMatchf asserts that the specified listA(array, slice...) is equal to specified
84 // listB(array, slice...) ignoring the order of the elements. If there are duplicate elements,
85 // the number of appearances of each of them in both lists should match.
86 //
87 // a.ElementsMatchf([1, 3, 2, 3], [1, 3, 3, 2], "error message %s", "formatted")
88 func (a *Assertions) ElementsMatchf(listA interface{}, listB interface{}, msg string, args ...interface{}) bool {
89 if h, ok := a.t.(tHelper); ok {
90 h.Helper()
91 }
92 return ElementsMatchf(a.t, listA, listB, msg, args...)
93 }
94
95 // Empty asserts that the specified object is empty. I.e. nil, "", false, 0 or either
96 // a slice or a channel with len == 0.
97 //
98 // a.Empty(obj)
99 func (a *Assertions) Empty(object interface{}, msgAndArgs ...interface{}) bool {
100 if h, ok := a.t.(tHelper); ok {
101 h.Helper()
102 }
103 return Empty(a.t, object, msgAndArgs...)
104 }
105
106 // Emptyf asserts that the specified object is empty. I.e. nil, "", false, 0 or either
107 // a slice or a channel with len == 0.
108 //
109 // a.Emptyf(obj, "error message %s", "formatted")
110 func (a *Assertions) Emptyf(object interface{}, msg string, args ...interface{}) bool {
111 if h, ok := a.t.(tHelper); ok {
112 h.Helper()
113 }
114 return Emptyf(a.t, object, msg, args...)
115 }
116
117 // Equal asserts that two objects are equal.
118 //
119 // a.Equal(123, 123)
120 //
121 // Pointer variable equality is determined based on the equality of the
122 // referenced values (as opposed to the memory addresses). Function equality
123 // cannot be determined and will always fail.
124 func (a *Assertions) Equal(expected interface{}, actual interface{}, msgAndArgs ...interface{}) bool {
125 if h, ok := a.t.(tHelper); ok {
126 h.Helper()
127 }
128 return Equal(a.t, expected, actual, msgAndArgs...)
129 }
130
131 // EqualError asserts that a function returned an error (i.e. not `nil`)
132 // and that it is equal to the provided error.
133 //
134 // actualObj, err := SomeFunction()
135 // a.EqualError(err, expectedErrorString)
136 func (a *Assertions) EqualError(theError error, errString string, msgAndArgs ...interface{}) bool {
137 if h, ok := a.t.(tHelper); ok {
138 h.Helper()
139 }
140 return EqualError(a.t, theError, errString, msgAndArgs...)
141 }
142
143 // EqualErrorf asserts that a function returned an error (i.e. not `nil`)
144 // and that it is equal to the provided error.
145 //
146 // actualObj, err := SomeFunction()
147 // a.EqualErrorf(err, expectedErrorString, "error message %s", "formatted")
148 func (a *Assertions) EqualErrorf(theError error, errString string, msg string, args ...interface{}) bool {
149 if h, ok := a.t.(tHelper); ok {
150 h.Helper()
151 }
152 return EqualErrorf(a.t, theError, errString, msg, args...)
153 }
154
155 // EqualValues asserts that two objects are equal or convertable to the same types
156 // and equal.
157 //
158 // a.EqualValues(uint32(123), int32(123))
159 func (a *Assertions) EqualValues(expected interface{}, actual interface{}, msgAndArgs ...interface{}) bool {
160 if h, ok := a.t.(tHelper); ok {
161 h.Helper()
162 }
163 return EqualValues(a.t, expected, actual, msgAndArgs...)
164 }
165
166 // EqualValuesf asserts that two objects are equal or convertable to the same types
167 // and equal.
168 //
169 // a.EqualValuesf(uint32(123, "error message %s", "formatted"), int32(123))
170 func (a *Assertions) EqualValuesf(expected interface{}, actual interface{}, msg string, args ...interface{}) bool {
171 if h, ok := a.t.(tHelper); ok {
172 h.Helper()
173 }
174 return EqualValuesf(a.t, expected, actual, msg, args...)
175 }
176
177 // Equalf asserts that two objects are equal.
178 //
179 // a.Equalf(123, 123, "error message %s", "formatted")
180 //
181 // Pointer variable equality is determined based on the equality of the
182 // referenced values (as opposed to the memory addresses). Function equality
183 // cannot be determined and will always fail.
184 func (a *Assertions) Equalf(expected interface{}, actual interface{}, msg string, args ...interface{}) bool {
185 if h, ok := a.t.(tHelper); ok {
186 h.Helper()
187 }
188 return Equalf(a.t, expected, actual, msg, args...)
189 }
190
191 // Error asserts that a function returned an error (i.e. not `nil`).
192 //
193 // actualObj, err := SomeFunction()
194 // if a.Error(err) {
195 // assert.Equal(t, expectedError, err)
196 // }
197 func (a *Assertions) Error(err error, msgAndArgs ...interface{}) bool {
198 if h, ok := a.t.(tHelper); ok {
199 h.Helper()
200 }
201 return Error(a.t, err, msgAndArgs...)
202 }
203
204 // Errorf asserts that a function returned an error (i.e. not `nil`).
205 //
206 // actualObj, err := SomeFunction()
207 // if a.Errorf(err, "error message %s", "formatted") {
208 // assert.Equal(t, expectedErrorf, err)
209 // }
210 func (a *Assertions) Errorf(err error, msg string, args ...interface{}) bool {
211 if h, ok := a.t.(tHelper); ok {
212 h.Helper()
213 }
214 return Errorf(a.t, err, msg, args...)
215 }
216
217 // Exactly asserts that two objects are equal in value and type.
218 //
219 // a.Exactly(int32(123), int64(123))
220 func (a *Assertions) Exactly(expected interface{}, actual interface{}, msgAndArgs ...interface{}) bool {
221 if h, ok := a.t.(tHelper); ok {
222 h.Helper()
223 }
224 return Exactly(a.t, expected, actual, msgAndArgs...)
225 }
226
227 // Exactlyf asserts that two objects are equal in value and type.
228 //
229 // a.Exactlyf(int32(123, "error message %s", "formatted"), int64(123))
230 func (a *Assertions) Exactlyf(expected interface{}, actual interface{}, msg string, args ...interface{}) bool {
231 if h, ok := a.t.(tHelper); ok {
232 h.Helper()
233 }
234 return Exactlyf(a.t, expected, actual, msg, args...)
235 }
236
237 // Fail reports a failure through
238 func (a *Assertions) Fail(failureMessage string, msgAndArgs ...interface{}) bool {
239 if h, ok := a.t.(tHelper); ok {
240 h.Helper()
241 }
242 return Fail(a.t, failureMessage, msgAndArgs...)
243 }
244
245 // FailNow fails test
246 func (a *Assertions) FailNow(failureMessage string, msgAndArgs ...interface{}) bool {
247 if h, ok := a.t.(tHelper); ok {
248 h.Helper()
249 }
250 return FailNow(a.t, failureMessage, msgAndArgs...)
251 }
252
253 // FailNowf fails test
254 func (a *Assertions) FailNowf(failureMessage string, msg string, args ...interface{}) bool {
255 if h, ok := a.t.(tHelper); ok {
256 h.Helper()
257 }
258 return FailNowf(a.t, failureMessage, msg, args...)
259 }
260
261 // Failf reports a failure through
262 func (a *Assertions) Failf(failureMessage string, msg string, args ...interface{}) bool {
263 if h, ok := a.t.(tHelper); ok {
264 h.Helper()
265 }
266 return Failf(a.t, failureMessage, msg, args...)
267 }
268
269 // False asserts that the specified value is false.
270 //
271 // a.False(myBool)
272 func (a *Assertions) False(value bool, msgAndArgs ...interface{}) bool {
273 if h, ok := a.t.(tHelper); ok {
274 h.Helper()
275 }
276 return False(a.t, value, msgAndArgs...)
277 }
278
279 // Falsef asserts that the specified value is false.
280 //
281 // a.Falsef(myBool, "error message %s", "formatted")
282 func (a *Assertions) Falsef(value bool, msg string, args ...interface{}) bool {
283 if h, ok := a.t.(tHelper); ok {
284 h.Helper()
285 }
286 return Falsef(a.t, value, msg, args...)
287 }
288
289 // FileExists checks whether a file exists in the given path. It also fails if the path points to a directory or there is an error when trying to check the file.
290 func (a *Assertions) FileExists(path string, msgAndArgs ...interface{}) bool {
291 if h, ok := a.t.(tHelper); ok {
292 h.Helper()
293 }
294 return FileExists(a.t, path, msgAndArgs...)
295 }
296
297 // FileExistsf checks whether a file exists in the given path. It also fails if the path points to a directory or there is an error when trying to check the file.
298 func (a *Assertions) FileExistsf(path string, msg string, args ...interface{}) bool {
299 if h, ok := a.t.(tHelper); ok {
300 h.Helper()
301 }
302 return FileExistsf(a.t, path, msg, args...)
303 }
304
305 // HTTPBodyContains asserts that a specified handler returns a
306 // body that contains a string.
307 //
308 // a.HTTPBodyContains(myHandler, "GET", "www.google.com", nil, "I'm Feeling Lucky")
309 //
310 // Returns whether the assertion was successful (true) or not (false).
311 func (a *Assertions) HTTPBodyContains(handler http.HandlerFunc, method string, url string, values url.Values, str interface{}, msgAndArgs ...interface{}) bool {
312 if h, ok := a.t.(tHelper); ok {
313 h.Helper()
314 }
315 return HTTPBodyContains(a.t, handler, method, url, values, str, msgAndArgs...)
316 }
317
318 // HTTPBodyContainsf asserts that a specified handler returns a
319 // body that contains a string.
320 //
321 // a.HTTPBodyContainsf(myHandler, "GET", "www.google.com", nil, "I'm Feeling Lucky", "error message %s", "formatted")
322 //
323 // Returns whether the assertion was successful (true) or not (false).
324 func (a *Assertions) HTTPBodyContainsf(handler http.HandlerFunc, method string, url string, values url.Values, str interface{}, msg string, args ...interface{}) bool {
325 if h, ok := a.t.(tHelper); ok {
326 h.Helper()
327 }
328 return HTTPBodyContainsf(a.t, handler, method, url, values, str, msg, args...)
329 }
330
331 // HTTPBodyNotContains asserts that a specified handler returns a
332 // body that does not contain a string.
333 //
334 // a.HTTPBodyNotContains(myHandler, "GET", "www.google.com", nil, "I'm Feeling Lucky")
335 //
336 // Returns whether the assertion was successful (true) or not (false).
337 func (a *Assertions) HTTPBodyNotContains(handler http.HandlerFunc, method string, url string, values url.Values, str interface{}, msgAndArgs ...interface{}) bool {
338 if h, ok := a.t.(tHelper); ok {
339 h.Helper()
340 }
341 return HTTPBodyNotContains(a.t, handler, method, url, values, str, msgAndArgs...)
342 }
343
344 // HTTPBodyNotContainsf asserts that a specified handler returns a
345 // body that does not contain a string.
346 //
347 // a.HTTPBodyNotContainsf(myHandler, "GET", "www.google.com", nil, "I'm Feeling Lucky", "error message %s", "formatted")
348 //
349 // Returns whether the assertion was successful (true) or not (false).
350 func (a *Assertions) HTTPBodyNotContainsf(handler http.HandlerFunc, method string, url string, values url.Values, str interface{}, msg string, args ...interface{}) bool {
351 if h, ok := a.t.(tHelper); ok {
352 h.Helper()
353 }
354 return HTTPBodyNotContainsf(a.t, handler, method, url, values, str, msg, args...)
355 }
356
357 // HTTPError asserts that a specified handler returns an error status code.
358 //
359 // a.HTTPError(myHandler, "POST", "/a/b/c", url.Values{"a": []string{"b", "c"}}
360 //
361 // Returns whether the assertion was successful (true) or not (false).
362 func (a *Assertions) HTTPError(handler http.HandlerFunc, method string, url string, values url.Values, msgAndArgs ...interface{}) bool {
363 if h, ok := a.t.(tHelper); ok {
364 h.Helper()
365 }
366 return HTTPError(a.t, handler, method, url, values, msgAndArgs...)
367 }
368
369 // HTTPErrorf asserts that a specified handler returns an error status code.
370 //
371 // a.HTTPErrorf(myHandler, "POST", "/a/b/c", url.Values{"a": []string{"b", "c"}}
372 //
373 // Returns whether the assertion was successful (true, "error message %s", "formatted") or not (false).
374 func (a *Assertions) HTTPErrorf(handler http.HandlerFunc, method string, url string, values url.Values, msg string, args ...interface{}) bool {
375 if h, ok := a.t.(tHelper); ok {
376 h.Helper()
377 }
378 return HTTPErrorf(a.t, handler, method, url, values, msg, args...)
379 }
380
381 // HTTPRedirect asserts that a specified handler returns a redirect status code.
382 //
383 // a.HTTPRedirect(myHandler, "GET", "/a/b/c", url.Values{"a": []string{"b", "c"}}
384 //
385 // Returns whether the assertion was successful (true) or not (false).
386 func (a *Assertions) HTTPRedirect(handler http.HandlerFunc, method string, url string, values url.Values, msgAndArgs ...interface{}) bool {
387 if h, ok := a.t.(tHelper); ok {
388 h.Helper()
389 }
390 return HTTPRedirect(a.t, handler, method, url, values, msgAndArgs...)
391 }
392
393 // HTTPRedirectf asserts that a specified handler returns a redirect status code.
394 //
395 // a.HTTPRedirectf(myHandler, "GET", "/a/b/c", url.Values{"a": []string{"b", "c"}}
396 //
397 // Returns whether the assertion was successful (true, "error message %s", "formatted") or not (false).
398 func (a *Assertions) HTTPRedirectf(handler http.HandlerFunc, method string, url string, values url.Values, msg string, args ...interface{}) bool {
399 if h, ok := a.t.(tHelper); ok {
400 h.Helper()
401 }
402 return HTTPRedirectf(a.t, handler, method, url, values, msg, args...)
403 }
404
405 // HTTPSuccess asserts that a specified handler returns a success status code.
406 //
407 // a.HTTPSuccess(myHandler, "POST", "http://www.google.com", nil)
408 //
409 // Returns whether the assertion was successful (true) or not (false).
410 func (a *Assertions) HTTPSuccess(handler http.HandlerFunc, method string, url string, values url.Values, msgAndArgs ...interface{}) bool {
411 if h, ok := a.t.(tHelper); ok {
412 h.Helper()
413 }
414 return HTTPSuccess(a.t, handler, method, url, values, msgAndArgs...)
415 }
416
417 // HTTPSuccessf asserts that a specified handler returns a success status code.
418 //
419 // a.HTTPSuccessf(myHandler, "POST", "http://www.google.com", nil, "error message %s", "formatted")
420 //
421 // Returns whether the assertion was successful (true) or not (false).
422 func (a *Assertions) HTTPSuccessf(handler http.HandlerFunc, method string, url string, values url.Values, msg string, args ...interface{}) bool {
423 if h, ok := a.t.(tHelper); ok {
424 h.Helper()
425 }
426 return HTTPSuccessf(a.t, handler, method, url, values, msg, args...)
427 }
428
429 // Implements asserts that an object is implemented by the specified interface.
430 //
431 // a.Implements((*MyInterface)(nil), new(MyObject))
432 func (a *Assertions) Implements(interfaceObject interface{}, object interface{}, msgAndArgs ...interface{}) bool {
433 if h, ok := a.t.(tHelper); ok {
434 h.Helper()
435 }
436 return Implements(a.t, interfaceObject, object, msgAndArgs...)
437 }
438
439 // Implementsf asserts that an object is implemented by the specified interface.
440 //
441 // a.Implementsf((*MyInterface, "error message %s", "formatted")(nil), new(MyObject))
442 func (a *Assertions) Implementsf(interfaceObject interface{}, object interface{}, msg string, args ...interface{}) bool {
443 if h, ok := a.t.(tHelper); ok {
444 h.Helper()
445 }
446 return Implementsf(a.t, interfaceObject, object, msg, args...)
447 }
448
449 // InDelta asserts that the two numerals are within delta of each other.
450 //
451 // a.InDelta(math.Pi, (22 / 7.0), 0.01)
452 func (a *Assertions) InDelta(expected interface{}, actual interface{}, delta float64, msgAndArgs ...interface{}) bool {
453 if h, ok := a.t.(tHelper); ok {
454 h.Helper()
455 }
456 return InDelta(a.t, expected, actual, delta, msgAndArgs...)
457 }
458
459 // InDeltaMapValues is the same as InDelta, but it compares all values between two maps. Both maps must have exactly the same keys.
460 func (a *Assertions) InDeltaMapValues(expected interface{}, actual interface{}, delta float64, msgAndArgs ...interface{}) bool {
461 if h, ok := a.t.(tHelper); ok {
462 h.Helper()
463 }
464 return InDeltaMapValues(a.t, expected, actual, delta, msgAndArgs...)
465 }
466
467 // InDeltaMapValuesf is the same as InDelta, but it compares all values between two maps. Both maps must have exactly the same keys.
468 func (a *Assertions) InDeltaMapValuesf(expected interface{}, actual interface{}, delta float64, msg string, args ...interface{}) bool {
469 if h, ok := a.t.(tHelper); ok {
470 h.Helper()
471 }
472 return InDeltaMapValuesf(a.t, expected, actual, delta, msg, args...)
473 }
474
475 // InDeltaSlice is the same as InDelta, except it compares two slices.
476 func (a *Assertions) InDeltaSlice(expected interface{}, actual interface{}, delta float64, msgAndArgs ...interface{}) bool {
477 if h, ok := a.t.(tHelper); ok {
478 h.Helper()
479 }
480 return InDeltaSlice(a.t, expected, actual, delta, msgAndArgs...)
481 }
482
483 // InDeltaSlicef is the same as InDelta, except it compares two slices.
484 func (a *Assertions) InDeltaSlicef(expected interface{}, actual interface{}, delta float64, msg string, args ...interface{}) bool {
485 if h, ok := a.t.(tHelper); ok {
486 h.Helper()
487 }
488 return InDeltaSlicef(a.t, expected, actual, delta, msg, args...)
489 }
490
491 // InDeltaf asserts that the two numerals are within delta of each other.
492 //
493 // a.InDeltaf(math.Pi, (22 / 7.0, "error message %s", "formatted"), 0.01)
494 func (a *Assertions) InDeltaf(expected interface{}, actual interface{}, delta float64, msg string, args ...interface{}) bool {
495 if h, ok := a.t.(tHelper); ok {
496 h.Helper()
497 }
498 return InDeltaf(a.t, expected, actual, delta, msg, args...)
499 }
500
501 // InEpsilon asserts that expected and actual have a relative error less than epsilon
502 func (a *Assertions) InEpsilon(expected interface{}, actual interface{}, epsilon float64, msgAndArgs ...interface{}) bool {
503 if h, ok := a.t.(tHelper); ok {
504 h.Helper()
505 }
506 return InEpsilon(a.t, expected, actual, epsilon, msgAndArgs...)
507 }
508
509 // InEpsilonSlice is the same as InEpsilon, except it compares each value from two slices.
510 func (a *Assertions) InEpsilonSlice(expected interface{}, actual interface{}, epsilon float64, msgAndArgs ...interface{}) bool {
511 if h, ok := a.t.(tHelper); ok {
512 h.Helper()
513 }
514 return InEpsilonSlice(a.t, expected, actual, epsilon, msgAndArgs...)
515 }
516
517 // InEpsilonSlicef is the same as InEpsilon, except it compares each value from two slices.
518 func (a *Assertions) InEpsilonSlicef(expected interface{}, actual interface{}, epsilon float64, msg string, args ...interface{}) bool {
519 if h, ok := a.t.(tHelper); ok {
520 h.Helper()
521 }
522 return InEpsilonSlicef(a.t, expected, actual, epsilon, msg, args...)
523 }
524
525 // InEpsilonf asserts that expected and actual have a relative error less than epsilon
526 func (a *Assertions) InEpsilonf(expected interface{}, actual interface{}, epsilon float64, msg string, args ...interface{}) bool {
527 if h, ok := a.t.(tHelper); ok {
528 h.Helper()
529 }
530 return InEpsilonf(a.t, expected, actual, epsilon, msg, args...)
531 }
532
533 // IsType asserts that the specified objects are of the same type.
534 func (a *Assertions) IsType(expectedType interface{}, object interface{}, msgAndArgs ...interface{}) bool {
535 if h, ok := a.t.(tHelper); ok {
536 h.Helper()
537 }
538 return IsType(a.t, expectedType, object, msgAndArgs...)
539 }
540
541 // IsTypef asserts that the specified objects are of the same type.
542 func (a *Assertions) IsTypef(expectedType interface{}, object interface{}, msg string, args ...interface{}) bool {
543 if h, ok := a.t.(tHelper); ok {
544 h.Helper()
545 }
546 return IsTypef(a.t, expectedType, object, msg, args...)
547 }
548
549 // JSONEq asserts that two JSON strings are equivalent.
550 //
551 // a.JSONEq(`{"hello": "world", "foo": "bar"}`, `{"foo": "bar", "hello": "world"}`)
552 func (a *Assertions) JSONEq(expected string, actual string, msgAndArgs ...interface{}) bool {
553 if h, ok := a.t.(tHelper); ok {
554 h.Helper()
555 }
556 return JSONEq(a.t, expected, actual, msgAndArgs...)
557 }
558
559 // JSONEqf asserts that two JSON strings are equivalent.
560 //
561 // a.JSONEqf(`{"hello": "world", "foo": "bar"}`, `{"foo": "bar", "hello": "world"}`, "error message %s", "formatted")
562 func (a *Assertions) JSONEqf(expected string, actual string, msg string, args ...interface{}) bool {
563 if h, ok := a.t.(tHelper); ok {
564 h.Helper()
565 }
566 return JSONEqf(a.t, expected, actual, msg, args...)
567 }
568
569 // Len asserts that the specified object has specific length.
570 // Len also fails if the object has a type that len() not accept.
571 //
572 // a.Len(mySlice, 3)
573 func (a *Assertions) Len(object interface{}, length int, msgAndArgs ...interface{}) bool {
574 if h, ok := a.t.(tHelper); ok {
575 h.Helper()
576 }
577 return Len(a.t, object, length, msgAndArgs...)
578 }
579
580 // Lenf asserts that the specified object has specific length.
581 // Lenf also fails if the object has a type that len() not accept.
582 //
583 // a.Lenf(mySlice, 3, "error message %s", "formatted")
584 func (a *Assertions) Lenf(object interface{}, length int, msg string, args ...interface{}) bool {
585 if h, ok := a.t.(tHelper); ok {
586 h.Helper()
587 }
588 return Lenf(a.t, object, length, msg, args...)
589 }
590
591 // Nil asserts that the specified object is nil.
592 //
593 // a.Nil(err)
594 func (a *Assertions) Nil(object interface{}, msgAndArgs ...interface{}) bool {
595 if h, ok := a.t.(tHelper); ok {
596 h.Helper()
597 }
598 return Nil(a.t, object, msgAndArgs...)
599 }
600
601 // Nilf asserts that the specified object is nil.
602 //
603 // a.Nilf(err, "error message %s", "formatted")
604 func (a *Assertions) Nilf(object interface{}, msg string, args ...interface{}) bool {
605 if h, ok := a.t.(tHelper); ok {
606 h.Helper()
607 }
608 return Nilf(a.t, object, msg, args...)
609 }
610
611 // NoError asserts that a function returned no error (i.e. `nil`).
612 //
613 // actualObj, err := SomeFunction()
614 // if a.NoError(err) {
615 // assert.Equal(t, expectedObj, actualObj)
616 // }
617 func (a *Assertions) NoError(err error, msgAndArgs ...interface{}) bool {
618 if h, ok := a.t.(tHelper); ok {
619 h.Helper()
620 }
621 return NoError(a.t, err, msgAndArgs...)
622 }
623
624 // NoErrorf asserts that a function returned no error (i.e. `nil`).
625 //
626 // actualObj, err := SomeFunction()
627 // if a.NoErrorf(err, "error message %s", "formatted") {
628 // assert.Equal(t, expectedObj, actualObj)
629 // }
630 func (a *Assertions) NoErrorf(err error, msg string, args ...interface{}) bool {
631 if h, ok := a.t.(tHelper); ok {
632 h.Helper()
633 }
634 return NoErrorf(a.t, err, msg, args...)
635 }
636
637 // NotContains asserts that the specified string, list(array, slice...) or map does NOT contain the
638 // specified substring or element.
639 //
640 // a.NotContains("Hello World", "Earth")
641 // a.NotContains(["Hello", "World"], "Earth")
642 // a.NotContains({"Hello": "World"}, "Earth")
643 func (a *Assertions) NotContains(s interface{}, contains interface{}, msgAndArgs ...interface{}) bool {
644 if h, ok := a.t.(tHelper); ok {
645 h.Helper()
646 }
647 return NotContains(a.t, s, contains, msgAndArgs...)
648 }
649
650 // NotContainsf asserts that the specified string, list(array, slice...) or map does NOT contain the
651 // specified substring or element.
652 //
653 // a.NotContainsf("Hello World", "Earth", "error message %s", "formatted")
654 // a.NotContainsf(["Hello", "World"], "Earth", "error message %s", "formatted")
655 // a.NotContainsf({"Hello": "World"}, "Earth", "error message %s", "formatted")
656 func (a *Assertions) NotContainsf(s interface{}, contains interface{}, msg string, args ...interface{}) bool {
657 if h, ok := a.t.(tHelper); ok {
658 h.Helper()
659 }
660 return NotContainsf(a.t, s, contains, msg, args...)
661 }
662
663 // NotEmpty asserts that the specified object is NOT empty. I.e. not nil, "", false, 0 or either
664 // a slice or a channel with len == 0.
665 //
666 // if a.NotEmpty(obj) {
667 // assert.Equal(t, "two", obj[1])
668 // }
669 func (a *Assertions) NotEmpty(object interface{}, msgAndArgs ...interface{}) bool {
670 if h, ok := a.t.(tHelper); ok {
671 h.Helper()
672 }
673 return NotEmpty(a.t, object, msgAndArgs...)
674 }
675
676 // NotEmptyf asserts that the specified object is NOT empty. I.e. not nil, "", false, 0 or either
677 // a slice or a channel with len == 0.
678 //
679 // if a.NotEmptyf(obj, "error message %s", "formatted") {
680 // assert.Equal(t, "two", obj[1])
681 // }
682 func (a *Assertions) NotEmptyf(object interface{}, msg string, args ...interface{}) bool {
683 if h, ok := a.t.(tHelper); ok {
684 h.Helper()
685 }
686 return NotEmptyf(a.t, object, msg, args...)
687 }
688
689 // NotEqual asserts that the specified values are NOT equal.
690 //
691 // a.NotEqual(obj1, obj2)
692 //
693 // Pointer variable equality is determined based on the equality of the
694 // referenced values (as opposed to the memory addresses).
695 func (a *Assertions) NotEqual(expected interface{}, actual interface{}, msgAndArgs ...interface{}) bool {
696 if h, ok := a.t.(tHelper); ok {
697 h.Helper()
698 }
699 return NotEqual(a.t, expected, actual, msgAndArgs...)
700 }
701
702 // NotEqualf asserts that the specified values are NOT equal.
703 //
704 // a.NotEqualf(obj1, obj2, "error message %s", "formatted")
705 //
706 // Pointer variable equality is determined based on the equality of the
707 // referenced values (as opposed to the memory addresses).
708 func (a *Assertions) NotEqualf(expected interface{}, actual interface{}, msg string, args ...interface{}) bool {
709 if h, ok := a.t.(tHelper); ok {
710 h.Helper()
711 }
712 return NotEqualf(a.t, expected, actual, msg, args...)
713 }
714
715 // NotNil asserts that the specified object is not nil.
716 //
717 // a.NotNil(err)
718 func (a *Assertions) NotNil(object interface{}, msgAndArgs ...interface{}) bool {
719 if h, ok := a.t.(tHelper); ok {
720 h.Helper()
721 }
722 return NotNil(a.t, object, msgAndArgs...)
723 }
724
725 // NotNilf asserts that the specified object is not nil.
726 //
727 // a.NotNilf(err, "error message %s", "formatted")
728 func (a *Assertions) NotNilf(object interface{}, msg string, args ...interface{}) bool {
729 if h, ok := a.t.(tHelper); ok {
730 h.Helper()
731 }
732 return NotNilf(a.t, object, msg, args...)
733 }
734
735 // NotPanics asserts that the code inside the specified PanicTestFunc does NOT panic.
736 //
737 // a.NotPanics(func(){ RemainCalm() })
738 func (a *Assertions) NotPanics(f PanicTestFunc, msgAndArgs ...interface{}) bool {
739 if h, ok := a.t.(tHelper); ok {
740 h.Helper()
741 }
742 return NotPanics(a.t, f, msgAndArgs...)
743 }
744
745 // NotPanicsf asserts that the code inside the specified PanicTestFunc does NOT panic.
746 //
747 // a.NotPanicsf(func(){ RemainCalm() }, "error message %s", "formatted")
748 func (a *Assertions) NotPanicsf(f PanicTestFunc, msg string, args ...interface{}) bool {
749 if h, ok := a.t.(tHelper); ok {
750 h.Helper()
751 }
752 return NotPanicsf(a.t, f, msg, args...)
753 }
754
755 // NotRegexp asserts that a specified regexp does not match a string.
756 //
757 // a.NotRegexp(regexp.MustCompile("starts"), "it's starting")
758 // a.NotRegexp("^start", "it's not starting")
759 func (a *Assertions) NotRegexp(rx interface{}, str interface{}, msgAndArgs ...interface{}) bool {
760 if h, ok := a.t.(tHelper); ok {
761 h.Helper()
762 }
763 return NotRegexp(a.t, rx, str, msgAndArgs...)
764 }
765
766 // NotRegexpf asserts that a specified regexp does not match a string.
767 //
768 // a.NotRegexpf(regexp.MustCompile("starts", "error message %s", "formatted"), "it's starting")
769 // a.NotRegexpf("^start", "it's not starting", "error message %s", "formatted")
770 func (a *Assertions) NotRegexpf(rx interface{}, str interface{}, msg string, args ...interface{}) bool {
771 if h, ok := a.t.(tHelper); ok {
772 h.Helper()
773 }
774 return NotRegexpf(a.t, rx, str, msg, args...)
775 }
776
777 // NotSubset asserts that the specified list(array, slice...) contains not all
778 // elements given in the specified subset(array, slice...).
779 //
780 // a.NotSubset([1, 3, 4], [1, 2], "But [1, 3, 4] does not contain [1, 2]")
781 func (a *Assertions) NotSubset(list interface{}, subset interface{}, msgAndArgs ...interface{}) bool {
782 if h, ok := a.t.(tHelper); ok {
783 h.Helper()
784 }
785 return NotSubset(a.t, list, subset, msgAndArgs...)
786 }
787
788 // NotSubsetf asserts that the specified list(array, slice...) contains not all
789 // elements given in the specified subset(array, slice...).
790 //
791 // a.NotSubsetf([1, 3, 4], [1, 2], "But [1, 3, 4] does not contain [1, 2]", "error message %s", "formatted")
792 func (a *Assertions) NotSubsetf(list interface{}, subset interface{}, msg string, args ...interface{}) bool {
793 if h, ok := a.t.(tHelper); ok {
794 h.Helper()
795 }
796 return NotSubsetf(a.t, list, subset, msg, args...)
797 }
798
799 // NotZero asserts that i is not the zero value for its type.
800 func (a *Assertions) NotZero(i interface{}, msgAndArgs ...interface{}) bool {
801 if h, ok := a.t.(tHelper); ok {
802 h.Helper()
803 }
804 return NotZero(a.t, i, msgAndArgs...)
805 }
806
807 // NotZerof asserts that i is not the zero value for its type.
808 func (a *Assertions) NotZerof(i interface{}, msg string, args ...interface{}) bool {
809 if h, ok := a.t.(tHelper); ok {
810 h.Helper()
811 }
812 return NotZerof(a.t, i, msg, args...)
813 }
814
815 // Panics asserts that the code inside the specified PanicTestFunc panics.
816 //
817 // a.Panics(func(){ GoCrazy() })
818 func (a *Assertions) Panics(f PanicTestFunc, msgAndArgs ...interface{}) bool {
819 if h, ok := a.t.(tHelper); ok {
820 h.Helper()
821 }
822 return Panics(a.t, f, msgAndArgs...)
823 }
824
825 // PanicsWithValue asserts that the code inside the specified PanicTestFunc panics, and that
826 // the recovered panic value equals the expected panic value.
827 //
828 // a.PanicsWithValue("crazy error", func(){ GoCrazy() })
829 func (a *Assertions) PanicsWithValue(expected interface{}, f PanicTestFunc, msgAndArgs ...interface{}) bool {
830 if h, ok := a.t.(tHelper); ok {
831 h.Helper()
832 }
833 return PanicsWithValue(a.t, expected, f, msgAndArgs...)
834 }
835
836 // PanicsWithValuef asserts that the code inside the specified PanicTestFunc panics, and that
837 // the recovered panic value equals the expected panic value.
838 //
839 // a.PanicsWithValuef("crazy error", func(){ GoCrazy() }, "error message %s", "formatted")
840 func (a *Assertions) PanicsWithValuef(expected interface{}, f PanicTestFunc, msg string, args ...interface{}) bool {
841 if h, ok := a.t.(tHelper); ok {
842 h.Helper()
843 }
844 return PanicsWithValuef(a.t, expected, f, msg, args...)
845 }
846
847 // Panicsf asserts that the code inside the specified PanicTestFunc panics.
848 //
849 // a.Panicsf(func(){ GoCrazy() }, "error message %s", "formatted")
850 func (a *Assertions) Panicsf(f PanicTestFunc, msg string, args ...interface{}) bool {
851 if h, ok := a.t.(tHelper); ok {
852 h.Helper()
853 }
854 return Panicsf(a.t, f, msg, args...)
855 }
856
857 // Regexp asserts that a specified regexp matches a string.
858 //
859 // a.Regexp(regexp.MustCompile("start"), "it's starting")
860 // a.Regexp("start...$", "it's not starting")
861 func (a *Assertions) Regexp(rx interface{}, str interface{}, msgAndArgs ...interface{}) bool {
862 if h, ok := a.t.(tHelper); ok {
863 h.Helper()
864 }
865 return Regexp(a.t, rx, str, msgAndArgs...)
866 }
867
868 // Regexpf asserts that a specified regexp matches a string.
869 //
870 // a.Regexpf(regexp.MustCompile("start", "error message %s", "formatted"), "it's starting")
871 // a.Regexpf("start...$", "it's not starting", "error message %s", "formatted")
872 func (a *Assertions) Regexpf(rx interface{}, str interface{}, msg string, args ...interface{}) bool {
873 if h, ok := a.t.(tHelper); ok {
874 h.Helper()
875 }
876 return Regexpf(a.t, rx, str, msg, args...)
877 }
878
879 // Subset asserts that the specified list(array, slice...) contains all
880 // elements given in the specified subset(array, slice...).
881 //
882 // a.Subset([1, 2, 3], [1, 2], "But [1, 2, 3] does contain [1, 2]")
883 func (a *Assertions) Subset(list interface{}, subset interface{}, msgAndArgs ...interface{}) bool {
884 if h, ok := a.t.(tHelper); ok {
885 h.Helper()
886 }
887 return Subset(a.t, list, subset, msgAndArgs...)
888 }
889
890 // Subsetf asserts that the specified list(array, slice...) contains all
891 // elements given in the specified subset(array, slice...).
892 //
893 // a.Subsetf([1, 2, 3], [1, 2], "But [1, 2, 3] does contain [1, 2]", "error message %s", "formatted")
894 func (a *Assertions) Subsetf(list interface{}, subset interface{}, msg string, args ...interface{}) bool {
895 if h, ok := a.t.(tHelper); ok {
896 h.Helper()
897 }
898 return Subsetf(a.t, list, subset, msg, args...)
899 }
900
901 // True asserts that the specified value is true.
902 //
903 // a.True(myBool)
904 func (a *Assertions) True(value bool, msgAndArgs ...interface{}) bool {
905 if h, ok := a.t.(tHelper); ok {
906 h.Helper()
907 }
908 return True(a.t, value, msgAndArgs...)
909 }
910
911 // Truef asserts that the specified value is true.
912 //
913 // a.Truef(myBool, "error message %s", "formatted")
914 func (a *Assertions) Truef(value bool, msg string, args ...interface{}) bool {
915 if h, ok := a.t.(tHelper); ok {
916 h.Helper()
917 }
918 return Truef(a.t, value, msg, args...)
919 }
920
921 // WithinDuration asserts that the two times are within duration delta of each other.
922 //
923 // a.WithinDuration(time.Now(), time.Now(), 10*time.Second)
924 func (a *Assertions) WithinDuration(expected time.Time, actual time.Time, delta time.Duration, msgAndArgs ...interface{}) bool {
925 if h, ok := a.t.(tHelper); ok {
926 h.Helper()
927 }
928 return WithinDuration(a.t, expected, actual, delta, msgAndArgs...)
929 }
930
931 // WithinDurationf asserts that the two times are within duration delta of each other.
932 //
933 // a.WithinDurationf(time.Now(), time.Now(), 10*time.Second, "error message %s", "formatted")
934 func (a *Assertions) WithinDurationf(expected time.Time, actual time.Time, delta time.Duration, msg string, args ...interface{}) bool {
935 if h, ok := a.t.(tHelper); ok {
936 h.Helper()
937 }
938 return WithinDurationf(a.t, expected, actual, delta, msg, args...)
939 }
940
941 // Zero asserts that i is the zero value for its type.
942 func (a *Assertions) Zero(i interface{}, msgAndArgs ...interface{}) bool {
943 if h, ok := a.t.(tHelper); ok {
944 h.Helper()
945 }
946 return Zero(a.t, i, msgAndArgs...)
947 }
948
949 // Zerof asserts that i is the zero value for its type.
950 func (a *Assertions) Zerof(i interface{}, msg string, args ...interface{}) bool {
951 if h, ok := a.t.(tHelper); ok {
952 h.Helper()
953 }
954 return Zerof(a.t, i, msg, args...)
955 }
0 {{.CommentWithoutT "a"}}
1 func (a *Assertions) {{.DocInfo.Name}}({{.Params}}) bool {
2 if h, ok := a.t.(tHelper); ok { h.Helper() }
3 return {{.DocInfo.Name}}(a.t, {{.ForwardedParams}})
4 }
0 package assert
1
2 import (
3 "bufio"
4 "bytes"
5 "encoding/json"
6 "errors"
7 "fmt"
8 "math"
9 "os"
10 "reflect"
11 "regexp"
12 "runtime"
13 "strings"
14 "time"
15 "unicode"
16 "unicode/utf8"
17
18 "github.com/davecgh/go-spew/spew"
19 "github.com/pmezard/go-difflib/difflib"
20 )
21
22 //go:generate go run ../_codegen/main.go -output-package=assert -template=assertion_format.go.tmpl
23
24 // TestingT is an interface wrapper around *testing.T
25 type TestingT interface {
26 Errorf(format string, args ...interface{})
27 }
28
29 // ComparisonAssertionFunc is a common function prototype when comparing two values. Can be useful
30 // for table driven tests.
31 type ComparisonAssertionFunc func(TestingT, interface{}, interface{}, ...interface{}) bool
32
33 // ValueAssertionFunc is a common function prototype when validating a single value. Can be useful
34 // for table driven tests.
35 type ValueAssertionFunc func(TestingT, interface{}, ...interface{}) bool
36
37 // BoolAssertionFunc is a common function prototype when validating a bool value. Can be useful
38 // for table driven tests.
39 type BoolAssertionFunc func(TestingT, bool, ...interface{}) bool
40
41 // ValuesAssertionFunc is a common function prototype when validating an error value. Can be useful
42 // for table driven tests.
43 type ErrorAssertionFunc func(TestingT, error, ...interface{}) bool
44
45 // Comparison a custom function that returns true on success and false on failure
46 type Comparison func() (success bool)
47
48 /*
49 Helper functions
50 */
51
52 // ObjectsAreEqual determines if two objects are considered equal.
53 //
54 // This function does no assertion of any kind.
55 func ObjectsAreEqual(expected, actual interface{}) bool {
56 if expected == nil || actual == nil {
57 return expected == actual
58 }
59
60 exp, ok := expected.([]byte)
61 if !ok {
62 return reflect.DeepEqual(expected, actual)
63 }
64
65 act, ok := actual.([]byte)
66 if !ok {
67 return false
68 }
69 if exp == nil || act == nil {
70 return exp == nil && act == nil
71 }
72 return bytes.Equal(exp, act)
73 }
74
75 // ObjectsAreEqualValues gets whether two objects are equal, or if their
76 // values are equal.
77 func ObjectsAreEqualValues(expected, actual interface{}) bool {
78 if ObjectsAreEqual(expected, actual) {
79 return true
80 }
81
82 actualType := reflect.TypeOf(actual)
83 if actualType == nil {
84 return false
85 }
86 expectedValue := reflect.ValueOf(expected)
87 if expectedValue.IsValid() && expectedValue.Type().ConvertibleTo(actualType) {
88 // Attempt comparison after type conversion
89 return reflect.DeepEqual(expectedValue.Convert(actualType).Interface(), actual)
90 }
91
92 return false
93 }
94
95 /* CallerInfo is necessary because the assert functions use the testing object
96 internally, causing it to print the file:line of the assert method, rather than where
97 the problem actually occurred in calling code.*/
98
99 // CallerInfo returns an array of strings containing the file and line number
100 // of each stack frame leading from the current test to the assert call that
101 // failed.
102 func CallerInfo() []string {
103
104 pc := uintptr(0)
105 file := ""
106 line := 0
107 ok := false
108 name := ""
109
110 callers := []string{}
111 for i := 0; ; i++ {
112 pc, file, line, ok = runtime.Caller(i)
113 if !ok {
114 // The breaks below failed to terminate the loop, and we ran off the
115 // end of the call stack.
116 break
117 }
118
119 // This is a huge edge case, but it will panic if this is the case, see #180
120 if file == "<autogenerated>" {
121 break
122 }
123
124 f := runtime.FuncForPC(pc)
125 if f == nil {
126 break
127 }
128 name = f.Name()
129
130 // testing.tRunner is the standard library function that calls
131 // tests. Subtests are called directly by tRunner, without going through
132 // the Test/Benchmark/Example function that contains the t.Run calls, so
133 // with subtests we should break when we hit tRunner, without adding it
134 // to the list of callers.
135 if name == "testing.tRunner" {
136 break
137 }
138
139 parts := strings.Split(file, "/")
140 file = parts[len(parts)-1]
141 if len(parts) > 1 {
142 dir := parts[len(parts)-2]
143 if (dir != "assert" && dir != "mock" && dir != "require") || file == "mock_test.go" {
144 callers = append(callers, fmt.Sprintf("%s:%d", file, line))
145 }
146 }
147
148 // Drop the package
149 segments := strings.Split(name, ".")
150 name = segments[len(segments)-1]
151 if isTest(name, "Test") ||
152 isTest(name, "Benchmark") ||
153 isTest(name, "Example") {
154 break
155 }
156 }
157
158 return callers
159 }
160
161 // Stolen from the `go test` tool.
162 // isTest tells whether name looks like a test (or benchmark, according to prefix).
163 // It is a Test (say) if there is a character after Test that is not a lower-case letter.
164 // We don't want TesticularCancer.
165 func isTest(name, prefix string) bool {
166 if !strings.HasPrefix(name, prefix) {
167 return false
168 }
169 if len(name) == len(prefix) { // "Test" is ok
170 return true
171 }
172 rune, _ := utf8.DecodeRuneInString(name[len(prefix):])
173 return !unicode.IsLower(rune)
174 }
175
176 func messageFromMsgAndArgs(msgAndArgs ...interface{}) string {
177 if len(msgAndArgs) == 0 || msgAndArgs == nil {
178 return ""
179 }
180 if len(msgAndArgs) == 1 {
181 return msgAndArgs[0].(string)
182 }
183 if len(msgAndArgs) > 1 {
184 return fmt.Sprintf(msgAndArgs[0].(string), msgAndArgs[1:]...)
185 }
186 return ""
187 }
188
189 // Aligns the provided message so that all lines after the first line start at the same location as the first line.
190 // Assumes that the first line starts at the correct location (after carriage return, tab, label, spacer and tab).
191 // The longestLabelLen parameter specifies the length of the longest label in the output (required becaues this is the
192 // basis on which the alignment occurs).
193 func indentMessageLines(message string, longestLabelLen int) string {
194 outBuf := new(bytes.Buffer)
195
196 for i, scanner := 0, bufio.NewScanner(strings.NewReader(message)); scanner.Scan(); i++ {
197 // no need to align first line because it starts at the correct location (after the label)
198 if i != 0 {
199 // append alignLen+1 spaces to align with "{{longestLabel}}:" before adding tab
200 outBuf.WriteString("\n\t" + strings.Repeat(" ", longestLabelLen+1) + "\t")
201 }
202 outBuf.WriteString(scanner.Text())
203 }
204
205 return outBuf.String()
206 }
207
208 type failNower interface {
209 FailNow()
210 }
211
212 // FailNow fails test
213 func FailNow(t TestingT, failureMessage string, msgAndArgs ...interface{}) bool {
214 if h, ok := t.(tHelper); ok {
215 h.Helper()
216 }
217 Fail(t, failureMessage, msgAndArgs...)
218
219 // We cannot extend TestingT with FailNow() and
220 // maintain backwards compatibility, so we fallback
221 // to panicking when FailNow is not available in
222 // TestingT.
223 // See issue #263
224
225 if t, ok := t.(failNower); ok {
226 t.FailNow()
227 } else {
228 panic("test failed and t is missing `FailNow()`")
229 }
230 return false
231 }
232
233 // Fail reports a failure through
234 func Fail(t TestingT, failureMessage string, msgAndArgs ...interface{}) bool {
235 if h, ok := t.(tHelper); ok {
236 h.Helper()
237 }
238 content := []labeledContent{
239 {"Error Trace", strings.Join(CallerInfo(), "\n\t\t\t")},
240 {"Error", failureMessage},
241 }
242
243 // Add test name if the Go version supports it
244 if n, ok := t.(interface {
245 Name() string
246 }); ok {
247 content = append(content, labeledContent{"Test", n.Name()})
248 }
249
250 message := messageFromMsgAndArgs(msgAndArgs...)
251 if len(message) > 0 {
252 content = append(content, labeledContent{"Messages", message})
253 }
254
255 t.Errorf("\n%s", ""+labeledOutput(content...))
256
257 return false
258 }
259
260 type labeledContent struct {
261 label string
262 content string
263 }
264
265 // labeledOutput returns a string consisting of the provided labeledContent. Each labeled output is appended in the following manner:
266 //
267 // \t{{label}}:{{align_spaces}}\t{{content}}\n
268 //
269 // The initial carriage return is required to undo/erase any padding added by testing.T.Errorf. The "\t{{label}}:" is for the label.
270 // If a label is shorter than the longest label provided, padding spaces are added to make all the labels match in length. Once this
271 // alignment is achieved, "\t{{content}}\n" is added for the output.
272 //
273 // If the content of the labeledOutput contains line breaks, the subsequent lines are aligned so that they start at the same location as the first line.
274 func labeledOutput(content ...labeledContent) string {
275 longestLabel := 0
276 for _, v := range content {
277 if len(v.label) > longestLabel {
278 longestLabel = len(v.label)
279 }
280 }
281 var output string
282 for _, v := range content {
283 output += "\t" + v.label + ":" + strings.Repeat(" ", longestLabel-len(v.label)) + "\t" + indentMessageLines(v.content, longestLabel) + "\n"
284 }
285 return output
286 }
287
288 // Implements asserts that an object is implemented by the specified interface.
289 //
290 // assert.Implements(t, (*MyInterface)(nil), new(MyObject))
291 func Implements(t TestingT, interfaceObject interface{}, object interface{}, msgAndArgs ...interface{}) bool {
292 if h, ok := t.(tHelper); ok {
293 h.Helper()
294 }
295 interfaceType := reflect.TypeOf(interfaceObject).Elem()
296
297 if object == nil {
298 return Fail(t, fmt.Sprintf("Cannot check if nil implements %v", interfaceType), msgAndArgs...)
299 }
300 if !reflect.TypeOf(object).Implements(interfaceType) {
301 return Fail(t, fmt.Sprintf("%T must implement %v", object, interfaceType), msgAndArgs...)
302 }
303
304 return true
305 }
306
307 // IsType asserts that the specified objects are of the same type.
308 func IsType(t TestingT, expectedType interface{}, object interface{}, msgAndArgs ...interface{}) bool {
309 if h, ok := t.(tHelper); ok {
310 h.Helper()
311 }
312
313 if !ObjectsAreEqual(reflect.TypeOf(object), reflect.TypeOf(expectedType)) {
314 return Fail(t, fmt.Sprintf("Object expected to be of type %v, but was %v", reflect.TypeOf(expectedType), reflect.TypeOf(object)), msgAndArgs...)
315 }
316
317 return true
318 }
319
320 // Equal asserts that two objects are equal.
321 //
322 // assert.Equal(t, 123, 123)
323 //
324 // Pointer variable equality is determined based on the equality of the
325 // referenced values (as opposed to the memory addresses). Function equality
326 // cannot be determined and will always fail.
327 func Equal(t TestingT, expected, actual interface{}, msgAndArgs ...interface{}) bool {
328 if h, ok := t.(tHelper); ok {
329 h.Helper()
330 }
331 if err := validateEqualArgs(expected, actual); err != nil {
332 return Fail(t, fmt.Sprintf("Invalid operation: %#v == %#v (%s)",
333 expected, actual, err), msgAndArgs...)
334 }
335
336 if !ObjectsAreEqual(expected, actual) {
337 diff := diff(expected, actual)
338 expected, actual = formatUnequalValues(expected, actual)
339 return Fail(t, fmt.Sprintf("Not equal: \n"+
340 "expected: %s\n"+
341 "actual : %s%s", expected, actual, diff), msgAndArgs...)
342 }
343
344 return true
345
346 }
347
348 // formatUnequalValues takes two values of arbitrary types and returns string
349 // representations appropriate to be presented to the user.
350 //
351 // If the values are not of like type, the returned strings will be prefixed
352 // with the type name, and the value will be enclosed in parenthesis similar
353 // to a type conversion in the Go grammar.
354 func formatUnequalValues(expected, actual interface{}) (e string, a string) {
355 if reflect.TypeOf(expected) != reflect.TypeOf(actual) {
356 return fmt.Sprintf("%T(%#v)", expected, expected),
357 fmt.Sprintf("%T(%#v)", actual, actual)
358 }
359
360 return fmt.Sprintf("%#v", expected),
361 fmt.Sprintf("%#v", actual)
362 }
363
364 // EqualValues asserts that two objects are equal or convertable to the same types
365 // and equal.
366 //
367 // assert.EqualValues(t, uint32(123), int32(123))
368 func EqualValues(t TestingT, expected, actual interface{}, msgAndArgs ...interface{}) bool {
369 if h, ok := t.(tHelper); ok {
370 h.Helper()
371 }
372
373 if !ObjectsAreEqualValues(expected, actual) {
374 diff := diff(expected, actual)
375 expected, actual = formatUnequalValues(expected, actual)
376 return Fail(t, fmt.Sprintf("Not equal: \n"+
377 "expected: %s\n"+
378 "actual : %s%s", expected, actual, diff), msgAndArgs...)
379 }
380
381 return true
382
383 }
384
385 // Exactly asserts that two objects are equal in value and type.
386 //
387 // assert.Exactly(t, int32(123), int64(123))
388 func Exactly(t TestingT, expected, actual interface{}, msgAndArgs ...interface{}) bool {
389 if h, ok := t.(tHelper); ok {
390 h.Helper()
391 }
392
393 aType := reflect.TypeOf(expected)
394 bType := reflect.TypeOf(actual)
395
396 if aType != bType {
397 return Fail(t, fmt.Sprintf("Types expected to match exactly\n\t%v != %v", aType, bType), msgAndArgs...)
398 }
399
400 return Equal(t, expected, actual, msgAndArgs...)
401
402 }
403
404 // NotNil asserts that the specified object is not nil.
405 //
406 // assert.NotNil(t, err)
407 func NotNil(t TestingT, object interface{}, msgAndArgs ...interface{}) bool {
408 if h, ok := t.(tHelper); ok {
409 h.Helper()
410 }
411 if !isNil(object) {
412 return true
413 }
414 return Fail(t, "Expected value not to be nil.", msgAndArgs...)
415 }
416
417 // isNil checks if a specified object is nil or not, without Failing.
418 func isNil(object interface{}) bool {
419 if object == nil {
420 return true
421 }
422
423 value := reflect.ValueOf(object)
424 kind := value.Kind()
425 if kind >= reflect.Chan && kind <= reflect.Slice && value.IsNil() {
426 return true
427 }
428
429 return false
430 }
431
432 // Nil asserts that the specified object is nil.
433 //
434 // assert.Nil(t, err)
435 func Nil(t TestingT, object interface{}, msgAndArgs ...interface{}) bool {
436 if h, ok := t.(tHelper); ok {
437 h.Helper()
438 }
439 if isNil(object) {
440 return true
441 }
442 return Fail(t, fmt.Sprintf("Expected nil, but got: %#v", object), msgAndArgs...)
443 }
444
445 // isEmpty gets whether the specified object is considered empty or not.
446 func isEmpty(object interface{}) bool {
447
448 // get nil case out of the way
449 if object == nil {
450 return true
451 }
452
453 objValue := reflect.ValueOf(object)
454
455 switch objValue.Kind() {
456 // collection types are empty when they have no element
457 case reflect.Array, reflect.Chan, reflect.Map, reflect.Slice:
458 return objValue.Len() == 0
459 // pointers are empty if nil or if the value they point to is empty
460 case reflect.Ptr:
461 if objValue.IsNil() {
462 return true
463 }
464 deref := objValue.Elem().Interface()
465 return isEmpty(deref)
466 // for all other types, compare against the zero value
467 default:
468 zero := reflect.Zero(objValue.Type())
469 return reflect.DeepEqual(object, zero.Interface())
470 }
471 }
472
473 // Empty asserts that the specified object is empty. I.e. nil, "", false, 0 or either
474 // a slice or a channel with len == 0.
475 //
476 // assert.Empty(t, obj)
477 func Empty(t TestingT, object interface{}, msgAndArgs ...interface{}) bool {
478 if h, ok := t.(tHelper); ok {
479 h.Helper()
480 }
481
482 pass := isEmpty(object)
483 if !pass {
484 Fail(t, fmt.Sprintf("Should be empty, but was %v", object), msgAndArgs...)
485 }
486
487 return pass
488
489 }
490
491 // NotEmpty asserts that the specified object is NOT empty. I.e. not nil, "", false, 0 or either
492 // a slice or a channel with len == 0.
493 //
494 // if assert.NotEmpty(t, obj) {
495 // assert.Equal(t, "two", obj[1])
496 // }
497 func NotEmpty(t TestingT, object interface{}, msgAndArgs ...interface{}) bool {
498 if h, ok := t.(tHelper); ok {
499 h.Helper()
500 }
501
502 pass := !isEmpty(object)
503 if !pass {
504 Fail(t, fmt.Sprintf("Should NOT be empty, but was %v", object), msgAndArgs...)
505 }
506
507 return pass
508
509 }
510
511 // getLen try to get length of object.
512 // return (false, 0) if impossible.
513 func getLen(x interface{}) (ok bool, length int) {
514 v := reflect.ValueOf(x)
515 defer func() {
516 if e := recover(); e != nil {
517 ok = false
518 }
519 }()
520 return true, v.Len()
521 }
522
523 // Len asserts that the specified object has specific length.
524 // Len also fails if the object has a type that len() not accept.
525 //
526 // assert.Len(t, mySlice, 3)
527 func Len(t TestingT, object interface{}, length int, msgAndArgs ...interface{}) bool {
528 if h, ok := t.(tHelper); ok {
529 h.Helper()
530 }
531 ok, l := getLen(object)
532 if !ok {
533 return Fail(t, fmt.Sprintf("\"%s\" could not be applied builtin len()", object), msgAndArgs...)
534 }
535
536 if l != length {
537 return Fail(t, fmt.Sprintf("\"%s\" should have %d item(s), but has %d", object, length, l), msgAndArgs...)
538 }
539 return true
540 }
541
542 // True asserts that the specified value is true.
543 //
544 // assert.True(t, myBool)
545 func True(t TestingT, value bool, msgAndArgs ...interface{}) bool {
546 if h, ok := t.(tHelper); ok {
547 h.Helper()
548 }
549 if h, ok := t.(interface {
550 Helper()
551 }); ok {
552 h.Helper()
553 }
554
555 if value != true {
556 return Fail(t, "Should be true", msgAndArgs...)
557 }
558
559 return true
560
561 }
562
563 // False asserts that the specified value is false.
564 //
565 // assert.False(t, myBool)
566 func False(t TestingT, value bool, msgAndArgs ...interface{}) bool {
567 if h, ok := t.(tHelper); ok {
568 h.Helper()
569 }
570
571 if value != false {
572 return Fail(t, "Should be false", msgAndArgs...)
573 }
574
575 return true
576
577 }
578
579 // NotEqual asserts that the specified values are NOT equal.
580 //
581 // assert.NotEqual(t, obj1, obj2)
582 //
583 // Pointer variable equality is determined based on the equality of the
584 // referenced values (as opposed to the memory addresses).
585 func NotEqual(t TestingT, expected, actual interface{}, msgAndArgs ...interface{}) bool {
586 if h, ok := t.(tHelper); ok {
587 h.Helper()
588 }
589 if err := validateEqualArgs(expected, actual); err != nil {
590 return Fail(t, fmt.Sprintf("Invalid operation: %#v != %#v (%s)",
591 expected, actual, err), msgAndArgs...)
592 }
593
594 if ObjectsAreEqual(expected, actual) {
595 return Fail(t, fmt.Sprintf("Should not be: %#v\n", actual), msgAndArgs...)
596 }
597
598 return true
599
600 }
601
602 // containsElement try loop over the list check if the list includes the element.
603 // return (false, false) if impossible.
604 // return (true, false) if element was not found.
605 // return (true, true) if element was found.
606 func includeElement(list interface{}, element interface{}) (ok, found bool) {
607
608 listValue := reflect.ValueOf(list)
609 elementValue := reflect.ValueOf(element)
610 defer func() {
611 if e := recover(); e != nil {
612 ok = false
613 found = false
614 }
615 }()
616
617 if reflect.TypeOf(list).Kind() == reflect.String {
618 return true, strings.Contains(listValue.String(), elementValue.String())
619 }
620
621 if reflect.TypeOf(list).Kind() == reflect.Map {
622 mapKeys := listValue.MapKeys()
623 for i := 0; i < len(mapKeys); i++ {
624 if ObjectsAreEqual(mapKeys[i].Interface(), element) {
625 return true, true
626 }
627 }
628 return true, false
629 }
630
631 for i := 0; i < listValue.Len(); i++ {
632 if ObjectsAreEqual(listValue.Index(i).Interface(), element) {
633 return true, true
634 }
635 }
636 return true, false
637
638 }
639
640 // Contains asserts that the specified string, list(array, slice...) or map contains the
641 // specified substring or element.
642 //
643 // assert.Contains(t, "Hello World", "World")
644 // assert.Contains(t, ["Hello", "World"], "World")
645 // assert.Contains(t, {"Hello": "World"}, "Hello")
646 func Contains(t TestingT, s, contains interface{}, msgAndArgs ...interface{}) bool {
647 if h, ok := t.(tHelper); ok {
648 h.Helper()
649 }
650
651 ok, found := includeElement(s, contains)
652 if !ok {
653 return Fail(t, fmt.Sprintf("\"%s\" could not be applied builtin len()", s), msgAndArgs...)
654 }
655 if !found {
656 return Fail(t, fmt.Sprintf("\"%s\" does not contain \"%s\"", s, contains), msgAndArgs...)
657 }
658
659 return true
660
661 }
662
663 // NotContains asserts that the specified string, list(array, slice...) or map does NOT contain the
664 // specified substring or element.
665 //
666 // assert.NotContains(t, "Hello World", "Earth")
667 // assert.NotContains(t, ["Hello", "World"], "Earth")
668 // assert.NotContains(t, {"Hello": "World"}, "Earth")
669 func NotContains(t TestingT, s, contains interface{}, msgAndArgs ...interface{}) bool {
670 if h, ok := t.(tHelper); ok {
671 h.Helper()
672 }
673
674 ok, found := includeElement(s, contains)
675 if !ok {
676 return Fail(t, fmt.Sprintf("\"%s\" could not be applied builtin len()", s), msgAndArgs...)
677 }
678 if found {
679 return Fail(t, fmt.Sprintf("\"%s\" should not contain \"%s\"", s, contains), msgAndArgs...)
680 }
681
682 return true
683
684 }
685
686 // Subset asserts that the specified list(array, slice...) contains all
687 // elements given in the specified subset(array, slice...).
688 //
689 // assert.Subset(t, [1, 2, 3], [1, 2], "But [1, 2, 3] does contain [1, 2]")
690 func Subset(t TestingT, list, subset interface{}, msgAndArgs ...interface{}) (ok bool) {
691 if h, ok := t.(tHelper); ok {
692 h.Helper()
693 }
694 if subset == nil {
695 return true // we consider nil to be equal to the nil set
696 }
697
698 subsetValue := reflect.ValueOf(subset)
699 defer func() {
700 if e := recover(); e != nil {
701 ok = false
702 }
703 }()
704
705 listKind := reflect.TypeOf(list).Kind()
706 subsetKind := reflect.TypeOf(subset).Kind()
707
708 if listKind != reflect.Array && listKind != reflect.Slice {
709 return Fail(t, fmt.Sprintf("%q has an unsupported type %s", list, listKind), msgAndArgs...)
710 }
711
712 if subsetKind != reflect.Array && subsetKind != reflect.Slice {
713 return Fail(t, fmt.Sprintf("%q has an unsupported type %s", subset, subsetKind), msgAndArgs...)
714 }
715
716 for i := 0; i < subsetValue.Len(); i++ {
717 element := subsetValue.Index(i).Interface()
718 ok, found := includeElement(list, element)
719 if !ok {
720 return Fail(t, fmt.Sprintf("\"%s\" could not be applied builtin len()", list), msgAndArgs...)
721 }
722 if !found {
723 return Fail(t, fmt.Sprintf("\"%s\" does not contain \"%s\"", list, element), msgAndArgs...)
724 }
725 }
726
727 return true
728 }
729
730 // NotSubset asserts that the specified list(array, slice...) contains not all
731 // elements given in the specified subset(array, slice...).
732 //
733 // assert.NotSubset(t, [1, 3, 4], [1, 2], "But [1, 3, 4] does not contain [1, 2]")
734 func NotSubset(t TestingT, list, subset interface{}, msgAndArgs ...interface{}) (ok bool) {
735 if h, ok := t.(tHelper); ok {
736 h.Helper()
737 }
738 if subset == nil {
739 return Fail(t, fmt.Sprintf("nil is the empty set which is a subset of every set"), msgAndArgs...)
740 }
741
742 subsetValue := reflect.ValueOf(subset)
743 defer func() {
744 if e := recover(); e != nil {
745 ok = false
746 }
747 }()
748
749 listKind := reflect.TypeOf(list).Kind()
750 subsetKind := reflect.TypeOf(subset).Kind()
751
752 if listKind != reflect.Array && listKind != reflect.Slice {
753 return Fail(t, fmt.Sprintf("%q has an unsupported type %s", list, listKind), msgAndArgs...)
754 }
755
756 if subsetKind != reflect.Array && subsetKind != reflect.Slice {
757 return Fail(t, fmt.Sprintf("%q has an unsupported type %s", subset, subsetKind), msgAndArgs...)
758 }
759
760 for i := 0; i < subsetValue.Len(); i++ {
761 element := subsetValue.Index(i).Interface()
762 ok, found := includeElement(list, element)
763 if !ok {
764 return Fail(t, fmt.Sprintf("\"%s\" could not be applied builtin len()", list), msgAndArgs...)
765 }
766 if !found {
767 return true
768 }
769 }
770
771 return Fail(t, fmt.Sprintf("%q is a subset of %q", subset, list), msgAndArgs...)
772 }
773
774 // ElementsMatch asserts that the specified listA(array, slice...) is equal to specified
775 // listB(array, slice...) ignoring the order of the elements. If there are duplicate elements,
776 // the number of appearances of each of them in both lists should match.
777 //
778 // assert.ElementsMatch(t, [1, 3, 2, 3], [1, 3, 3, 2])
779 func ElementsMatch(t TestingT, listA, listB interface{}, msgAndArgs ...interface{}) (ok bool) {
780 if h, ok := t.(tHelper); ok {
781 h.Helper()
782 }
783 if isEmpty(listA) && isEmpty(listB) {
784 return true
785 }
786
787 aKind := reflect.TypeOf(listA).Kind()
788 bKind := reflect.TypeOf(listB).Kind()
789
790 if aKind != reflect.Array && aKind != reflect.Slice {
791 return Fail(t, fmt.Sprintf("%q has an unsupported type %s", listA, aKind), msgAndArgs...)
792 }
793
794 if bKind != reflect.Array && bKind != reflect.Slice {
795 return Fail(t, fmt.Sprintf("%q has an unsupported type %s", listB, bKind), msgAndArgs...)
796 }
797
798 aValue := reflect.ValueOf(listA)
799 bValue := reflect.ValueOf(listB)
800
801 aLen := aValue.Len()
802 bLen := bValue.Len()
803
804 if aLen != bLen {
805 return Fail(t, fmt.Sprintf("lengths don't match: %d != %d", aLen, bLen), msgAndArgs...)
806 }
807
808 // Mark indexes in bValue that we already used
809 visited := make([]bool, bLen)
810 for i := 0; i < aLen; i++ {
811 element := aValue.Index(i).Interface()
812 found := false
813 for j := 0; j < bLen; j++ {
814 if visited[j] {
815 continue
816 }
817 if ObjectsAreEqual(bValue.Index(j).Interface(), element) {
818 visited[j] = true
819 found = true
820 break
821 }
822 }
823 if !found {
824 return Fail(t, fmt.Sprintf("element %s appears more times in %s than in %s", element, aValue, bValue), msgAndArgs...)
825 }
826 }
827
828 return true
829 }
830
831 // Condition uses a Comparison to assert a complex condition.
832 func Condition(t TestingT, comp Comparison, msgAndArgs ...interface{}) bool {
833 if h, ok := t.(tHelper); ok {
834 h.Helper()
835 }
836 result := comp()
837 if !result {
838 Fail(t, "Condition failed!", msgAndArgs...)
839 }
840 return result
841 }
842
843 // PanicTestFunc defines a func that should be passed to the assert.Panics and assert.NotPanics
844 // methods, and represents a simple func that takes no arguments, and returns nothing.
845 type PanicTestFunc func()
846
847 // didPanic returns true if the function passed to it panics. Otherwise, it returns false.
848 func didPanic(f PanicTestFunc) (bool, interface{}) {
849
850 didPanic := false
851 var message interface{}
852 func() {
853
854 defer func() {
855 if message = recover(); message != nil {
856 didPanic = true
857 }
858 }()
859
860 // call the target function
861 f()
862
863 }()
864
865 return didPanic, message
866
867 }
868
869 // Panics asserts that the code inside the specified PanicTestFunc panics.
870 //
871 // assert.Panics(t, func(){ GoCrazy() })
872 func Panics(t TestingT, f PanicTestFunc, msgAndArgs ...interface{}) bool {
873 if h, ok := t.(tHelper); ok {
874 h.Helper()
875 }
876
877 if funcDidPanic, panicValue := didPanic(f); !funcDidPanic {
878 return Fail(t, fmt.Sprintf("func %#v should panic\n\tPanic value:\t%#v", f, panicValue), msgAndArgs...)
879 }
880
881 return true
882 }
883
884 // PanicsWithValue asserts that the code inside the specified PanicTestFunc panics, and that
885 // the recovered panic value equals the expected panic value.
886 //
887 // assert.PanicsWithValue(t, "crazy error", func(){ GoCrazy() })
888 func PanicsWithValue(t TestingT, expected interface{}, f PanicTestFunc, msgAndArgs ...interface{}) bool {
889 if h, ok := t.(tHelper); ok {
890 h.Helper()
891 }
892
893 funcDidPanic, panicValue := didPanic(f)
894 if !funcDidPanic {
895 return Fail(t, fmt.Sprintf("func %#v should panic\n\tPanic value:\t%#v", f, panicValue), msgAndArgs...)
896 }
897 if panicValue != expected {
898 return Fail(t, fmt.Sprintf("func %#v should panic with value:\t%#v\n\tPanic value:\t%#v", f, expected, panicValue), msgAndArgs...)
899 }
900
901 return true
902 }
903
904 // NotPanics asserts that the code inside the specified PanicTestFunc does NOT panic.
905 //
906 // assert.NotPanics(t, func(){ RemainCalm() })
907 func NotPanics(t TestingT, f PanicTestFunc, msgAndArgs ...interface{}) bool {
908 if h, ok := t.(tHelper); ok {
909 h.Helper()
910 }
911
912 if funcDidPanic, panicValue := didPanic(f); funcDidPanic {
913 return Fail(t, fmt.Sprintf("func %#v should not panic\n\tPanic value:\t%v", f, panicValue), msgAndArgs...)
914 }
915
916 return true
917 }
918
919 // WithinDuration asserts that the two times are within duration delta of each other.
920 //
921 // assert.WithinDuration(t, time.Now(), time.Now(), 10*time.Second)
922 func WithinDuration(t TestingT, expected, actual time.Time, delta time.Duration, msgAndArgs ...interface{}) bool {
923 if h, ok := t.(tHelper); ok {
924 h.Helper()
925 }
926
927 dt := expected.Sub(actual)
928 if dt < -delta || dt > delta {
929 return Fail(t, fmt.Sprintf("Max difference between %v and %v allowed is %v, but difference was %v", expected, actual, delta, dt), msgAndArgs...)
930 }
931
932 return true
933 }
934
935 func toFloat(x interface{}) (float64, bool) {
936 var xf float64
937 xok := true
938
939 switch xn := x.(type) {
940 case uint8:
941 xf = float64(xn)
942 case uint16:
943 xf = float64(xn)
944 case uint32:
945 xf = float64(xn)
946 case uint64:
947 xf = float64(xn)
948 case int:
949 xf = float64(xn)
950 case int8:
951 xf = float64(xn)
952 case int16:
953 xf = float64(xn)
954 case int32:
955 xf = float64(xn)
956 case int64:
957 xf = float64(xn)
958 case float32:
959 xf = float64(xn)
960 case float64:
961 xf = float64(xn)
962 case time.Duration:
963 xf = float64(xn)
964 default:
965 xok = false
966 }
967
968 return xf, xok
969 }
970
971 // InDelta asserts that the two numerals are within delta of each other.
972 //
973 // assert.InDelta(t, math.Pi, (22 / 7.0), 0.01)
974 func InDelta(t TestingT, expected, actual interface{}, delta float64, msgAndArgs ...interface{}) bool {
975 if h, ok := t.(tHelper); ok {
976 h.Helper()
977 }
978
979 af, aok := toFloat(expected)
980 bf, bok := toFloat(actual)
981
982 if !aok || !bok {
983 return Fail(t, fmt.Sprintf("Parameters must be numerical"), msgAndArgs...)
984 }
985
986 if math.IsNaN(af) {
987 return Fail(t, fmt.Sprintf("Expected must not be NaN"), msgAndArgs...)
988 }
989
990 if math.IsNaN(bf) {
991 return Fail(t, fmt.Sprintf("Expected %v with delta %v, but was NaN", expected, delta), msgAndArgs...)
992 }
993
994 dt := af - bf
995 if dt < -delta || dt > delta {
996 return Fail(t, fmt.Sprintf("Max difference between %v and %v allowed is %v, but difference was %v", expected, actual, delta, dt), msgAndArgs...)
997 }
998
999 return true
1000 }
1001
1002 // InDeltaSlice is the same as InDelta, except it compares two slices.
1003 func InDeltaSlice(t TestingT, expected, actual interface{}, delta float64, msgAndArgs ...interface{}) bool {
1004 if h, ok := t.(tHelper); ok {
1005 h.Helper()
1006 }
1007 if expected == nil || actual == nil ||
1008 reflect.TypeOf(actual).Kind() != reflect.Slice ||
1009 reflect.TypeOf(expected).Kind() != reflect.Slice {
1010 return Fail(t, fmt.Sprintf("Parameters must be slice"), msgAndArgs...)
1011 }
1012
1013 actualSlice := reflect.ValueOf(actual)
1014 expectedSlice := reflect.ValueOf(expected)
1015
1016 for i := 0; i < actualSlice.Len(); i++ {
1017 result := InDelta(t, actualSlice.Index(i).Interface(), expectedSlice.Index(i).Interface(), delta, msgAndArgs...)
1018 if !result {
1019 return result
1020 }
1021 }
1022
1023 return true
1024 }
1025
1026 // InDeltaMapValues is the same as InDelta, but it compares all values between two maps. Both maps must have exactly the same keys.
1027 func InDeltaMapValues(t TestingT, expected, actual interface{}, delta float64, msgAndArgs ...interface{}) bool {
1028 if h, ok := t.(tHelper); ok {
1029 h.Helper()
1030 }
1031 if expected == nil || actual == nil ||
1032 reflect.TypeOf(actual).Kind() != reflect.Map ||
1033 reflect.TypeOf(expected).Kind() != reflect.Map {
1034 return Fail(t, "Arguments must be maps", msgAndArgs...)
1035 }
1036
1037 expectedMap := reflect.ValueOf(expected)
1038 actualMap := reflect.ValueOf(actual)
1039
1040 if expectedMap.Len() != actualMap.Len() {
1041 return Fail(t, "Arguments must have the same number of keys", msgAndArgs...)
1042 }
1043
1044 for _, k := range expectedMap.MapKeys() {
1045 ev := expectedMap.MapIndex(k)
1046 av := actualMap.MapIndex(k)
1047
1048 if !ev.IsValid() {
1049 return Fail(t, fmt.Sprintf("missing key %q in expected map", k), msgAndArgs...)
1050 }
1051
1052 if !av.IsValid() {
1053 return Fail(t, fmt.Sprintf("missing key %q in actual map", k), msgAndArgs...)
1054 }
1055
1056 if !InDelta(
1057 t,
1058 ev.Interface(),
1059 av.Interface(),
1060 delta,
1061 msgAndArgs...,
1062 ) {
1063 return false
1064 }
1065 }
1066
1067 return true
1068 }
1069
1070 func calcRelativeError(expected, actual interface{}) (float64, error) {
1071 af, aok := toFloat(expected)
1072 if !aok {
1073 return 0, fmt.Errorf("expected value %q cannot be converted to float", expected)
1074 }
1075 if af == 0 {
1076 return 0, fmt.Errorf("expected value must have a value other than zero to calculate the relative error")
1077 }
1078 bf, bok := toFloat(actual)
1079 if !bok {
1080 return 0, fmt.Errorf("actual value %q cannot be converted to float", actual)
1081 }
1082
1083 return math.Abs(af-bf) / math.Abs(af), nil
1084 }
1085
1086 // InEpsilon asserts that expected and actual have a relative error less than epsilon
1087 func InEpsilon(t TestingT, expected, actual interface{}, epsilon float64, msgAndArgs ...interface{}) bool {
1088 if h, ok := t.(tHelper); ok {
1089 h.Helper()
1090 }
1091 actualEpsilon, err := calcRelativeError(expected, actual)
1092 if err != nil {
1093 return Fail(t, err.Error(), msgAndArgs...)
1094 }
1095 if actualEpsilon > epsilon {
1096 return Fail(t, fmt.Sprintf("Relative error is too high: %#v (expected)\n"+
1097 " < %#v (actual)", epsilon, actualEpsilon), msgAndArgs...)
1098 }
1099
1100 return true
1101 }
1102
1103 // InEpsilonSlice is the same as InEpsilon, except it compares each value from two slices.
1104 func InEpsilonSlice(t TestingT, expected, actual interface{}, epsilon float64, msgAndArgs ...interface{}) bool {
1105 if h, ok := t.(tHelper); ok {
1106 h.Helper()
1107 }
1108 if expected == nil || actual == nil ||
1109 reflect.TypeOf(actual).Kind() != reflect.Slice ||
1110 reflect.TypeOf(expected).Kind() != reflect.Slice {
1111 return Fail(t, fmt.Sprintf("Parameters must be slice"), msgAndArgs...)
1112 }
1113
1114 actualSlice := reflect.ValueOf(actual)
1115 expectedSlice := reflect.ValueOf(expected)
1116
1117 for i := 0; i < actualSlice.Len(); i++ {
1118 result := InEpsilon(t, actualSlice.Index(i).Interface(), expectedSlice.Index(i).Interface(), epsilon)
1119 if !result {
1120 return result
1121 }
1122 }
1123
1124 return true
1125 }
1126
1127 /*
1128 Errors
1129 */
1130
1131 // NoError asserts that a function returned no error (i.e. `nil`).
1132 //
1133 // actualObj, err := SomeFunction()
1134 // if assert.NoError(t, err) {
1135 // assert.Equal(t, expectedObj, actualObj)
1136 // }
1137 func NoError(t TestingT, err error, msgAndArgs ...interface{}) bool {
1138 if h, ok := t.(tHelper); ok {
1139 h.Helper()
1140 }
1141 if err != nil {
1142 return Fail(t, fmt.Sprintf("Received unexpected error:\n%+v", err), msgAndArgs...)
1143 }
1144
1145 return true
1146 }
1147
1148 // Error asserts that a function returned an error (i.e. not `nil`).
1149 //
1150 // actualObj, err := SomeFunction()
1151 // if assert.Error(t, err) {
1152 // assert.Equal(t, expectedError, err)
1153 // }
1154 func Error(t TestingT, err error, msgAndArgs ...interface{}) bool {
1155 if h, ok := t.(tHelper); ok {
1156 h.Helper()
1157 }
1158
1159 if err == nil {
1160 return Fail(t, "An error is expected but got nil.", msgAndArgs...)
1161 }
1162
1163 return true
1164 }
1165
1166 // EqualError asserts that a function returned an error (i.e. not `nil`)
1167 // and that it is equal to the provided error.
1168 //
1169 // actualObj, err := SomeFunction()
1170 // assert.EqualError(t, err, expectedErrorString)
1171 func EqualError(t TestingT, theError error, errString string, msgAndArgs ...interface{}) bool {
1172 if h, ok := t.(tHelper); ok {
1173 h.Helper()
1174 }
1175 if !Error(t, theError, msgAndArgs...) {
1176 return false
1177 }
1178 expected := errString
1179 actual := theError.Error()
1180 // don't need to use deep equals here, we know they are both strings
1181 if expected != actual {
1182 return Fail(t, fmt.Sprintf("Error message not equal:\n"+
1183 "expected: %q\n"+
1184 "actual : %q", expected, actual), msgAndArgs...)
1185 }
1186 return true
1187 }
1188
1189 // matchRegexp return true if a specified regexp matches a string.
1190 func matchRegexp(rx interface{}, str interface{}) bool {
1191
1192 var r *regexp.Regexp
1193 if rr, ok := rx.(*regexp.Regexp); ok {
1194 r = rr
1195 } else {
1196 r = regexp.MustCompile(fmt.Sprint(rx))
1197 }
1198
1199 return (r.FindStringIndex(fmt.Sprint(str)) != nil)
1200
1201 }
1202
1203 // Regexp asserts that a specified regexp matches a string.
1204 //
1205 // assert.Regexp(t, regexp.MustCompile("start"), "it's starting")
1206 // assert.Regexp(t, "start...$", "it's not starting")
1207 func Regexp(t TestingT, rx interface{}, str interface{}, msgAndArgs ...interface{}) bool {
1208 if h, ok := t.(tHelper); ok {
1209 h.Helper()
1210 }
1211
1212 match := matchRegexp(rx, str)
1213
1214 if !match {
1215 Fail(t, fmt.Sprintf("Expect \"%v\" to match \"%v\"", str, rx), msgAndArgs...)
1216 }
1217
1218 return match
1219 }
1220
1221 // NotRegexp asserts that a specified regexp does not match a string.
1222 //
1223 // assert.NotRegexp(t, regexp.MustCompile("starts"), "it's starting")
1224 // assert.NotRegexp(t, "^start", "it's not starting")
1225 func NotRegexp(t TestingT, rx interface{}, str interface{}, msgAndArgs ...interface{}) bool {
1226 if h, ok := t.(tHelper); ok {
1227 h.Helper()
1228 }
1229 match := matchRegexp(rx, str)
1230
1231 if match {
1232 Fail(t, fmt.Sprintf("Expect \"%v\" to NOT match \"%v\"", str, rx), msgAndArgs...)
1233 }
1234
1235 return !match
1236
1237 }
1238
1239 // Zero asserts that i is the zero value for its type.
1240 func Zero(t TestingT, i interface{}, msgAndArgs ...interface{}) bool {
1241 if h, ok := t.(tHelper); ok {
1242 h.Helper()
1243 }
1244 if i != nil && !reflect.DeepEqual(i, reflect.Zero(reflect.TypeOf(i)).Interface()) {
1245 return Fail(t, fmt.Sprintf("Should be zero, but was %v", i), msgAndArgs...)
1246 }
1247 return true
1248 }
1249
1250 // NotZero asserts that i is not the zero value for its type.
1251 func NotZero(t TestingT, i interface{}, msgAndArgs ...interface{}) bool {
1252 if h, ok := t.(tHelper); ok {
1253 h.Helper()
1254 }
1255 if i == nil || reflect.DeepEqual(i, reflect.Zero(reflect.TypeOf(i)).Interface()) {
1256 return Fail(t, fmt.Sprintf("Should not be zero, but was %v", i), msgAndArgs...)
1257 }
1258 return true
1259 }
1260
1261 // FileExists checks whether a file exists in the given path. It also fails if the path points to a directory or there is an error when trying to check the file.
1262 func FileExists(t TestingT, path string, msgAndArgs ...interface{}) bool {
1263 if h, ok := t.(tHelper); ok {
1264 h.Helper()
1265 }
1266 info, err := os.Lstat(path)
1267 if err != nil {
1268 if os.IsNotExist(err) {
1269 return Fail(t, fmt.Sprintf("unable to find file %q", path), msgAndArgs...)
1270 }
1271 return Fail(t, fmt.Sprintf("error when running os.Lstat(%q): %s", path, err), msgAndArgs...)
1272 }
1273 if info.IsDir() {
1274 return Fail(t, fmt.Sprintf("%q is a directory", path), msgAndArgs...)
1275 }
1276 return true
1277 }
1278
1279 // DirExists checks whether a directory exists in the given path. It also fails if the path is a file rather a directory or there is an error checking whether it exists.
1280 func DirExists(t TestingT, path string, msgAndArgs ...interface{}) bool {
1281 if h, ok := t.(tHelper); ok {
1282 h.Helper()
1283 }
1284 info, err := os.Lstat(path)
1285 if err != nil {
1286 if os.IsNotExist(err) {
1287 return Fail(t, fmt.Sprintf("unable to find file %q", path), msgAndArgs...)
1288 }
1289 return Fail(t, fmt.Sprintf("error when running os.Lstat(%q): %s", path, err), msgAndArgs...)
1290 }
1291 if !info.IsDir() {
1292 return Fail(t, fmt.Sprintf("%q is a file", path), msgAndArgs...)
1293 }
1294 return true
1295 }
1296
1297 // JSONEq asserts that two JSON strings are equivalent.
1298 //
1299 // assert.JSONEq(t, `{"hello": "world", "foo": "bar"}`, `{"foo": "bar", "hello": "world"}`)
1300 func JSONEq(t TestingT, expected string, actual string, msgAndArgs ...interface{}) bool {
1301 if h, ok := t.(tHelper); ok {
1302 h.Helper()
1303 }
1304 var expectedJSONAsInterface, actualJSONAsInterface interface{}
1305
1306 if err := json.Unmarshal([]byte(expected), &expectedJSONAsInterface); err != nil {
1307 return Fail(t, fmt.Sprintf("Expected value ('%s') is not valid json.\nJSON parsing error: '%s'", expected, err.Error()), msgAndArgs...)
1308 }
1309
1310 if err := json.Unmarshal([]byte(actual), &actualJSONAsInterface); err != nil {
1311 return Fail(t, fmt.Sprintf("Input ('%s') needs to be valid json.\nJSON parsing error: '%s'", actual, err.Error()), msgAndArgs...)
1312 }
1313
1314 return Equal(t, expectedJSONAsInterface, actualJSONAsInterface, msgAndArgs...)
1315 }
1316
1317 func typeAndKind(v interface{}) (reflect.Type, reflect.Kind) {
1318 t := reflect.TypeOf(v)
1319 k := t.Kind()
1320
1321 if k == reflect.Ptr {
1322 t = t.Elem()
1323 k = t.Kind()
1324 }
1325 return t, k
1326 }
1327
1328 // diff returns a diff of both values as long as both are of the same type and
1329 // are a struct, map, slice or array. Otherwise it returns an empty string.
1330 func diff(expected interface{}, actual interface{}) string {
1331 if expected == nil || actual == nil {
1332 return ""
1333 }
1334
1335 et, ek := typeAndKind(expected)
1336 at, _ := typeAndKind(actual)
1337
1338 if et != at {
1339 return ""
1340 }
1341
1342 if ek != reflect.Struct && ek != reflect.Map && ek != reflect.Slice && ek != reflect.Array && ek != reflect.String {
1343 return ""
1344 }
1345
1346 var e, a string
1347 if ek != reflect.String {
1348 e = spewConfig.Sdump(expected)
1349 a = spewConfig.Sdump(actual)
1350 } else {
1351 e = expected.(string)
1352 a = actual.(string)
1353 }
1354
1355 diff, _ := difflib.GetUnifiedDiffString(difflib.UnifiedDiff{
1356 A: difflib.SplitLines(e),
1357 B: difflib.SplitLines(a),
1358 FromFile: "Expected",
1359 FromDate: "",
1360 ToFile: "Actual",
1361 ToDate: "",
1362 Context: 1,
1363 })
1364
1365 return "\n\nDiff:\n" + diff
1366 }
1367
1368 // validateEqualArgs checks whether provided arguments can be safely used in the
1369 // Equal/NotEqual functions.
1370 func validateEqualArgs(expected, actual interface{}) error {
1371 if isFunction(expected) || isFunction(actual) {
1372 return errors.New("cannot take func type as argument")
1373 }
1374 return nil
1375 }
1376
1377 func isFunction(arg interface{}) bool {
1378 if arg == nil {
1379 return false
1380 }
1381 return reflect.TypeOf(arg).Kind() == reflect.Func
1382 }
1383
1384 var spewConfig = spew.ConfigState{
1385 Indent: " ",
1386 DisablePointerAddresses: true,
1387 DisableCapacities: true,
1388 SortKeys: true,
1389 }
1390
1391 type tHelper interface {
1392 Helper()
1393 }
0 // Package assert provides a set of comprehensive testing tools for use with the normal Go testing system.
1 //
2 // Example Usage
3 //
4 // The following is a complete example using assert in a standard test function:
5 // import (
6 // "testing"
7 // "github.com/stretchr/testify/assert"
8 // )
9 //
10 // func TestSomething(t *testing.T) {
11 //
12 // var a string = "Hello"
13 // var b string = "Hello"
14 //
15 // assert.Equal(t, a, b, "The two words should be the same.")
16 //
17 // }
18 //
19 // if you assert many times, use the format below:
20 //
21 // import (
22 // "testing"
23 // "github.com/stretchr/testify/assert"
24 // )
25 //
26 // func TestSomething(t *testing.T) {
27 // assert := assert.New(t)
28 //
29 // var a string = "Hello"
30 // var b string = "Hello"
31 //
32 // assert.Equal(a, b, "The two words should be the same.")
33 // }
34 //
35 // Assertions
36 //
37 // Assertions allow you to easily write test code, and are global funcs in the `assert` package.
38 // All assertion functions take, as the first argument, the `*testing.T` object provided by the
39 // testing framework. This allows the assertion funcs to write the failings and other details to
40 // the correct place.
41 //
42 // Every assertion function also takes an optional string message as the final argument,
43 // allowing custom error messages to be appended to the message the assertion method outputs.
44 package assert
0 package assert
1
2 import (
3 "errors"
4 )
5
6 // AnError is an error instance useful for testing. If the code does not care
7 // about error specifics, and only needs to return the error for example, this
8 // error should be used to make the test code more readable.
9 var AnError = errors.New("assert.AnError general error for testing")
0 package assert
1
2 // Assertions provides assertion methods around the
3 // TestingT interface.
4 type Assertions struct {
5 t TestingT
6 }
7
8 // New makes a new Assertions object for the specified TestingT.
9 func New(t TestingT) *Assertions {
10 return &Assertions{
11 t: t,
12 }
13 }
14
15 //go:generate go run ../_codegen/main.go -output-package=assert -template=assertion_forward.go.tmpl -include-format-funcs
0 package assert
1
2 import (
3 "fmt"
4 "net/http"
5 "net/http/httptest"
6 "net/url"
7 "strings"
8 )
9
10 // httpCode is a helper that returns HTTP code of the response. It returns -1 and
11 // an error if building a new request fails.
12 func httpCode(handler http.HandlerFunc, method, url string, values url.Values) (int, error) {
13 w := httptest.NewRecorder()
14 req, err := http.NewRequest(method, url, nil)
15 if err != nil {
16 return -1, err
17 }
18 req.URL.RawQuery = values.Encode()
19 handler(w, req)
20 return w.Code, nil
21 }
22
23 // HTTPSuccess asserts that a specified handler returns a success status code.
24 //
25 // assert.HTTPSuccess(t, myHandler, "POST", "http://www.google.com", nil)
26 //
27 // Returns whether the assertion was successful (true) or not (false).
28 func HTTPSuccess(t TestingT, handler http.HandlerFunc, method, url string, values url.Values, msgAndArgs ...interface{}) bool {
29 if h, ok := t.(tHelper); ok {
30 h.Helper()
31 }
32 code, err := httpCode(handler, method, url, values)
33 if err != nil {
34 Fail(t, fmt.Sprintf("Failed to build test request, got error: %s", err))
35 return false
36 }
37
38 isSuccessCode := code >= http.StatusOK && code <= http.StatusPartialContent
39 if !isSuccessCode {
40 Fail(t, fmt.Sprintf("Expected HTTP success status code for %q but received %d", url+"?"+values.Encode(), code))
41 }
42
43 return isSuccessCode
44 }
45
46 // HTTPRedirect asserts that a specified handler returns a redirect status code.
47 //
48 // assert.HTTPRedirect(t, myHandler, "GET", "/a/b/c", url.Values{"a": []string{"b", "c"}}
49 //
50 // Returns whether the assertion was successful (true) or not (false).
51 func HTTPRedirect(t TestingT, handler http.HandlerFunc, method, url string, values url.Values, msgAndArgs ...interface{}) bool {
52 if h, ok := t.(tHelper); ok {
53 h.Helper()
54 }
55 code, err := httpCode(handler, method, url, values)
56 if err != nil {
57 Fail(t, fmt.Sprintf("Failed to build test request, got error: %s", err))
58 return false
59 }
60
61 isRedirectCode := code >= http.StatusMultipleChoices && code <= http.StatusTemporaryRedirect
62 if !isRedirectCode {
63 Fail(t, fmt.Sprintf("Expected HTTP redirect status code for %q but received %d", url+"?"+values.Encode(), code))
64 }
65
66 return isRedirectCode
67 }
68
69 // HTTPError asserts that a specified handler returns an error status code.
70 //
71 // assert.HTTPError(t, myHandler, "POST", "/a/b/c", url.Values{"a": []string{"b", "c"}}
72 //
73 // Returns whether the assertion was successful (true) or not (false).
74 func HTTPError(t TestingT, handler http.HandlerFunc, method, url string, values url.Values, msgAndArgs ...interface{}) bool {
75 if h, ok := t.(tHelper); ok {
76 h.Helper()
77 }
78 code, err := httpCode(handler, method, url, values)
79 if err != nil {
80 Fail(t, fmt.Sprintf("Failed to build test request, got error: %s", err))
81 return false
82 }
83
84 isErrorCode := code >= http.StatusBadRequest
85 if !isErrorCode {
86 Fail(t, fmt.Sprintf("Expected HTTP error status code for %q but received %d", url+"?"+values.Encode(), code))
87 }
88
89 return isErrorCode
90 }
91
92 // HTTPBody is a helper that returns HTTP body of the response. It returns
93 // empty string if building a new request fails.
94 func HTTPBody(handler http.HandlerFunc, method, url string, values url.Values) string {
95 w := httptest.NewRecorder()
96 req, err := http.NewRequest(method, url+"?"+values.Encode(), nil)
97 if err != nil {
98 return ""
99 }
100 handler(w, req)
101 return w.Body.String()
102 }
103
104 // HTTPBodyContains asserts that a specified handler returns a
105 // body that contains a string.
106 //
107 // assert.HTTPBodyContains(t, myHandler, "GET", "www.google.com", nil, "I'm Feeling Lucky")
108 //
109 // Returns whether the assertion was successful (true) or not (false).
110 func HTTPBodyContains(t TestingT, handler http.HandlerFunc, method, url string, values url.Values, str interface{}, msgAndArgs ...interface{}) bool {
111 if h, ok := t.(tHelper); ok {
112 h.Helper()
113 }
114 body := HTTPBody(handler, method, url, values)
115
116 contains := strings.Contains(body, fmt.Sprint(str))
117 if !contains {
118 Fail(t, fmt.Sprintf("Expected response body for \"%s\" to contain \"%s\" but found \"%s\"", url+"?"+values.Encode(), str, body))
119 }
120
121 return contains
122 }
123
124 // HTTPBodyNotContains asserts that a specified handler returns a
125 // body that does not contain a string.
126 //
127 // assert.HTTPBodyNotContains(t, myHandler, "GET", "www.google.com", nil, "I'm Feeling Lucky")
128 //
129 // Returns whether the assertion was successful (true) or not (false).
130 func HTTPBodyNotContains(t TestingT, handler http.HandlerFunc, method, url string, values url.Values, str interface{}, msgAndArgs ...interface{}) bool {
131 if h, ok := t.(tHelper); ok {
132 h.Helper()
133 }
134 body := HTTPBody(handler, method, url, values)
135
136 contains := strings.Contains(body, fmt.Sprint(str))
137 if contains {
138 Fail(t, fmt.Sprintf("Expected response body for \"%s\" to NOT contain \"%s\" but found \"%s\"", url+"?"+values.Encode(), str, body))
139 }
140
141 return !contains
142 }
0 # github.com/davecgh/go-spew v1.1.1
1 github.com/davecgh/go-spew/spew
2 # github.com/gorilla/websocket v1.2.0
3 github.com/gorilla/websocket
4 # github.com/pkg/errors v0.8.0
5 github.com/pkg/errors
6 # github.com/pmezard/go-difflib v1.0.0
7 github.com/pmezard/go-difflib/difflib
8 # github.com/stretchr/testify v1.2.2
9 github.com/stretchr/testify/assert
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 }