slack_message 1.8.0 → 2.2.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 455886af2c6e775c37db510da7d5003abd131711107e9a18af2e10a714f9c154
4
- data.tar.gz: '029d322491f3e63df60cd61eb2c6528132cce722a4ca0fa2db47fd790702fa04'
3
+ metadata.gz: 368533ab7e4dae44ffbef5ccf8afef048def96d964a6af64c294c23ba617dfa3
4
+ data.tar.gz: f9397472bbc5c5db81dd1c9d7715e5015f4ec455948e7a29afbb11ef8c8a3d01
5
5
  SHA512:
6
- metadata.gz: dd4bc35189956140e5409d1d2e325e2cb6b9423e23fbcb6f81399281b10e4447b8c1bed8e8447f7dac59b5282e9c39b4907231225f0858abe6116fe9419b9269
7
- data.tar.gz: d91718d74c17d0b1c79485de6311a70a579b1f80916c4328afa813800735b12d29e1069cfdc1721f1954839a6b55d25c0fa292ed41d02350ac0c1665a3475aa2
6
+ metadata.gz: 5cce143a95d508694221f1c667428f850ce7aa537e73f61c1869498b68a5751afe2ac386215b96702a1a27bcad3758c474ba09870a1cd7a31a7c33a17ebd4d53
7
+ data.tar.gz: 7f81e1876f231899588b174c87c4a0a3ca3bcbc4057216092311b300cff7d23d6541083b5e481e811795042538e2e6713b1a9f5eb5f6eba345e79960335cad91
data/CHANGELOG.md CHANGED
@@ -1,9 +1,34 @@
1
1
  # Changelog
2
2
 
3
- ## [Unreleased]
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
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- slack_message (1.7.1)
4
+ slack_message (2.2.0)
5
5
 
6
6
  GEM
7
7
  remote: https://rubygems.org/
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. It has zero dependencies and
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 configure at least one profile to use to post
26
- to slack. Get a [Webhook URL](https://slack.com/help/articles/115005265063-Incoming-webhooks-for-Slack)
27
- from Slack and configure it like this:
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
- webhook_url = 'https://hooks.slack.com/services/T00000000/B00000000/XXXXXXXXXXXXXXXXXXXXXXXX'
50
+ api_token = 'xoxb-11111111111-2222222222-33333333333333333'
32
51
 
33
- config.add_profile(name: 'Slack Notifier', url: webhook_url)
52
+ config.add_profile(api_token: api_token)
34
53
  end
35
54
  ```
36
55
 
37
- You should probably keep that webhook in a safe place like `ENV`. If using this
38
- gem with Rails, place this code in somewhere like
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 you want to post to multiple different webhook addresses (say, if you have
44
- several different bots that post to different channels as different identities),
45
- you can configure those profiles as well, by giving each of them a name:
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', url: ENV['SLACK_WEBHOOK_URL'])
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, name: 'Prod Alert Bot', url: ENV['SLACK_PROD_ALERT_WEBHOOK_URL'])
54
- config.add_profile(:sidekiq_bot, name: 'Sidekiq Bot', url: ENV['SLACK_SIDEKIQ_WEBHOOK_URL'])
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
- If you frequently ping the same channel with the same bot, and don't want to
59
- continually specify the channel name, you can specify a default channel and
60
- post using the `post_as` method. It is otherwise identical to `post_to`, but
61
- allows you to omit the channel argument:
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(:prod_alert_bot,
66
- name: 'Prod Alert Bot',
67
- url: ENV['SLACK_PROD_ALERT_WEBHOOK_URL'],
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(:prod_alert_bot) do
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
- Note that `post_as` does not allow you to choose a channel (because that's just
78
- the same as using `post_to`), so you really do have to specify `default_channel`.
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
- If you've configured an API key for user search (see above in configuration),
151
- it's just as easy to send messages directly to users:
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
- SlackMessage.post_to('hello@joemastey.com') do
155
- text "We did it! :thumbsup:"
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, see Slack's [Block Kit
162
- Builder](https://app.slack.com/block-kit-builder/) to understand the structure
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 their name:
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", "https://yoursite.com/sidekiq", style: :danger
226
+ link_button "Sidekiq Dashboard", sidekiq_dashboard_url, style: :danger
204
227
  end
205
228
  ```
206
229
 
207
- You can also use a custom name when sending a message:
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
- Since this gem was built out of an existing need that _didn't_ include most of
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
- Also, some behaviors that are still planned but not yet added:
350
+ Some behaviors that are still planned but not yet added:
274
351
 
275
352
  * some API documentation amirite?
276
- * allow custom http_options in configuration
353
+ * custom http_options in configuration
277
354
  * more of BlockKit's options
278
- * any interactive elements at all (I don't understand them yet)
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 pulls.
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
 
@@ -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 #{token}"
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:\n#{response.body}"
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
- response = execute_post_form(uri, params, profile[:handle])
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 response.body == "invalid_token"
54
- raise "Couldn't send slack message because the URL for profile '#{profile[:handle]}' is wrong."
55
- elsif response.body == "channel_not_found"
56
- raise "Tried to send Slack message to non-existent channel or user '#{target}'"
57
- elsif response.body == "missing_text_or_fallback_or_attachments"
58
- raise "Tried to send Slack message with invalid payload."
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 webhook URL for '#{profile[:handle]}'."
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
- def self.execute_post_form(uri, params, _profile)
69
- Net::HTTP.post_form uri, { payload: params.to_json }
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, name:, url:, default_channel: nil)
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] = { name: name, url: url, handle: handle, default_channel: default_channel }
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, custom_name: nil)
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].tap do |profile|
48
- profile[:name] = custom_name if !custom_name.nil?
49
- end
37
+ @@profiles[handle]
50
38
  end
51
39
  end
@@ -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
- @default_section = Section.new
13
- @custom_bot_name = nil
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
- # custom bot name
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 Arguments, "please pass an array" unless elements.respond_to?(:map)
123
- text(
124
- elements.map { |text| "#{EMSPACE}• #{text}" }.join("\n")
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 Arguments, "please pass an array" unless elements.respond_to?(:map)
130
- text(
131
- elements.map.with_index(1) { |text, idx| "#{EMSPACE}#{idx}. #{text}" }.join("\n")
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
@@ -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(:execute_post_form)
25
- SlackMessage::Api.define_singleton_method(:execute_post_form) do |uri, params, profile|
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, uri: uri))
36
+ listener.record_call(params.merge(profile: profile))
28
37
  end
29
38
 
30
- return FauxResponse.new('200', 'ok')
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 |content_expectation|
68
+ chain :with_content_matching do |content|
44
69
  @instance ||= PostTo.new
45
- @instance.with_content_matching(content_expectation)
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
- @content_expectation = nil
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 with_content_matching(content_expectation)
72
- raise ArgumentError unless content_expectation.is_a? Regexp
73
- @content_expectation = content_expectation
181
+ def with_icon(icon)
182
+ @icon = icon
74
183
  end
75
184
 
76
- def enforce_expectations
77
- SlackMessage::RSpec.unregister_expectation_listener(self)
78
- matching_messages.any? { |msg| body_matches_expectation?(msg.fetch(:blocks)) }
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 matching_messages
82
- @captured_calls.select { |c| c[:channel] == @channel }
195
+ def with_profile(profile)
196
+ @profile = profile
83
197
  end
84
198
 
85
- def body_matches_expectation?(sent)
86
- return true unless @content_expectation
199
+ def enforce_expectations
200
+ SlackMessage::RSpec.unregister_expectation_listener(self)
87
201
 
88
- sent.to_s =~ @content_expectation
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
- if @content_expectation
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
- if @content_expectation
102
- "expected block not to post slack message to '#{@channel}' with content matching #{@content_expectation.inspect}"
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
- "expected block not to post slack message to '#{@channel}'"
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.user_id_for(email) # spooky undocumented public method 👻
15
- Api::user_id_for(email)
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
- payload = Dsl.new(block).tap do |instance|
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
- profile = Configuration.profile(as, custom_name: payload.custom_bot_name)
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.render, target, profile)
30
+ Api.post(payload, target, profile)
27
31
  end
28
32
 
29
33
  def self.post_as(profile_name, &block)
30
- payload = Dsl.new(block).tap do |instance|
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.render, target, profile)
46
+ Api.post(payload, target, profile)
43
47
  end
44
48
 
45
- def self.build(&block)
46
- Dsl.new(block).tap do |instance|
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
@@ -1,6 +1,6 @@
1
1
  Gem::Specification.new do |gem|
2
2
  gem.name = 'slack_message'
3
- gem.version = "1.8.0"
3
+ gem.version = "2.2.0"
4
4
  gem.summary = "A nice DSL for composing rich messages in Slack"
5
5
  gem.authors = ["Joe Mastey"]
6
6
  gem.email = 'hello@joemastey.com'
@@ -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.add_profile(name: 'default profile', url: 'http://hooks.slack.com/1234/')
60
- config.add_profile(:nonstandard, name: 'another profile', url: 'http://hooks.slack.com/1234/')
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', url: 'http://hooks.slack.com/1234/')
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 "is not stateful" do
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: 1.8.0
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-10-07 00:00:00.000000000 Z
11
+ date: 2021-11-20 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: rspec