slack_message 1.0.0 → 1.4.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: a57ae906ede72e7fa4d7a1425403bff7ceac205867076da31ba8d134b29cb9d3
4
- data.tar.gz: 75c900293ec441d1ce75847617df5b3bb0a427df847a844e423fa9b2de9d803d
3
+ metadata.gz: 858633fd4b83922e886bbec641c0075aab65aa9551d0a268cab9b51e3f3722fc
4
+ data.tar.gz: 3bc79397b71344abe4b535424d9913ca8a7f37259b4904da43acc0149674a6c6
5
5
  SHA512:
6
- metadata.gz: 98ed70ad1291694b3e7c468e7094db4b915ef6ee5539a820473e4aaa62bc163272dddd004dcfec159beeeac828aef6f8f8e78ccf709356bf1ffeb3e804c36bc6
7
- data.tar.gz: 10faaa8dee70db69d2437cd436bc67e456919d2a7ad358c5e242ab80f6c6bd44dc665131f4657cf439472f016d2d01626edeae44fe0a1a9306917bea93c6cfce
6
+ metadata.gz: d8cb6fcfaeeade894cba87671231d49de003917069e597906d53a166a8d2e74a25a1e2f55aaadefbe606b940106f8123afe02b9d820191210bfccd3e8962ce95
7
+ data.tar.gz: fa49ddfecbd19538bfe91d62f0e7decc88cc52ef42e6d0c880138820671b27d23334dd90fd7a970e080a85221bdbf5b40929e3ee3d6e73db0e039efc1d17acd9
data/.gitignore ADDED
@@ -0,0 +1 @@
1
+ slack_message*.gem
data/.ruby-version ADDED
@@ -0,0 +1 @@
1
+ ruby-2.7.3
data/CHANGELOG.md ADDED
@@ -0,0 +1,24 @@
1
+ # Changelog
2
+
3
+ ## [Unreleased]
4
+
5
+ ## [1.4.0] - 2021-09-27
6
+ - Moved image to accessory_image to differentiate between the image block
7
+ and the accessory image within a block.
8
+
9
+ ## [1.3.0] - 2021-09-27
10
+ - Added ability to use custom names when posting.
11
+ - Added ability to post images within sections.
12
+ - Added warnings for potentially invalid URLs.
13
+
14
+ ## [1.2.0] - 2021-09-26
15
+ - Turns out gemspec was broken. Fixed that.
16
+
17
+ ## [1.1.0] - 2021-09-26
18
+ - Expanded the README significantly w/ usage instructions.
19
+ - Added lots of error handling to requests.
20
+
21
+ ## [1.0.0] - 2021-09-25
22
+
23
+ - Added the base gem w/ a DSL for constructing blocks using sections.
24
+ - Added a changelog, apparently.
@@ -0,0 +1,74 @@
1
+ # Contributor Covenant Code of Conduct
2
+
3
+ ## Our Pledge
4
+
5
+ In the interest of fostering an open and welcoming environment, we as
6
+ contributors and maintainers pledge to making participation in our project and
7
+ our community a harassment-free experience for everyone, regardless of age, body
8
+ size, disability, ethnicity, gender identity and expression, level of experience,
9
+ nationality, personal appearance, race, religion, or sexual identity and
10
+ orientation.
11
+
12
+ ## Our Standards
13
+
14
+ Examples of behavior that contributes to creating a positive environment
15
+ include:
16
+
17
+ * Using welcoming and inclusive language
18
+ * Being respectful of differing viewpoints and experiences
19
+ * Gracefully accepting constructive criticism
20
+ * Focusing on what is best for the community
21
+ * Showing empathy towards other community members
22
+
23
+ Examples of unacceptable behavior by participants include:
24
+
25
+ * The use of sexualized language or imagery and unwelcome sexual attention or
26
+ advances
27
+ * Trolling, insulting/derogatory comments, and personal or political attacks
28
+ * Public or private harassment
29
+ * Publishing others' private information, such as a physical or electronic
30
+ address, without explicit permission
31
+ * Other conduct which could reasonably be considered inappropriate in a
32
+ professional setting
33
+
34
+ ## Our Responsibilities
35
+
36
+ Project maintainers are responsible for clarifying the standards of acceptable
37
+ behavior and are expected to take appropriate and fair corrective action in
38
+ response to any instances of unacceptable behavior.
39
+
40
+ Project maintainers have the right and responsibility to remove, edit, or
41
+ reject comments, commits, code, wiki edits, issues, and other contributions
42
+ that are not aligned to this Code of Conduct, or to ban temporarily or
43
+ permanently any contributor for other behaviors that they deem inappropriate,
44
+ threatening, offensive, or harmful.
45
+
46
+ ## Scope
47
+
48
+ This Code of Conduct applies both within project spaces and in public spaces
49
+ when an individual is representing the project or its community. Examples of
50
+ representing a project or community include using an official project e-mail
51
+ address, posting via an official social media account, or acting as an appointed
52
+ representative at an online or offline event. Representation of a project may be
53
+ further defined and clarified by project maintainers.
54
+
55
+ ## Enforcement
56
+
57
+ Instances of abusive, harassing, or otherwise unacceptable behavior may be
58
+ reported by contacting the project team at jmmastey@gmail.com. All
59
+ complaints will be reviewed and investigated and will result in a response that
60
+ is deemed necessary and appropriate to the circumstances. The project team is
61
+ obligated to maintain confidentiality with regard to the reporter of an incident.
62
+ Further details of specific enforcement policies may be posted separately.
63
+
64
+ Project maintainers who do not follow or enforce the Code of Conduct in good
65
+ faith may face temporary or permanent repercussions as determined by other
66
+ members of the project's leadership.
67
+
68
+ ## Attribution
69
+
70
+ This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4,
71
+ available at [http://contributor-covenant.org/version/1/4][version]
72
+
73
+ [homepage]: http://contributor-covenant.org
74
+ [version]: http://contributor-covenant.org/version/1/4/
data/Gemfile ADDED
@@ -0,0 +1,3 @@
1
+ source 'https://rubygems.org'
2
+
3
+ gemspec
data/Gemfile.lock ADDED
@@ -0,0 +1,40 @@
1
+ PATH
2
+ remote: .
3
+ specs:
4
+ slack_message (1.2.0)
5
+
6
+ GEM
7
+ remote: https://rubygems.org/
8
+ specs:
9
+ coderay (1.1.3)
10
+ diff-lcs (1.4.4)
11
+ method_source (1.0.0)
12
+ pry (0.14.1)
13
+ coderay (~> 1.1)
14
+ method_source (~> 1.0)
15
+ rb-readline (0.5.5)
16
+ rspec (3.10.0)
17
+ rspec-core (~> 3.10.0)
18
+ rspec-expectations (~> 3.10.0)
19
+ rspec-mocks (~> 3.10.0)
20
+ rspec-core (3.10.1)
21
+ rspec-support (~> 3.10.0)
22
+ rspec-expectations (3.10.1)
23
+ diff-lcs (>= 1.2.0, < 2.0)
24
+ rspec-support (~> 3.10.0)
25
+ rspec-mocks (3.10.2)
26
+ diff-lcs (>= 1.2.0, < 2.0)
27
+ rspec-support (~> 3.10.0)
28
+ rspec-support (3.10.2)
29
+
30
+ PLATFORMS
31
+ ruby
32
+
33
+ DEPENDENCIES
34
+ pry (= 0.14.1)
35
+ rb-readline (= 0.5.5)
36
+ rspec (= 3.10.0)
37
+ slack_message!
38
+
39
+ BUNDLED WITH
40
+ 2.1.4
data/MIT-LICENSE ADDED
@@ -0,0 +1,20 @@
1
+ Copyright 2021 Joseph Mastey
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining
4
+ a copy of this software and associated documentation files (the
5
+ "Software"), to deal in the Software without restriction, including
6
+ without limitation the rights to use, copy, modify, merge, publish,
7
+ distribute, sublicense, and/or sell copies of the Software, and to
8
+ permit persons to whom the Software is furnished to do so, subject to
9
+ the following conditions:
10
+
11
+ The above copyright notice and this permission notice shall be
12
+ included in all copies or substantial portions of the Software.
13
+
14
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,224 @@
1
+ SlackMessage: a Friendly DSL for Slack
2
+ =============
3
+
4
+ SlackMessage is a wrapper over the [Block Kit
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.
8
+
9
+ Posting a message to Slack should be this easy:
10
+
11
+ ```ruby
12
+ SlackMessage.post_to('#general') do
13
+ text "We did it @here! :thumbsup:"
14
+ end
15
+ ```
16
+
17
+ To install, just add `slack_message` to your bundle and you're ready to go.
18
+
19
+
20
+ Usage
21
+ ------------
22
+
23
+ ### Configuration
24
+
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:
28
+
29
+ ```ruby
30
+ SlackMessage.configure do |config|
31
+ webhook_url = 'https://hooks.slack.com/services/T00000000/B00000000/XXXXXXXXXXXXXXXXXXXXXXXX'
32
+
33
+ config.add_profile(name: 'Slack Notifier', url: webhook_url)
34
+ end
35
+ ```
36
+
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`.
40
+
41
+ #### Additional Profiles
42
+
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:
46
+
47
+ ```ruby
48
+ SlackMessage.configure do |config|
49
+ # default profile
50
+ config.add_profile(name: 'Slack Notifier', url: ENV['SLACK_WEBHOOK_URL'])
51
+
52
+ # 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'])
55
+ end
56
+ ```
57
+
58
+ #### Configuring User Search
59
+
60
+ Slack's API no longer allows you to send DMs to users by username. You need to
61
+ look up a user's internal ID and send to that ID. Thankfully, there is a lookup
62
+ by email endpoint for this. If you'd like to post messages to users by their
63
+ email address, you'll need a
64
+ [separate API Token](https://api.slack.com/tutorials/tracks/getting-a-token):
65
+
66
+ ```ruby
67
+ SlackMessage.configure do |config|
68
+ config.api_token = 'xoxb-11111111111-2222222222-33333333333333333'
69
+ end
70
+ ```
71
+
72
+ ### Posting Messages
73
+
74
+ As mentioned at the top, posting a message to Slack is dang easy:
75
+
76
+ ```ruby
77
+ SlackMessage.post_to('#general') do
78
+ text "We did it @here! :thumbsup:"
79
+ end
80
+ ```
81
+
82
+ That's it! SlackMessage will automatically serialize for the API like this:
83
+
84
+ ```json
85
+ [{"type":"section","text":{"type":"mrkdwn","text":"We did it! :thumbsup:"}}]
86
+ ```
87
+
88
+ Details like remembering that Slack made a mystifying decision to force you to
89
+ request "mrkdwn", or requiring your text to be wrapped into a section are handled
90
+ for you.
91
+
92
+ Building up messages is meant to be as user-friendly as possible:
93
+
94
+ ```ruby
95
+ SlackMessage.build do
96
+ text "haiku are easy"
97
+ text "but sometimes they don't make sense"
98
+ text "refrigerator"
99
+
100
+ context "- unknown author"
101
+ end
102
+ ```
103
+
104
+ SlackMessage will combine your text declarations and add any necessary wrappers
105
+ automatically:
106
+
107
+ ```json
108
+ [
109
+ {
110
+ "type": "section",
111
+ "text": {
112
+ "type": "mrkdwn",
113
+ "text": "haiku are easy\nbut sometimes they don't make sense\nrefrigerator"
114
+ }
115
+ },
116
+ {
117
+ "type": "context",
118
+ "elements": [
119
+ {
120
+ "type": "mrkdwn",
121
+ "text": "- unknown author"
122
+ }
123
+ ]
124
+ }
125
+ ]
126
+ ```
127
+
128
+ If you've configured an API key for user search (see above in configuration),
129
+ it's just as easy to send messages directly to users:
130
+
131
+ ```ruby
132
+ SlackMessage.post_to('hello@joemastey.com') do
133
+ text "We did it! :thumbsup:"
134
+ end
135
+ ```
136
+
137
+ SlackMessage is able to build all kinds of rich messages for you, and has been
138
+ a real joy to use for the author at least. To understand a bit more about the
139
+ possibilities of blocks, see Slack's [Block Kit
140
+ Builder](https://app.slack.com/block-kit-builder/) to understand the structure
141
+ better. There are lots of options:
142
+
143
+ ```ruby
144
+ SlackMessage.post_to('#general') do
145
+ section do
146
+ text "A job has generated some output for you to review."
147
+ text 'And More' * 10
148
+ link_button "See Results", "https://google.com"
149
+ end
150
+
151
+ section do
152
+ text ":unlock-new: New Data Summary"
153
+
154
+ list_item "Date", "09/05/2021"
155
+ list_item "Total Imported", 45_004
156
+ list_item "Total Errors", 5
157
+ end
158
+
159
+ divider
160
+
161
+ section do
162
+ text "See more here: #{link('result', 'https://google.com')}"
163
+ end
164
+
165
+ text ":rocketship: hello@joemastey.com"
166
+
167
+ context ":custom_slack_emoji: An example footer *with some markdown*."
168
+ end
169
+ ```
170
+
171
+ SlackMessage will compose this into Block Kit syntax and send it on its way!
172
+ For now you'll need to read a bit of the source code to get the entire API. Sorry,
173
+ working on it.
174
+
175
+ If you've defined multiple profiles in configuration, you can specify which to
176
+ use for your message by specifying their name:
177
+
178
+ ```ruby
179
+ SlackMessage.post_to('#general', as: :sidekiq_bot) do
180
+ text ":octagonal_sign: A job has failed permanently and needs to be rescued."
181
+ link_button "Sidekiq Dashboard", "https://yoursite.com/sidekiq", style: :danger
182
+ end
183
+ ```
184
+
185
+ You can also use a custom name when sending a message:
186
+
187
+ ```ruby
188
+ SlackMessage.post_to('#general') do
189
+ bot_name "CoffeeBot"
190
+
191
+ text ":coffee::clock: Time to take a break!"
192
+ end
193
+ ```
194
+
195
+ What it Doesn't Do
196
+ ------------
197
+
198
+ This gem is intended to stay fairly simple. Other gems have lots of config
199
+ options and abilities, which is wonderful, but overall complicates usage. If
200
+ you want to add a feature, open an issue on Github first to see if it's likely
201
+ to be merged.
202
+
203
+ Since this gem was built out of an existing need that _didn't_ include most of
204
+ the block API, I'd be inclined to merge features that sustainably expand the
205
+ DSL to include more of the block API itself.
206
+
207
+ Also, some behaviors that are still planned but not yet added:
208
+
209
+ * allow custom http_options in configuration
210
+ * more of BlockKit's options
211
+ * any interactive elements at all (I don't understand them yet)
212
+ * more interesting return types for your message
213
+
214
+ Contributing
215
+ ------------
216
+
217
+ Contributions are very welcome. Fork, fix, submit pulls.
218
+
219
+ Contribution is expected to conform to the [Contributor Covenant](https://github.com/jmmastey/slack_message/blob/master/CODE_OF_CONDUCT.md).
220
+
221
+ License
222
+ ------------
223
+
224
+ This software is released under the [MIT License](https://github.com/jmmastey/slack_message/blob/master/MIT-LICENSE).
@@ -0,0 +1,57 @@
1
+ require 'net/http'
2
+ require 'net/https'
3
+
4
+ class SlackMessage::Api
5
+ def self.user_id_for(email)
6
+ token = SlackMessage.configuration.api_token
7
+
8
+ uri = URI("https://slack.com/api/users.lookupByEmail?email=#{email}")
9
+ request = Net::HTTP::Get.new(uri).tap do |req|
10
+ req['Authorization'] = "Bearer #{token}"
11
+ req['Content-type'] = "application/json"
12
+ end
13
+
14
+ response = Net::HTTP.start(uri.hostname, uri.port, use_ssl: true) do |http|
15
+ http.request(request)
16
+ end
17
+
18
+ if response.code != "200"
19
+ raise "Got an error back from the Slack API (HTTP #{response.code}):\n#{response.body}"
20
+ end
21
+
22
+ payload = JSON.parse(response.body)
23
+ if payload.include?("error") && payload["error"] == "invalid_auth"
24
+ raise "Received an error because your authentication token isn't properly configured:\n#{response.body}"
25
+ elsif payload.include?("error")
26
+ raise "Received error response from Slack during user lookup:\n#{response.body}"
27
+ end
28
+
29
+ payload["user"]["id"]
30
+ end
31
+
32
+ def self.post(payload, target, profile)
33
+ profile[:url] = profile[:url]
34
+
35
+ uri = URI.parse(profile[:url])
36
+ params = {
37
+ channel: target,
38
+ username: profile[:name],
39
+ blocks: payload
40
+ }.to_json
41
+
42
+ response = Net::HTTP.post_form uri, { payload: params }
43
+
44
+ # let's try to be helpful about error messages
45
+ if response.body == "invalid_token"
46
+ raise "Couldn't send slack message because the URL for profile '#{profile[:handle]}' is wrong."
47
+ elsif response.body == "channel_not_found"
48
+ raise "Tried to send Slack message to non-existent channel or user '#{target}'"
49
+ elsif response.body == "missing_text_or_fallback_or_attachments"
50
+ raise "Tried to send Slack message with invalid payload."
51
+ elsif response.code != "200"
52
+ raise "Got an error back from the Slack API (HTTP #{response.code}):\n#{response.body}"
53
+ end
54
+
55
+ response
56
+ end
57
+ end
@@ -0,0 +1,47 @@
1
+ module SlackMessage::Configuration
2
+ @@api_token = nil
3
+ @@profiles = {}
4
+
5
+ def self.reset
6
+ @@api_token = nil
7
+ @@profiles = {}
8
+ end
9
+
10
+ def self.configure
11
+ yield self
12
+ end
13
+
14
+ ###
15
+
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
+ def self.add_profile(handle = :default, name:, url:)
31
+ if @@profiles.include?(handle)
32
+ warn "WARNING: Overriding profile '#{handle}' in SlackMessage config"
33
+ end
34
+
35
+ @@profiles[handle] = { name: name, url: url, handle: handle }
36
+ end
37
+
38
+ def self.profile(handle, custom_name: nil)
39
+ unless @@profiles.include?(handle)
40
+ raise ArgumentError, "Unknown SlackMessage profile '#{handle}'."
41
+ end
42
+
43
+ @@profiles[handle].tap do |profile|
44
+ profile[:name] = custom_name if !custom_name.nil?
45
+ end
46
+ end
47
+ end
@@ -0,0 +1,205 @@
1
+ class SlackMessage::Dsl
2
+ attr_reader :body, :default_section, :custom_bot_name
3
+
4
+ def initialize
5
+ @body = []
6
+ @default_section = Section.new
7
+ @custom_bot_name = nil
8
+ end
9
+
10
+ # allowable top-level entities within a block
11
+
12
+ def section(&block)
13
+ finalize_default_section
14
+
15
+ section = Section.new.tap do |s|
16
+ s.instance_eval(&block)
17
+ end
18
+
19
+ @body.push(section.render)
20
+ end
21
+
22
+ def divider
23
+ finalize_default_section
24
+
25
+ @body.push({ type: "divider" })
26
+ end
27
+
28
+ def image(url, alt_text:, title: nil)
29
+ finalize_default_section
30
+
31
+ config = {
32
+ type: "image",
33
+ image_url: url,
34
+ alt_text: alt_text,
35
+ }
36
+
37
+ if !title.nil?
38
+ config[:title] = {
39
+ type: "plain_text", text: title, emoji: true
40
+ }
41
+ end
42
+
43
+ @body.push(config)
44
+ end
45
+
46
+ def context(text)
47
+ finalize_default_section
48
+
49
+ @body.push({ type: "context", elements: [{
50
+ type: "mrkdwn", text: text
51
+ }]})
52
+ end
53
+
54
+ # end entities
55
+
56
+ # delegation to allow terse syntax without e.g. `section`
57
+
58
+ def text(*args); default_section.text(*args); end
59
+ def link_button(*args); default_section.link_button(*args); end
60
+ def accessory_image(*args); default_section.accessory_image(*args); end
61
+ def blank_line(*args); default_section.blank_line(*args); end
62
+ def link(*args); default_section.link(*args); end
63
+ def list_item(*args); default_section.list_item(*args); end
64
+
65
+ # end delegation
66
+
67
+ # custom bot name
68
+
69
+ def bot_name(name)
70
+ @custom_bot_name = name
71
+ end
72
+
73
+ # end bot name
74
+
75
+ def render
76
+ finalize_default_section
77
+ @body
78
+ end
79
+
80
+ private
81
+
82
+ # when doing things that would generate new top-levels, first try
83
+ # to finish the implicit section.
84
+ def finalize_default_section
85
+ if default_section.has_content?
86
+ @body.push(default_section.body)
87
+ end
88
+
89
+ @default_section = Section.new
90
+ end
91
+
92
+ class Section
93
+ attr_reader :body
94
+
95
+ def initialize
96
+ @body = { type: "section" }
97
+ @list = List.new
98
+ end
99
+
100
+ def text(msg)
101
+ if @body.include?(:text)
102
+ @body[:text][:text] << "\n#{msg}"
103
+
104
+ else
105
+ @body.merge!({ text: { type: "mrkdwn", text: msg } })
106
+ end
107
+ end
108
+
109
+ # styles: default, primary, danger
110
+ def link_button(label, target, style: :primary)
111
+ if !@body[:accessory].nil?
112
+ previous_type = @body[:accessory][:type]
113
+ warn "WARNING: Overriding previous #{previous_type} in section to use link_button instead: #{label}"
114
+ end
115
+
116
+ unless /(^|\s)((https?:\/\/)?[\w-]+(\.[\w-]+)+\.?(:\d+)?(\/\S*)?)/i =~ target
117
+ warn "WARNING: Passing a probably-invalid URL to link button #{label} (url: '#{target}')"
118
+ end
119
+
120
+ config = {
121
+ accessory: {
122
+ type: "button",
123
+ url: target,
124
+ text: {
125
+ type: "plain_text",
126
+ text: label,
127
+ emoji: true
128
+ },
129
+ }
130
+ }
131
+
132
+ if style != :default
133
+ config[:accessory][:style] = style
134
+ end
135
+
136
+ @body.merge!(config)
137
+ end
138
+
139
+ def accessory_image(url, alt_text: nil)
140
+ if !@body[:accessory].nil?
141
+ previous_type = @body[:accessory][:type]
142
+ warn "WARNING: Overriding previous #{previous_type} in section to use accessory image instead: #{url}"
143
+ end
144
+
145
+ config = {
146
+ accessory: {
147
+ type: "image",
148
+ image_url: url
149
+ }
150
+ }
151
+
152
+ config[:accessory][:alt_text] = alt_text if !alt_text.nil?
153
+
154
+ @body.merge!(config)
155
+ end
156
+
157
+ # for markdown links
158
+ def link(label, target)
159
+ "<#{target}|#{label}>"
160
+ end
161
+
162
+ def list_item(title, value)
163
+ @list.add(title, value)
164
+ end
165
+
166
+ def blank_line
167
+ text " " # unicode emspace
168
+ end
169
+
170
+ def has_content?
171
+ @body.keys.length > 1 || @list.any?
172
+ end
173
+
174
+ def render
175
+ body[:fields] = @list.render if @list.any?
176
+ body
177
+ end
178
+ end
179
+
180
+ class List
181
+ def initialize
182
+ @items = []
183
+ end
184
+
185
+ def any?
186
+ @items.any?
187
+ end
188
+
189
+ def add(title, value)
190
+ @items.push(["*#{title}*", value])
191
+ end
192
+
193
+ def render
194
+ @items.push([' ', ' ']) if @items.length % 2 == 1
195
+ @items.each_slice(2).flat_map do |(first, second)|
196
+ [
197
+ { type: "mrkdwn", text: first[0] },
198
+ { type: "mrkdwn", text: second[0] },
199
+ { type: "mrkdwn", text: first[1] },
200
+ { type: "mrkdwn", text: second[1] },
201
+ ]
202
+ end
203
+ end
204
+ end
205
+ end
data/lib/slack_message.rb CHANGED
@@ -16,11 +16,14 @@ module SlackMessage
16
16
  end
17
17
 
18
18
  def self.post_to(target, as: :default, &block)
19
- payload = build(&block)
20
- profile = Configuration.profile(as)
19
+ payload = Dsl.new.tap do |instance|
20
+ instance.instance_eval(&block)
21
+ end
22
+
23
+ profile = Configuration.profile(as, custom_name: payload.custom_bot_name)
21
24
  target = user_id_for(target) if target =~ /^\S{1,}@\S{2,}\.\S{2,}$/
22
25
 
23
- Api.post(payload, target, profile)
26
+ Api.post(payload.render, target, profile)
24
27
  end
25
28
 
26
29
  def self.build(&block)
@@ -0,0 +1,24 @@
1
+ Gem::Specification.new do |gem|
2
+ gem.name = 'slack_message'
3
+ gem.version = "1.4.0"
4
+ gem.summary = "A nice DSL for composing rich messages in Slack"
5
+ gem.authors = ["Joe Mastey"]
6
+ gem.email = 'hello@joemastey.com'
7
+ gem.homepage = 'https://rubygemgem.org/gems/slack_message'
8
+ gem.licenses = 'MIT'
9
+
10
+ glob = lambda { |patterns| gem.files & Dir[*patterns] }
11
+
12
+ gem.files = `git ls-files`.split($/)
13
+ gem.test_files = glob['{spec/{**/}*_spec.rb']
14
+
15
+ gem.metadata = {
16
+ "homepage_uri" => "http://github.com/jmmastey/slack_message",
17
+ "changelog_uri" => "https://github.com/jmmastey/slack_message/blob/master/CHANGELOG.md",
18
+ "source_code_uri" => "http://github.com/jmmastey/slack_message",
19
+ }
20
+
21
+ gem.add_development_dependency "rspec", "3.10.0"
22
+ gem.add_development_dependency "pry", "0.14.1"
23
+ gem.add_development_dependency "rb-readline", "0.5.5"
24
+ end
@@ -0,0 +1,75 @@
1
+ require 'spec_helper'
2
+
3
+ RSpec.describe SlackMessage do
4
+ it "includes a bunch of stuff" do
5
+ expect(SlackMessage).to respond_to(:post_to)
6
+ end
7
+
8
+ describe "API convenience" do
9
+ it "can grab user IDs" do
10
+ allow(SlackMessage::Api).to receive(:api_request)
11
+ .with(/hello@joemastey.com/)
12
+ .and_return({ "user" => { "id" => "ABC123" }})
13
+
14
+ result = SlackMessage.user_id_for("hello@joemastey.com")
15
+ expect(result).to eq("ABC123")
16
+ end
17
+ end
18
+
19
+ describe "DSL" do
20
+ describe "#build" do
21
+ it "renders some JSON" do
22
+ expected_output = [
23
+ { type: "section",
24
+ text: { text: "foo", type: "mrkdwn" }
25
+ }
26
+ ]
27
+
28
+ output = SlackMessage.build do
29
+ text "foo"
30
+ end
31
+
32
+ expect(output).to eq(expected_output)
33
+ end
34
+ end
35
+ end
36
+
37
+ describe "configuration" do
38
+ after do
39
+ SlackMessage.configuration.reset
40
+ end
41
+
42
+ it "allows you to set an API key" do
43
+ SlackMessage.configure do |config|
44
+ config.api_token = "abc123"
45
+ end
46
+
47
+ expect(SlackMessage.configuration.api_token).to eq("abc123")
48
+ end
49
+
50
+ it "raises errors for missing configuration" do
51
+ SlackMessage.configure do |config|
52
+ #config.api_token = "abc123"
53
+ end
54
+
55
+ expect {
56
+ SlackMessage.configuration.api_token
57
+ }.to raise_error(ArgumentError)
58
+ end
59
+
60
+ it "lets you add and fetch profiles" do
61
+ SlackMessage.configure do |config|
62
+ config.add_profile(name: 'default profile', url: 'http://hooks.slack.com/1234/')
63
+ config.add_profile(:nonstandard, name: 'another profile', url: 'http://hooks.slack.com/1234/')
64
+ end
65
+
66
+
67
+ expect(SlackMessage.configuration.profile(:default)[:name]).to eq('default profile')
68
+ expect(SlackMessage.configuration.profile(:nonstandard)[:name]).to eq('another profile')
69
+
70
+ expect {
71
+ SlackMessage.configuration.profile(:missing)
72
+ }.to raise_error(ArgumentError)
73
+ end
74
+ end
75
+ end
@@ -0,0 +1,52 @@
1
+ require_relative '../lib/slack_message'
2
+
3
+ # This file was generated by the `rspec --init` command. Conventionally, all
4
+ # specs live under a `spec` directory, which RSpec adds to the `$LOAD_PATH`.
5
+ # The generated `.rspec` file contains `--require spec_helper` which will cause
6
+ # this file to always be loaded, without a need to explicitly require it in any
7
+ # files.
8
+ #
9
+ # Given that it is always loaded, you are encouraged to keep this file as
10
+ # light-weight as possible. Requiring heavyweight dependencies from this file
11
+ # will add to the boot time of your test suite on EVERY test run, even for an
12
+ # individual file that may not need all of that loaded. Instead, consider making
13
+ # a separate helper file that requires the additional dependencies and performs
14
+ # the additional setup, and require it from the spec files that actually need
15
+ # it.
16
+ #
17
+ # The `.rspec` file also contains a few flags that are not defaults but that
18
+ # users commonly want.
19
+ #
20
+ # See http://rubydoc.info/gems/rspec-core/RSpec/Core/Configuration
21
+
22
+ RSpec.configure do |config|
23
+ # This allows you to limit a spec run to individual examples or groups
24
+ # you care about by tagging them with `:focus` metadata. When nothing
25
+ # is tagged with `:focus`, all examples get run. RSpec also provides
26
+ # aliases for `it`, `describe`, and `context` that include `:focus`
27
+ # metadata: `fit`, `fdescribe` and `fcontext`, respectively.
28
+ config.filter_run_when_matching :focus
29
+
30
+ # Limits the available syntax to the non-monkey patched syntax that is
31
+ # recommended. For more details, see:
32
+ # - http://rspec.info/blog/2012/06/rspecs-new-expectation-syntax/
33
+ # - http://www.teaisaweso.me/blog/2013/05/27/rspecs-new-message-expectation-syntax/
34
+ # - http://rspec.info/blog/2014/05/notable-changes-in-rspec-3/#zero-monkey-patching-mode
35
+ config.disable_monkey_patching!
36
+
37
+ # This setting enables warnings. It's recommended, but in some cases may
38
+ # be too noisy due to issues in dependencies.
39
+ config.warnings = true
40
+
41
+ # Run specs in random order to surface order dependencies. If you find an
42
+ # order dependency and want to debug it, you can fix the order by providing
43
+ # the seed, which is printed after each run.
44
+ # --seed 1234
45
+ config.order = :random
46
+
47
+ # Seed global randomization in this process using the `--seed` CLI option.
48
+ # Setting this allows you to use `--seed` to deterministically reproduce
49
+ # test failures related to randomization by passing the same `--seed` value
50
+ # as the one that triggered the failure.
51
+ Kernel.srand config.seed
52
+ 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.0.0
4
+ version: 1.4.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-09-26 00:00:00.000000000 Z
11
+ date: 2021-09-27 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: rspec
@@ -58,7 +58,21 @@ executables: []
58
58
  extensions: []
59
59
  extra_rdoc_files: []
60
60
  files:
61
+ - ".gitignore"
62
+ - ".ruby-version"
63
+ - CHANGELOG.md
64
+ - CODE_OF_CONDUCT.md
65
+ - Gemfile
66
+ - Gemfile.lock
67
+ - MIT-LICENSE
68
+ - README.md
61
69
  - lib/slack_message.rb
70
+ - lib/slack_message/api.rb
71
+ - lib/slack_message/configuration.rb
72
+ - lib/slack_message/dsl.rb
73
+ - slack_message.gemspec
74
+ - spec/slack_message_spec.rb
75
+ - spec/spec_helper.rb
62
76
  homepage: https://rubygemgem.org/gems/slack_message
63
77
  licenses:
64
78
  - MIT