slack-messenger 2.3.3

Sign up to get free protection for your applications and to get access to all the features.
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