slack_message 1.1.0 → 1.5.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: f1d15c6c55f10954900c551dd0547ac3b3b4b83761f72dff1c2e7628634f1014
4
- data.tar.gz: ebeedb567588d122f15906069056c8f4061b98e9b52f2f4affefcf010a260357
3
+ metadata.gz: c5a3275f61d16198e8a8283d22f1258dc9480ad7b66b977b6505fcfe79ef69fd
4
+ data.tar.gz: 8cf6cdddbfbfcd1cb476aaa6384de6f94a7030fd813bdeef4aeda70e34811d2a
5
5
  SHA512:
6
- metadata.gz: 44a9ac634c8bdd1c3225dab9421b838e1399199dd509f219aaf22f6b8e4e6947eefc6db73660f2e92b211d49bb1a454b9f40fc33cb3e0d049b4c4bc2b5c7aa0d
7
- data.tar.gz: 244d9a37799a75f90715aa16ea5426234cdb806aef3e00b74c38d45f98eac094e2181a85d191c3f03e1687fe7fbb20d2e3e97abdb3c1fe876dbcc1a2f880865b
6
+ metadata.gz: 5a2395b51855660a9eba01115f3dbef9859c554c6365ede4d12c74353e72b117a763c0c526fe211f44b1489281fccb2a8fa3e53cc2883aa8cbc935d932598df8
7
+ data.tar.gz: af50cc1f1bd0d93124115f34eae72bbe90e9309d79ec862b0a62e4d8b6c7bb0bc5e32e950bc9c7bc86f22fa30787c54d110974dcdd2f320d85b7afc0fb26dcc0
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,27 @@
1
+ # Changelog
2
+
3
+ ## [Unreleased]
4
+
5
+ ## [1.5.0] - 2021-10-01
6
+ - Added `ol` and `ul` to sections w/ some formatting
7
+
8
+ ## [1.4.0] - 2021-09-27
9
+ - Moved image to accessory_image to differentiate between the image block
10
+ and the accessory image within a block.
11
+
12
+ ## [1.3.0] - 2021-09-27
13
+ - Added ability to use custom names when posting.
14
+ - Added ability to post images within sections.
15
+ - Added warnings for potentially invalid URLs.
16
+
17
+ ## [1.2.0] - 2021-09-26
18
+ - Turns out gemspec was broken. Fixed that.
19
+
20
+ ## [1.1.0] - 2021-09-26
21
+ - Expanded the README significantly w/ usage instructions.
22
+ - Added lots of error handling to requests.
23
+
24
+ ## [1.0.0] - 2021-09-25
25
+
26
+ - Added the base gem w/ a DSL for constructing blocks using sections.
27
+ - 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,238 @@
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
+ Opinionated Stances
196
+ ------------
197
+
198
+ Slack's API has a lot of options available to you! But this gem takes some
199
+ opinionated stances on how to make use of that API. For instance:
200
+
201
+ * Unless you request otherwise, text is always rendered using `mrkdwn`. If you
202
+ want plaintext, you'll need to ask for it.
203
+ * Generally, same goes for the `emoji` flag on almost every text element.
204
+ * It's possible to ask for a `blank_line` in sections, even though that concept
205
+ isn't real. In this case, a text line containing only an emspace is rendered.
206
+
207
+ What it Doesn't Do
208
+ ------------
209
+
210
+ This gem is intended to stay fairly simple. Other gems have lots of config
211
+ options and abilities, which is wonderful, but overall complicates usage. If
212
+ you want to add a feature, open an issue on Github first to see if it's likely
213
+ to be merged.
214
+
215
+ Since this gem was built out of an existing need that _didn't_ include most of
216
+ the block API, I'd be inclined to merge features that sustainably expand the
217
+ DSL to include more of the block API itself.
218
+
219
+ Also, some behaviors that are still planned but not yet added:
220
+
221
+ * allow custom http_options in configuration
222
+ * more of BlockKit's options
223
+ * any interactive elements at all (I don't understand them yet)
224
+ * more interesting return types for your message
225
+ * some way to specify default channel for a given profile (and omit param to post_to)
226
+ * richer text formatting (ul is currently a hack)
227
+
228
+ Contributing
229
+ ------------
230
+
231
+ Contributions are very welcome. Fork, fix, submit pulls.
232
+
233
+ Contribution is expected to conform to the [Contributor Covenant](https://github.com/jmmastey/slack_message/blob/master/CODE_OF_CONDUCT.md).
234
+
235
+ License
236
+ ------------
237
+
238
+ 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,223 @@
1
+ class SlackMessage::Dsl
2
+ attr_reader :body, :default_section, :custom_bot_name
3
+
4
+ EMSPACE = " " # unicode emspace
5
+
6
+ def initialize
7
+ @body = []
8
+ @default_section = Section.new
9
+ @custom_bot_name = nil
10
+ end
11
+
12
+ # allowable top-level entities within a block
13
+
14
+ def section(&block)
15
+ finalize_default_section
16
+
17
+ section = Section.new.tap do |s|
18
+ s.instance_eval(&block)
19
+ end
20
+
21
+ @body.push(section.render)
22
+ end
23
+
24
+ def divider
25
+ finalize_default_section
26
+
27
+ @body.push({ type: "divider" })
28
+ end
29
+
30
+ def image(url, alt_text:, title: nil)
31
+ finalize_default_section
32
+
33
+ config = {
34
+ type: "image",
35
+ image_url: url,
36
+ alt_text: alt_text,
37
+ }
38
+
39
+ if !title.nil?
40
+ config[:title] = {
41
+ type: "plain_text", text: title, emoji: true
42
+ }
43
+ end
44
+
45
+ @body.push(config)
46
+ end
47
+
48
+ def context(text)
49
+ finalize_default_section
50
+
51
+ @body.push({ type: "context", elements: [{
52
+ type: "mrkdwn", text: text
53
+ }]})
54
+ end
55
+
56
+ # end entities
57
+
58
+ # delegation to allow terse syntax without e.g. `section`
59
+
60
+ def text(*args); default_section.text(*args); end
61
+ def link_button(*args); default_section.link_button(*args); end
62
+ def accessory_image(*args); default_section.accessory_image(*args); end
63
+ def blank_line(*args); default_section.blank_line(*args); end
64
+ def link(*args); default_section.link(*args); end
65
+ def list_item(*args); default_section.list_item(*args); end
66
+ def ul(*args); default_section.ul(*args); end
67
+ def ol(*args); default_section.ol(*args); end
68
+
69
+ # end delegation
70
+
71
+ # custom bot name
72
+
73
+ def bot_name(name)
74
+ @custom_bot_name = name
75
+ end
76
+
77
+ # end bot name
78
+
79
+ def render
80
+ finalize_default_section
81
+ @body
82
+ end
83
+
84
+ private
85
+
86
+ # when doing things that would generate new top-levels, first try
87
+ # to finish the implicit section.
88
+ def finalize_default_section
89
+ if default_section.has_content?
90
+ @body.push(default_section.body)
91
+ end
92
+
93
+ @default_section = Section.new
94
+ end
95
+
96
+ class Section
97
+ attr_reader :body
98
+
99
+ def initialize
100
+ @body = { type: "section" }
101
+ @list = List.new
102
+ end
103
+
104
+ def text(msg)
105
+ if @body.include?(:text)
106
+ @body[:text][:text] << "\n#{msg}"
107
+
108
+ else
109
+ @body.merge!({ text: { type: "mrkdwn", text: msg } })
110
+ end
111
+ end
112
+
113
+ def ul(elements)
114
+ raise Arguments, "please pass an array" unless elements.respond_to?(:map)
115
+ text(
116
+ elements.map { |text| "#{EMSPACE}• #{text}" }.join("\n")
117
+ )
118
+ end
119
+
120
+ def ol(elements)
121
+ raise Arguments, "please pass an array" unless elements.respond_to?(:map)
122
+ text(
123
+ elements.map.with_index(1) { |text, idx| "#{EMSPACE}#{idx}. #{text}" }.join("\n")
124
+ )
125
+ end
126
+
127
+ # styles: default, primary, danger
128
+ def link_button(label, target, style: :primary)
129
+ if !@body[:accessory].nil?
130
+ previous_type = @body[:accessory][:type]
131
+ warn "WARNING: Overriding previous #{previous_type} in section to use link_button instead: #{label}"
132
+ end
133
+
134
+ unless /(^|\s)((https?:\/\/)?[\w-]+(\.[\w-]+)+\.?(:\d+)?(\/\S*)?)/i =~ target
135
+ warn "WARNING: Passing a probably-invalid URL to link button #{label} (url: '#{target}')"
136
+ end
137
+
138
+ config = {
139
+ accessory: {
140
+ type: "button",
141
+ url: target,
142
+ text: {
143
+ type: "plain_text",
144
+ text: label,
145
+ emoji: true
146
+ },
147
+ }
148
+ }
149
+
150
+ if style != :default
151
+ config[:accessory][:style] = style
152
+ end
153
+
154
+ @body.merge!(config)
155
+ end
156
+
157
+ def accessory_image(url, alt_text: nil)
158
+ if !@body[:accessory].nil?
159
+ previous_type = @body[:accessory][:type]
160
+ warn "WARNING: Overriding previous #{previous_type} in section to use accessory image instead: #{url}"
161
+ end
162
+
163
+ config = {
164
+ accessory: {
165
+ type: "image",
166
+ image_url: url
167
+ }
168
+ }
169
+
170
+ config[:accessory][:alt_text] = alt_text if !alt_text.nil?
171
+
172
+ @body.merge!(config)
173
+ end
174
+
175
+ # for markdown links
176
+ def link(label, target)
177
+ "<#{target}|#{label}>"
178
+ end
179
+
180
+ def list_item(title, value)
181
+ @list.add(title, value)
182
+ end
183
+
184
+ def blank_line
185
+ text EMSPACE
186
+ end
187
+
188
+ def has_content?
189
+ @body.keys.length > 1 || @list.any?
190
+ end
191
+
192
+ def render
193
+ body[:fields] = @list.render if @list.any?
194
+ body
195
+ end
196
+ end
197
+
198
+ class List
199
+ def initialize
200
+ @items = []
201
+ end
202
+
203
+ def any?
204
+ @items.any?
205
+ end
206
+
207
+ def add(title, value)
208
+ @items.push(["*#{title}*", value])
209
+ end
210
+
211
+ def render
212
+ @items.push([' ', ' ']) if @items.length % 2 == 1
213
+ @items.each_slice(2).flat_map do |(first, second)|
214
+ [
215
+ { type: "mrkdwn", text: first[0] },
216
+ { type: "mrkdwn", text: second[0] },
217
+ { type: "mrkdwn", text: first[1] },
218
+ { type: "mrkdwn", text: second[1] },
219
+ ]
220
+ end
221
+ end
222
+ end
223
+ 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.5.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.1.0
4
+ version: 1.5.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-10-01 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