slack-notifier 1.5.1 → 2.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.
Files changed (33) hide show
  1. checksums.yaml +5 -5
  2. data/lib/slack-notifier/config.rb +43 -0
  3. data/lib/slack-notifier/payload_middleware/at.rb +36 -0
  4. data/lib/slack-notifier/payload_middleware/base.rb +34 -0
  5. data/lib/slack-notifier/payload_middleware/channels.rb +21 -0
  6. data/lib/slack-notifier/payload_middleware/format_attachments.rb +44 -0
  7. data/lib/slack-notifier/payload_middleware/format_message.rb +20 -0
  8. data/lib/slack-notifier/payload_middleware/stack.rb +50 -0
  9. data/lib/slack-notifier/payload_middleware.rb +24 -0
  10. data/lib/slack-notifier/util/escape.rb +16 -0
  11. data/lib/slack-notifier/util/http_client.rb +64 -0
  12. data/lib/slack-notifier/util/link_formatter.rb +80 -0
  13. data/lib/slack-notifier/version.rb +3 -1
  14. data/lib/slack-notifier.rb +38 -60
  15. data/spec/end_to_end_spec.rb +95 -0
  16. data/spec/integration/ping_integration_test.rb +14 -5
  17. data/spec/lib/slack-notifier/config_spec.rb +71 -0
  18. data/spec/lib/slack-notifier/payload_middleware/at_spec.rb +25 -0
  19. data/spec/lib/slack-notifier/payload_middleware/base_spec.rb +76 -0
  20. data/spec/lib/slack-notifier/payload_middleware/channels_spec.rb +20 -0
  21. data/spec/lib/slack-notifier/payload_middleware/format_attachments_spec.rb +48 -0
  22. data/spec/lib/slack-notifier/payload_middleware/format_message_spec.rb +27 -0
  23. data/spec/lib/slack-notifier/payload_middleware/stack_spec.rb +119 -0
  24. data/spec/lib/slack-notifier/payload_middleware_spec.rb +33 -0
  25. data/spec/lib/slack-notifier/util/http_client_spec.rb +55 -0
  26. data/spec/lib/slack-notifier/util/link_formatter_spec.rb +163 -0
  27. data/spec/lib/slack-notifier_spec.rb +63 -128
  28. data/spec/spec_helper.rb +20 -5
  29. metadata +39 -13
  30. data/lib/slack-notifier/default_http_client.rb +0 -51
  31. data/lib/slack-notifier/link_formatter.rb +0 -62
  32. data/spec/lib/slack-notifier/default_http_client_spec.rb +0 -37
  33. data/spec/lib/slack-notifier/link_formatter_spec.rb +0 -78
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
- SHA1:
3
- metadata.gz: e74413c9d125dd2a973d6035bdc86e6f28549839
4
- data.tar.gz: 6081e63972a12dc07acc866e13f8e6bae8a1992d
2
+ SHA256:
3
+ metadata.gz: 2b0deceb91f70d0812f01b82db70eacb8dbf01d4d1985ca02375ac13b36d829e
4
+ data.tar.gz: 5ec7d0eb36e91bbee6f510411646b4c57935aa661706b791e1dc0941fa66dbd9
5
5
  SHA512:
6
- metadata.gz: 252d1d23a8bb02cfe9ebee51c8d68436799daa59c95572dd18bc481797069ed2fa6fac96869845ffeae900fbf5d73c9df9b4e910f9190f7ca85937293e8d3e31
7
- data.tar.gz: bda7747d3bd8e34a0ca2798d50c06c4f2b6e426c2552c2d389247f6ed60ce48a82d26e7f77e7e9c179388e831163d36a237dfe139cdddec689fdb154ee3b7d1b
6
+ metadata.gz: 0d42ebede5966444cf3101f9410f442e3fad0cdd3a9a7a25f67ff8c068c474bbd49f08d120070cca8652ffd351d68e99f90f648090b97469e3519761b167f513
7
+ data.tar.gz: 950d6e0ebc557de14b56b8ea5982e6057eb54530ec7b5b406f3c28ca22c3aaf26744c2ce6d69654b39bc75c8525d43204687b8adb97c331cf21c91f9629bd97f
@@ -0,0 +1,43 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Slack
4
+ class Notifier
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,36 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Slack
4
+ class Notifier
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 Notifier
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 :notifier, :options
22
+
23
+ def initialize notifier, opts={}
24
+ @notifier = notifier
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 Notifier
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 Notifier
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 Notifier
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 Notifier
5
+ class PayloadMiddleware
6
+ class Stack
7
+ attr_reader :notifier,
8
+ :stack
9
+
10
+ def initialize notifier
11
+ @notifier = notifier
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(*[notifier, 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,24 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Slack
4
+ class Notifier
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,16 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Slack
4
+ class Notifier
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 Notifier
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::Notifier::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,80 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Slack
4
+ class Notifier
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
+ # the path portion of a url can contain these characters
18
+ VALID_PATH_CHARS = '\w\-\.\~\/\?\#\='
19
+
20
+ # Attempt at only matching pairs of parens per
21
+ # the markdown spec http://spec.commonmark.org/0.27/#links
22
+ #
23
+ # http://rubular.com/r/y107aevxqT
24
+ MARKDOWN_PATTERN = %r{
25
+ \[ ([^\[\]]*?) \]
26
+ \( ((https?://.*?) | (mailto:.*?)) \)
27
+ (?! [#{VALID_PATH_CHARS}]* \) )
28
+ }x
29
+
30
+ class << self
31
+ def format string, opts={}
32
+ LinkFormatter.new(string, **opts).formatted
33
+ end
34
+ end
35
+
36
+ attr_reader :formats
37
+
38
+ def initialize string, formats: %i[html markdown]
39
+ @formats = formats
40
+ @orig = string.respond_to?(:scrub) ? string.scrub : string
41
+ end
42
+
43
+ # rubocop:disable Lint/RescueWithoutErrorClass
44
+ def formatted
45
+ return @orig unless @orig.respond_to?(:gsub)
46
+
47
+ sub_markdown_links(sub_html_links(@orig))
48
+ rescue => e
49
+ raise e unless RUBY_VERSION < "2.1" && e.message.include?("invalid byte sequence")
50
+ raise e, "#{e.message}. Consider including the 'string-scrub' gem to strip invalid characters"
51
+ end
52
+ # rubocop:enable Lint/RescueWithoutErrorClass
53
+
54
+ private
55
+
56
+ def sub_html_links string
57
+ return string unless formats.include?(:html)
58
+
59
+ string.gsub(HTML_PATTERN) do
60
+ slack_link Regexp.last_match[1], Regexp.last_match[2]
61
+ end
62
+ end
63
+
64
+ def sub_markdown_links string
65
+ return string unless formats.include?(:markdown)
66
+
67
+ string.gsub(MARKDOWN_PATTERN) do
68
+ slack_link Regexp.last_match[2], Regexp.last_match[1]
69
+ end
70
+ end
71
+
72
+ def slack_link link, text=nil
73
+ "<#{link}" \
74
+ "#{text && !text.empty? ? "|#{text}" : ''}" \
75
+ ">"
76
+ end
77
+ end
78
+ end
79
+ end
80
+ end
@@ -1,5 +1,7 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Slack
2
4
  class Notifier
3
- VERSION = "1.5.1"
5
+ VERSION = "2.4.0".freeze # rubocop:disable Style/RedundantFreeze
4
6
  end
5
7
  end
@@ -1,81 +1,59 @@
1
- require 'net/http'
2
- require 'uri'
3
- require 'json'
1
+ # frozen_string_literal: true
4
2
 
5
- require_relative 'slack-notifier/default_http_client'
6
- require_relative 'slack-notifier/link_formatter'
3
+ require "uri"
4
+ require "json"
5
+
6
+ require_relative "slack-notifier/util/http_client"
7
+ require_relative "slack-notifier/util/link_formatter"
8
+ require_relative "slack-notifier/util/escape"
9
+ require_relative "slack-notifier/payload_middleware"
10
+ require_relative "slack-notifier/config"
7
11
 
8
12
  module Slack
9
13
  class Notifier
10
- attr_reader :endpoint, :default_payload
11
-
12
- def initialize webhook_url, options={}
13
- @endpoint = URI.parse webhook_url
14
- @default_payload = options
15
- end
16
-
17
- def ping message, options={}
18
- if message.is_a?(Hash)
19
- message, options = nil, message
20
- end
21
-
22
- if attachments = options[:attachments] || options["attachments"]
23
- wrap_array(attachments).each do |attachment|
24
- ["text", :text].each do |key|
25
- attachment[key] = LinkFormatter.format(attachment[key]) if attachment.has_key?(key)
26
- end
27
- end
28
- end
14
+ attr_reader :endpoint
29
15
 
30
- payload = default_payload.merge(options)
31
- client = payload.delete(:http_client) || http_client
32
- http_options = payload.delete(:http_options)
16
+ def initialize webhook_url, options={}, &block
17
+ @endpoint = URI.parse webhook_url
33
18
 
34
- unless message.nil?
35
- payload.merge!(text: LinkFormatter.format(message))
36
- end
37
-
38
- params = { payload: payload.to_json }
39
- params[:http_options] = http_options if http_options
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?
40
22
 
41
- client.post endpoint, params
23
+ middleware.set config.middleware
42
24
  end
43
25
 
44
- def http_client
45
- default_payload.fetch :http_client, DefaultHTTPClient
26
+ def config
27
+ @_config ||= Config.new
46
28
  end
47
29
 
48
- def channel
49
- default_payload[:channel]
50
- end
30
+ def ping message, options={}
31
+ if message.is_a?(Hash)
32
+ options = message
33
+ else
34
+ options[:text] = message
35
+ end
51
36
 
52
- def channel= channel
53
- default_payload[:channel] = channel
37
+ post options
54
38
  end
55
39
 
56
- def username
57
- default_payload[:username]
58
- end
40
+ def post payload={}
41
+ params = {}
42
+ client = payload.delete(:http_client) || config.http_client
43
+ payload = config.defaults.merge(payload)
59
44
 
60
- def username= username
61
- default_payload[:username] = username
62
- end
45
+ params[:http_options] = payload.delete(:http_options) if payload.key?(:http_options)
63
46
 
64
- HTML_ESCAPE_REGEXP = /[&><]/
65
- HTML_ESCAPE = { '&' => '&amp;', '>' => '&gt;', '<' => '&lt;' }
66
-
67
- def escape(text)
68
- text.gsub(HTML_ESCAPE_REGEXP, HTML_ESCAPE)
47
+ middleware.call(payload).map do |pld|
48
+ params[:payload] = pld.to_json
49
+ client.post endpoint, params
50
+ end
69
51
  end
70
52
 
71
- def wrap_array(object)
72
- if object.nil?
73
- []
74
- elsif object.respond_to?(:to_ary)
75
- object.to_ary || [object]
76
- else
77
- [object]
53
+ private
54
+
55
+ def middleware
56
+ @middleware ||= PayloadMiddleware::Stack.new(self)
78
57
  end
79
- end
80
58
  end
81
59
  end
@@ -0,0 +1,95 @@
1
+ # frozen_string_literal: true
2
+ # encoding: utf-8
3
+
4
+ require "spec_helper"
5
+
6
+ RSpec.describe Slack::Notifier do
7
+ {
8
+ { text: "hello" } =>
9
+ { payload: { text: "hello" } },
10
+
11
+ { text: "[hello](http://example.com/world)" } =>
12
+ { payload: { text: "<http://example.com/world|hello>" } },
13
+
14
+ { text: '<a href="http://example.com">example</a>' } =>
15
+ { payload: { text: "<http://example.com|example>" } },
16
+
17
+ { text: "hello/こんにちは from notifier test" } =>
18
+ { payload: { text: "hello/こんにちは from notifier test" } },
19
+
20
+ { text: "Hello World, enjoy [](http://example.com)." } =>
21
+ { payload: { text: "Hello World, enjoy <http://example.com>." } },
22
+
23
+ { text: "Hello World, enjoy [this](http://example.com)[this2](http://example2.com)" } =>
24
+ { payload: { text: "Hello World, enjoy <http://example.com|this><http://example2.com|this2>" } },
25
+
26
+ { text: "[John](mailto:john@example.com)" } =>
27
+ { payload: { text: "<mailto:john@example.com|John>" } },
28
+
29
+ { text: '<a href="mailto:john@example.com">John</a>' } =>
30
+ { payload: { text: "<mailto:john@example.com|John>" } },
31
+
32
+ { text: "hello", channel: "hodor" } =>
33
+ { payload: { text: "hello", channel: "hodor" } },
34
+
35
+ { text: nil, attachments: [{ text: "attachment message" }] } =>
36
+ { payload: { text: nil, attachments: [{ text: "attachment message" }] } },
37
+
38
+ { text: "the message", channel: "foo", attachments: [{ color: "#000",
39
+ text: "attachment message",
40
+ fallback: "fallback message" }] } =>
41
+ { payload: { text: "the message",
42
+ channel: "foo",
43
+ attachments: [{ color: "#000",
44
+ text: "attachment message",
45
+ fallback: "fallback message" }] } },
46
+
47
+ { attachments: [{ color: "#000",
48
+ text: "attachment message",
49
+ fallback: "fallback message" }] } =>
50
+ { payload: { attachments: [{ color: "#000",
51
+ text: "attachment message",
52
+ fallback: "fallback message" }] } },
53
+
54
+ { attachments: { color: "#000",
55
+ text: "attachment message [hodor](http://winterfell.com)",
56
+ fallback: "fallback message" } } =>
57
+ { payload: { attachments: [{ color: "#000",
58
+ text: "attachment message <http://winterfell.com|hodor>",
59
+ fallback: "fallback message" }] } },
60
+
61
+ { attachments: { color: "#000",
62
+ text: nil,
63
+ fallback: "fallback message" } } =>
64
+ { payload: { attachments: [{ color: "#000",
65
+ text: nil,
66
+ fallback: "fallback message" }] } },
67
+
68
+ { text: "hello", http_options: { timeout: 5 } } =>
69
+ { http_options: { timeout: 5 }, payload: { text: "hello" } }
70
+ }.each do |args, payload|
71
+ it "sends correct payload for #post(#{args})" do
72
+ http_client = class_double("Slack::Notifier::Util::HTTPClient", post: nil)
73
+ notifier = Slack::Notifier.new "http://example.com", http_client: http_client
74
+ payload[:payload] = payload[:payload].to_json
75
+
76
+ expect(http_client).to receive(:post)
77
+ .with(URI.parse("http://example.com"), payload)
78
+
79
+ notifier.post(args)
80
+ end
81
+ end
82
+
83
+ it "applies options given to middleware" do
84
+ client = class_double("Slack::Notifier::Util::HTTPClient", post: nil)
85
+ notifier = Slack::Notifier.new "http://example.com" do
86
+ http_client client
87
+ middleware format_message: { formats: [] }
88
+ end
89
+
90
+ expect(client).to receive(:post)
91
+ .with(URI.parse("http://example.com"), payload: { text: "Hello [world](http://example.com)!" }.to_json)
92
+
93
+ notifier.post text: "Hello [world](http://example.com)!"
94
+ end
95
+ end