slack_message 2.1.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: 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