slack_message 1.0.0 → 1.4.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 +4 -4
- data/.gitignore +1 -0
- data/.ruby-version +1 -0
- data/CHANGELOG.md +24 -0
- data/CODE_OF_CONDUCT.md +74 -0
- data/Gemfile +3 -0
- data/Gemfile.lock +40 -0
- data/MIT-LICENSE +20 -0
- data/README.md +224 -0
- data/lib/slack_message/api.rb +57 -0
- data/lib/slack_message/configuration.rb +47 -0
- data/lib/slack_message/dsl.rb +205 -0
- data/lib/slack_message.rb +6 -3
- data/slack_message.gemspec +24 -0
- data/spec/slack_message_spec.rb +75 -0
- data/spec/spec_helper.rb +52 -0
- metadata +16 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 858633fd4b83922e886bbec641c0075aab65aa9551d0a268cab9b51e3f3722fc
|
4
|
+
data.tar.gz: 3bc79397b71344abe4b535424d9913ca8a7f37259b4904da43acc0149674a6c6
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
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.
|
data/CODE_OF_CONDUCT.md
ADDED
@@ -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
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 =
|
20
|
-
|
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
|
data/spec/spec_helper.rb
ADDED
@@ -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.
|
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-
|
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
|