slack-messenger 2.3.3
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/lib/slack-messenger.rb +59 -0
- data/lib/slack-messenger/config.rb +43 -0
- data/lib/slack-messenger/payload_middleware.rb +24 -0
- data/lib/slack-messenger/payload_middleware/at.rb +36 -0
- data/lib/slack-messenger/payload_middleware/base.rb +34 -0
- data/lib/slack-messenger/payload_middleware/channels.rb +21 -0
- data/lib/slack-messenger/payload_middleware/format_attachments.rb +44 -0
- data/lib/slack-messenger/payload_middleware/format_message.rb +20 -0
- data/lib/slack-messenger/payload_middleware/stack.rb +50 -0
- data/lib/slack-messenger/util/escape.rb +16 -0
- data/lib/slack-messenger/util/http_client.rb +64 -0
- data/lib/slack-messenger/util/link_formatter.rb +81 -0
- data/lib/slack-messenger/version.rb +7 -0
- data/spec/end_to_end_spec.rb +95 -0
- data/spec/integration/ping_integration_test.rb +16 -0
- data/spec/lib/slack-messenger/config_spec.rb +71 -0
- data/spec/lib/slack-messenger/payload_middleware/at_spec.rb +25 -0
- data/spec/lib/slack-messenger/payload_middleware/base_spec.rb +76 -0
- data/spec/lib/slack-messenger/payload_middleware/channels_spec.rb +20 -0
- data/spec/lib/slack-messenger/payload_middleware/format_attachments_spec.rb +48 -0
- data/spec/lib/slack-messenger/payload_middleware/format_message_spec.rb +27 -0
- data/spec/lib/slack-messenger/payload_middleware/stack_spec.rb +119 -0
- data/spec/lib/slack-messenger/payload_middleware_spec.rb +33 -0
- data/spec/lib/slack-messenger/util/http_client_spec.rb +55 -0
- data/spec/lib/slack-messenger/util/link_formatter_spec.rb +155 -0
- data/spec/lib/slack-messenger_spec.rb +98 -0
- data/spec/spec_helper.rb +28 -0
- metadata +84 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: f07dd2e13c155464bd8d99c0cbba74ff48da2d8e84a19ccbd5026070e9c95a78
|
4
|
+
data.tar.gz: 723e2b2a499ce27ab08b1d1920a20e6473802cba49232edcabc936ab5dbdd434
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: db2415864731bdc11c27c691c0d7ee70d6157517234fd6c7aa426b26824198818cfddc093e89851b17efbc9cd2262decfb0b4fba5fe18692a4d2d94b7ad07929
|
7
|
+
data.tar.gz: 834a3ba19a3919b8ea1faac84372077f98c5c4a8ba28ca6e6b0a3d310a80099b3184beeb16ec12b70b8690f4dea2542f79c718b82264358dadd839920f8c4c34
|
@@ -0,0 +1,59 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "uri"
|
4
|
+
require "json"
|
5
|
+
|
6
|
+
require_relative "slack-messenger/util/http_client"
|
7
|
+
require_relative "slack-messenger/util/link_formatter"
|
8
|
+
require_relative "slack-messenger/util/escape"
|
9
|
+
require_relative "slack-messenger/payload_middleware"
|
10
|
+
require_relative "slack-messenger/config"
|
11
|
+
|
12
|
+
module Slack
|
13
|
+
class Messenger
|
14
|
+
attr_reader :endpoint
|
15
|
+
|
16
|
+
def initialize webhook_url, options={}, &block
|
17
|
+
@endpoint = URI.parse webhook_url
|
18
|
+
|
19
|
+
config.http_client(options.delete(:http_client)) if options.key?(:http_client)
|
20
|
+
config.defaults options
|
21
|
+
config.instance_exec(&block) if block_given?
|
22
|
+
|
23
|
+
middleware.set config.middleware
|
24
|
+
end
|
25
|
+
|
26
|
+
def config
|
27
|
+
@_config ||= Config.new
|
28
|
+
end
|
29
|
+
|
30
|
+
def ping message, options={}
|
31
|
+
if message.is_a?(Hash)
|
32
|
+
options = message
|
33
|
+
else
|
34
|
+
options[:text] = message
|
35
|
+
end
|
36
|
+
|
37
|
+
post options
|
38
|
+
end
|
39
|
+
|
40
|
+
def post payload={}
|
41
|
+
params = {}
|
42
|
+
client = payload.delete(:http_client) || config.http_client
|
43
|
+
payload = config.defaults.merge(payload)
|
44
|
+
|
45
|
+
params[:http_options] = payload.delete(:http_options) if payload.key?(:http_options)
|
46
|
+
|
47
|
+
middleware.call(payload).map do |pld|
|
48
|
+
params[:payload] = pld.to_json
|
49
|
+
client.post endpoint, params
|
50
|
+
end
|
51
|
+
end
|
52
|
+
|
53
|
+
private
|
54
|
+
|
55
|
+
def middleware
|
56
|
+
@middleware ||= PayloadMiddleware::Stack.new(self)
|
57
|
+
end
|
58
|
+
end
|
59
|
+
end
|
@@ -0,0 +1,43 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Slack
|
4
|
+
class Messenger
|
5
|
+
class Config
|
6
|
+
def initialize
|
7
|
+
@http_client = Util::HTTPClient
|
8
|
+
@defaults = {}
|
9
|
+
@middleware = %i[
|
10
|
+
format_message
|
11
|
+
format_attachments
|
12
|
+
at
|
13
|
+
channels
|
14
|
+
]
|
15
|
+
end
|
16
|
+
|
17
|
+
def http_client client=nil
|
18
|
+
return @http_client if client.nil?
|
19
|
+
raise ArgumentError, "the http client must respond to ::post" unless client.respond_to?(:post)
|
20
|
+
|
21
|
+
@http_client = client
|
22
|
+
end
|
23
|
+
|
24
|
+
def defaults new_defaults=nil
|
25
|
+
return @defaults if new_defaults.nil?
|
26
|
+
raise ArgumentError, "the defaults must be a Hash" unless new_defaults.is_a?(Hash)
|
27
|
+
|
28
|
+
@defaults = new_defaults
|
29
|
+
end
|
30
|
+
|
31
|
+
def middleware *args
|
32
|
+
return @middleware if args.empty?
|
33
|
+
|
34
|
+
@middleware =
|
35
|
+
if args.length == 1 && args.first.is_a?(Array) || args.first.is_a?(Hash)
|
36
|
+
args.first
|
37
|
+
else
|
38
|
+
args
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
@@ -0,0 +1,24 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Slack
|
4
|
+
class Messenger
|
5
|
+
class PayloadMiddleware
|
6
|
+
class << self
|
7
|
+
def registry
|
8
|
+
@registry ||= {}
|
9
|
+
end
|
10
|
+
|
11
|
+
def register middleware, name
|
12
|
+
registry[name] = middleware
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
require_relative "payload_middleware/stack"
|
20
|
+
require_relative "payload_middleware/base"
|
21
|
+
require_relative "payload_middleware/format_message"
|
22
|
+
require_relative "payload_middleware/format_attachments"
|
23
|
+
require_relative "payload_middleware/at"
|
24
|
+
require_relative "payload_middleware/channels"
|
@@ -0,0 +1,36 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Slack
|
4
|
+
class Messenger
|
5
|
+
class PayloadMiddleware
|
6
|
+
class At < Base
|
7
|
+
middleware_name :at
|
8
|
+
|
9
|
+
options at: []
|
10
|
+
|
11
|
+
def call payload={}
|
12
|
+
return payload unless payload[:at]
|
13
|
+
|
14
|
+
payload[:text] = "#{format_ats(payload.delete(:at))}#{payload[:text]}"
|
15
|
+
payload
|
16
|
+
end
|
17
|
+
|
18
|
+
private
|
19
|
+
|
20
|
+
def format_ats ats
|
21
|
+
Array(ats).map { |at| "<#{at_cmd_char(at)}#{at}> " }
|
22
|
+
.join("")
|
23
|
+
end
|
24
|
+
|
25
|
+
def at_cmd_char at
|
26
|
+
case at
|
27
|
+
when :here, :channel, :everyone, :group
|
28
|
+
"!"
|
29
|
+
else
|
30
|
+
"@"
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
@@ -0,0 +1,34 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Slack
|
4
|
+
class Messenger
|
5
|
+
class PayloadMiddleware
|
6
|
+
class Base
|
7
|
+
class << self
|
8
|
+
def middleware_name name
|
9
|
+
PayloadMiddleware.register self, name.to_sym
|
10
|
+
end
|
11
|
+
|
12
|
+
def options default_opts
|
13
|
+
@default_opts = default_opts
|
14
|
+
end
|
15
|
+
|
16
|
+
def default_opts
|
17
|
+
@default_opts ||= {}
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
attr_reader :messenger, :options
|
22
|
+
|
23
|
+
def initialize messenger, opts={}
|
24
|
+
@messenger = messenger
|
25
|
+
@options = self.class.default_opts.merge opts
|
26
|
+
end
|
27
|
+
|
28
|
+
def call _payload={}
|
29
|
+
raise NoMethodError, "method `call` not defined for class #{self.class}"
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
@@ -0,0 +1,21 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Slack
|
4
|
+
class Messenger
|
5
|
+
class PayloadMiddleware
|
6
|
+
class Channels < Base
|
7
|
+
middleware_name :channels
|
8
|
+
|
9
|
+
def call payload={}
|
10
|
+
return payload unless payload[:channel].respond_to?(:to_ary)
|
11
|
+
|
12
|
+
payload[:channel].to_ary.map do |channel|
|
13
|
+
pld = payload.dup
|
14
|
+
pld[:channel] = channel
|
15
|
+
pld
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
@@ -0,0 +1,44 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Slack
|
4
|
+
class Messenger
|
5
|
+
class PayloadMiddleware
|
6
|
+
class FormatAttachments < Base
|
7
|
+
middleware_name :format_attachments
|
8
|
+
|
9
|
+
options formats: %i[html markdown]
|
10
|
+
|
11
|
+
def call payload={}
|
12
|
+
payload = payload.dup
|
13
|
+
attachments = payload.delete(:attachments)
|
14
|
+
attachments ||= payload.delete("attachments")
|
15
|
+
|
16
|
+
attachments = wrap_array(attachments).map do |attachment|
|
17
|
+
["text", :text].each do |key|
|
18
|
+
if attachment.key?(key)
|
19
|
+
attachment[key] = Util::LinkFormatter.format(attachment[key], options)
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
attachment
|
24
|
+
end
|
25
|
+
|
26
|
+
payload[:attachments] = attachments if attachments && !attachments.empty?
|
27
|
+
payload
|
28
|
+
end
|
29
|
+
|
30
|
+
private
|
31
|
+
|
32
|
+
def wrap_array object
|
33
|
+
if object.nil?
|
34
|
+
[]
|
35
|
+
elsif object.respond_to?(:to_ary)
|
36
|
+
object.to_ary || [object]
|
37
|
+
else
|
38
|
+
[object]
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
@@ -0,0 +1,20 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Slack
|
4
|
+
class Messenger
|
5
|
+
class PayloadMiddleware
|
6
|
+
class FormatMessage < Base
|
7
|
+
middleware_name :format_message
|
8
|
+
|
9
|
+
options formats: %i[html markdown]
|
10
|
+
|
11
|
+
def call payload={}
|
12
|
+
return payload unless payload[:text]
|
13
|
+
payload[:text] = Util::LinkFormatter.format(payload[:text], options)
|
14
|
+
|
15
|
+
payload
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
@@ -0,0 +1,50 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Slack
|
4
|
+
class Messenger
|
5
|
+
class PayloadMiddleware
|
6
|
+
class Stack
|
7
|
+
attr_reader :messenger,
|
8
|
+
:stack
|
9
|
+
|
10
|
+
def initialize messenger
|
11
|
+
@messenger = messenger
|
12
|
+
@stack = []
|
13
|
+
end
|
14
|
+
|
15
|
+
def set *middlewares
|
16
|
+
middlewares =
|
17
|
+
if middlewares.length == 1 && middlewares.first.is_a?(Hash)
|
18
|
+
middlewares.first
|
19
|
+
else
|
20
|
+
middlewares.flatten
|
21
|
+
end
|
22
|
+
|
23
|
+
@stack = middlewares.map do |key, opts|
|
24
|
+
PayloadMiddleware.registry.fetch(key).new(*[messenger, opts].compact)
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
def call payload={}
|
29
|
+
result = stack.inject payload do |pld, middleware|
|
30
|
+
as_array(pld).flat_map do |p|
|
31
|
+
middleware.call(p)
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
as_array(result)
|
36
|
+
end
|
37
|
+
|
38
|
+
private
|
39
|
+
|
40
|
+
def as_array args
|
41
|
+
if args.respond_to?(:to_ary)
|
42
|
+
args.to_ary
|
43
|
+
else
|
44
|
+
[args]
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|
@@ -0,0 +1,16 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Slack
|
4
|
+
class Messenger
|
5
|
+
module Util
|
6
|
+
module Escape
|
7
|
+
HTML_REGEXP = /[&><]/
|
8
|
+
HTML_REPLACE = { "&" => "&", ">" => ">", "<" => "<" }.freeze
|
9
|
+
|
10
|
+
def self.html string
|
11
|
+
string.gsub(HTML_REGEXP, HTML_REPLACE)
|
12
|
+
end
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
@@ -0,0 +1,64 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "net/http"
|
4
|
+
|
5
|
+
module Slack
|
6
|
+
class Messenger
|
7
|
+
class APIError < StandardError; end
|
8
|
+
|
9
|
+
module Util
|
10
|
+
class HTTPClient
|
11
|
+
class << self
|
12
|
+
def post uri, params
|
13
|
+
HTTPClient.new(uri, params).call
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
attr_reader :uri, :params, :http_options
|
18
|
+
|
19
|
+
def initialize uri, params
|
20
|
+
@uri = uri
|
21
|
+
@http_options = params.delete(:http_options) || {}
|
22
|
+
@params = params
|
23
|
+
end
|
24
|
+
|
25
|
+
# rubocop:disable Layout/IndentHeredoc
|
26
|
+
def call
|
27
|
+
http_obj.request(request_obj).tap do |response|
|
28
|
+
unless response.is_a?(Net::HTTPSuccess)
|
29
|
+
raise Slack::Messenger::APIError, <<-MSG
|
30
|
+
The slack API returned an error: #{response.body} (HTTP Code #{response.code})
|
31
|
+
Check the "Handling Errors" section on https://api.slack.com/incoming-webhooks for more information
|
32
|
+
MSG
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
36
|
+
# rubocop:enable Layout/IndentHeredoc
|
37
|
+
|
38
|
+
private
|
39
|
+
|
40
|
+
def request_obj
|
41
|
+
req = Net::HTTP::Post.new uri.request_uri
|
42
|
+
req.set_form_data params
|
43
|
+
|
44
|
+
req
|
45
|
+
end
|
46
|
+
|
47
|
+
def http_obj
|
48
|
+
http = Net::HTTP.new uri.host, uri.port
|
49
|
+
http.use_ssl = (uri.scheme == "https")
|
50
|
+
|
51
|
+
http_options.each do |opt, val|
|
52
|
+
if http.respond_to? "#{opt}="
|
53
|
+
http.send "#{opt}=", val
|
54
|
+
else
|
55
|
+
warn "Net::HTTP doesn't respond to `#{opt}=`, ignoring that option"
|
56
|
+
end
|
57
|
+
end
|
58
|
+
|
59
|
+
http
|
60
|
+
end
|
61
|
+
end
|
62
|
+
end
|
63
|
+
end
|
64
|
+
end
|
@@ -0,0 +1,81 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Slack
|
4
|
+
class Messenger
|
5
|
+
module Util
|
6
|
+
class LinkFormatter
|
7
|
+
# http://rubular.com/r/19cNXW5qbH
|
8
|
+
HTML_PATTERN = %r{
|
9
|
+
<a
|
10
|
+
(?:.*?)
|
11
|
+
href=['"](.+?)['"]
|
12
|
+
(?:.*?)>
|
13
|
+
(.+?)
|
14
|
+
</a>
|
15
|
+
}x
|
16
|
+
|
17
|
+
VALID_URI_CHARS = '\w\-\.\~\:\/\?\#\[\]\@\!\$\&\'\*\+\,\;\='
|
18
|
+
|
19
|
+
# Attempt at only matching pairs of parens per
|
20
|
+
# the markdown spec http://spec.commonmark.org/0.27/#links
|
21
|
+
#
|
22
|
+
# https://rubular.com/r/WfdZ1arvF6PNWO
|
23
|
+
MARKDOWN_PATTERN = %r{
|
24
|
+
\[ ([^\[\]]*?) \]
|
25
|
+
\(
|
26
|
+
( (?:https?:\/\/|mailto:)
|
27
|
+
(?:[#{VALID_URI_CHARS}]*?|[#{VALID_URI_CHARS}]*?\([#{VALID_URI_CHARS}]*?\)[#{VALID_URI_CHARS}]*?) )
|
28
|
+
\)
|
29
|
+
}x
|
30
|
+
|
31
|
+
class << self
|
32
|
+
def format string, opts={}
|
33
|
+
LinkFormatter.new(string, opts).formatted
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
attr_reader :formats
|
38
|
+
|
39
|
+
def initialize string, formats: %i[html markdown]
|
40
|
+
@formats = formats
|
41
|
+
@orig = string.respond_to?(:scrub) ? string.scrub : string
|
42
|
+
end
|
43
|
+
|
44
|
+
# rubocop:disable Lint/RescueWithoutErrorClass
|
45
|
+
def formatted
|
46
|
+
return @orig unless @orig.respond_to?(:gsub)
|
47
|
+
|
48
|
+
sub_markdown_links(sub_html_links(@orig))
|
49
|
+
rescue => e
|
50
|
+
raise e unless RUBY_VERSION < "2.1" && e.message.include?("invalid byte sequence")
|
51
|
+
raise e, "#{e.message}. Consider including the 'string-scrub' gem to strip invalid characters"
|
52
|
+
end
|
53
|
+
# rubocop:enable Lint/RescueWithoutErrorClass
|
54
|
+
|
55
|
+
private
|
56
|
+
|
57
|
+
def sub_html_links string
|
58
|
+
return string unless formats.include?(:html)
|
59
|
+
|
60
|
+
string.gsub(HTML_PATTERN) do
|
61
|
+
slack_link Regexp.last_match[1], Regexp.last_match[2]
|
62
|
+
end
|
63
|
+
end
|
64
|
+
|
65
|
+
def sub_markdown_links string
|
66
|
+
return string unless formats.include?(:markdown)
|
67
|
+
|
68
|
+
string.gsub(MARKDOWN_PATTERN) do
|
69
|
+
slack_link Regexp.last_match[2], Regexp.last_match[1]
|
70
|
+
end
|
71
|
+
end
|
72
|
+
|
73
|
+
def slack_link link, text=nil
|
74
|
+
"<#{link}" \
|
75
|
+
"#{text && !text.empty? ? "|#{text}" : ''}" \
|
76
|
+
">"
|
77
|
+
end
|
78
|
+
end
|
79
|
+
end
|
80
|
+
end
|
81
|
+
end
|