hooksniff 1.1.1 → 1.3.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.
- checksums.yaml +4 -4
- data/Gemfile.lock +2 -2
- data/README.md +45 -20
- data/hooksniff.gemspec +1 -1
- data/lib/hooksniff/api/alert.rb +34 -0
- data/lib/hooksniff/api/analytics.rb +21 -0
- data/lib/hooksniff/api/api_key.rb +26 -0
- data/lib/hooksniff/api/application.rb +30 -0
- data/lib/hooksniff/api/audit_log.rb +17 -0
- data/lib/hooksniff/api/authentication.rb +0 -13
- data/lib/hooksniff/api/background_task.rb +21 -0
- data/lib/hooksniff/api/billing.rb +34 -0
- data/lib/hooksniff/api/connector.rb +33 -0
- data/lib/hooksniff/api/custom_domain.rb +26 -0
- data/lib/hooksniff/api/environment.rb +47 -0
- data/lib/hooksniff/api/inbound.rb +30 -0
- data/lib/hooksniff/api/integration.rb +8 -8
- data/lib/hooksniff/api/message_poller.rb +21 -0
- data/lib/hooksniff/api/notification.rb +30 -0
- data/lib/hooksniff/api/operational_webhook.rb +34 -0
- data/lib/hooksniff/api/playground.rb +17 -0
- data/lib/hooksniff/api/portal.rb +33 -0
- data/lib/hooksniff/api/rate_limit.rb +26 -0
- data/lib/hooksniff/api/routing.rb +21 -0
- data/lib/hooksniff/api/schema.rb +25 -0
- data/lib/hooksniff/api/search.rb +13 -0
- data/lib/hooksniff/api/service_token.rb +22 -0
- data/lib/hooksniff/api/sso.rb +26 -0
- data/lib/hooksniff/api/statistics.rb +0 -13
- data/lib/hooksniff/api/stream.rb +28 -9
- data/lib/hooksniff/api/team.rb +42 -0
- data/lib/hooksniff/api/template.rb +21 -0
- data/lib/hooksniff/errors.rb +79 -0
- data/lib/hooksniff/hooksniff_http_client.rb +48 -9
- data/lib/hooksniff/models/aggregate_event_types_out.rb +59 -0
- data/lib/hooksniff/models/message_attempt_recovered_event.rb +53 -0
- data/lib/hooksniff/models/message_attempt_recovered_event_data.rb +70 -0
- data/lib/hooksniff/models/message_in.rb +1 -0
- data/lib/hooksniff/options.rb +31 -0
- data/lib/hooksniff/paginator.rb +45 -0
- data/lib/hooksniff/response_metadata.rb +48 -0
- data/lib/hooksniff/version.rb +1 -1
- data/lib/hooksniff/webhook.rb +62 -0
- data/lib/hooksniff/webhook_event.rb +295 -0
- data/lib/hooksniff.rb +135 -66
- data/test/test_hooksniff.rb +0 -11
- data/test/test_typed_events.rb +260 -0
- metadata +36 -13
- data/lib/hooksniff/background_task.rb +0 -21
- data/lib/hooksniff/connector.rb +0 -33
- data/lib/hooksniff/environment.rb +0 -53
- data/lib/hooksniff/inbound.rb +0 -25
- data/lib/hooksniff/message_poller.rb +0 -32
- data/lib/hooksniff/operational_webhook.rb +0 -12
|
@@ -63,6 +63,7 @@ module HookSniff
|
|
|
63
63
|
def self.deserialize(attributes = {})
|
|
64
64
|
attributes = attributes.transform_keys(&:to_s)
|
|
65
65
|
attrs = Hash.new
|
|
66
|
+
attrs["application"] = HookSniff::ApplicationIn.deserialize(attributes["application"]) if attributes["application"]
|
|
66
67
|
attrs["channels"] = attributes["channels"]
|
|
67
68
|
attrs["deliver_at"] = DateTime.rfc3339(attributes["deliverAt"]).to_time if attributes["deliverAt"]
|
|
68
69
|
attrs["event_id"] = attributes["eventId"]
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module HookSniff
|
|
4
|
+
# Configuration options for the HookSniff client.
|
|
5
|
+
#
|
|
6
|
+
# @example
|
|
7
|
+
# options = HookSniff::Options.new(
|
|
8
|
+
# server_url: "https://custom.hooksniff.com",
|
|
9
|
+
# timeout: 60,
|
|
10
|
+
# debug: true,
|
|
11
|
+
# headers: { "X-Custom" => "value" }
|
|
12
|
+
# )
|
|
13
|
+
# client = HookSniff::HookSniff.new("token", options)
|
|
14
|
+
class Options
|
|
15
|
+
attr_accessor :server_url, :timeout, :debug, :headers, :retry_schedule
|
|
16
|
+
|
|
17
|
+
def initialize(
|
|
18
|
+
server_url: nil,
|
|
19
|
+
timeout: 30,
|
|
20
|
+
debug: false,
|
|
21
|
+
headers: {},
|
|
22
|
+
retry_schedule: [1, 2, 4]
|
|
23
|
+
)
|
|
24
|
+
@server_url = server_url
|
|
25
|
+
@timeout = timeout
|
|
26
|
+
@debug = debug
|
|
27
|
+
@headers = headers || {}
|
|
28
|
+
@retry_schedule = retry_schedule
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
end
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
# Pagination Helper for HookSniff Ruby SDK.
|
|
2
|
+
#
|
|
3
|
+
# Usage:
|
|
4
|
+
# hs.message.list_all(limit: 100).each do |msg|
|
|
5
|
+
# puts msg.id
|
|
6
|
+
# end
|
|
7
|
+
#
|
|
8
|
+
# # Or collect all
|
|
9
|
+
# all_messages = hs.message.list_all(limit: 100).to_a
|
|
10
|
+
|
|
11
|
+
module HookSniff
|
|
12
|
+
class Paginator
|
|
13
|
+
include Enumerable
|
|
14
|
+
|
|
15
|
+
def initialize(fetch_page, limit: nil)
|
|
16
|
+
@fetch_page = fetch_page
|
|
17
|
+
@limit = limit
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def each(&block)
|
|
21
|
+
return enum_for(:each) unless block_given?
|
|
22
|
+
|
|
23
|
+
iterator = nil
|
|
24
|
+
|
|
25
|
+
loop do
|
|
26
|
+
page = @fetch_page.call(limit: @limit, iterator: iterator)
|
|
27
|
+
|
|
28
|
+
page.data.each(&block)
|
|
29
|
+
|
|
30
|
+
break if page.done || page.iterator.nil? || page.iterator.empty?
|
|
31
|
+
iterator = page.iterator
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
# Collect all items into an array
|
|
36
|
+
def to_a
|
|
37
|
+
each.to_a
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
# Count all items
|
|
41
|
+
def count
|
|
42
|
+
each.count
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
end
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module HookSniff
|
|
4
|
+
# Response metadata from the last API request.
|
|
5
|
+
#
|
|
6
|
+
# Access via +client.last_response+ after any API call.
|
|
7
|
+
#
|
|
8
|
+
# @example
|
|
9
|
+
# endpoints = client.endpoint.list
|
|
10
|
+
# puts client.last_response.request_id
|
|
11
|
+
# puts client.last_response.rate_limit_remaining
|
|
12
|
+
class ResponseMetadata
|
|
13
|
+
# @return [Integer] HTTP status code
|
|
14
|
+
attr_reader :status_code
|
|
15
|
+
|
|
16
|
+
# @return [String, nil] x-request-id header
|
|
17
|
+
attr_reader :request_id
|
|
18
|
+
|
|
19
|
+
# @return [Integer, nil] x-ratelimit-remaining header
|
|
20
|
+
attr_reader :rate_limit_remaining
|
|
21
|
+
|
|
22
|
+
# @return [Integer, nil] x-ratelimit-reset header (Unix timestamp)
|
|
23
|
+
attr_reader :rate_limit_reset
|
|
24
|
+
|
|
25
|
+
# @return [Hash] All response headers
|
|
26
|
+
attr_reader :headers
|
|
27
|
+
|
|
28
|
+
def initialize(status_code:, request_id: nil, rate_limit_remaining: nil, rate_limit_reset: nil, headers: {})
|
|
29
|
+
@status_code = status_code
|
|
30
|
+
@request_id = request_id
|
|
31
|
+
@rate_limit_remaining = rate_limit_remaining
|
|
32
|
+
@rate_limit_reset = rate_limit_reset
|
|
33
|
+
@headers = headers
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
# Create from a Net::HTTP response.
|
|
37
|
+
def self.from_http_response(response)
|
|
38
|
+
headers = response.to_hash.transform_values { |v| v.is_a?(Array) ? v.first : v }
|
|
39
|
+
new(
|
|
40
|
+
status_code: response.code.to_i,
|
|
41
|
+
request_id: headers["x-request-id"],
|
|
42
|
+
rate_limit_remaining: headers["x-ratelimit-remaining"]&.to_i,
|
|
43
|
+
rate_limit_reset: headers["x-ratelimit-reset"]&.to_i,
|
|
44
|
+
headers: headers
|
|
45
|
+
)
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
end
|
data/lib/hooksniff/version.rb
CHANGED
data/lib/hooksniff/webhook.rb
CHANGED
|
@@ -1,6 +1,13 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
|
+
require "base64"
|
|
3
|
+
require "json"
|
|
2
4
|
|
|
3
5
|
module HookSniff
|
|
6
|
+
# Error raised when webhook signature verification fails
|
|
7
|
+
class WebhookVerificationError < StandardError; end
|
|
8
|
+
# Error raised when webhook signing fails
|
|
9
|
+
class WebhookSigningError < StandardError; end
|
|
10
|
+
|
|
4
11
|
class Webhook
|
|
5
12
|
|
|
6
13
|
def self.new_using_raw_bytes(secret)
|
|
@@ -15,6 +22,15 @@ module HookSniff
|
|
|
15
22
|
@secret = Base64.decode64(secret)
|
|
16
23
|
end
|
|
17
24
|
|
|
25
|
+
# Verify and parse a webhook payload.
|
|
26
|
+
#
|
|
27
|
+
# Verifies the HMAC-SHA256 signature, then parses the payload
|
|
28
|
+
# into a typed WebhookEvent with +event+, +data+, and +timestamp+.
|
|
29
|
+
#
|
|
30
|
+
# @param payload [String] raw request body
|
|
31
|
+
# @param headers [Hash] request headers containing hooksniff-id, hooksniff-timestamp, hooksniff-signature
|
|
32
|
+
# @return [WebhookEvent] parsed webhook event
|
|
33
|
+
# @raise [WebhookVerificationError] if signature is invalid or timestamp is outside tolerance
|
|
18
34
|
def verify(payload, headers)
|
|
19
35
|
msgId = headers["hooksniff-id"]
|
|
20
36
|
msgSignature = headers["hooksniff-signature"]
|
|
@@ -32,6 +48,45 @@ module HookSniff
|
|
|
32
48
|
|
|
33
49
|
_, signature = sign(msgId, msgTimestamp, payload).split(",", 2)
|
|
34
50
|
|
|
51
|
+
passedSignatures = msgSignature.split(" ")
|
|
52
|
+
passedSignatures.each do |versionedSignature|
|
|
53
|
+
version, expectedSignature = versionedSignature.split(",", 2)
|
|
54
|
+
if version != "v1"
|
|
55
|
+
next
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
if ::HookSniff::secure_compare(signature, expectedSignature)
|
|
59
|
+
return parse_payload(payload)
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
raise WebhookVerificationError, "No matching signature found"
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
# Verify and return raw payload without parsing.
|
|
67
|
+
# Use this when you need the raw hash instead of a typed event.
|
|
68
|
+
#
|
|
69
|
+
# @param payload [String] raw request body
|
|
70
|
+
# @param headers [Hash] request headers
|
|
71
|
+
# @return [Hash, nil] parsed JSON hash
|
|
72
|
+
# @raise [WebhookVerificationError] if signature is invalid
|
|
73
|
+
def verify_raw(payload, headers)
|
|
74
|
+
msgId = headers["hooksniff-id"]
|
|
75
|
+
msgSignature = headers["hooksniff-signature"]
|
|
76
|
+
msgTimestamp = headers["hooksniff-timestamp"]
|
|
77
|
+
if !msgSignature || !msgId || !msgTimestamp
|
|
78
|
+
msgId = headers["webhook-id"]
|
|
79
|
+
msgSignature = headers["webhook-signature"]
|
|
80
|
+
msgTimestamp = headers["webhook-timestamp"]
|
|
81
|
+
if !msgSignature || !msgId || !msgTimestamp
|
|
82
|
+
raise WebhookVerificationError, "Missing required headers"
|
|
83
|
+
end
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
verify_timestamp(msgTimestamp)
|
|
87
|
+
|
|
88
|
+
_, signature = sign(msgId, msgTimestamp, payload).split(",", 2)
|
|
89
|
+
|
|
35
90
|
passedSignatures = msgSignature.split(" ")
|
|
36
91
|
passedSignatures.each do |versionedSignature|
|
|
37
92
|
version, expectedSignature = versionedSignature.split(",", 2)
|
|
@@ -80,5 +135,12 @@ module HookSniff
|
|
|
80
135
|
raise WebhookVerificationError, "Message timestamp too new"
|
|
81
136
|
end
|
|
82
137
|
end
|
|
138
|
+
|
|
139
|
+
def parse_payload(payload)
|
|
140
|
+
return WebhookEvent.new(event: "", data: {}, timestamp: "") if payload.empty?
|
|
141
|
+
|
|
142
|
+
parsed = JSON.parse(payload, symbolize_names: true)
|
|
143
|
+
WebhookEvent.parse(parsed)
|
|
144
|
+
end
|
|
83
145
|
end
|
|
84
146
|
end
|
|
@@ -0,0 +1,295 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module HookSniff
|
|
4
|
+
# ─── Event Data Classes ─────────────────────────────────────────
|
|
5
|
+
|
|
6
|
+
# Data payload for endpoint.created events.
|
|
7
|
+
class EndpointCreatedEventData
|
|
8
|
+
attr_reader :app_id, :endpoint_id, :app_uid
|
|
9
|
+
|
|
10
|
+
def initialize(app_id:, endpoint_id:, app_uid: nil)
|
|
11
|
+
@app_id = app_id
|
|
12
|
+
@endpoint_id = endpoint_id
|
|
13
|
+
@app_uid = app_uid
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
# Data payload for endpoint.updated events.
|
|
18
|
+
class EndpointUpdatedEventData
|
|
19
|
+
attr_reader :app_id, :endpoint_id, :app_uid
|
|
20
|
+
|
|
21
|
+
def initialize(app_id:, endpoint_id:, app_uid: nil)
|
|
22
|
+
@app_id = app_id
|
|
23
|
+
@endpoint_id = endpoint_id
|
|
24
|
+
@app_uid = app_uid
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
# Data payload for endpoint.deleted events.
|
|
29
|
+
class EndpointDeletedEventData
|
|
30
|
+
attr_reader :app_id, :endpoint_id, :app_uid
|
|
31
|
+
|
|
32
|
+
def initialize(app_id:, endpoint_id:, app_uid: nil)
|
|
33
|
+
@app_id = app_id
|
|
34
|
+
@endpoint_id = endpoint_id
|
|
35
|
+
@app_uid = app_uid
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
# Data payload for endpoint.enabled events.
|
|
40
|
+
class EndpointEnabledEventData
|
|
41
|
+
attr_reader :app_id, :endpoint_id, :app_uid
|
|
42
|
+
|
|
43
|
+
def initialize(app_id:, endpoint_id:, app_uid: nil)
|
|
44
|
+
@app_id = app_id
|
|
45
|
+
@endpoint_id = endpoint_id
|
|
46
|
+
@app_uid = app_uid
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
# Data payload for endpoint.disabled events.
|
|
51
|
+
class EndpointDisabledEventData
|
|
52
|
+
attr_reader :app_id, :endpoint_id, :app_uid, :fail_since, :trigger
|
|
53
|
+
|
|
54
|
+
def initialize(app_id:, endpoint_id:, app_uid: nil, fail_since: nil, trigger: nil)
|
|
55
|
+
@app_id = app_id
|
|
56
|
+
@endpoint_id = endpoint_id
|
|
57
|
+
@app_uid = app_uid
|
|
58
|
+
@fail_since = fail_since
|
|
59
|
+
@trigger = trigger
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
# Info about the last delivery attempt.
|
|
64
|
+
class LastAttemptInfo
|
|
65
|
+
attr_reader :id, :timestamp, :response_status_code
|
|
66
|
+
|
|
67
|
+
def initialize(id:, timestamp:, response_status_code:)
|
|
68
|
+
@id = id
|
|
69
|
+
@timestamp = timestamp
|
|
70
|
+
@response_status_code = response_status_code
|
|
71
|
+
end
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
# Info about a delivery attempt.
|
|
75
|
+
class AttemptInfo
|
|
76
|
+
attr_reader :id, :timestamp, :response_status_code
|
|
77
|
+
|
|
78
|
+
def initialize(id:, timestamp:, response_status_code:)
|
|
79
|
+
@id = id
|
|
80
|
+
@timestamp = timestamp
|
|
81
|
+
@response_status_code = response_status_code
|
|
82
|
+
end
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
# Data payload for message.attempt.exhausted events.
|
|
86
|
+
class MessageAttemptExhaustedEventData
|
|
87
|
+
attr_reader :app_id, :msg_id, :last_attempt, :app_uid
|
|
88
|
+
|
|
89
|
+
def initialize(app_id:, msg_id:, last_attempt:, app_uid: nil)
|
|
90
|
+
@app_id = app_id
|
|
91
|
+
@msg_id = msg_id
|
|
92
|
+
@last_attempt = last_attempt
|
|
93
|
+
@app_uid = app_uid
|
|
94
|
+
end
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
# Data payload for message.attempt.failing events.
|
|
98
|
+
class MessageAttemptFailingEventData
|
|
99
|
+
attr_reader :app_id, :msg_id, :attempt, :app_uid
|
|
100
|
+
|
|
101
|
+
def initialize(app_id:, msg_id:, attempt:, app_uid: nil)
|
|
102
|
+
@app_id = app_id
|
|
103
|
+
@msg_id = msg_id
|
|
104
|
+
@attempt = attempt
|
|
105
|
+
@app_uid = app_uid
|
|
106
|
+
end
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
# Data payload for message.attempt.recovered events.
|
|
110
|
+
class MessageAttemptRecoveredEventData
|
|
111
|
+
attr_reader :app_id, :msg_id, :attempt, :app_uid
|
|
112
|
+
|
|
113
|
+
def initialize(app_id:, msg_id:, attempt:, app_uid: nil)
|
|
114
|
+
@app_id = app_id
|
|
115
|
+
@msg_id = msg_id
|
|
116
|
+
@attempt = attempt
|
|
117
|
+
@app_uid = app_uid
|
|
118
|
+
end
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
# ─── WebhookEvent (base class — must come before subclasses) ────
|
|
122
|
+
|
|
123
|
+
# Represents a parsed webhook event from HookSniff.
|
|
124
|
+
class WebhookEvent
|
|
125
|
+
# @return [String] event type name (e.g., "endpoint.created")
|
|
126
|
+
attr_reader :event
|
|
127
|
+
|
|
128
|
+
# @return [Object] event payload data (typed data class or Hash)
|
|
129
|
+
attr_reader :data
|
|
130
|
+
|
|
131
|
+
# @return [String] ISO 8601 timestamp string
|
|
132
|
+
attr_reader :timestamp
|
|
133
|
+
|
|
134
|
+
def initialize(event:, data:, timestamp:)
|
|
135
|
+
@event = event
|
|
136
|
+
@data = data
|
|
137
|
+
@timestamp = timestamp
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
# Alias for +event+ — the event type name.
|
|
141
|
+
def event_type
|
|
142
|
+
@event
|
|
143
|
+
end
|
|
144
|
+
|
|
145
|
+
# Get a value from the data (Hash or typed object).
|
|
146
|
+
def get(key)
|
|
147
|
+
if @data.is_a?(Hash)
|
|
148
|
+
@data[key.to_s] || @data[key.to_sym]
|
|
149
|
+
else
|
|
150
|
+
@data.respond_to?(key.to_sym) ? @data.send(key.to_sym) : nil
|
|
151
|
+
end
|
|
152
|
+
end
|
|
153
|
+
|
|
154
|
+
# Access data values with bracket notation (backward compat).
|
|
155
|
+
def [](key)
|
|
156
|
+
get(key)
|
|
157
|
+
end
|
|
158
|
+
|
|
159
|
+
# Check if key exists in data.
|
|
160
|
+
def key?(key)
|
|
161
|
+
if @data.is_a?(Hash)
|
|
162
|
+
@data.key?(key.to_s) || @data.key?(key.to_sym)
|
|
163
|
+
else
|
|
164
|
+
@data.respond_to?(key.to_sym)
|
|
165
|
+
end
|
|
166
|
+
end
|
|
167
|
+
|
|
168
|
+
def to_s
|
|
169
|
+
"#<#{self.class.name} event=#{@event} timestamp=#{@timestamp}>"
|
|
170
|
+
end
|
|
171
|
+
|
|
172
|
+
def inspect
|
|
173
|
+
to_s
|
|
174
|
+
end
|
|
175
|
+
|
|
176
|
+
# Map of known event types to their classes
|
|
177
|
+
EVENT_TYPE_MAP = {
|
|
178
|
+
"endpoint.created" => "EndpointCreatedEvent",
|
|
179
|
+
"endpoint.updated" => "EndpointUpdatedEvent",
|
|
180
|
+
"endpoint.deleted" => "EndpointDeletedEvent",
|
|
181
|
+
"endpoint.enabled" => "EndpointEnabledEvent",
|
|
182
|
+
"endpoint.disabled" => "EndpointDisabledEvent",
|
|
183
|
+
"message.attempt.exhausted" => "MessageAttemptExhaustedEvent",
|
|
184
|
+
"message.attempt.failing" => "MessageAttemptFailingEvent",
|
|
185
|
+
"message.atattempt.failing" => "MessageAttemptFailingEvent",
|
|
186
|
+
"message.attempt.recovered" => "MessageAttemptRecoveredEvent",
|
|
187
|
+
"message.atattempt.recovered" => "MessageAttemptRecoveredEvent",
|
|
188
|
+
}.freeze
|
|
189
|
+
|
|
190
|
+
# Parse a webhook payload hash into a typed WebhookEvent.
|
|
191
|
+
def self.parse(data)
|
|
192
|
+
event_type = data[:event] || data["event"] || data[:eventType] || data["eventType"] || ""
|
|
193
|
+
raw_data = data[:data] || data["data"] || {}
|
|
194
|
+
timestamp = data[:timestamp] || data["timestamp"] || ""
|
|
195
|
+
|
|
196
|
+
parsed_data = parse_event_data(event_type, raw_data)
|
|
197
|
+
class_name = EVENT_TYPE_MAP[event_type]
|
|
198
|
+
event_class = class_name ? const_get(class_name) : WebhookEvent
|
|
199
|
+
|
|
200
|
+
event_class.new(event: event_type, data: parsed_data, timestamp: timestamp)
|
|
201
|
+
end
|
|
202
|
+
|
|
203
|
+
private
|
|
204
|
+
|
|
205
|
+
def self.parse_event_data(event_type, raw)
|
|
206
|
+
case event_type
|
|
207
|
+
when "endpoint.created"
|
|
208
|
+
EndpointCreatedEventData.new(
|
|
209
|
+
app_id: raw[:appId] || raw["appId"] || raw[:app_id] || raw["app_id"] || "",
|
|
210
|
+
endpoint_id: raw[:endpointId] || raw["endpointId"] || raw[:endpoint_id] || raw["endpoint_id"] || "",
|
|
211
|
+
app_uid: raw[:appUid] || raw["appUid"] || raw[:app_uid] || raw["app_uid"]
|
|
212
|
+
)
|
|
213
|
+
when "endpoint.updated"
|
|
214
|
+
EndpointUpdatedEventData.new(
|
|
215
|
+
app_id: raw[:appId] || raw["appId"] || raw[:app_id] || raw["app_id"] || "",
|
|
216
|
+
endpoint_id: raw[:endpointId] || raw["endpointId"] || raw[:endpoint_id] || raw["endpoint_id"] || "",
|
|
217
|
+
app_uid: raw[:appUid] || raw["appUid"] || raw[:app_uid] || raw["app_uid"]
|
|
218
|
+
)
|
|
219
|
+
when "endpoint.deleted"
|
|
220
|
+
EndpointDeletedEventData.new(
|
|
221
|
+
app_id: raw[:appId] || raw["appId"] || raw[:app_id] || raw["app_id"] || "",
|
|
222
|
+
endpoint_id: raw[:endpointId] || raw["endpointId"] || raw[:endpoint_id] || raw["endpoint_id"] || "",
|
|
223
|
+
app_uid: raw[:appUid] || raw["appUid"] || raw[:app_uid] || raw["app_uid"]
|
|
224
|
+
)
|
|
225
|
+
when "endpoint.enabled"
|
|
226
|
+
EndpointEnabledEventData.new(
|
|
227
|
+
app_id: raw[:appId] || raw["appId"] || raw[:app_id] || raw["app_id"] || "",
|
|
228
|
+
endpoint_id: raw[:endpointId] || raw["endpointId"] || raw[:endpoint_id] || raw["endpoint_id"] || "",
|
|
229
|
+
app_uid: raw[:appUid] || raw["appUid"] || raw[:app_uid] || raw["app_uid"]
|
|
230
|
+
)
|
|
231
|
+
when "endpoint.disabled"
|
|
232
|
+
EndpointDisabledEventData.new(
|
|
233
|
+
app_id: raw[:appId] || raw["appId"] || raw[:app_id] || raw["app_id"] || "",
|
|
234
|
+
endpoint_id: raw[:endpointId] || raw["endpointId"] || raw[:endpoint_id] || raw["endpoint_id"] || "",
|
|
235
|
+
app_uid: raw[:appUid] || raw["appUid"] || raw[:app_uid] || raw["app_uid"],
|
|
236
|
+
fail_since: raw[:failSince] || raw["failSince"] || raw[:fail_since] || raw["fail_since"],
|
|
237
|
+
trigger: raw[:trigger] || raw["trigger"]
|
|
238
|
+
)
|
|
239
|
+
when "message.attempt.exhausted"
|
|
240
|
+
last_raw = raw[:lastAttempt] || raw["lastAttempt"] || raw[:last_attempt] || raw["last_attempt"] || {}
|
|
241
|
+
MessageAttemptExhaustedEventData.new(
|
|
242
|
+
app_id: raw[:appId] || raw["appId"] || raw[:app_id] || raw["app_id"] || "",
|
|
243
|
+
msg_id: raw[:msgId] || raw["msgId"] || raw[:msg_id] || raw["msg_id"] || "",
|
|
244
|
+
last_attempt: parse_last_attempt(last_raw),
|
|
245
|
+
app_uid: raw[:appUid] || raw["appUid"] || raw[:app_uid] || raw["app_uid"]
|
|
246
|
+
)
|
|
247
|
+
when "message.attempt.failing", "message.atattempt.failing"
|
|
248
|
+
attempt_raw = raw[:attempt] || raw["attempt"] || {}
|
|
249
|
+
MessageAttemptFailingEventData.new(
|
|
250
|
+
app_id: raw[:appId] || raw["appId"] || raw[:app_id] || raw["app_id"] || "",
|
|
251
|
+
msg_id: raw[:msgId] || raw["msgId"] || raw[:msg_id] || raw["msg_id"] || "",
|
|
252
|
+
attempt: parse_attempt(attempt_raw),
|
|
253
|
+
app_uid: raw[:appUid] || raw["appUid"] || raw[:app_uid] || raw["app_uid"]
|
|
254
|
+
)
|
|
255
|
+
when "message.atattempt.recovered", "message.attempt.recovered"
|
|
256
|
+
attempt_raw = raw[:attempt] || raw["attempt"] || {}
|
|
257
|
+
MessageAttemptRecoveredEventData.new(
|
|
258
|
+
app_id: raw[:appId] || raw["appId"] || raw[:app_id] || raw["app_id"] || "",
|
|
259
|
+
msg_id: raw[:msgId] || raw["msgId"] || raw[:msg_id] || raw["msg_id"] || "",
|
|
260
|
+
attempt: parse_attempt(attempt_raw),
|
|
261
|
+
app_uid: raw[:appUid] || raw["appUid"] || raw[:app_uid] || raw["app_uid"]
|
|
262
|
+
)
|
|
263
|
+
else
|
|
264
|
+
raw
|
|
265
|
+
end
|
|
266
|
+
end
|
|
267
|
+
|
|
268
|
+
def self.parse_last_attempt(raw)
|
|
269
|
+
LastAttemptInfo.new(
|
|
270
|
+
id: raw[:id] || raw["id"] || "",
|
|
271
|
+
timestamp: raw[:timestamp] || raw["timestamp"] || "",
|
|
272
|
+
response_status_code: raw[:responseStatusCode] || raw["responseStatusCode"] || raw[:response_status_code] || raw["response_status_code"] || 0
|
|
273
|
+
)
|
|
274
|
+
end
|
|
275
|
+
|
|
276
|
+
def self.parse_attempt(raw)
|
|
277
|
+
AttemptInfo.new(
|
|
278
|
+
id: raw[:id] || raw["id"] || "",
|
|
279
|
+
timestamp: raw[:timestamp] || raw["timestamp"] || "",
|
|
280
|
+
response_status_code: raw[:responseStatusCode] || raw["responseStatusCode"] || raw[:response_status_code] || raw["response_status_code"] || 0
|
|
281
|
+
)
|
|
282
|
+
end
|
|
283
|
+
end
|
|
284
|
+
|
|
285
|
+
# ─── Typed Event Subclasses ─────────────────────────────────────
|
|
286
|
+
|
|
287
|
+
class EndpointCreatedEvent < WebhookEvent; end
|
|
288
|
+
class EndpointUpdatedEvent < WebhookEvent; end
|
|
289
|
+
class EndpointDeletedEvent < WebhookEvent; end
|
|
290
|
+
class EndpointEnabledEvent < WebhookEvent; end
|
|
291
|
+
class EndpointDisabledEvent < WebhookEvent; end
|
|
292
|
+
class MessageAttemptExhaustedEvent < WebhookEvent; end
|
|
293
|
+
class MessageAttemptFailingEvent < WebhookEvent; end
|
|
294
|
+
class MessageAttemptRecoveredEvent < WebhookEvent; end
|
|
295
|
+
end
|