Codebase list golang-github-nlopes-slack / run/33fb859d-7349-4038-abd4-9c43e3922006/main
New upstream release. Debian Janitor 4 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)