slack_message 2.1.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: e92b2faa8e8b8118a0e168cef488d64b94255c841d774909647ede3d9a9faf6b
4
- data.tar.gz: d494af801908aa9f5cf25141b012df26e23a69f8ef9389c3c8bb580c46641133
3
+ metadata.gz: 368533ab7e4dae44ffbef5ccf8afef048def96d964a6af64c294c23ba617dfa3
4
+ data.tar.gz: f9397472bbc5c5db81dd1c9d7715e5015f4ec455948e7a29afbb11ef8c8a3d01
5
5
  SHA512:
6
- metadata.gz: 6fceee6aeb36f7fc6b855c1ce90f647b67ce480c78e62ec2fe45bbfc347e6c9efa6ca910804c51b0edb12321953d96014ac137572d2140328109444f68512ca4
7
- data.tar.gz: a9fe4697d85ac2f91197a39dacc46d1740c251f8472dcb91e8ba2132caac5c9f029cb5f88894f39542e1436ce554562d87108661863c2574655e64c866c5115e
6
+ metadata.gz: 5cce143a95d508694221f1c667428f850ce7aa537e73f61c1869498b68a5751afe2ac386215b96702a1a27bcad3758c474ba09870a1cd7a31a7c33a17ebd4d53
7
+ data.tar.gz: 7f81e1876f231899588b174c87c4a0a3ca3bcbc4057216092311b300cff7d23d6541083b5e481e811795042538e2e6713b1a9f5eb5f6eba345e79960335cad91
data/CHANGELOG.md CHANGED
@@ -1,6 +1,13 @@
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.
4
11
 
5
12
  ## [2.1.0] - 2021-11-01
6
13
  - Change to use Slack Apps for all profiles. This should allow growth toward
data/Gemfile.lock CHANGED
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- slack_message (2.1.0)
4
+ slack_message (2.2.0)
5
5
 
6
6
  GEM
7
7
  remote: https://rubygems.org/
data/README.md CHANGED
@@ -30,8 +30,8 @@ opinionated stances on how to make use of that API. For instance:
30
30
  look it up as an email address.
31
31
  * A few little hacks on the block syntax, such as adding a `blank_line` (which
32
32
  doesn't exist in the API), or leading spaces.
33
- * Configuration should be as simple as possible. But as much work as possible
34
- should be moved from callers into configuration.
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.
35
35
 
36
36
  Usage
37
37
  ------------
@@ -238,6 +238,55 @@ SlackMessage.post_to('#general') do
238
238
  end
239
239
  ```
240
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
+
241
290
  ### Testing
242
291
 
243
292
  You can do some basic testing against SlackMessage, at least if you use RSpec!
@@ -301,12 +350,12 @@ expand the DSL to include more useful features.
301
350
  Some behaviors that are still planned but not yet added:
302
351
 
303
352
  * some API documentation amirite?
304
- * allow custom http_options in configuration
353
+ * custom http_options in configuration
305
354
  * more of BlockKit's options
306
- * any interactive elements at all (I don't understand them yet)
355
+ * any interactive elements at all
307
356
  * editing / updating messages
308
357
  * multiple recipients
309
- * more interesting return types for your message (probably related to the above)
358
+ * more interesting return types for your message
310
359
  * richer text formatting (for instance, `ul` is currently a hack)
311
360
 
312
361
  Contributing
@@ -15,21 +15,23 @@ class SlackMessage::Api
15
15
  end
16
16
 
17
17
  if response.code != "200"
18
- 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}"
19
19
  elsif response.body == ""
20
- 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."
21
21
  end
22
22
 
23
23
  begin
24
24
  payload = JSON.parse(response.body)
25
25
  rescue
26
- 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}"
27
27
  end
28
28
 
29
29
  if payload.include?("error") && payload["error"] == "invalid_auth"
30
- 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}'."
31
33
  elsif payload.include?("error")
32
- 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}"
33
35
  end
34
36
 
35
37
  payload["user"]["id"]
@@ -40,9 +42,13 @@ class SlackMessage::Api
40
42
  channel: target,
41
43
  username: payload.custom_bot_name || profile[:name],
42
44
  blocks: payload.render,
43
- text: payload.notification_text,
45
+ text: payload.custom_notification,
44
46
  }
45
47
 
48
+ if params[:blocks].length == 0
49
+ raise ArgumentError, "Tried to send an entirely empty message."
50
+ end
51
+
46
52
  icon = payload.custom_bot_icon || profile[:icon]
47
53
  if icon =~ /^:\w+:$/
48
54
  params[:icon_emoji] = icon
@@ -58,17 +64,17 @@ class SlackMessage::Api
58
64
 
59
65
  # let's try to be helpful about error messages
60
66
  if ["token_revoked", "token_expired", "invalid_auth", "not_authed"].include?(error)
61
- raise "Couldn't send slack message because the API key for profile '#{profile[:handle]}' is wrong."
67
+ raise SlackMessage::ApiError, "Couldn't send slack message because the API key for profile '#{profile[:handle]}' is wrong."
62
68
  elsif ["no_permission", "ekm_access_denied"].include?(error)
63
- raise "Couldn't send slack message because the API key for profile '#{profile[:handle]}' isn't allowed to post messages."
69
+ raise SlackMessage::ApiError, "Couldn't send slack message because the API key for profile '#{profile[:handle]}' isn't allowed to post messages."
64
70
  elsif error == "channel_not_found"
65
- raise "Tried to send Slack message to non-existent channel or user '#{target}'"
71
+ raise SlackMessage::ApiError, "Tried to send Slack message to non-existent channel or user '#{target}'"
66
72
  elsif error == "invalid_arguments"
67
- raise "Tried to send Slack message with invalid payload."
73
+ raise SlackMessage::ApiError, "Tried to send Slack message with invalid payload."
68
74
  elsif response.code == "302"
69
- raise "Got 302 response while posting to Slack. Check your API key for profile '#{profile[:handle]}'."
75
+ raise SlackMessage::ApiError, "Got 302 response while posting to Slack. Check your API key for profile '#{profile[:handle]}'."
70
76
  elsif response.code != "200"
71
- 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}"
72
78
  end
73
79
 
74
80
  response
@@ -1,19 +1,21 @@
1
1
  class SlackMessage::Dsl
2
- attr_reader :body, :default_section, :custom_bot_name, :custom_bot_icon
3
- attr_accessor :notification_text
2
+ attr_reader :body, :default_section, :custom_bot_name, :custom_bot_icon, :profile
3
+ attr_accessor :custom_notification
4
4
 
5
5
  EMSPACE = " " # unicode emspace
6
6
 
7
- def initialize(block)
7
+ def initialize(block, profile)
8
8
  # Delegate missing methods to caller scope. Thanks 2008:
9
9
  # https://www.dan-manges.com/blog/ruby-dsls-instance-eval-with-delegation
10
10
  @caller_self = eval("self", block.binding)
11
11
 
12
- @body = []
13
- @default_section = Section.new(self)
14
- @custom_bot_name = nil
15
- @custom_bot_icon = nil
16
- @notification_text = 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
17
19
  end
18
20
 
19
21
  # allowable top-level entities within a block
@@ -56,9 +58,11 @@ class SlackMessage::Dsl
56
58
  finalize_default_section
57
59
 
58
60
  if text == "" || text.nil?
59
- raise ArgumentError, "tried to create a context block without a value"
61
+ raise ArgumentError, "Cannot create a context block without a value."
60
62
  end
61
63
 
64
+ text = self.enrich_text(text)
65
+
62
66
  @body.push({ type: "context", elements: [{
63
67
  type: "mrkdwn", text: text
64
68
  }]})
@@ -79,7 +83,7 @@ class SlackMessage::Dsl
79
83
 
80
84
  # end delegation
81
85
 
82
- # custom bot name
86
+ # bot / notification overrides
83
87
 
84
88
  def bot_name(name)
85
89
  @custom_bot_name = name
@@ -89,6 +93,10 @@ class SlackMessage::Dsl
89
93
  @custom_bot_icon = icon
90
94
  end
91
95
 
96
+ def notification_text(msg)
97
+ @custom_notification = msg
98
+ end
99
+
92
100
  # end bot name
93
101
 
94
102
  def render
@@ -100,6 +108,22 @@ class SlackMessage::Dsl
100
108
  @caller_self.send meth, *args, &blk
101
109
  end
102
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
+
103
127
  private
104
128
 
105
129
  # when doing things that would generate new top-levels, first try
@@ -123,9 +147,11 @@ class SlackMessage::Dsl
123
147
 
124
148
  def text(msg)
125
149
  if msg == "" || msg.nil?
126
- raise ArgumentError, "tried to create text node without a value"
150
+ raise ArgumentError, "Cannot create a text node without a value."
127
151
  end
128
152
 
153
+ msg = @parent.enrich_text(msg)
154
+
129
155
  if @body.include?(:text)
130
156
  @body[:text][:text] << "\n#{msg}"
131
157
 
@@ -135,19 +161,21 @@ class SlackMessage::Dsl
135
161
  end
136
162
 
137
163
  def ul(elements)
138
- raise ArgumentError, "please pass an array" unless elements.respond_to?(:map)
164
+ raise ArgumentError, "Please pass an array when creating a ul." unless elements.respond_to?(:map)
139
165
 
140
- text(
141
- elements.map { |text| "#{EMSPACE}• #{text}" }.join("\n")
142
- )
166
+ msg = elements.map { |text| "#{EMSPACE}• #{text}" }.join("\n")
167
+ msg = @parent.enrich_text(msg)
168
+
169
+ text(msg)
143
170
  end
144
171
 
145
172
  def ol(elements)
146
- raise ArgumentError, "please pass an array" unless elements.respond_to?(:map)
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)
147
177
 
148
- text(
149
- elements.map.with_index(1) { |text, idx| "#{EMSPACE}#{idx}. #{text}" }.join("\n")
150
- )
178
+ text(msg)
151
179
  end
152
180
 
153
181
  # styles: default, primary, danger
@@ -205,9 +233,10 @@ class SlackMessage::Dsl
205
233
 
206
234
  def list_item(title, value)
207
235
  if value == "" || value.nil?
208
- raise ArgumentError, "can't create a list item for '#{title}' without a value"
236
+ raise ArgumentError, "Can't create a list item for '#{title}' without a value."
209
237
  end
210
238
 
239
+ value = @parent.enrich_text(value)
211
240
  @list.add(title, value)
212
241
  end
213
242
 
@@ -220,10 +249,14 @@ class SlackMessage::Dsl
220
249
  end
221
250
 
222
251
  def render
252
+ unless has_content?
253
+ raise ArgumentError, "Can't create a section with no content."
254
+ end
255
+
223
256
  body[:fields] = @list.render if @list.any?
224
257
 
225
- if body[:text] && body[:text][:text] && !@parent.notification_text
226
- @parent.notification_text = body[:text][:text]
258
+ if body[:text] && body[:text][:text] && !@parent.custom_notification
259
+ @parent.notification_text(body[:text][:text])
227
260
  end
228
261
 
229
262
  body
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,35 +13,43 @@ module SlackMessage
11
13
  configuration.configure(&block)
12
14
  end
13
15
 
16
+ def self.user_id(email, profile_name = :default)
17
+ profile = Configuration.profile(profile_name)
18
+ Api.user_id_for(email, profile)
19
+ end
20
+
14
21
  def self.post_to(target, as: :default, &block)
15
- payload = Dsl.new(block).tap do |instance|
22
+ profile = Configuration.profile(as)
23
+
24
+ payload = Dsl.new(block, profile).tap do |instance|
16
25
  instance.instance_eval(&block)
17
26
  end
18
27
 
19
- profile = Configuration.profile(as)
20
28
  target = Api::user_id_for(target, profile) if target =~ /^\S{1,}@\S{2,}\.\S{2,}$/
21
29
 
22
30
  Api.post(payload, target, profile)
23
31
  end
24
32
 
25
33
  def self.post_as(profile_name, &block)
26
- payload = Dsl.new(block).tap do |instance|
27
- instance.instance_eval(&block)
28
- end
29
-
30
34
  profile = Configuration.profile(profile_name)
31
35
  if profile[:default_channel].nil?
32
36
  raise ArgumentError, "Sorry, you need to specify a default_channel for profile #{profile_name} to use post_as"
33
37
  end
34
38
 
39
+ payload = Dsl.new(block, profile).tap do |instance|
40
+ instance.instance_eval(&block)
41
+ end
42
+
35
43
  target = profile[:default_channel]
36
44
  target = Api::user_id_for(target, profile) if target =~ /^\S{1,}@\S{2,}\.\S{2,}$/
37
45
 
38
46
  Api.post(payload, target, profile)
39
47
  end
40
48
 
41
- def self.build(&block)
42
- 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|
43
53
  instance.instance_eval(&block)
44
54
  end.send(:render)
45
55
  end
@@ -1,6 +1,6 @@
1
1
  Gem::Specification.new do |gem|
2
2
  gem.name = 'slack_message'
3
- gem.version = "2.1.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,31 +1,14 @@
1
1
  require 'spec_helper'
2
2
 
3
3
  RSpec.describe SlackMessage do
4
- describe "API convenience" do
5
- before do
6
- SlackMessage.configure do |config|
7
- config.add_profile(name: 'default profile', api_token: 'abc123')
8
- end
9
- end
10
-
11
- after do
12
- SlackMessage.configuration.reset
13
- end
14
-
15
- it "can grab user IDs" do
16
- profile = SlackMessage::Configuration.profile(:default)
17
- allow(Net::HTTP).to receive(:start).and_return(
18
- double(code: "200", body: '{ "user": { "id": "ABC123" }}')
19
- )
20
-
21
- result = SlackMessage::Api.user_id_for("hello@joemastey.com", profile)
22
- expect(result).to eq("ABC123")
23
- end
24
- end
25
-
26
4
  describe "DSL" do
27
5
  describe "#build" do
28
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
+
29
12
  expected_output = [
30
13
  { type: "section",
31
14
  text: { text: "foo", type: "mrkdwn" }
@@ -42,12 +25,9 @@ RSpec.describe SlackMessage do
42
25
  end
43
26
 
44
27
  describe "configuration" do
45
- after do
46
- SlackMessage.configuration.reset
47
- end
48
-
49
28
  it "lets you add and fetch profiles" do
50
29
  SlackMessage.configure do |config|
30
+ config.clear_profiles!
51
31
  config.add_profile(name: 'default profile', api_token: 'abc123')
52
32
  config.add_profile(:nonstandard, name: 'another profile', api_token: 'abc123')
53
33
  end
@@ -141,4 +121,43 @@ RSpec.describe SlackMessage do
141
121
  }.to post_to_slack.with_content_matching(/foo/)
142
122
  end
143
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
162
+ end
144
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: 2.1.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-11-02 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