New upstream release.
Debian Janitor
4 months ago
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. |
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 | } |
0 | 0 | language: go |
1 | 1 | |
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 | |
10 | 6 | |
11 | 7 | before_install: |
12 | 8 | - export PATH=$HOME/gopath/bin:$PATH |
9 | # install gometalinter | |
10 | - curl -L https://git.io/vp6lP | sh | |
13 | 11 | |
14 | 12 | script: |
15 | - go test -race ./... | |
16 | - go test -cover ./... | |
13 | - PATH=$PWD/bin:$PATH gometalinter ./... | |
14 | - go test -race -cover ./... | |
17 | 15 | |
18 | 16 | 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 ./... | |
21 | 32 | |
22 | 33 | git: |
23 | 34 | 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 | ||
0 | 47 | ### v0.1.0 - May 28, 2017 |
1 | 48 | |
2 | 49 | This is released before adding context support. |
0 | 0 | Slack API in Go [](https://godoc.org/github.com/nlopes/slack) [](https://travis-ci.org/nlopes/slack) |
1 | 1 | =============== |
2 | ||
3 | [](https://gitter.im/go-slack/Lobby?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge) | |
2 | 4 | |
3 | 5 | This library supports most if not all of the `api.slack.com` REST |
4 | 6 | calls, as well as the Real-Time Messaging protocol over websocket, in |
5 | 7 | a fully managed way. |
6 | 8 | |
7 | ## Change log | |
8 | 9 | |
9 | ### v0.1.0 - May 28, 2017 | |
10 | 10 | |
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. | |
14 | 11 | |
15 | Please check [0.1.0](https://github.com/nlopes/slack/releases/tag/v0.1.0) | |
12 | ## Changelog | |
16 | 13 | |
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. | |
20 | 15 | |
21 | 16 | ## Installing |
22 | 17 | |
39 | 34 | api := slack.New("YOUR_TOKEN_HERE") |
40 | 35 | // If you set debugging, it will log all requests to the console |
41 | 36 | // Useful when encountering issues |
42 | // api.SetDebug(true) | |
37 | // slack.New("YOUR_TOKEN_HERE", slack.OptionDebug(true)) | |
43 | 38 | groups, err := api.GetGroups(false) |
44 | 39 | if err != nil { |
45 | 40 | fmt.Printf("%s\n", err) |
76 | 71 | See https://github.com/nlopes/slack/blob/master/examples/websocket/websocket.go |
77 | 72 | |
78 | 73 | |
74 | ## Minimal EventsAPI usage: | |
75 | ||
76 | See https://github.com/nlopes/slack/blob/master/examples/eventsapi/events.go | |
77 | ||
78 | ||
79 | 79 | ## Contributing |
80 | 80 | |
81 | 81 | You are more than welcome to contribute to this project. Fork and |
0 | 0 | package slack |
1 | 1 | |
2 | 2 | import ( |
3 | "errors" | |
3 | "context" | |
4 | 4 | "fmt" |
5 | 5 | "net/url" |
6 | "strings" | |
6 | 7 | ) |
7 | 8 | |
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() | |
25 | 17 | } |
26 | 18 | |
27 | 19 | // DisableUser disabled a user account, given a user ID |
28 | 20 | 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 { | |
29 | 26 | values := url.Values{ |
30 | 27 | "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) | |
39 | 35 | } |
40 | 36 | |
41 | 37 | return nil |
42 | 38 | } |
43 | 39 | |
44 | 40 | // 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 { | |
52 | 47 | values := url.Values{ |
53 | 48 | "email": {emailAddress}, |
54 | 49 | "channels": {channel}, |
55 | 50 | "first_name": {firstName}, |
56 | 51 | "last_name": {lastName}, |
57 | 52 | "ultra_restricted": {"1"}, |
58 | "token": {api.config.token}, | |
53 | "token": {api.token}, | |
54 | "resend": {"true"}, | |
59 | 55 | "set_active": {"true"}, |
60 | 56 | "_attempts": {"1"}, |
61 | 57 | } |
62 | 58 | |
63 | _, err := adminRequest("invite", teamName, values, api.debug) | |
59 | err := api.adminRequest(ctx, "invite", teamName, values) | |
64 | 60 | if err != nil { |
65 | 61 | return fmt.Errorf("Failed to invite single-channel guest: %s", err) |
66 | 62 | } |
69 | 65 | } |
70 | 66 | |
71 | 67 | // 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 { | |
79 | 74 | values := url.Values{ |
80 | 75 | "email": {emailAddress}, |
81 | 76 | "channels": {channel}, |
82 | 77 | "first_name": {firstName}, |
83 | 78 | "last_name": {lastName}, |
84 | 79 | "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) | |
91 | 87 | if err != nil { |
92 | 88 | return fmt.Errorf("Failed to restricted account: %s", err) |
93 | 89 | } |
96 | 92 | } |
97 | 93 | |
98 | 94 | // 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 { | |
105 | 101 | values := url.Values{ |
106 | 102 | "email": {emailAddress}, |
107 | 103 | "first_name": {firstName}, |
108 | 104 | "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) | |
115 | 111 | if err != nil { |
116 | 112 | return fmt.Errorf("Failed to invite to team: %s", err) |
117 | 113 | } |
120 | 116 | } |
121 | 117 | |
122 | 118 | // 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 { | |
124 | 125 | values := url.Values{ |
125 | 126 | "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) | |
132 | 133 | if err != nil { |
133 | 134 | return fmt.Errorf("Failed to change the user (%s) to a regular user: %s", user, err) |
134 | 135 | } |
137 | 138 | } |
138 | 139 | |
139 | 140 | // 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 { | |
141 | 147 | values := url.Values{ |
142 | 148 | "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) | |
149 | 155 | if err != nil { |
150 | 156 | return fmt.Errorf("Failed to send SSO binding email for user (%s): %s", user, err) |
151 | 157 | } |
155 | 161 | |
156 | 162 | // SetUltraRestricted converts a user into a single-channel guest |
157 | 163 | 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 { | |
158 | 169 | values := url.Values{ |
159 | 170 | "user": {uid}, |
160 | 171 | "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) | |
167 | 178 | if err != nil { |
168 | 179 | return fmt.Errorf("Failed to ultra-restrict account: %s", err) |
169 | 180 | } |
172 | 183 | } |
173 | 184 | |
174 | 185 | // 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 { | |
176 | 192 | values := url.Values{ |
177 | 193 | "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 | } |
16 | 16 | Name string `json:"name"` // Required. |
17 | 17 | Text string `json:"text"` // Required. |
18 | 18 | 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". | |
20 | 20 | Value string `json:"value,omitempty"` // Optional. |
21 | 21 | DataSource string `json:"data_source,omitempty"` // Optional. |
22 | 22 | MinQueryLength int `json:"min_query_length,omitempty"` // Optional. Default value is 1. |
24 | 24 | SelectedOptions []AttachmentActionOption `json:"selected_options,omitempty"` // Optional. The first element of this array will be set as the pre-selected option for this menu. |
25 | 25 | OptionGroups []AttachmentActionOptionGroup `json:"option_groups,omitempty"` // Optional. |
26 | 26 | 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 | |
27 | 33 | } |
28 | 34 | |
29 | 35 | // AttachmentActionOption the individual option to appear in action menu. |
40 | 46 | } |
41 | 47 | |
42 | 48 | // 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 | |
58 | 51 | |
59 | 52 | // ConfirmationField are used to ask users to confirm actions |
60 | 53 | type ConfirmationField struct { |
70 | 63 | Fallback string `json:"fallback"` |
71 | 64 | |
72 | 65 | CallbackID string `json:"callback_id,omitempty"` |
66 | ID int `json:"id,omitempty"` | |
73 | 67 | |
68 | AuthorID string `json:"author_id,omitempty"` | |
74 | 69 | AuthorName string `json:"author_name,omitempty"` |
75 | 70 | AuthorSubname string `json:"author_subname,omitempty"` |
76 | 71 | 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 | } |
0 | 0 | package slack |
1 | 1 | |
2 | 2 | import ( |
3 | "math" | |
4 | 3 | "math/rand" |
5 | 4 | "time" |
6 | 5 | ) |
13 | 12 | // conjunction with the time package. |
14 | 13 | type backoff struct { |
15 | 14 | 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 | |
22 | 21 | } |
23 | 22 | |
24 | 23 | // Returns the current value of the counter and then multiplies it |
25 | 24 | // 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 | |
32 | 28 | if b.Max == 0 { |
33 | 29 | b.Max = 10 * time.Second |
34 | 30 | } |
35 | if b.Factor == 0 { | |
36 | b.Factor = 2 | |
31 | ||
32 | if b.Initial == 0 { | |
33 | b.Initial = 100 * time.Millisecond | |
37 | 34 | } |
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 | |
42 | 41 | } |
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))) | |
46 | 45 | } |
47 | //bump attempts count | |
46 | ||
47 | // bump attempts count | |
48 | 48 | b.attempts++ |
49 | //return as a time.Duration | |
50 | return time.Duration(dur) | |
49 | ||
50 | return dur | |
51 | 51 | } |
52 | 52 | |
53 | 53 | //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 | } |
0 | 0 | package slack |
1 | 1 | |
2 | 2 | import ( |
3 | "errors" | |
3 | "context" | |
4 | 4 | "net/url" |
5 | 5 | ) |
6 | 6 | |
17 | 17 | SlackResponse |
18 | 18 | } |
19 | 19 | |
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) { | |
21 | 21 | response := &botResponseFull{} |
22 | err := post(path, values, response, debug) | |
22 | err := api.postMethod(ctx, path, values, response) | |
23 | 23 | if err != nil { |
24 | 24 | return nil, err |
25 | 25 | } |
26 | if !response.Ok { | |
27 | return nil, errors.New(response.Error) | |
26 | ||
27 | if err := response.Err(); err != nil { | |
28 | return nil, err | |
28 | 29 | } |
30 | ||
29 | 31 | return response, nil |
30 | 32 | } |
31 | 33 | |
32 | 34 | // GetBotInfo will retrieve the complete bot information |
33 | 35 | 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) { | |
34 | 41 | values := url.Values{ |
35 | "token": {api.config.token}, | |
36 | "bot": {bot}, | |
42 | "token": {api.token}, | |
37 | 43 | } |
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) | |
39 | 50 | if err != nil { |
40 | 51 | return nil, err |
41 | 52 | } |
23 | 23 | http.HandleFunc("/bots.info", getBotInfo) |
24 | 24 | |
25 | 25 | once.Do(startServer) |
26 | SLACK_API = "http://" + serverAddr + "/" | |
27 | api := New("testing-token") | |
26 | api := New("testing-token", OptionAPIURL("http://"+serverAddr+"/")) | |
28 | 27 | |
29 | 28 | bot, err := api.GetBotInfo("B02875YLA") |
30 | 29 | if err != nil { |
0 | 0 | package slack |
1 | 1 | |
2 | 2 | import ( |
3 | "errors" | |
3 | "context" | |
4 | 4 | "net/url" |
5 | 5 | "strconv" |
6 | 6 | ) |
17 | 17 | |
18 | 18 | // Channel contains information about the channel |
19 | 19 | 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) { | |
27 | 28 | 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 | } | |
36 | 58 | } |
37 | 59 | |
38 | 60 | // 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 | |
49 | 76 | } |
50 | 77 | |
51 | 78 | // 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 | |
62 | 94 | } |
63 | 95 | |
64 | 96 | // 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) | |
71 | 111 | if err != nil { |
72 | 112 | return nil, err |
73 | 113 | } |
75 | 115 | } |
76 | 116 | |
77 | 117 | // 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}, | |
82 | 129 | } |
83 | 130 | if params.Latest != DEFAULT_HISTORY_LATEST { |
84 | 131 | values.Add("latest", params.Latest) |
96 | 143 | values.Add("inclusive", "0") |
97 | 144 | } |
98 | 145 | } |
146 | ||
99 | 147 | if params.Unreads != DEFAULT_HISTORY_UNREADS { |
100 | 148 | if params.Unreads { |
101 | 149 | values.Add("unreads", "1") |
103 | 151 | values.Add("unreads", "0") |
104 | 152 | } |
105 | 153 | } |
106 | response, err := channelRequest("channels.history", values, api.debug) | |
154 | ||
155 | response, err := api.channelRequest(ctx, "channels.history", values) | |
107 | 156 | if err != nil { |
108 | 157 | return nil, err |
109 | 158 | } |
111 | 160 | } |
112 | 161 | |
113 | 162 | // 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) | |
120 | 178 | if err != nil { |
121 | 179 | return nil, err |
122 | 180 | } |
124 | 182 | } |
125 | 183 | |
126 | 184 | // 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}, | |
131 | 196 | "user": {user}, |
132 | 197 | } |
133 | response, err := channelRequest("channels.invite", values, api.debug) | |
198 | ||
199 | response, err := api.channelRequest(ctx, "channels.invite", values) | |
134 | 200 | if err != nil { |
135 | 201 | return nil, err |
136 | 202 | } |
138 | 204 | } |
139 | 205 | |
140 | 206 | // 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) | |
147 | 221 | if err != nil { |
148 | 222 | return nil, err |
149 | 223 | } |
151 | 225 | } |
152 | 226 | |
153 | 227 | // 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) | |
160 | 242 | if err != nil { |
161 | 243 | return false, err |
162 | 244 | } |
163 | if response.NotInChannel { | |
164 | return response.NotInChannel, nil | |
165 | } | |
166 | return false, nil | |
245 | ||
246 | return response.NotInChannel, nil | |
167 | 247 | } |
168 | 248 | |
169 | 249 | // 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}, | |
174 | 261 | "user": {user}, |
175 | 262 | } |
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 | |
181 | 266 | } |
182 | 267 | |
183 | 268 | // 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 | ||
188 | 283 | 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) | |
192 | 294 | if err != nil { |
193 | 295 | return nil, err |
194 | 296 | } |
200 | 302 | // timer before making the call. In this way, any further updates needed during the timeout will not generate extra calls |
201 | 303 | // (just one per channel). This is useful for when reading scroll-back history, or following a busy live channel. A |
202 | 304 | // 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}, | |
207 | 317 | "ts": {ts}, |
208 | 318 | } |
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 | |
214 | 322 | } |
215 | 323 | |
216 | 324 | // 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}, | |
221 | 336 | "name": {name}, |
222 | 337 | } |
338 | ||
223 | 339 | // XXX: the created entry in this call returns a string instead of a number |
224 | 340 | // 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) | |
226 | 342 | if err != nil { |
227 | 343 | return nil, err |
228 | 344 | } |
229 | 345 | 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}, | |
239 | 360 | "purpose": {purpose}, |
240 | 361 | } |
241 | response, err := channelRequest("channels.setPurpose", values, api.debug) | |
362 | ||
363 | response, err := api.channelRequest(ctx, "channels.setPurpose", values) | |
242 | 364 | if err != nil { |
243 | 365 | return "", err |
244 | 366 | } |
246 | 368 | } |
247 | 369 | |
248 | 370 | // 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}, | |
253 | 382 | "topic": {topic}, |
254 | 383 | } |
255 | response, err := channelRequest("channels.setTopic", values, api.debug) | |
384 | ||
385 | response, err := api.channelRequest(ctx, "channels.setTopic", values) | |
256 | 386 | if err != nil { |
257 | 387 | return "", err |
258 | 388 | } |
260 | 390 | } |
261 | 391 | |
262 | 392 | // 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}, | |
267 | 404 | "thread_ts": {thread_ts}, |
268 | 405 | } |
269 | response, err := channelRequest("channels.replies", values, api.debug) | |
406 | response, err := api.channelRequest(ctx, "channels.replies", values) | |
270 | 407 | if err != nil { |
271 | 408 | return nil, err |
272 | 409 | } |
0 | 0 | package slack |
1 | 1 | |
2 | 2 | import ( |
3 | "context" | |
3 | 4 | "encoding/json" |
4 | "errors" | |
5 | "net/http" | |
5 | 6 | "net/url" |
6 | "strings" | |
7 | ||
8 | "github.com/nlopes/slack/slackutilsx" | |
7 | 9 | ) |
8 | 10 | |
9 | 11 | const ( |
10 | 12 | DEFAULT_MESSAGE_USERNAME = "" |
11 | DEFAULT_MESSAGE_THREAD_TIMESTAMP = "" | |
13 | DEFAULT_MESSAGE_REPLY_BROADCAST = false | |
12 | 14 | DEFAULT_MESSAGE_ASUSER = false |
13 | 15 | DEFAULT_MESSAGE_PARSE = "" |
16 | DEFAULT_MESSAGE_THREAD_TIMESTAMP = "" | |
14 | 17 | DEFAULT_MESSAGE_LINK_NAMES = 0 |
15 | 18 | DEFAULT_MESSAGE_UNFURL_LINKS = false |
16 | 19 | DEFAULT_MESSAGE_UNFURL_MEDIA = true |
21 | 24 | ) |
22 | 25 | |
23 | 26 | 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"` | |
27 | 31 | 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 | |
28 | 42 | } |
29 | 43 | |
30 | 44 | // PostMessageParameters contains all the parameters necessary (including the optional ones) for a PostMessage() request |
31 | 45 | 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"` | |
45 | 62 | } |
46 | 63 | |
47 | 64 | // NewPostMessageParameters provides an instance of PostMessageParameters with all the sane default values set |
48 | 65 | func NewPostMessageParameters() PostMessageParameters { |
49 | 66 | 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, | |
61 | 79 | } |
62 | 80 | } |
63 | 81 | |
64 | 82 | // DeleteMessage deletes a message in a channel |
65 | 83 | 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)) | |
67 | 91 | return respChannel, respTimestamp, err |
68 | 92 | } |
69 | 93 | |
70 | 94 | // PostMessage sends a message to a channel. |
71 | 95 | // Message is escaped by default according to https://api.slack.com/docs/formatting |
72 | 96 | // 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...), | |
79 | 103 | ) |
80 | 104 | return respChannel, respTimestamp, err |
81 | 105 | } |
82 | 106 | |
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 | ||
83 | 138 | // 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...)) | |
86 | 151 | } |
87 | 152 | |
88 | 153 | // SendMessage more flexible method for configuring messages. |
89 | 154 | 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 { | |
92 | 167 | return "", "", "", err |
93 | 168 | } |
94 | 169 | |
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 { | |
97 | 171 | return "", "", "", err |
98 | 172 | } |
99 | 173 | |
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) { | |
105 | 186 | config := sendConfig{ |
106 | mode: chatPostMessage, | |
187 | apiurl: apiurl, | |
188 | endpoint: apiurl + string(chatPostMessage), | |
107 | 189 | values: url.Values{ |
108 | 190 | "token": {token}, |
109 | 191 | "channel": {channel}, |
112 | 194 | |
113 | 195 | for _, opt := range options { |
114 | 196 | 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("&", "&", "<", "<", ">", ">") | |
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 | } | |
137 | 209 | } |
138 | 210 | |
139 | 211 | type sendMode string |
140 | 212 | |
141 | 213 | 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" | |
145 | 221 | ) |
146 | 222 | |
147 | 223 | 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 | |
150 | 280 | } |
151 | 281 | |
152 | 282 | // MsgOption option provided when sending a message. |
155 | 285 | // MsgOptionPost posts a messages, this is the default. |
156 | 286 | func MsgOptionPost() MsgOption { |
157 | 287 | return func(config *sendConfig) error { |
158 | config.mode = chatPostMessage | |
288 | config.endpoint = config.apiurl + string(chatPostMessage) | |
159 | 289 | 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) | |
160 | 309 | return nil |
161 | 310 | } |
162 | 311 | } |
164 | 313 | // MsgOptionUpdate updates a message based on the timestamp. |
165 | 314 | func MsgOptionUpdate(timestamp string) MsgOption { |
166 | 315 | return func(config *sendConfig) error { |
167 | config.mode = chatUpdate | |
316 | config.endpoint = config.apiurl + string(chatUpdate) | |
168 | 317 | config.values.Add("ts", timestamp) |
169 | 318 | return nil |
170 | 319 | } |
173 | 322 | // MsgOptionDelete deletes a message based on the timestamp. |
174 | 323 | func MsgOptionDelete(timestamp string) MsgOption { |
175 | 324 | return func(config *sendConfig) error { |
176 | config.mode = chatDelete | |
325 | config.endpoint = config.apiurl + string(chatDelete) | |
177 | 326 | 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") | |
178 | 351 | return nil |
179 | 352 | } |
180 | 353 | } |
185 | 358 | if b != DEFAULT_MESSAGE_ASUSER { |
186 | 359 | config.values.Set("as_user", "true") |
187 | 360 | } |
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) | |
188 | 377 | return nil |
189 | 378 | } |
190 | 379 | } |
194 | 383 | func MsgOptionText(text string, escape bool) MsgOption { |
195 | 384 | return func(config *sendConfig) error { |
196 | 385 | if escape { |
197 | text = escapeMessage(text) | |
386 | text = slackutilsx.EscapeMessage(text) | |
198 | 387 | } |
199 | 388 | config.values.Add("text", text) |
200 | 389 | return nil |
208 | 397 | return nil |
209 | 398 | } |
210 | 399 | |
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) | |
212 | 407 | 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)) | |
214 | 425 | } |
215 | 426 | return err |
216 | 427 | } |
224 | 435 | } |
225 | 436 | } |
226 | 437 | |
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 | ||
227 | 446 | // MsgOptionDisableMediaUnfurl disables media unfurling. |
228 | 447 | func MsgOptionDisableMediaUnfurl() MsgOption { |
229 | 448 | return func(config *sendConfig) error { |
240 | 459 | } |
241 | 460 | } |
242 | 461 | |
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 | ||
243 | 532 | // MsgOptionPostMessageParameters maintain backwards compatibility. |
244 | 533 | func MsgOptionPostMessageParameters(params PostMessageParameters) MsgOption { |
245 | 534 | return func(config *sendConfig) error { |
246 | 535 | 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) | |
248 | 542 | } |
249 | 543 | |
250 | 544 | // never generates an error. |
251 | 545 | MsgOptionAsUser(params.AsUser)(config) |
252 | 546 | |
253 | 547 | if params.Parse != DEFAULT_MESSAGE_PARSE { |
254 | config.values.Set("parse", string(params.Parse)) | |
548 | config.values.Set("parse", params.Parse) | |
255 | 549 | } |
256 | 550 | if params.LinkNames != DEFAULT_MESSAGE_LINK_NAMES { |
257 | 551 | config.values.Set("link_names", "1") |
282 | 576 | if params.ThreadTimestamp != DEFAULT_MESSAGE_THREAD_TIMESTAMP { |
283 | 577 | config.values.Set("thread_ts", params.ThreadTimestamp) |
284 | 578 | } |
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) |