event_trigger 0.1.1 → 0.1.2

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.
@@ -46,6 +46,7 @@ module EventTrigger
46
46
  true
47
47
  end
48
48
 
49
+
49
50
  private
50
51
 
51
52
  def action_mailer_delivery?
@@ -68,7 +69,24 @@ module EventTrigger
68
69
  def deliver_via_smtp(from, to, subject, body)
69
70
  require "net/smtp"
70
71
  settings = smtp_settings
71
- message = <<~MSG
72
+ message = build_message(from, to, subject, body)
73
+ smtp = Net::SMTP.new(settings[:address] || "localhost", settings[:port] || 25)
74
+ apply_smtp_timeouts(smtp, settings)
75
+ if settings[:enable_starttls_auto] && smtp.respond_to?(:enable_starttls_auto)
76
+ smtp.enable_starttls_auto
77
+ end
78
+ smtp.start(
79
+ settings[:domain] || "localhost",
80
+ settings[:user_name] || settings[:username],
81
+ settings[:password],
82
+ settings[:authentication] || :plain
83
+ ) do
84
+ smtp.send_message(message, from, Array(to))
85
+ end
86
+ end
87
+
88
+ def build_message(from, to, subject, body)
89
+ <<~MSG
72
90
  From: #{from}
73
91
  To: #{Array(to).join(", ")}
74
92
  Subject: #{subject}
@@ -77,20 +95,29 @@ module EventTrigger
77
95
 
78
96
  #{body}
79
97
  MSG
80
- Net::SMTP.start(
81
- settings[:address] || "localhost",
82
- settings[:port] || 25,
83
- settings[:domain] || "localhost",
84
- settings[:user_name],
85
- settings[:password],
86
- settings[:authentication] || :plain
87
- ) do |smtp|
88
- smtp.send_message(message, from, Array(to))
89
- end
90
98
  end
91
99
 
100
+ # Guard against a stalled/unreachable SMTP server blocking the app
101
+ # forever (configurable via config.email.smtp timeouts).
102
+ def apply_smtp_timeouts(smtp, settings)
103
+ smtp.open_timeout = settings[:open_timeout] || 5 if smtp.respond_to?(:open_timeout=)
104
+ smtp.read_timeout = settings[:read_timeout] || 10 if smtp.respond_to?(:read_timeout=)
105
+ smtp.write_timeout = settings[:write_timeout] || 10 if smtp.respond_to?(:write_timeout=)
106
+ rescue StandardError
107
+ nil
108
+ end
109
+
110
+ # NEW additive alias: spec uses config.email.smtp = {...}.
111
+ # Existing config.email.smtp_settings keeps working; smtp wins
112
+ # when both are set. Accepts string or symbol keys.
92
113
  def smtp_settings
93
- config.smtp_settings || {}
114
+ raw = config.smtp || config.smtp_settings || {}
115
+ raw = {} unless raw.is_a?(Hash)
116
+ raw.each_with_object({}) do |(k, v), memo|
117
+ key = k.to_sym
118
+ key = :user_name if key == :username # Net::SMTP expects :user_name
119
+ memo[key] = v
120
+ end
94
121
  end
95
122
 
96
123
  def build_subject(event)
@@ -12,6 +12,9 @@ module EventTrigger
12
12
  # request itself raises (DNS, timeout, connection error), it falls
13
13
  # back to Faraday when available. HTTP error statuses raise
14
14
  # EventTrigger::Error directly (no retry — same result expected).
15
+ #
16
+ # NOTE: faraday is loaded lazily (only on fallback) so apps that
17
+ # never hit the network path pay no load cost.
15
18
  module HttpPoster
16
19
  private
17
20
 
@@ -26,31 +29,73 @@ module EventTrigger
26
29
  end
27
30
  end
28
31
 
32
+ def request_json(method, url, body, headers = { "Content-Type" => "application/json" }, service: "HTTP")
33
+ payload = JSON.generate(body)
34
+ begin
35
+ net_http_request(method, url, payload, headers, service)
36
+ rescue EventTrigger::Error
37
+ raise
38
+ rescue StandardError => e
39
+ # Only POST has a Faraday fallback today; other verbs re-raise.
40
+ raise e unless method.to_s.downcase == "post"
41
+
42
+ faraday_post(url, payload, headers, service, e)
43
+ end
44
+ end
45
+
29
46
  def net_http_post(url, payload, headers, service)
47
+ net_http_request(:post, url, payload, headers, service)
48
+ end
49
+
50
+ def net_http_request(method, url, payload, headers, service)
30
51
  uri = URI.parse(url.to_s)
31
52
  http = Net::HTTP.new(uri.host, uri.port)
32
53
  http.use_ssl = uri.scheme == "https"
54
+ # Hard timeouts at both ends: connect, read AND write. Without these
55
+ # a stalled peer could block the host app's web worker indefinitely.
33
56
  http.open_timeout = 5
34
57
  http.read_timeout = 10
35
- request = Net::HTTP::Post.new(uri.request_uri, headers)
36
- request.body = payload
58
+ http.write_timeout = 10 if http.respond_to?(:write_timeout=)
59
+ http.continue_timeout = 5 if http.respond_to?(:continue_timeout=)
60
+ klass = case method.to_s.downcase
61
+ when "get" then Net::HTTP::Get
62
+ when "put" then Net::HTTP::Put
63
+ when "patch" then Net::HTTP::Patch
64
+ when "delete" then Net::HTTP::Delete
65
+ else Net::HTTP::Post
66
+ end
67
+ request = klass.new(uri.request_uri, headers)
68
+ request.body = payload unless method.to_s.downcase == "get"
37
69
  response = http.request(request)
38
70
  unless response.is_a?(Net::HTTPSuccess)
39
- raise Error, "#{service} delivery failed (HTTP #{response.code}): #{response.body}"
71
+ raise Error, "#{service} delivery failed (HTTP #{response.code}): #{truncate(response.body)}"
40
72
  end
41
73
  response
42
74
  end
43
75
 
44
76
  def faraday_post(url, payload, headers, service, original_error)
45
77
  require "faraday"
46
- response = Faraday.post(url.to_s, payload, headers)
78
+ conn = Faraday.new do |f|
79
+ f.options.timeout = 10
80
+ f.options.open_timeout = 5
81
+ end
82
+ response = conn.post(url.to_s, payload, headers)
47
83
  unless response.success?
48
- raise Error, "#{service} delivery failed (HTTP #{response.status}): #{response.body}"
84
+ raise Error, "#{service} delivery failed (HTTP #{response.status}): #{truncate(response.body)}"
49
85
  end
50
86
  response
51
87
  rescue LoadError
52
88
  raise original_error
53
89
  end
90
+
91
+ # Keep error messages bounded — a huge HTML error page must not blow
92
+ # up memory or logs.
93
+ def truncate(str, limit = 500)
94
+ s = str.to_s
95
+ s.length > limit ? "#{s[0, limit]}…" : s
96
+ rescue StandardError
97
+ ""
98
+ end
54
99
  end
55
100
  end
56
101
  end
@@ -5,41 +5,86 @@ require "uri"
5
5
  require "json"
6
6
  require "time"
7
7
  require_relative "../provider"
8
+ require_relative "http_poster"
8
9
 
9
10
  module EventTrigger
10
11
  module Providers
11
- # Sends event notifications to Slack (or any Slack-compatible /
12
- # Discord-compatible incoming-webhook URL).
12
+ # Sends event notifications to Slack.
13
13
  #
14
- # Config:
15
- # config.slack.enabled = true
16
- # config.slack.webhook_url = ENV["SLACK_WEBHOOK_URL"]
17
- # config.slack.events = ["loan.activated"]
14
+ # Two modes (existing webhook mode is untouched):
15
+ #
16
+ # 1. Legacy incoming-webhook mode (unchanged):
17
+ # config.slack.webhook_url = ENV["SLACK_WEBHOOK_URL"]
18
+ # - hooks.slack.com URLs receive { text: ... }.
19
+ # - any other URL receives the Discord-style embed payload.
20
+ #
21
+ # 2. NEW Slack Web API mode (chat.postMessage), used only when BOTH
22
+ # token + channel are set:
23
+ # config.slack.token = ENV["SLACK_BOT_TOKEN"]
24
+ # config.slack.channel = ENV["SLACK_CHANNEL"] # e.g. "#alerts" or "C123"
25
+ # API: POST https://slack.com/api/chat.postMessage
26
+ # Auth: Authorization: Bearer <token>
18
27
  #
19
- # Payload behaviour:
20
- # - Slack incoming webhooks (hooks.slack.com) receive { text: ... }.
21
- # - All other URLs (e.g. Discord-style webhooks) receive a rich embed
22
- # payload with error/backtrace/context fields, truncated to stay
23
- # within message limits.
28
+ # Common:
29
+ # config.slack.enabled = true
30
+ # config.slack.events = ["loan.activated", "payment.received"]
24
31
  class SlackProvider < Provider
25
32
  MAX_TEXT = 1500
26
33
 
34
+ WEB_API_URL = "https://slack.com/api/chat.postMessage"
35
+
27
36
  def self.provider_name
28
37
  :slack
29
38
  end
30
39
 
40
+ include HttpPoster
41
+
31
42
  def deliver(event)
43
+ # --- NEW: Web API mode (additive; webhook path below untouched) ---
44
+ token = config.token
45
+ channel = config.channel
46
+ if present?(token) && present?(channel)
47
+ return deliver_via_web_api(event, token.to_s, channel.to_s)
48
+ end
49
+
50
+ # --- Existing webhook mode (byte-for-byte behaviour preserved) ---
32
51
  url = config.webhook_url || config.url
33
52
  raise Error, "Slack webhook_url is not configured" if url.nil? || url.to_s.strip.empty?
34
53
 
35
54
  body = build_payload(url.to_s, event)
36
- post_json(url.to_s, body)
55
+ post_json(url.to_s, body, { "Content-Type" => "application/json" }, service: "Slack")
37
56
  log("delivered event '#{event.name}'")
38
57
  true
39
58
  end
40
59
 
60
+
41
61
  private
42
62
 
63
+ def deliver_via_web_api(event, token, channel)
64
+ body = { channel: channel, text: slack_text(event, event.payload) }
65
+ response = post_json(
66
+ WEB_API_URL, body,
67
+ { "Content-Type" => "application/json", "Authorization" => "Bearer #{token}" },
68
+ service: "Slack"
69
+ )
70
+ parsed = parse_json(response.body)
71
+ unless parsed.is_a?(Hash) && parsed["ok"] == true
72
+ raise Error, "Slack Web API error: #{response.body}"
73
+ end
74
+ log("delivered event '#{event.name}' via chat.postMessage")
75
+ true
76
+ end
77
+
78
+ def parse_json(str)
79
+ JSON.parse(str.to_s)
80
+ rescue StandardError
81
+ nil
82
+ end
83
+
84
+ def present?(value)
85
+ !value.nil? && !value.to_s.strip.empty?
86
+ end
87
+
43
88
  def build_payload(url, event)
44
89
  payload = event.payload
45
90
  if url.include?("hooks.slack.com")
@@ -103,21 +148,6 @@ module EventTrigger
103
148
  def truncate_for_discord(str, limit = 1000)
104
149
  truncate(str, limit)
105
150
  end
106
-
107
- def post_json(url, body)
108
- uri = URI.parse(url)
109
- http = Net::HTTP.new(uri.host, uri.port)
110
- http.use_ssl = uri.scheme == "https"
111
- http.open_timeout = 5
112
- http.read_timeout = 10
113
- request = Net::HTTP::Post.new(uri.request_uri, "Content-Type" => "application/json")
114
- request.body = JSON.generate(body)
115
- response = http.request(request)
116
- unless response.is_a?(Net::HTTPSuccess)
117
- raise Error, "Slack delivery failed (HTTP #{response.code}): #{response.body}"
118
- end
119
- response
120
- end
121
151
  end
122
152
  end
123
153
  end
@@ -0,0 +1,83 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../provider"
4
+
5
+ module EventTrigger
6
+ module Providers
7
+ # Sends SMS notifications. Pluggable interface — today backed by
8
+ # Twilio (lazy-loaded, only when this provider is enabled/used).
9
+ #
10
+ # Twilio Messaging API base: https://api.twilio.com/2010-04-01
11
+ #
12
+ # Config:
13
+ # config.sms.enabled = true
14
+ # config.sms.provider = :twilio # only :twilio supported today
15
+ # config.sms.account_sid = ENV["TWILIO_ACCOUNT_SID"]
16
+ # config.sms.auth_token = ENV["TWILIO_AUTH_TOKEN"]
17
+ # config.sms.from = ENV["TWILIO_FROM"] # E.164 number, e.g. +15551234567
18
+ # config.sms.events = ["payment.received"]
19
+ #
20
+ # Per-trigger overrides: data: { sms_to:, sms_body: }
21
+ # Crash-safe: failures raise EventTrigger::Error (Dispatcher logs them).
22
+ class SmsProvider < Provider
23
+ MAX_BODY = 1000
24
+
25
+ def self.provider_name
26
+ :sms
27
+ end
28
+
29
+ def deliver(event)
30
+ name = (config.provider || :twilio).to_sym
31
+ raise Error, "SMS provider #{name.inspect} is not supported (only :twilio)" unless name == :twilio
32
+
33
+ client_class = twilio_client_class
34
+ unless client_class
35
+ raise Error, TwilioClient::GEM_MISSING_MESSAGE
36
+ end
37
+
38
+ sid = config.account_sid || config.twilio_sid
39
+ token = config.auth_token || config.twilio_token
40
+ from = config.from || config.twilio_from
41
+ to = event.payload[:sms_to] || event.payload["sms_to"] || config.to
42
+ raise Error, "SMS account_sid is not configured" if blank?(sid)
43
+ raise Error, "SMS auth_token is not configured" if blank?(token)
44
+ raise Error, "SMS sender (from) is not configured" if blank?(from)
45
+ raise Error, "SMS recipient (to) is not configured" if blank?(to)
46
+
47
+ body = event.payload[:sms_body] || event.payload["sms_body"] || build_body(event)
48
+ Twilio::REST::Client.new(sid.to_s, token.to_s).messages.create(
49
+ from: from.to_s, to: to.to_s, body: truncate(body.to_s)
50
+ )
51
+ log("delivered event '#{event.name}' to #{to}")
52
+ true
53
+ end
54
+
55
+
56
+ private
57
+
58
+ # Thin wrapper around the lazy resolver — kept as an instance method
59
+ # so tests can stub it (e.g. simulate "gem missing" deterministically).
60
+ def twilio_client_class
61
+ TwilioClient.resolve
62
+ end
63
+
64
+ def build_body(event)
65
+ err = event.payload[:error] || event.payload["error"]
66
+ return err.to_s if err
67
+
68
+ summary = event.payload.map { |k, v| "#{k}: #{v}" }.join(", ")
69
+ summary = event.payload.inspect if summary.empty?
70
+ "[#{event.name}] #{summary}"
71
+ end
72
+
73
+ def blank?(value)
74
+ value.nil? || value.to_s.strip.empty?
75
+ end
76
+
77
+ def truncate(str, limit = MAX_BODY)
78
+ s = str.to_s
79
+ s.length > limit ? "#{s[0, limit]}…" : s
80
+ end
81
+ end
82
+ end
83
+ end
@@ -0,0 +1,67 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../provider"
4
+ require_relative "http_poster"
5
+
6
+ module EventTrigger
7
+ module Providers
8
+ # Sends event notifications to a Microsoft Teams channel via the
9
+ # Microsoft Graph API.
10
+ #
11
+ # API: POST https://graph.microsoft.com/v1.0/teams/{team-id}/channels/{channel-id}/messages
12
+ # Auth: Authorization: Bearer <ACCESS_TOKEN>
13
+ #
14
+ # Config:
15
+ # config.teams.enabled = true
16
+ # config.teams.access_token = ENV["TEAMS_ACCESS_TOKEN"]
17
+ # config.teams.team_id = ENV["TEAMS_TEAM_ID"]
18
+ # config.teams.channel_id = ENV["TEAMS_CHANNEL_ID"]
19
+ # config.teams.events = ["loan.activated"]
20
+ #
21
+ # Crash-safe: failures raise EventTrigger::Error (Dispatcher logs them).
22
+ class TeamsProvider < Provider
23
+ include HttpPoster
24
+
25
+ GRAPH_BASE = "https://graph.microsoft.com/v1.0"
26
+
27
+ def self.provider_name
28
+ :teams
29
+ end
30
+
31
+ def deliver(event)
32
+ token = config.access_token
33
+ team_id = config.team_id
34
+ channel_id = config.channel_id
35
+ raise Error, "Teams access_token is not configured" if blank?(token)
36
+ raise Error, "Teams team_id is not configured" if blank?(team_id)
37
+ raise Error, "Teams channel_id is not configured" if blank?(channel_id)
38
+
39
+ url = "#{GRAPH_BASE}/teams/#{team_id}/channels/#{channel_id}/messages"
40
+ post_json(
41
+ url, { body: { contentType: "text", content: message_text(event) } },
42
+ { "Content-Type" => "application/json", "Authorization" => "Bearer #{token}" },
43
+ service: "Teams"
44
+ )
45
+ log("delivered event '#{event.name}'")
46
+ true
47
+ end
48
+
49
+
50
+ private
51
+
52
+ def message_text(event)
53
+ payload = event.payload
54
+ err = payload[:error] || payload["error"]
55
+ return err.to_s if err
56
+
57
+ summary = payload.map { |k, v| "#{k}: #{v}" }.join(", ")
58
+ summary = payload.inspect if summary.empty?
59
+ "[#{event.name}] #{summary}"
60
+ end
61
+
62
+ def blank?(value)
63
+ value.nil? || value.to_s.strip.empty?
64
+ end
65
+ end
66
+ end
67
+ end
@@ -0,0 +1,87 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../provider"
4
+ require_relative "http_poster"
5
+
6
+ module EventTrigger
7
+ module Providers
8
+ # Sends event notifications to Telegram via the Bot API.
9
+ #
10
+ # API: POST https://api.telegram.org/bot<BOT_TOKEN>/sendMessage
11
+ #
12
+ # Config:
13
+ # config.telegram.enabled = true
14
+ # config.telegram.bot_token = ENV["TELEGRAM_BOT_TOKEN"]
15
+ # config.telegram.chat_id = ENV["TELEGRAM_CHAT_ID"]
16
+ # config.telegram.events = ["loan.overdue"]
17
+ #
18
+ # Per-trigger override: data: { telegram_chat_id: "..." }
19
+ # Crash-safe: failures raise EventTrigger::Error (Dispatcher logs them).
20
+ class TelegramProvider < Provider
21
+ include HttpPoster
22
+
23
+ MAX_TEXT = 4000
24
+
25
+ def self.provider_name
26
+ :telegram
27
+ end
28
+
29
+ def deliver(event)
30
+ token = config.bot_token
31
+ chat_id = event.payload[:telegram_chat_id] || event.payload["telegram_chat_id"] || config.chat_id
32
+ raise Error, "Telegram bot_token is not configured" if blank?(token)
33
+ raise Error, "Telegram chat_id is not configured" if blank?(chat_id)
34
+
35
+ url = "https://api.telegram.org/bot#{token}/sendMessage"
36
+ response = post_json(url, { chat_id: chat_id.to_s, text: message_text(event) }, service: "Telegram")
37
+ parsed = parse_json(response_body(response))
38
+ # Telegram returns { "ok": true, ... } on success.
39
+ if parsed.is_a?(Hash) && parsed.key?("ok") && parsed["ok"] != true
40
+ raise Error, "Telegram delivery failed: #{response_body(response)}"
41
+ end
42
+ log("delivered event '#{event.name}'")
43
+ true
44
+ end
45
+
46
+
47
+ private
48
+
49
+ def message_text(event)
50
+ payload = event.payload
51
+ err = payload[:error] || payload["error"]
52
+ text =
53
+ if err
54
+ bt = payload[:backtrace] || payload["backtrace"]
55
+ lines = ["🚨 #{err}"]
56
+ lines << Array(bt).first.to_s if bt
57
+ lines.join("\n")
58
+ else
59
+ summary = payload.map { |k, v| "#{k}: #{v}" }.join(", ")
60
+ summary = payload.inspect if summary.empty?
61
+ "[#{event.name}] #{summary}"
62
+ end
63
+ truncate(text)
64
+ end
65
+
66
+ def response_body(response)
67
+ response.respond_to?(:body) ? response.body : response.to_s
68
+ end
69
+
70
+ def parse_json(str)
71
+ require "json"
72
+ JSON.parse(str.to_s)
73
+ rescue StandardError
74
+ nil
75
+ end
76
+
77
+ def blank?(value)
78
+ value.nil? || value.to_s.strip.empty?
79
+ end
80
+
81
+ def truncate(str, limit = MAX_TEXT)
82
+ s = str.to_s
83
+ s.length > limit ? "#{s[0, limit]}…" : s
84
+ end
85
+ end
86
+ end
87
+ end
@@ -0,0 +1,45 @@
1
+ # frozen_string_literal: true
2
+
3
+ module EventTrigger
4
+ module Providers
5
+ # Lazy, crash-safe resolver for the Twilio client class.
6
+ #
7
+ # - Returns the already-loaded (or test-stubbed) client when present,
8
+ # so `twilio-ruby` is never loaded until a Twilio-backed provider
9
+ # (SMS / WhatsApp) is actually used.
10
+ # - Requires the gem on demand on first use.
11
+ # - Raises EventTrigger::Error (never LoadError/NameError) when the
12
+ # gem is missing or broken, so the Dispatcher can log it and the
13
+ # host app keeps running.
14
+ module TwilioClient
15
+ GEM_MISSING_MESSAGE =
16
+ 'twilio-ruby gem is not installed (add `gem "twilio-ruby"` to your Gemfile)'.freeze
17
+
18
+ def self.resolve
19
+ klass = loaded_client
20
+ return klass if klass
21
+
22
+ begin
23
+ require "twilio-ruby"
24
+ rescue LoadError, StandardError => e
25
+ raise Error, "#{GEM_MISSING_MESSAGE} (#{e.class}: #{e.message})"
26
+ end
27
+ loaded_client || raise(Error, GEM_MISSING_MESSAGE)
28
+ end
29
+
30
+ # Explicit Object-space lookup (instead of lexical `defined?`) so
31
+ # test doubles (stub_const / hide_const) behave predictably.
32
+ def self.loaded_client
33
+ return nil unless ::Object.const_defined?(:Twilio)
34
+
35
+ rest = ::Twilio.const_defined?(:REST, false) ? ::Twilio::REST : nil
36
+ return nil unless rest
37
+ return nil unless rest.const_defined?(:Client, false)
38
+
39
+ rest.const_get(:Client, false)
40
+ rescue StandardError
41
+ nil
42
+ end
43
+ end
44
+ end
45
+ end
@@ -4,17 +4,28 @@ require "net/http"
4
4
  require "uri"
5
5
  require "json"
6
6
  require_relative "../provider"
7
+ require_relative "http_poster"
7
8
 
8
9
  module EventTrigger
9
10
  module Providers
10
- # POSTs the event as JSON to a configured HTTP endpoint.
11
+ # Generic HTTP webhook provider for any custom system.
11
12
  #
12
- # Config:
13
- # config.webhook.enabled = true
14
- # config.webhook.url = "https://example.com/hooks/events"
13
+ # Existing behaviour (unchanged): POSTs { event:, payload:,
14
+ # timestamp: } as JSON to config.webhook.url with optional
15
+ # config.webhook.headers.
16
+ #
17
+ # NEW additive options (only used when set):
18
+ # config.webhook.method = :post # :post (default), :put, :patch, :delete, :get
19
+ # NOTE: ProviderConfig defines Object#method, so write the verb via
20
+ # `config.webhook[:method] = :post` (reader also via config[:method]).
21
+ # config.webhook.url = ENV["EVENT_WEBHOOK_URL"]
15
22
  # config.webhook.headers = { "Authorization" => "Bearer ..." }
16
- # config.webhook.events = ["loan.activated"]
23
+ #
24
+ # method is normalized (:POST/"post" all work); unknown verbs fall back
25
+ # to POST so old configs never break.
17
26
  class WebhookProvider < Provider
27
+ include HttpPoster
28
+
18
29
  def self.provider_name
19
30
  :webhook
20
31
  end
@@ -23,13 +34,19 @@ module EventTrigger
23
34
  url = config.url
24
35
  raise Error, "Webhook url is not configured" if url.nil? || url.to_s.strip.empty?
25
36
 
26
- post_json(url.to_s, event_to_json(event), headers)
37
+ request_json(http_method, url.to_s, event_to_json(event), headers, service: "Webhook")
27
38
  log("delivered event '#{event.name}' to #{url}")
28
39
  true
29
40
  end
30
41
 
42
+
31
43
  private
32
44
 
45
+ def http_method
46
+ m = ((config[:method] rescue nil) || :post).to_s.downcase
47
+ %w[get post put patch delete].include?(m) ? m.to_sym : :post
48
+ end
49
+
33
50
  def headers
34
51
  { "Content-Type" => "application/json" }.merge(config.headers || {})
35
52
  end
@@ -45,21 +62,6 @@ module EventTrigger
45
62
  def stringify(hash)
46
63
  hash.each_with_object({}) { |(k, v), m| m[k.to_s] = v }
47
64
  end
48
-
49
- def post_json(url, body, headers)
50
- uri = URI.parse(url)
51
- http = Net::HTTP.new(uri.host, uri.port)
52
- http.use_ssl = uri.scheme == "https"
53
- http.open_timeout = 5
54
- http.read_timeout = 10
55
- request = Net::HTTP::Post.new(uri.request_uri, headers)
56
- request.body = JSON.generate(body)
57
- response = http.request(request)
58
- unless response.is_a?(Net::HTTPSuccess)
59
- raise Error, "Webhook delivery failed (HTTP #{response.code}): #{response.body}"
60
- end
61
- response
62
- end
63
65
  end
64
66
  end
65
67
  end