slack-messenger 2.3.3

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.
Files changed (29) hide show
  1. checksums.yaml +7 -0
  2. data/lib/slack-messenger.rb +59 -0
  3. data/lib/slack-messenger/config.rb +43 -0
  4. data/lib/slack-messenger/payload_middleware.rb +24 -0
  5. data/lib/slack-messenger/payload_middleware/at.rb +36 -0
  6. data/lib/slack-messenger/payload_middleware/base.rb +34 -0
  7. data/lib/slack-messenger/payload_middleware/channels.rb +21 -0
  8. data/lib/slack-messenger/payload_middleware/format_attachments.rb +44 -0
  9. data/lib/slack-messenger/payload_middleware/format_message.rb +20 -0
  10. data/lib/slack-messenger/payload_middleware/stack.rb +50 -0
  11. data/lib/slack-messenger/util/escape.rb +16 -0
  12. data/lib/slack-messenger/util/http_client.rb +64 -0
  13. data/lib/slack-messenger/util/link_formatter.rb +81 -0
  14. data/lib/slack-messenger/version.rb +7 -0
  15. data/spec/end_to_end_spec.rb +95 -0
  16. data/spec/integration/ping_integration_test.rb +16 -0
  17. data/spec/lib/slack-messenger/config_spec.rb +71 -0
  18. data/spec/lib/slack-messenger/payload_middleware/at_spec.rb +25 -0
  19. data/spec/lib/slack-messenger/payload_middleware/base_spec.rb +76 -0
  20. data/spec/lib/slack-messenger/payload_middleware/channels_spec.rb +20 -0
  21. data/spec/lib/slack-messenger/payload_middleware/format_attachments_spec.rb +48 -0
  22. data/spec/lib/slack-messenger/payload_middleware/format_message_spec.rb +27 -0
  23. data/spec/lib/slack-messenger/payload_middleware/stack_spec.rb +119 -0
  24. data/spec/lib/slack-messenger/payload_middleware_spec.rb +33 -0
  25. data/spec/lib/slack-messenger/util/http_client_spec.rb +55 -0
  26. data/spec/lib/slack-messenger/util/link_formatter_spec.rb +155 -0
  27. data/spec/lib/slack-messenger_spec.rb +98 -0
  28. data/spec/spec_helper.rb +28 -0
  29. 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 = { "&" => "&amp;", ">" => "&gt;", "<" => "&lt;" }.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