slack_message 1.8.0 → 2.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +27 -2
- data/Gemfile.lock +1 -1
- data/README.md +157 -78
- data/lib/slack_message/api.rb +53 -26
- data/lib/slack_message/configuration.rb +10 -22
- data/lib/slack_message/dsl.rb +77 -17
- data/lib/slack_message/rspec.rb +160 -27
- data/lib/slack_message.rb +21 -15
- data/slack_message.gemspec +1 -1
- data/spec/slack_message_spec.rb +103 -38
- metadata +2 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 368533ab7e4dae44ffbef5ccf8afef048def96d964a6af64c294c23ba617dfa3
|
4
|
+
data.tar.gz: f9397472bbc5c5db81dd1c9d7715e5015f4ec455948e7a29afbb11ef8c8a3d01
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 5cce143a95d508694221f1c667428f850ce7aa537e73f61c1869498b68a5751afe2ac386215b96702a1a27bcad3758c474ba09870a1cd7a31a7c33a17ebd4d53
|
7
|
+
data.tar.gz: 7f81e1876f231899588b174c87c4a0a3ca3bcbc4057216092311b300cff7d23d6541083b5e481e811795042538e2e6713b1a9f5eb5f6eba345e79960335cad91
|
data/CHANGELOG.md
CHANGED
@@ -1,9 +1,34 @@
|
|
1
1
|
# Changelog
|
2
2
|
|
3
|
-
## [
|
3
|
+
## [2.2.0] - 2021-11-20
|
4
|
+
- When sending text, it is now possible to mention users and have their user
|
5
|
+
IDs automatically converted using `<email@email.com>` within text nodes.
|
6
|
+
- It's now possible to override notification text.
|
7
|
+
- Errors received from the Slack API now raise `SlackMessage::ApiError`.
|
8
|
+
- Re-exposed a top-level method for getting user IDs, `SlackMessage.user_id`.
|
9
|
+
- Raising some better errors when no message payload is present.
|
10
|
+
- Using `build` now requires a profile, so configuration must exist.
|
11
|
+
|
12
|
+
## [2.1.0] - 2021-11-01
|
13
|
+
- Change to use Slack Apps for all profiles. This should allow growth toward
|
14
|
+
updating messages, working with interactive messages etc.
|
15
|
+
- As a result, allow custom icons per profile / message.
|
16
|
+
- When sending a message, the first `text` block is used for the notification
|
17
|
+
content. Should resolve "this content cannot be displayed".
|
18
|
+
- Significant restructuring of README.
|
19
|
+
|
20
|
+
## [2.0.0] - 2021-11-01
|
21
|
+
- Yeah that was all broken.
|
22
|
+
|
23
|
+
## [1.9.0] - 2021-10-27
|
24
|
+
- Add many validations so that trying to add e.g. empty text won't succeed.
|
25
|
+
Previously that would be accepted but return `invalid_blocks` from the API.
|
26
|
+
|
27
|
+
## [1.8.1] - 2021-10-08
|
28
|
+
- Cleaned that rspec code a bit, added more matchers for real world use.
|
4
29
|
|
5
30
|
## [1.8.0] - 2021-10-07
|
6
|
-
- Added the ability to test in RSpec
|
31
|
+
- Added the ability to test in RSpec.
|
7
32
|
|
8
33
|
## [1.7.1] - 2021-10-06
|
9
34
|
- Fixed literally a syntax issue.
|
data/Gemfile.lock
CHANGED
data/README.md
CHANGED
@@ -3,8 +3,7 @@ SlackMessage: a Friendly DSL for Slack
|
|
3
3
|
|
4
4
|
SlackMessage is a wrapper over the [Block Kit
|
5
5
|
API](https://app.slack.com/block-kit-builder/) to make it easy to read and
|
6
|
-
write messages to slack in your ruby application.
|
7
|
-
is built to be opinionated to keep your configuration needs low.
|
6
|
+
write messages to slack in your ruby application.
|
8
7
|
|
9
8
|
Posting a message to Slack should be this easy:
|
10
9
|
|
@@ -16,80 +15,105 @@ end
|
|
16
15
|
|
17
16
|
To install, just add `slack_message` to your bundle and you're ready to go.
|
18
17
|
|
18
|
+
Opinionated Stances
|
19
|
+
------------
|
20
|
+
|
21
|
+
Slack's API has a lot of options available to you! But this gem takes some
|
22
|
+
opinionated stances on how to make use of that API. For instance:
|
23
|
+
|
24
|
+
* No dependencies. Your lockfile is enough of a mess already.
|
25
|
+
* Webhooks are passé. Only Slack Apps are supported now.
|
26
|
+
* Unless you request otherwise, text is always rendered using `mrkdwn`. If you
|
27
|
+
want plaintext, you'll need to ask for it. Same for the `emoji` flag.
|
28
|
+
* As many API semantics as possible are hidden. For instance, if you post to
|
29
|
+
something that looks like an email address, `slack_message` is going to try to
|
30
|
+
look it up as an email address.
|
31
|
+
* A few little hacks on the block syntax, such as adding a `blank_line` (which
|
32
|
+
doesn't exist in the API), or leading spaces.
|
33
|
+
* Configuration is kept as simple as possible. But, as much heavy lifting as
|
34
|
+
possible should occur just once via configuration and not on every call.
|
19
35
|
|
20
36
|
Usage
|
21
37
|
------------
|
22
38
|
|
23
39
|
### Configuration
|
24
40
|
|
25
|
-
To get started, you'll need to
|
26
|
-
to
|
27
|
-
|
41
|
+
To get started, you'll need to create a Slack App with some appropriate
|
42
|
+
permissions. It used to be possible to use the Webhook API, but that's long
|
43
|
+
since been deprecated, and apps are pretty [straightforward to
|
44
|
+
create](https://api.slack.com/tutorials/tracks/getting-a-token).
|
45
|
+
|
46
|
+
Generally, make sure your token has permissions for `users:read` and `chat:write`.
|
28
47
|
|
29
48
|
```ruby
|
30
49
|
SlackMessage.configure do |config|
|
31
|
-
|
50
|
+
api_token = 'xoxb-11111111111-2222222222-33333333333333333'
|
32
51
|
|
33
|
-
config.add_profile(
|
52
|
+
config.add_profile(api_token: api_token)
|
34
53
|
end
|
35
54
|
```
|
36
55
|
|
37
|
-
You should
|
38
|
-
|
39
|
-
`config/initializers/slack_message.rb`.
|
56
|
+
You should keep your token in a safe place like `ENV`. If using this gem with
|
57
|
+
Rails, place this code in somewhere like `config/initializers/slack_message.rb`.
|
40
58
|
|
41
59
|
#### Additional Profiles
|
42
60
|
|
43
|
-
If
|
44
|
-
|
45
|
-
|
61
|
+
If your app uses slack messages for several different purposes, it's common to
|
62
|
+
want to post to different channels as different names / icons / etc. To do that
|
63
|
+
more easily and consistently, you can specify multiple profiles:
|
46
64
|
|
47
65
|
```ruby
|
48
66
|
SlackMessage.configure do |config|
|
67
|
+
api_token = 'xoxb-11111111111-2222222222-33333333333333333'
|
68
|
+
|
49
69
|
# default profile
|
50
|
-
config.add_profile(name: 'Slack Notifier'
|
70
|
+
config.add_profile(api_token: api_token, name: 'Slack Notifier')
|
51
71
|
|
52
72
|
# additional profiles (see below for usage)
|
53
|
-
config.add_profile(:prod_alert_bot,
|
54
|
-
|
73
|
+
config.add_profile(:prod_alert_bot,
|
74
|
+
name: 'Prod Alert Bot'
|
75
|
+
icon: ':mooseandsquirrel:'
|
76
|
+
)
|
77
|
+
config.add_profile(:sidekiq_bot,
|
78
|
+
api_token: ENV.fetch('SIDEKIQ_SLACK_APP_API_TOKEN'),
|
79
|
+
name: 'Sidekiq Bot',
|
80
|
+
)
|
55
81
|
end
|
56
82
|
```
|
57
83
|
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
84
|
+
A number of parameters are available to make it simpler to use a profile without
|
85
|
+
specifying repetitive information. Most all have corresponding options when
|
86
|
+
composing a message:
|
87
|
+
|
88
|
+
| Config | Default | Value |
|
89
|
+
|-----------------|-----------------|-----------------------------------------------------------------|
|
90
|
+
| api_token | None | Your Slack App API Key. |
|
91
|
+
| name | From Slack App | The bot name for your message. |
|
92
|
+
| icon | From Slack App | Profile icon for your message. Specify as :emoji: or image URL. |
|
93
|
+
| default_channel | None (optional) | Channel / user to post to by default. |
|
94
|
+
|
95
|
+
|
96
|
+
Setting a `default_channel` specifically will allow you to use `post_as`, which
|
97
|
+
is a convenient shortcut for bots that repeatedly post to one channel as a
|
98
|
+
consistent identity:
|
62
99
|
|
63
100
|
```ruby
|
64
101
|
SlackMessage.configure do |config|
|
65
|
-
config.add_profile(:
|
66
|
-
|
67
|
-
|
102
|
+
config.add_profile(:red_alert_bot,
|
103
|
+
api_token: ENV.fetch('SLACK_API_TOKEN'),
|
104
|
+
name: 'Red Alerts',
|
105
|
+
icon: ':klaxon:',
|
68
106
|
default_channel: '#red_alerts'
|
69
107
|
)
|
70
108
|
end
|
71
109
|
|
72
|
-
SlackMessage.post_as(:
|
110
|
+
SlackMessage.post_as(:red_alert_bot) do
|
73
111
|
text ":ambulance: weeooo weeooo something went wrong"
|
74
112
|
end
|
75
113
|
```
|
76
114
|
|
77
|
-
|
78
|
-
|
79
|
-
|
80
|
-
#### Configuring User Search
|
81
|
-
|
82
|
-
Slack's API no longer allows you to send DMs to users by username. You need to
|
83
|
-
look up a user's internal ID and send to that ID. Thankfully, there is a lookup
|
84
|
-
by email endpoint for this. If you'd like to post messages to users by their
|
85
|
-
email address, you'll need a
|
86
|
-
[separate API Token](https://api.slack.com/tutorials/tracks/getting-a-token):
|
87
|
-
|
88
|
-
```ruby
|
89
|
-
SlackMessage.configure do |config|
|
90
|
-
config.api_token = 'xoxb-11111111111-2222222222-33333333333333333'
|
91
|
-
end
|
92
|
-
```
|
115
|
+
There's no reason you can't use the same API key for several profiles. Profiles
|
116
|
+
are most useful to create consistent name / icon setups for apps with many bots.
|
93
117
|
|
94
118
|
### Posting Messages
|
95
119
|
|
@@ -104,14 +128,12 @@ end
|
|
104
128
|
That's it! SlackMessage will automatically serialize for the API like this:
|
105
129
|
|
106
130
|
```json
|
107
|
-
[{"type":"section","text":{"type":"mrkdwn","text":"We did it! :thumbsup:"}}]
|
131
|
+
[{"type":"section","text":{"type":"mrkdwn","text":"We did it @here! :thumbsup:"}}]
|
108
132
|
```
|
109
133
|
|
110
134
|
Details like remembering that Slack made a mystifying decision to force you to
|
111
135
|
request "mrkdwn", or requiring your text to be wrapped into a section are handled
|
112
|
-
for you.
|
113
|
-
|
114
|
-
Building up messages is meant to be as user-friendly as possible:
|
136
|
+
for you. Building up messages is meant to be as user-friendly as possible:
|
115
137
|
|
116
138
|
```ruby
|
117
139
|
SlackMessage.build do
|
@@ -147,20 +169,21 @@ automatically:
|
|
147
169
|
]
|
148
170
|
```
|
149
171
|
|
150
|
-
|
151
|
-
|
172
|
+
It's just as easy to send messages directly to users. SlackMessage will look for
|
173
|
+
targets that are email-addressish, and look them up for you automatically:
|
152
174
|
|
153
175
|
```ruby
|
154
|
-
|
155
|
-
|
176
|
+
user_email = 'hello@joemastey.com'
|
177
|
+
|
178
|
+
SlackMessage.post_to(user_email) do
|
179
|
+
text "You specifically did it! :thumbsup:"
|
156
180
|
end
|
157
181
|
```
|
158
182
|
|
159
183
|
SlackMessage is able to build all kinds of rich messages for you, and has been
|
160
184
|
a real joy to use for the author at least. To understand a bit more about the
|
161
|
-
possibilities of blocks,
|
162
|
-
Builder](https://app.slack.com/block-kit-builder/)
|
163
|
-
better. There are lots of options:
|
185
|
+
possibilities of blocks, you should play around with Slack's [Block Kit
|
186
|
+
Builder](https://app.slack.com/block-kit-builder/). There are lots of options:
|
164
187
|
|
165
188
|
```ruby
|
166
189
|
SlackMessage.post_to('#general') do
|
@@ -195,25 +218,75 @@ For now you'll need to read a bit of the source code to get the entire API. Sorr
|
|
195
218
|
working on it.
|
196
219
|
|
197
220
|
If you've defined multiple profiles in configuration, you can specify which to
|
198
|
-
use for your message by specifying
|
221
|
+
use for your message by specifying its name:
|
199
222
|
|
200
223
|
```ruby
|
201
224
|
SlackMessage.post_to('#general', as: :sidekiq_bot) do
|
202
225
|
text ":octagonal_sign: A job has failed permanently and needs to be rescued."
|
203
|
-
link_button "Sidekiq Dashboard",
|
226
|
+
link_button "Sidekiq Dashboard", sidekiq_dashboard_url, style: :danger
|
204
227
|
end
|
205
228
|
```
|
206
229
|
|
207
|
-
You can also
|
230
|
+
You can also override profile bot details when sending a message:
|
208
231
|
|
209
232
|
```ruby
|
210
233
|
SlackMessage.post_to('#general') do
|
211
234
|
bot_name "CoffeeBot"
|
235
|
+
bot_icon ":coffee:"
|
212
236
|
|
213
237
|
text ":coffee::clock: Time to take a break!"
|
214
238
|
end
|
215
239
|
```
|
216
240
|
|
241
|
+
#### Notifying Users
|
242
|
+
|
243
|
+
There are several supported ways to tag and notify users. Mentioned above, it's
|
244
|
+
possible to DM a user by email:
|
245
|
+
|
246
|
+
```ruby
|
247
|
+
SlackMessage.post_to('hello@joemastey.com') do
|
248
|
+
text "Hi there!"
|
249
|
+
end
|
250
|
+
```
|
251
|
+
|
252
|
+
You can also mention a user by email within a channel by wrapping their name
|
253
|
+
in tags:
|
254
|
+
|
255
|
+
```ruby
|
256
|
+
SlackMessage.post_to('#general') do
|
257
|
+
bot_name "CoffeeBot"
|
258
|
+
bot_icon ":coffee:"
|
259
|
+
|
260
|
+
text ":coffee: It's your turn to make coffee <hello@joemastey.com>."
|
261
|
+
end
|
262
|
+
```
|
263
|
+
|
264
|
+
Emails that are not wrapped in tags will be rendered as normal clickable email
|
265
|
+
addresses. Additionally, Slack will automatically convert a number of channel
|
266
|
+
names and tags you're probably already used to:
|
267
|
+
|
268
|
+
```ruby
|
269
|
+
SlackMessage.post_to('#general') do
|
270
|
+
bot_name "CoffeeBot"
|
271
|
+
bot_icon ":coffee:"
|
272
|
+
|
273
|
+
text "@here there's no coffee left!"
|
274
|
+
end
|
275
|
+
```
|
276
|
+
|
277
|
+
By default, the desktop notification for a message will be the text of the
|
278
|
+
message itself. However, you can customize desktop notifications if you prefer:
|
279
|
+
|
280
|
+
```ruby
|
281
|
+
SlackMessage.post_to('hello@joemastey.com') do
|
282
|
+
bot_name "CoffeeBot"
|
283
|
+
bot_icon ":coffee:"
|
284
|
+
|
285
|
+
notification_text "It's a coffee emergency!"
|
286
|
+
text "There's no coffee left!"
|
287
|
+
end
|
288
|
+
```
|
289
|
+
|
217
290
|
### Testing
|
218
291
|
|
219
292
|
You can do some basic testing against SlackMessage, at least if you use RSpec!
|
@@ -237,6 +310,26 @@ some custom matchers:
|
|
237
310
|
expect {
|
238
311
|
SlackMessage.post_to('#general') { text "foo" }
|
239
312
|
}.to post_slack_message_to('#general').with_content_matching(/foo/)
|
313
|
+
|
314
|
+
expect {
|
315
|
+
SlackMessage.post_as(:schmoebot) { text "foo" }
|
316
|
+
}.to post_slack_message_as(:schmoebot)
|
317
|
+
|
318
|
+
expect {
|
319
|
+
SlackMessage.post_as(:schmoebot) { text "foo" }
|
320
|
+
}.to post_slack_message_as('Schmoe Bot')
|
321
|
+
|
322
|
+
expect {
|
323
|
+
SlackMessage.post_as(:schmoebot) { text "foo" }
|
324
|
+
}.to post_slack_message_with_icon(':schmoebot:')
|
325
|
+
|
326
|
+
expect {
|
327
|
+
SlackMessage.post_as(:schmoebot) { text "foo" }
|
328
|
+
}.to post_slack_message_with_icon_matching(/gravatar/)
|
329
|
+
|
330
|
+
expect {
|
331
|
+
SlackMessage.post_to('#general') { text "foo" }
|
332
|
+
}.to post_to_slack
|
240
333
|
```
|
241
334
|
|
242
335
|
Be forewarned, I'm frankly not that great at more complicated RSpec matchers,
|
@@ -244,45 +337,31 @@ so I'm guessing there are some bugs. Also, because the content of a message
|
|
244
337
|
gets turned into a complex JSON object, matching against content isn't capable
|
245
338
|
of very complicated regexes.
|
246
339
|
|
247
|
-
Opinionated Stances
|
248
|
-
------------
|
249
|
-
|
250
|
-
Slack's API has a lot of options available to you! But this gem takes some
|
251
|
-
opinionated stances on how to make use of that API. For instance:
|
252
|
-
|
253
|
-
* Unless you request otherwise, text is always rendered using `mrkdwn`. If you
|
254
|
-
want plaintext, you'll need to ask for it.
|
255
|
-
* Generally, same goes for the `emoji` flag on almost every text element.
|
256
|
-
* It's possible to ask for a `blank_line` in sections, even though that concept
|
257
|
-
isn't real. In this case, a text line containing only an emspace is rendered.
|
258
|
-
* It's easy to configure a bot for consistent name / channel use. My previous
|
259
|
-
use of SlackNotifier led to frequently inconsistent names.
|
260
|
-
|
261
340
|
What it Doesn't Do
|
262
341
|
------------
|
263
342
|
|
264
343
|
This gem is intended to stay fairly simple. Other gems have lots of config
|
265
344
|
options and abilities, which is wonderful, but overall complicates usage. If
|
266
345
|
you want to add a feature, open an issue on Github first to see if it's likely
|
267
|
-
to be merged.
|
268
|
-
|
269
|
-
|
270
|
-
the block API, I'd be inclined to merge features that sustainably expand the
|
271
|
-
DSL to include more of the block API itself.
|
346
|
+
to be merged. This gem was built out of an existing need that _didn't_ include
|
347
|
+
most of the block API, but I'd be inclined to merge features that sustainably
|
348
|
+
expand the DSL to include more useful features.
|
272
349
|
|
273
|
-
|
350
|
+
Some behaviors that are still planned but not yet added:
|
274
351
|
|
275
352
|
* some API documentation amirite?
|
276
|
-
*
|
353
|
+
* custom http_options in configuration
|
277
354
|
* more of BlockKit's options
|
278
|
-
* any interactive elements at all
|
355
|
+
* any interactive elements at all
|
356
|
+
* editing / updating messages
|
357
|
+
* multiple recipients
|
279
358
|
* more interesting return types for your message
|
280
|
-
* richer text formatting (ul is currently a hack)
|
359
|
+
* richer text formatting (for instance, `ul` is currently a hack)
|
281
360
|
|
282
361
|
Contributing
|
283
362
|
------------
|
284
363
|
|
285
|
-
Contributions are very welcome. Fork, fix, submit
|
364
|
+
Contributions are very welcome. Fork, fix, submit pull.
|
286
365
|
|
287
366
|
Contribution is expected to conform to the [Contributor Covenant](https://github.com/jmmastey/slack_message/blob/master/CODE_OF_CONDUCT.md).
|
288
367
|
|
data/lib/slack_message/api.rb
CHANGED
@@ -3,13 +3,11 @@ require 'net/https'
|
|
3
3
|
require 'json'
|
4
4
|
|
5
5
|
class SlackMessage::Api
|
6
|
-
def self.user_id_for(email)
|
7
|
-
token = SlackMessage.configuration.api_token
|
8
|
-
|
6
|
+
def self.user_id_for(email, profile)
|
9
7
|
uri = URI("https://slack.com/api/users.lookupByEmail?email=#{email}")
|
10
8
|
request = Net::HTTP::Get.new(uri).tap do |req|
|
11
|
-
req['Authorization'] = "Bearer #{
|
12
|
-
req['Content-type'] = "application/json"
|
9
|
+
req['Authorization'] = "Bearer #{profile[:api_token]}"
|
10
|
+
req['Content-type'] = "application/json; charset=utf-8"
|
13
11
|
end
|
14
12
|
|
15
13
|
response = Net::HTTP.start(uri.hostname, uri.port, use_ssl: true) do |http|
|
@@ -17,55 +15,84 @@ class SlackMessage::Api
|
|
17
15
|
end
|
18
16
|
|
19
17
|
if response.code != "200"
|
20
|
-
raise "Got an error back from the Slack API (HTTP #{response.code}):\n#{response.body}"
|
18
|
+
raise SlackMessage::ApiError, "Got an error back from the Slack API (HTTP #{response.code}):\n#{response.body}"
|
21
19
|
elsif response.body == ""
|
22
|
-
raise "Received empty 200 response from Slack when looking up user info. Check your API key."
|
20
|
+
raise SlackMessage::ApiError, "Received empty 200 response from Slack when looking up user info. Check your API key."
|
23
21
|
end
|
24
22
|
|
25
23
|
begin
|
26
24
|
payload = JSON.parse(response.body)
|
27
25
|
rescue
|
28
|
-
raise "Unable to parse JSON response from Slack API\n#{response.body}"
|
26
|
+
raise SlackMessage::ApiError, "Unable to parse JSON response from Slack API\n#{response.body}"
|
29
27
|
end
|
30
28
|
|
31
29
|
if payload.include?("error") && payload["error"] == "invalid_auth"
|
32
|
-
raise "Received an error because your authentication token isn't properly configured
|
30
|
+
raise SlackMessage::ApiError, "Received an error because your authentication token isn't properly configured."
|
31
|
+
elsif payload.include?("error") && payload["error"] == "users_not_found"
|
32
|
+
raise SlackMessage::ApiError, "Couldn't find a user with the email '#{email}'."
|
33
33
|
elsif payload.include?("error")
|
34
|
-
raise "Received error response from Slack during user lookup:\n#{response.body}"
|
34
|
+
raise SlackMessage::ApiError, "Received error response from Slack during user lookup:\n#{response.body}"
|
35
35
|
end
|
36
36
|
|
37
37
|
payload["user"]["id"]
|
38
38
|
end
|
39
39
|
|
40
40
|
def self.post(payload, target, profile)
|
41
|
-
profile[:url] = profile[:url]
|
42
|
-
|
43
|
-
uri = URI.parse(profile[:url])
|
44
41
|
params = {
|
45
42
|
channel: target,
|
46
|
-
username: profile[:name],
|
47
|
-
blocks: payload
|
43
|
+
username: payload.custom_bot_name || profile[:name],
|
44
|
+
blocks: payload.render,
|
45
|
+
text: payload.custom_notification,
|
48
46
|
}
|
49
47
|
|
50
|
-
|
48
|
+
if params[:blocks].length == 0
|
49
|
+
raise ArgumentError, "Tried to send an entirely empty message."
|
50
|
+
end
|
51
|
+
|
52
|
+
icon = payload.custom_bot_icon || profile[:icon]
|
53
|
+
if icon =~ /^:\w+:$/
|
54
|
+
params[:icon_emoji] = icon
|
55
|
+
elsif icon =~ /^(https?:\/\/)?[0-9a-z]+\.[-_0-9a-z]+/ # very naive regex, I know. it'll be fine.
|
56
|
+
params[:icon_url] = icon
|
57
|
+
elsif !(icon.nil? || icon == '')
|
58
|
+
raise ArgumentError, "Couldn't figure out icon '#{icon}'. Try :emoji: or a URL."
|
59
|
+
end
|
60
|
+
|
61
|
+
response = post_message(profile, params)
|
62
|
+
body = JSON.parse(response.body)
|
63
|
+
error = body.fetch("error", "")
|
51
64
|
|
52
65
|
# let's try to be helpful about error messages
|
53
|
-
if
|
54
|
-
raise "Couldn't send slack message because the
|
55
|
-
elsif
|
56
|
-
raise "
|
57
|
-
elsif
|
58
|
-
raise "Tried to send Slack message
|
66
|
+
if ["token_revoked", "token_expired", "invalid_auth", "not_authed"].include?(error)
|
67
|
+
raise SlackMessage::ApiError, "Couldn't send slack message because the API key for profile '#{profile[:handle]}' is wrong."
|
68
|
+
elsif ["no_permission", "ekm_access_denied"].include?(error)
|
69
|
+
raise SlackMessage::ApiError, "Couldn't send slack message because the API key for profile '#{profile[:handle]}' isn't allowed to post messages."
|
70
|
+
elsif error == "channel_not_found"
|
71
|
+
raise SlackMessage::ApiError, "Tried to send Slack message to non-existent channel or user '#{target}'"
|
72
|
+
elsif error == "invalid_arguments"
|
73
|
+
raise SlackMessage::ApiError, "Tried to send Slack message with invalid payload."
|
59
74
|
elsif response.code == "302"
|
60
|
-
raise "Got 302 response while posting to Slack. Check your
|
75
|
+
raise SlackMessage::ApiError, "Got 302 response while posting to Slack. Check your API key for profile '#{profile[:handle]}'."
|
61
76
|
elsif response.code != "200"
|
62
|
-
raise "Got an error back from the Slack API (HTTP #{response.code}):\n#{response.body}"
|
77
|
+
raise SlackMessage::ApiError, "Got an error back from the Slack API (HTTP #{response.code}):\n#{response.body}"
|
63
78
|
end
|
64
79
|
|
65
80
|
response
|
66
81
|
end
|
67
82
|
|
68
|
-
|
69
|
-
|
83
|
+
# mostly test harness
|
84
|
+
def self.post_message(profile, params)
|
85
|
+
uri = URI("https://slack.com/api/chat.postMessage")
|
86
|
+
request = Net::HTTP::Post.new(uri).tap do |req|
|
87
|
+
req['Authorization'] = "Bearer #{profile[:api_token]}"
|
88
|
+
req['Content-type'] = "application/json; charset=utf-8"
|
89
|
+
req.body = params.to_json
|
90
|
+
end
|
91
|
+
|
92
|
+
Net::HTTP.start(uri.hostname, uri.port, use_ssl: true) do |http|
|
93
|
+
http.request(request)
|
94
|
+
end
|
70
95
|
end
|
96
|
+
|
97
|
+
private_class_method :post_message
|
71
98
|
end
|
@@ -1,9 +1,7 @@
|
|
1
1
|
module SlackMessage::Configuration
|
2
|
-
@@api_token = nil
|
3
2
|
@@profiles = {}
|
4
3
|
|
5
4
|
def self.reset
|
6
|
-
@@api_token = nil
|
7
5
|
@@profiles = {}
|
8
6
|
end
|
9
7
|
|
@@ -13,39 +11,29 @@ module SlackMessage::Configuration
|
|
13
11
|
|
14
12
|
###
|
15
13
|
|
16
|
-
def self.api_token=(token)
|
17
|
-
@@api_token = token
|
18
|
-
end
|
19
|
-
|
20
|
-
def self.api_token
|
21
|
-
unless @@api_token.is_a? String
|
22
|
-
raise ArgumentError, "Please set an API token to use API features."
|
23
|
-
end
|
24
|
-
|
25
|
-
@@api_token
|
26
|
-
end
|
27
|
-
|
28
|
-
###
|
29
|
-
|
30
14
|
def self.clear_profiles! # test harness, mainly
|
31
15
|
@@profiles = {}
|
32
16
|
end
|
33
17
|
|
34
|
-
def self.add_profile(handle = :default,
|
18
|
+
def self.add_profile(handle = :default, api_token:, name: nil, icon: nil, default_channel: nil)
|
35
19
|
if @@profiles.include?(handle)
|
36
20
|
warn "WARNING: Overriding profile '#{handle}' in SlackMessage config"
|
37
21
|
end
|
38
22
|
|
39
|
-
@@profiles[handle] = {
|
23
|
+
@@profiles[handle] = {
|
24
|
+
handle: handle,
|
25
|
+
api_token: api_token,
|
26
|
+
name: name,
|
27
|
+
icon: icon,
|
28
|
+
default_channel: default_channel
|
29
|
+
}
|
40
30
|
end
|
41
31
|
|
42
|
-
def self.profile(handle
|
32
|
+
def self.profile(handle)
|
43
33
|
unless @@profiles.include?(handle)
|
44
34
|
raise ArgumentError, "Unknown SlackMessage profile '#{handle}'."
|
45
35
|
end
|
46
36
|
|
47
|
-
@@profiles[handle]
|
48
|
-
profile[:name] = custom_name if !custom_name.nil?
|
49
|
-
end
|
37
|
+
@@profiles[handle]
|
50
38
|
end
|
51
39
|
end
|
data/lib/slack_message/dsl.rb
CHANGED
@@ -1,16 +1,21 @@
|
|
1
1
|
class SlackMessage::Dsl
|
2
|
-
attr_reader :body, :default_section, :custom_bot_name
|
2
|
+
attr_reader :body, :default_section, :custom_bot_name, :custom_bot_icon, :profile
|
3
|
+
attr_accessor :custom_notification
|
3
4
|
|
4
5
|
EMSPACE = " " # unicode emspace
|
5
6
|
|
6
|
-
def initialize(block)
|
7
|
+
def initialize(block, profile)
|
7
8
|
# Delegate missing methods to caller scope. Thanks 2008:
|
8
9
|
# https://www.dan-manges.com/blog/ruby-dsls-instance-eval-with-delegation
|
9
10
|
@caller_self = eval("self", block.binding)
|
10
11
|
|
11
|
-
@body
|
12
|
-
@
|
13
|
-
@
|
12
|
+
@body = []
|
13
|
+
@profile = profile
|
14
|
+
@default_section = Section.new(self)
|
15
|
+
|
16
|
+
@custom_bot_name = nil
|
17
|
+
@custom_bot_icon = nil
|
18
|
+
@custom_notification = nil
|
14
19
|
end
|
15
20
|
|
16
21
|
# allowable top-level entities within a block
|
@@ -18,7 +23,7 @@ class SlackMessage::Dsl
|
|
18
23
|
def section(&block)
|
19
24
|
finalize_default_section
|
20
25
|
|
21
|
-
section = Section.new.tap do |s|
|
26
|
+
section = Section.new(self).tap do |s|
|
22
27
|
s.instance_eval(&block)
|
23
28
|
end
|
24
29
|
|
@@ -52,6 +57,12 @@ class SlackMessage::Dsl
|
|
52
57
|
def context(text)
|
53
58
|
finalize_default_section
|
54
59
|
|
60
|
+
if text == "" || text.nil?
|
61
|
+
raise ArgumentError, "Cannot create a context block without a value."
|
62
|
+
end
|
63
|
+
|
64
|
+
text = self.enrich_text(text)
|
65
|
+
|
55
66
|
@body.push({ type: "context", elements: [{
|
56
67
|
type: "mrkdwn", text: text
|
57
68
|
}]})
|
@@ -72,12 +83,20 @@ class SlackMessage::Dsl
|
|
72
83
|
|
73
84
|
# end delegation
|
74
85
|
|
75
|
-
#
|
86
|
+
# bot / notification overrides
|
76
87
|
|
77
88
|
def bot_name(name)
|
78
89
|
@custom_bot_name = name
|
79
90
|
end
|
80
91
|
|
92
|
+
def bot_icon(icon)
|
93
|
+
@custom_bot_icon = icon
|
94
|
+
end
|
95
|
+
|
96
|
+
def notification_text(msg)
|
97
|
+
@custom_notification = msg
|
98
|
+
end
|
99
|
+
|
81
100
|
# end bot name
|
82
101
|
|
83
102
|
def render
|
@@ -89,6 +108,22 @@ class SlackMessage::Dsl
|
|
89
108
|
@caller_self.send meth, *args, &blk
|
90
109
|
end
|
91
110
|
|
111
|
+
EMAIL_TAG_PATTERN = /<[^@ \t\r\n\<]+@[^@ \t\r\n]+\.[^@ \t\r\n]+>/
|
112
|
+
|
113
|
+
# replace emails w/ real user IDs
|
114
|
+
def enrich_text(text_body)
|
115
|
+
text_body.scan(EMAIL_TAG_PATTERN).each do |email_tag|
|
116
|
+
raw_email = email_tag.gsub(/[><]/, '')
|
117
|
+
user_id = SlackMessage::Api::user_id_for(raw_email, profile)
|
118
|
+
|
119
|
+
text_body.gsub!(email_tag, "<@#{user_id}>") if user_id
|
120
|
+
rescue SlackMessage::ApiError => e
|
121
|
+
# swallow errors for not-found users
|
122
|
+
end
|
123
|
+
|
124
|
+
text_body
|
125
|
+
end
|
126
|
+
|
92
127
|
private
|
93
128
|
|
94
129
|
# when doing things that would generate new top-levels, first try
|
@@ -98,18 +133,25 @@ class SlackMessage::Dsl
|
|
98
133
|
@body.push(default_section.render)
|
99
134
|
end
|
100
135
|
|
101
|
-
@default_section = Section.new
|
136
|
+
@default_section = Section.new(self)
|
102
137
|
end
|
103
138
|
|
104
139
|
class Section
|
105
140
|
attr_reader :body
|
106
141
|
|
107
|
-
def initialize
|
142
|
+
def initialize(parent)
|
143
|
+
@parent = parent
|
108
144
|
@body = { type: "section" }
|
109
145
|
@list = List.new
|
110
146
|
end
|
111
147
|
|
112
148
|
def text(msg)
|
149
|
+
if msg == "" || msg.nil?
|
150
|
+
raise ArgumentError, "Cannot create a text node without a value."
|
151
|
+
end
|
152
|
+
|
153
|
+
msg = @parent.enrich_text(msg)
|
154
|
+
|
113
155
|
if @body.include?(:text)
|
114
156
|
@body[:text][:text] << "\n#{msg}"
|
115
157
|
|
@@ -119,17 +161,21 @@ class SlackMessage::Dsl
|
|
119
161
|
end
|
120
162
|
|
121
163
|
def ul(elements)
|
122
|
-
raise
|
123
|
-
|
124
|
-
|
125
|
-
)
|
164
|
+
raise ArgumentError, "Please pass an array when creating a ul." unless elements.respond_to?(:map)
|
165
|
+
|
166
|
+
msg = elements.map { |text| "#{EMSPACE}• #{text}" }.join("\n")
|
167
|
+
msg = @parent.enrich_text(msg)
|
168
|
+
|
169
|
+
text(msg)
|
126
170
|
end
|
127
171
|
|
128
172
|
def ol(elements)
|
129
|
-
raise
|
130
|
-
|
131
|
-
|
132
|
-
)
|
173
|
+
raise ArgumentError, "Please pass an array when creating an ol." unless elements.respond_to?(:map)
|
174
|
+
|
175
|
+
msg = elements.map.with_index(1) { |text, idx| "#{EMSPACE}#{idx}. #{text}" }.join("\n")
|
176
|
+
msg = @parent.enrich_text(msg)
|
177
|
+
|
178
|
+
text(msg)
|
133
179
|
end
|
134
180
|
|
135
181
|
# styles: default, primary, danger
|
@@ -186,6 +232,11 @@ class SlackMessage::Dsl
|
|
186
232
|
end
|
187
233
|
|
188
234
|
def list_item(title, value)
|
235
|
+
if value == "" || value.nil?
|
236
|
+
raise ArgumentError, "Can't create a list item for '#{title}' without a value."
|
237
|
+
end
|
238
|
+
|
239
|
+
value = @parent.enrich_text(value)
|
189
240
|
@list.add(title, value)
|
190
241
|
end
|
191
242
|
|
@@ -198,7 +249,16 @@ class SlackMessage::Dsl
|
|
198
249
|
end
|
199
250
|
|
200
251
|
def render
|
252
|
+
unless has_content?
|
253
|
+
raise ArgumentError, "Can't create a section with no content."
|
254
|
+
end
|
255
|
+
|
201
256
|
body[:fields] = @list.render if @list.any?
|
257
|
+
|
258
|
+
if body[:text] && body[:text][:text] && !@parent.custom_notification
|
259
|
+
@parent.notification_text(body[:text][:text])
|
260
|
+
end
|
261
|
+
|
202
262
|
body
|
203
263
|
end
|
204
264
|
end
|
data/lib/slack_message/rspec.rb
CHANGED
@@ -4,6 +4,15 @@ require 'rspec/mocks'
|
|
4
4
|
# Honestly, this code is what happens when you do not understand the RSpec
|
5
5
|
# custom expectation API really at all, but you really want to create your
|
6
6
|
# matcher. This code is soo baaad.
|
7
|
+
#
|
8
|
+
# We override API calls by entirely replacing the low-level API method. Then we
|
9
|
+
# use our overridden version to capture and record calls. When someone creates
|
10
|
+
# a new expectation, an object is created, so we allow that object to register
|
11
|
+
# itself to receive notification when a slack message _would have_ been posted.
|
12
|
+
#
|
13
|
+
# Then once the expectation is fulfilled, that class unregisters itself so that
|
14
|
+
# it can be cleaned up properly.
|
15
|
+
#
|
7
16
|
|
8
17
|
module SlackMessage::RSpec
|
9
18
|
extend RSpec::Matchers::DSL
|
@@ -21,16 +30,32 @@ module SlackMessage::RSpec
|
|
21
30
|
FauxResponse = Struct.new(:code, :body)
|
22
31
|
|
23
32
|
def self.included(_)
|
24
|
-
SlackMessage::Api.singleton_class.undef_method(:
|
25
|
-
SlackMessage::Api.define_singleton_method(:
|
33
|
+
SlackMessage::Api.singleton_class.undef_method(:post_message)
|
34
|
+
SlackMessage::Api.define_singleton_method(:post_message) do |profile, params|
|
26
35
|
@@listeners.each do |listener|
|
27
|
-
listener.record_call(params.merge(profile: profile
|
36
|
+
listener.record_call(params.merge(profile: profile))
|
28
37
|
end
|
29
38
|
|
30
|
-
|
39
|
+
response = {"ok"=>true,
|
40
|
+
"channel"=>"D12345678",
|
41
|
+
"ts"=>"1635863996.002300",
|
42
|
+
"message"=>
|
43
|
+
{"type"=>"message", "subtype"=>"bot_message",
|
44
|
+
"text"=>"foo",
|
45
|
+
"ts"=>"1635863996.002300",
|
46
|
+
"username"=>"SlackMessage",
|
47
|
+
"icons"=>{"emoji"=>":successkid:"},
|
48
|
+
"bot_id"=>"B1234567890",
|
49
|
+
"blocks"=>
|
50
|
+
[{"type"=>"section",
|
51
|
+
"block_id"=>"hAh7",
|
52
|
+
"text"=>{"type"=>"mrkdwn", "text"=>"foo", "verbatim"=>false}}]}}
|
53
|
+
|
54
|
+
return FauxResponse.new('200', response.to_json)
|
31
55
|
end
|
32
56
|
end
|
33
57
|
|
58
|
+
# w/ channel
|
34
59
|
matcher :post_slack_message_to do |expected|
|
35
60
|
match do |actual|
|
36
61
|
@instance ||= PostTo.new
|
@@ -40,9 +65,91 @@ module SlackMessage::RSpec
|
|
40
65
|
@instance.enforce_expectations
|
41
66
|
end
|
42
67
|
|
43
|
-
chain :with_content_matching do |
|
68
|
+
chain :with_content_matching do |content|
|
44
69
|
@instance ||= PostTo.new
|
45
|
-
@instance.with_content_matching(
|
70
|
+
@instance.with_content_matching(content)
|
71
|
+
end
|
72
|
+
|
73
|
+
failure_message { @instance.failure_message }
|
74
|
+
failure_message_when_negated { @instance.failure_message_when_negated }
|
75
|
+
|
76
|
+
supports_block_expectations
|
77
|
+
end
|
78
|
+
|
79
|
+
# no channel
|
80
|
+
matcher :post_to_slack do |expected|
|
81
|
+
match do |actual|
|
82
|
+
@instance ||= PostTo.new
|
83
|
+
|
84
|
+
actual.call
|
85
|
+
@instance.enforce_expectations
|
86
|
+
end
|
87
|
+
|
88
|
+
chain :with_content_matching do |content|
|
89
|
+
@instance ||= PostTo.new
|
90
|
+
@instance.with_content_matching(content)
|
91
|
+
end
|
92
|
+
|
93
|
+
failure_message { @instance.failure_message }
|
94
|
+
failure_message_when_negated { @instance.failure_message_when_negated }
|
95
|
+
|
96
|
+
supports_block_expectations
|
97
|
+
end
|
98
|
+
|
99
|
+
# name / profile matcher
|
100
|
+
matcher :post_slack_message_as do |expected|
|
101
|
+
match do |actual|
|
102
|
+
@instance ||= PostTo.new
|
103
|
+
@instance.with_profile(expected)
|
104
|
+
|
105
|
+
actual.call
|
106
|
+
@instance.enforce_expectations
|
107
|
+
end
|
108
|
+
|
109
|
+
chain :with_content_matching do |content|
|
110
|
+
@instance ||= PostTo.new
|
111
|
+
@instance.with_content_matching(content)
|
112
|
+
end
|
113
|
+
|
114
|
+
failure_message { @instance.failure_message }
|
115
|
+
failure_message_when_negated { @instance.failure_message_when_negated }
|
116
|
+
|
117
|
+
supports_block_expectations
|
118
|
+
end
|
119
|
+
|
120
|
+
# icon matcher
|
121
|
+
matcher :post_slack_message_with_icon do |expected|
|
122
|
+
match do |actual|
|
123
|
+
@instance ||= PostTo.new
|
124
|
+
@instance.with_icon(expected)
|
125
|
+
|
126
|
+
actual.call
|
127
|
+
@instance.enforce_expectations
|
128
|
+
end
|
129
|
+
|
130
|
+
chain :with_content_matching do |content|
|
131
|
+
@instance ||= PostTo.new
|
132
|
+
@instance.with_content_matching(content)
|
133
|
+
end
|
134
|
+
|
135
|
+
failure_message { @instance.failure_message }
|
136
|
+
failure_message_when_negated { @instance.failure_message_when_negated }
|
137
|
+
|
138
|
+
supports_block_expectations
|
139
|
+
end
|
140
|
+
|
141
|
+
matcher :post_slack_message_with_icon_matching do |expected|
|
142
|
+
match do |actual|
|
143
|
+
@instance ||= PostTo.new
|
144
|
+
@instance.with_icon_matching(expected)
|
145
|
+
|
146
|
+
actual.call
|
147
|
+
@instance.enforce_expectations
|
148
|
+
end
|
149
|
+
|
150
|
+
chain :with_content_matching do |content|
|
151
|
+
@instance ||= PostTo.new
|
152
|
+
@instance.with_content_matching(content)
|
46
153
|
end
|
47
154
|
|
48
155
|
failure_message { @instance.failure_message }
|
@@ -54,8 +161,11 @@ module SlackMessage::RSpec
|
|
54
161
|
class PostTo
|
55
162
|
def initialize
|
56
163
|
@captured_calls = []
|
57
|
-
@
|
164
|
+
@content = nil
|
58
165
|
@channel = nil
|
166
|
+
@profile = nil
|
167
|
+
@icon = nil
|
168
|
+
@icon_matching = nil
|
59
169
|
|
60
170
|
SlackMessage::RSpec.register_expectation_listener(self)
|
61
171
|
end
|
@@ -68,41 +178,64 @@ module SlackMessage::RSpec
|
|
68
178
|
@channel = channel
|
69
179
|
end
|
70
180
|
|
71
|
-
def
|
72
|
-
|
73
|
-
@content_expectation = content_expectation
|
181
|
+
def with_icon(icon)
|
182
|
+
@icon = icon
|
74
183
|
end
|
75
184
|
|
76
|
-
def
|
77
|
-
|
78
|
-
|
185
|
+
def with_icon_matching(icon)
|
186
|
+
raise ArgumentError unless icon.is_a? Regexp
|
187
|
+
@icon_matching = icon
|
188
|
+
end
|
189
|
+
|
190
|
+
def with_content_matching(content)
|
191
|
+
raise ArgumentError unless content.is_a? Regexp
|
192
|
+
@content = content
|
79
193
|
end
|
80
194
|
|
81
|
-
def
|
82
|
-
@
|
195
|
+
def with_profile(profile)
|
196
|
+
@profile = profile
|
83
197
|
end
|
84
198
|
|
85
|
-
def
|
86
|
-
|
199
|
+
def enforce_expectations
|
200
|
+
SlackMessage::RSpec.unregister_expectation_listener(self)
|
87
201
|
|
88
|
-
|
202
|
+
@captured_calls
|
203
|
+
.filter { |call| !@channel || call[:channel] == @channel }
|
204
|
+
.filter { |call| !@profile || [call[:profile][:handle], call[:username]].include?(@profile) }
|
205
|
+
.filter { |call| !@content || call.fetch(:blocks).to_s =~ @content }
|
206
|
+
.filter { |call| !@icon || call.fetch(:icon_emoji, call.fetch(:icon_url, '')) == @icon }
|
207
|
+
.filter { |call| !@icon_matching || call.fetch(:icon_emoji, call.fetch(:icon_url, '')) =~ @icon_matching }
|
208
|
+
.any?
|
89
209
|
end
|
90
210
|
|
91
211
|
def failure_message
|
92
|
-
|
93
|
-
"expected block to post slack message to '#{@channel}' with content matching #{@content_expectation.inspect}"
|
94
|
-
else
|
95
|
-
"expected block to post slack message to '#{@channel}'"
|
96
|
-
end
|
212
|
+
"expected block to #{failure_expression}"
|
97
213
|
end
|
98
214
|
|
99
|
-
# TODO: does content_matching even make sense for negated test?
|
100
215
|
def failure_message_when_negated
|
101
|
-
|
102
|
-
|
216
|
+
"expected block not to #{failure_expression}"
|
217
|
+
end
|
218
|
+
|
219
|
+
def failure_expression
|
220
|
+
concat = []
|
221
|
+
|
222
|
+
if @channel
|
223
|
+
concat << "post a slack message to '#{@channel}'"
|
224
|
+
elsif @profile
|
225
|
+
concat << "post a slack message as '#{@profile}'"
|
226
|
+
elsif @icon
|
227
|
+
concat << "post a slack message with icon '#{@icon}'"
|
228
|
+
elsif @icon_matching
|
229
|
+
concat << "post a slack message with icon matching '#{@icon_matching.inspect}'"
|
103
230
|
else
|
104
|
-
|
231
|
+
concat << "post a slack message"
|
232
|
+
end
|
233
|
+
|
234
|
+
if @content
|
235
|
+
concat << "with content matching #{@content.inspect}"
|
105
236
|
end
|
237
|
+
|
238
|
+
concat.join " "
|
106
239
|
end
|
107
240
|
end
|
108
241
|
end
|
data/lib/slack_message.rb
CHANGED
@@ -3,6 +3,8 @@ module SlackMessage
|
|
3
3
|
require 'slack_message/api'
|
4
4
|
require 'slack_message/configuration'
|
5
5
|
|
6
|
+
class ApiError < RuntimeError; end
|
7
|
+
|
6
8
|
def self.configuration
|
7
9
|
Configuration
|
8
10
|
end
|
@@ -11,39 +13,43 @@ module SlackMessage
|
|
11
13
|
configuration.configure(&block)
|
12
14
|
end
|
13
15
|
|
14
|
-
def self.
|
15
|
-
|
16
|
+
def self.user_id(email, profile_name = :default)
|
17
|
+
profile = Configuration.profile(profile_name)
|
18
|
+
Api.user_id_for(email, profile)
|
16
19
|
end
|
17
20
|
|
18
21
|
def self.post_to(target, as: :default, &block)
|
19
|
-
|
22
|
+
profile = Configuration.profile(as)
|
23
|
+
|
24
|
+
payload = Dsl.new(block, profile).tap do |instance|
|
20
25
|
instance.instance_eval(&block)
|
21
26
|
end
|
22
27
|
|
23
|
-
|
24
|
-
target = user_id_for(target) if target =~ /^\S{1,}@\S{2,}\.\S{2,}$/
|
28
|
+
target = Api::user_id_for(target, profile) if target =~ /^\S{1,}@\S{2,}\.\S{2,}$/
|
25
29
|
|
26
|
-
Api.post(payload
|
30
|
+
Api.post(payload, target, profile)
|
27
31
|
end
|
28
32
|
|
29
33
|
def self.post_as(profile_name, &block)
|
30
|
-
|
31
|
-
instance.instance_eval(&block)
|
32
|
-
end
|
33
|
-
|
34
|
-
profile = Configuration.profile(profile_name, custom_name: payload.custom_bot_name)
|
34
|
+
profile = Configuration.profile(profile_name)
|
35
35
|
if profile[:default_channel].nil?
|
36
36
|
raise ArgumentError, "Sorry, you need to specify a default_channel for profile #{profile_name} to use post_as"
|
37
37
|
end
|
38
38
|
|
39
|
+
payload = Dsl.new(block, profile).tap do |instance|
|
40
|
+
instance.instance_eval(&block)
|
41
|
+
end
|
42
|
+
|
39
43
|
target = profile[:default_channel]
|
40
|
-
target = user_id_for(target) if target =~ /^\S{1,}@\S{2,}\.\S{2,}$/
|
44
|
+
target = Api::user_id_for(target, profile) if target =~ /^\S{1,}@\S{2,}\.\S{2,}$/
|
41
45
|
|
42
|
-
Api.post(payload
|
46
|
+
Api.post(payload, target, profile)
|
43
47
|
end
|
44
48
|
|
45
|
-
def self.build(&block)
|
46
|
-
|
49
|
+
def self.build(profile_name = :default, &block)
|
50
|
+
profile = Configuration.profile(profile_name)
|
51
|
+
|
52
|
+
Dsl.new(block, profile).tap do |instance|
|
47
53
|
instance.instance_eval(&block)
|
48
54
|
end.send(:render)
|
49
55
|
end
|
data/slack_message.gemspec
CHANGED
data/spec/slack_message_spec.rb
CHANGED
@@ -1,21 +1,14 @@
|
|
1
1
|
require 'spec_helper'
|
2
2
|
|
3
3
|
RSpec.describe SlackMessage do
|
4
|
-
describe "API convenience" do
|
5
|
-
it "can grab user IDs" do
|
6
|
-
SlackMessage.configure { |c| c.api_token = "asdf" }
|
7
|
-
allow(Net::HTTP).to receive(:start).and_return(
|
8
|
-
double(code: "200", body: '{ "user": { "id": "ABC123" }}')
|
9
|
-
)
|
10
|
-
|
11
|
-
result = SlackMessage.user_id_for("hello@joemastey.com")
|
12
|
-
expect(result).to eq("ABC123")
|
13
|
-
end
|
14
|
-
end
|
15
|
-
|
16
4
|
describe "DSL" do
|
17
5
|
describe "#build" do
|
18
6
|
it "renders some JSON" do
|
7
|
+
SlackMessage.configure do |config|
|
8
|
+
config.clear_profiles!
|
9
|
+
config.add_profile(name: 'default profile', api_token: 'abc123')
|
10
|
+
end
|
11
|
+
|
19
12
|
expected_output = [
|
20
13
|
{ type: "section",
|
21
14
|
text: { text: "foo", type: "mrkdwn" }
|
@@ -32,32 +25,11 @@ RSpec.describe SlackMessage do
|
|
32
25
|
end
|
33
26
|
|
34
27
|
describe "configuration" do
|
35
|
-
after do
|
36
|
-
SlackMessage.configuration.reset
|
37
|
-
end
|
38
|
-
|
39
|
-
it "allows you to set an API key" do
|
40
|
-
SlackMessage.configure do |config|
|
41
|
-
config.api_token = "abc123"
|
42
|
-
end
|
43
|
-
|
44
|
-
expect(SlackMessage.configuration.api_token).to eq("abc123")
|
45
|
-
end
|
46
|
-
|
47
|
-
it "raises errors for missing configuration" do
|
48
|
-
SlackMessage.configure do |config|
|
49
|
-
#config.api_token = "abc123"
|
50
|
-
end
|
51
|
-
|
52
|
-
expect {
|
53
|
-
SlackMessage.configuration.api_token
|
54
|
-
}.to raise_error(ArgumentError)
|
55
|
-
end
|
56
|
-
|
57
28
|
it "lets you add and fetch profiles" do
|
58
29
|
SlackMessage.configure do |config|
|
59
|
-
config.
|
60
|
-
config.add_profile(
|
30
|
+
config.clear_profiles!
|
31
|
+
config.add_profile(name: 'default profile', api_token: 'abc123')
|
32
|
+
config.add_profile(:nonstandard, name: 'another profile', api_token: 'abc123')
|
61
33
|
end
|
62
34
|
|
63
35
|
expect(SlackMessage.configuration.profile(:default)[:name]).to eq('default profile')
|
@@ -73,7 +45,7 @@ RSpec.describe SlackMessage do
|
|
73
45
|
before do
|
74
46
|
SlackMessage.configure do |config|
|
75
47
|
config.clear_profiles!
|
76
|
-
config.add_profile(name: 'default profile',
|
48
|
+
config.add_profile(name: 'default profile', api_token: 'abc123')
|
77
49
|
end
|
78
50
|
end
|
79
51
|
|
@@ -87,12 +59,105 @@ RSpec.describe SlackMessage do
|
|
87
59
|
}.to post_slack_message_to('#general').with_content_matching(/foo/)
|
88
60
|
end
|
89
61
|
|
90
|
-
it "
|
62
|
+
it "resets state properly" do
|
91
63
|
expect {
|
92
64
|
SlackMessage.post_to('#general') { text "foo" }
|
93
65
|
}.to post_slack_message_to('#general')
|
94
66
|
|
95
67
|
expect { }.not_to post_slack_message_to('#general')
|
96
68
|
end
|
69
|
+
|
70
|
+
it "lets you assert by profile name" do
|
71
|
+
SlackMessage.configure do |config|
|
72
|
+
config.clear_profiles!
|
73
|
+
config.add_profile(:schmoebot, name: 'Schmoe', api_token: 'abc123', icon: ':schmoebot:', default_channel: '#schmoes')
|
74
|
+
end
|
75
|
+
|
76
|
+
expect {
|
77
|
+
SlackMessage.post_as(:schmoebot) { text "foo" }
|
78
|
+
}.to post_slack_message_to('#schmoes')
|
79
|
+
|
80
|
+
expect {
|
81
|
+
SlackMessage.post_as(:schmoebot) { text "foo" }
|
82
|
+
}.to post_slack_message_as(:schmoebot)
|
83
|
+
|
84
|
+
expect {
|
85
|
+
SlackMessage.post_as(:schmoebot) { text "foo" }
|
86
|
+
}.to post_slack_message_as('Schmoe').with_content_matching(/foo/)
|
87
|
+
end
|
88
|
+
|
89
|
+
it "lets you assert by profile image" do
|
90
|
+
SlackMessage.configure do |config|
|
91
|
+
config.clear_profiles!
|
92
|
+
config.add_profile(:schmoebot, name: 'Schmoe', api_token: 'abc123', icon: ':schmoebot:', default_channel: '#schmoes')
|
93
|
+
end
|
94
|
+
|
95
|
+
expect {
|
96
|
+
SlackMessage.post_as(:schmoebot) { text "foo" }
|
97
|
+
}.to post_slack_message_with_icon(':schmoebot:')
|
98
|
+
|
99
|
+
expect {
|
100
|
+
SlackMessage.post_as(:schmoebot) do
|
101
|
+
bot_icon ':schmalternate:'
|
102
|
+
text "foo"
|
103
|
+
end
|
104
|
+
}.to post_slack_message_with_icon(':schmalternate:')
|
105
|
+
|
106
|
+
expect {
|
107
|
+
SlackMessage.post_as(:schmoebot) do
|
108
|
+
bot_icon 'https://thispersondoesnotexist.com/image'
|
109
|
+
text "foo"
|
110
|
+
end
|
111
|
+
}.to post_slack_message_with_icon_matching(/thisperson/)
|
112
|
+
end
|
113
|
+
|
114
|
+
it "lets you assert notification text" do
|
115
|
+
# TODO :|
|
116
|
+
end
|
117
|
+
|
118
|
+
it "can assert more generally too tbh" do
|
119
|
+
expect {
|
120
|
+
SlackMessage.post_to('#general') { text "foo" }
|
121
|
+
}.to post_to_slack.with_content_matching(/foo/)
|
122
|
+
end
|
123
|
+
end
|
124
|
+
|
125
|
+
describe "API convenience" do
|
126
|
+
let(:profile) { SlackMessage::Configuration.profile(:default) }
|
127
|
+
|
128
|
+
before do
|
129
|
+
SlackMessage.configure do |config|
|
130
|
+
config.clear_profiles!
|
131
|
+
config.add_profile(name: 'default profile', api_token: 'abc123')
|
132
|
+
end
|
133
|
+
end
|
134
|
+
|
135
|
+
it "can grab user IDs" do
|
136
|
+
allow(Net::HTTP).to receive(:start).and_return(
|
137
|
+
double(code: "200", body: '{ "user": { "id": "ABC123" }}')
|
138
|
+
)
|
139
|
+
|
140
|
+
result = SlackMessage::Api.user_id_for("hello@joemastey.com", profile)
|
141
|
+
expect(result).to eq("ABC123")
|
142
|
+
end
|
143
|
+
|
144
|
+
it "converts user IDs within text when tagged properly" do
|
145
|
+
allow(SlackMessage::Api).to receive(:user_id_for).and_return('ABC123')
|
146
|
+
|
147
|
+
expect {
|
148
|
+
SlackMessage.post_to('#general') { text("Working: <hello@joemastey.com> ") }
|
149
|
+
}.to post_to_slack.with_content_matching(/ABC123/)
|
150
|
+
|
151
|
+
expect {
|
152
|
+
SlackMessage.post_to('#general') { text("Not Tagged: hello@joemastey.com ") }
|
153
|
+
}.to post_to_slack.with_content_matching(/hello@joemastey.com/)
|
154
|
+
|
155
|
+
|
156
|
+
allow(SlackMessage::Api).to receive(:user_id_for).and_raise(SlackMessage::ApiError)
|
157
|
+
|
158
|
+
expect {
|
159
|
+
SlackMessage.post_to('#general') { text("Not User: <nuffin@nuffin.nuffin>") }
|
160
|
+
}.to post_to_slack.with_content_matching(/\<nuffin@nuffin.nuffin\>/)
|
161
|
+
end
|
97
162
|
end
|
98
163
|
end
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: slack_message
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version:
|
4
|
+
version: 2.2.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Joe Mastey
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2021-
|
11
|
+
date: 2021-11-20 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: rspec
|