sendly 3.38.0 → 3.40.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/CHANGELOG.md +48 -0
- data/Gemfile.lock +1 -1
- data/README.md +191 -3
- data/lib/sendly/client.rb +12 -4
- data/lib/sendly/errors.rb +4 -2
- data/lib/sendly/rcs_resource.rb +944 -11
- data/lib/sendly/version.rb +1 -1
- data/lib/sendly/webhooks.rb +369 -78
- metadata +2 -2
data/lib/sendly/version.rb
CHANGED
data/lib/sendly/webhooks.rb
CHANGED
|
@@ -20,9 +20,13 @@ module Sendly
|
|
|
20
20
|
#
|
|
21
21
|
# case event.type
|
|
22
22
|
# when 'message.delivered'
|
|
23
|
-
# puts "Message delivered: #{event.
|
|
23
|
+
# puts "Message delivered: #{event.message.id}"
|
|
24
24
|
# when 'message.failed'
|
|
25
|
-
# puts "Message failed: #{event.
|
|
25
|
+
# puts "Message failed: #{event.message.error}"
|
|
26
|
+
# when 'rcs_agent.live'
|
|
27
|
+
# # A lifecycle payload is not message-shaped. event.message is nil
|
|
28
|
+
# # for it; read data.object through event.data or event.raw_object.
|
|
29
|
+
# puts "RCS agent live: #{event.data[:agent_id]}"
|
|
26
30
|
# end
|
|
27
31
|
#
|
|
28
32
|
# head :ok
|
|
@@ -36,9 +40,13 @@ module Sendly
|
|
|
36
40
|
|
|
37
41
|
# Webhook event type string constants. Use these when subscribing
|
|
38
42
|
# instead of string literals so you catch typos at load time.
|
|
43
|
+
# Deprecated: the API has never emitted this and rejects it when you
|
|
44
|
+
# subscribe. It will be removed in the next major version.
|
|
39
45
|
EVENT_MESSAGE_QUEUED = "message.queued"
|
|
46
|
+
|
|
40
47
|
EVENT_MESSAGE_SENT = "message.sent"
|
|
41
48
|
EVENT_MESSAGE_DELIVERED = "message.delivered"
|
|
49
|
+
EVENT_MESSAGE_READ = "message.read"
|
|
42
50
|
EVENT_MESSAGE_FAILED = "message.failed"
|
|
43
51
|
EVENT_MESSAGE_BOUNCED = "message.bounced"
|
|
44
52
|
EVENT_MESSAGE_RETRYING = "message.retrying"
|
|
@@ -52,6 +60,11 @@ module Sendly
|
|
|
52
60
|
EVENT_VERIFICATION_FAILED = "verification.failed"
|
|
53
61
|
EVENT_VERIFICATION_RESENT = "verification.resent"
|
|
54
62
|
EVENT_VERIFICATION_DELIVERY_FAILED = "verification.delivery_failed"
|
|
63
|
+
EVENT_CONVERSATION_CREATED = "conversation.created"
|
|
64
|
+
EVENT_CONVERSATION_UPDATED = "conversation.updated"
|
|
65
|
+
EVENT_DRAFT_CREATED = "draft.created"
|
|
66
|
+
EVENT_DRAFT_APPROVED = "draft.approved"
|
|
67
|
+
EVENT_DRAFT_REJECTED = "draft.rejected"
|
|
55
68
|
EVENT_CONTACT_AUTO_FLAGGED = "contact.auto_flagged"
|
|
56
69
|
EVENT_CONTACT_MARKED_VALID = "contact.marked_valid"
|
|
57
70
|
EVENT_CONTACTS_LOOKUP_COMPLETED = "contacts.lookup_completed"
|
|
@@ -63,6 +76,12 @@ module Sendly
|
|
|
63
76
|
EVENT_CAMPAIGN_SUSPENDED = "campaign.suspended"
|
|
64
77
|
EVENT_ASSIGNMENT_CONFIRMED = "assignment.confirmed"
|
|
65
78
|
EVENT_ASSIGNMENT_FAILED = "assignment.failed"
|
|
79
|
+
EVENT_RCS_BRAND_VERIFIED = "rcs_brand.verified"
|
|
80
|
+
EVENT_RCS_BRAND_FAILED = "rcs_brand.failed"
|
|
81
|
+
EVENT_RCS_AGENT_TESTING = "rcs_agent.testing"
|
|
82
|
+
EVENT_RCS_AGENT_LIVE = "rcs_agent.live"
|
|
83
|
+
EVENT_RCS_AGENT_REJECTED = "rcs_agent.rejected"
|
|
84
|
+
EVENT_RCS_AGENT_ACTION_REQUIRED = "rcs_agent.action_required"
|
|
66
85
|
EVENT_PORT_COMPLETED = "port.completed"
|
|
67
86
|
EVENT_PORT_OUT_REQUESTED = "port_out.requested"
|
|
68
87
|
EVENT_PORT_OUT_COMPLETED = "port_out.completed"
|
|
@@ -72,6 +91,14 @@ module Sendly
|
|
|
72
91
|
EVENT_NUMBER_FAILED = "number.failed"
|
|
73
92
|
EVENT_NUMBER_REQUIREMENTS_REQUIRED = "number.requirements_required"
|
|
74
93
|
EVENT_NUMBER_RELEASED = "number.released"
|
|
94
|
+
EVENT_WHATSAPP_ACCOUNT_CONNECTED = "whatsapp_account.connected"
|
|
95
|
+
EVENT_WHATSAPP_ACCOUNT_FAILED = "whatsapp_account.failed"
|
|
96
|
+
EVENT_WHATSAPP_TEMPLATE_APPROVED = "whatsapp_template.approved"
|
|
97
|
+
EVENT_WHATSAPP_TEMPLATE_REJECTED = "whatsapp_template.rejected"
|
|
98
|
+
EVENT_WHATSAPP_TEMPLATE_PAUSED = "whatsapp_template.paused"
|
|
99
|
+
EVENT_CALL_STARTED = "call.started"
|
|
100
|
+
EVENT_CALL_COMPLETED = "call.completed"
|
|
101
|
+
EVENT_CALL_RECORDING_READY = "call.recording.ready"
|
|
75
102
|
|
|
76
103
|
# Source of a list-health event. Frozen enum — new values will be
|
|
77
104
|
# added in minor SDK versions, never removed.
|
|
@@ -163,105 +190,369 @@ module Sendly
|
|
|
163
190
|
end
|
|
164
191
|
end
|
|
165
192
|
|
|
166
|
-
|
|
167
|
-
|
|
193
|
+
# A hash-like view of a webhook event's +data.object+.
|
|
194
|
+
#
|
|
195
|
+
# Every key the payload carried is reachable — by +[]+ with a String or a
|
|
196
|
+
# Symbol, by {#to_h}, or as a reader method of the same name — and nothing
|
|
197
|
+
# else is. A key the payload did not carry is absent rather than filled in
|
|
198
|
+
# with a plausible-looking default, and a key that arrived as JSON +null+
|
|
199
|
+
# stays +nil+.
|
|
200
|
+
#
|
|
201
|
+
# @example An rcs_agent.live payload
|
|
202
|
+
# event.data[:agent_id] # => "bb22cc33-dd44-4e55-9f66-001122334455"
|
|
203
|
+
# event.data.stage # => "live"
|
|
204
|
+
# event.data.key?(:from) # => false — an RCS agent event has no from
|
|
205
|
+
class WebhookObject
|
|
206
|
+
include Enumerable
|
|
168
207
|
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
208
|
+
# Names that must keep Ruby's meaning even if a payload carries them as a
|
|
209
|
+
# key. Overriding these on an instance breaks object identity, equality or
|
|
210
|
+
# dispatch, so a reader is never defined for them — reach those keys with
|
|
211
|
+
# +[]+, +fetch+ or +to_h+, which always read the payload.
|
|
212
|
+
RESERVED = %i[
|
|
213
|
+
__send__ __id__ object_id class singleton_class method methods
|
|
214
|
+
instance_variable_get instance_variable_set instance_variables
|
|
215
|
+
respond_to? equal? is_a? kind_of? instance_of? nil? tap raw
|
|
216
|
+
].freeze
|
|
217
|
+
|
|
218
|
+
# @return [Hash] +data.object+ exactly as it arrived
|
|
219
|
+
attr_reader :raw
|
|
220
|
+
|
|
221
|
+
# @param raw [Hash] the parsed +data.object+
|
|
222
|
+
def initialize(raw = {})
|
|
223
|
+
@raw = raw.is_a?(Hash) ? raw : {}
|
|
224
|
+
define_payload_readers
|
|
177
225
|
end
|
|
178
226
|
|
|
179
|
-
|
|
180
|
-
|
|
227
|
+
# @param key [String, Symbol]
|
|
228
|
+
# @return [Object, nil] nil when the payload did not carry the key
|
|
229
|
+
def [](key)
|
|
230
|
+
resolved = resolve_key(key)
|
|
231
|
+
resolved.nil? ? nil : @raw[resolved]
|
|
232
|
+
end
|
|
233
|
+
|
|
234
|
+
# @param key [String, Symbol]
|
|
235
|
+
# @return [Object] the value, the default, or the block's result
|
|
236
|
+
# @raise [KeyError] if the key is absent and no default was given
|
|
237
|
+
def fetch(key, *default, &block)
|
|
238
|
+
resolved = resolve_key(key)
|
|
239
|
+
return @raw[resolved] unless resolved.nil?
|
|
240
|
+
return default.first unless default.empty?
|
|
241
|
+
return block.call(key) if block
|
|
242
|
+
|
|
243
|
+
raise KeyError, "key not found: #{key.inspect}"
|
|
244
|
+
end
|
|
245
|
+
|
|
246
|
+
def dig(key, *rest)
|
|
247
|
+
value = self[key]
|
|
248
|
+
return value if rest.empty? || value.nil?
|
|
249
|
+
|
|
250
|
+
value.dig(*rest)
|
|
181
251
|
end
|
|
182
252
|
|
|
253
|
+
# @return [Boolean] whether the payload carried this key at all. Use it to
|
|
254
|
+
# tell "absent" from "arrived as null".
|
|
255
|
+
def key?(key)
|
|
256
|
+
!resolve_key(key).nil?
|
|
257
|
+
end
|
|
258
|
+
alias has_key? key?
|
|
259
|
+
|
|
260
|
+
def keys
|
|
261
|
+
@raw.keys
|
|
262
|
+
end
|
|
263
|
+
|
|
264
|
+
def values
|
|
265
|
+
@raw.values
|
|
266
|
+
end
|
|
267
|
+
|
|
268
|
+
def each(&block)
|
|
269
|
+
@raw.each(&block)
|
|
270
|
+
end
|
|
271
|
+
|
|
272
|
+
def empty?
|
|
273
|
+
@raw.empty?
|
|
274
|
+
end
|
|
275
|
+
|
|
276
|
+
def size
|
|
277
|
+
@raw.size
|
|
278
|
+
end
|
|
279
|
+
alias length size
|
|
280
|
+
|
|
281
|
+
# @return [Hash] a copy of +data.object+. Absent keys stay absent and
|
|
282
|
+
# nulls stay nil; nothing is added.
|
|
183
283
|
def to_h
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
284
|
+
@raw.dup
|
|
285
|
+
end
|
|
286
|
+
|
|
287
|
+
def ==(other)
|
|
288
|
+
case other
|
|
289
|
+
when WebhookObject then @raw == other.raw
|
|
290
|
+
when Hash then @raw == other
|
|
291
|
+
else false
|
|
292
|
+
end
|
|
293
|
+
end
|
|
294
|
+
|
|
295
|
+
def inspect
|
|
296
|
+
"#<#{self.class.name} #{@raw.inspect}>"
|
|
297
|
+
end
|
|
298
|
+
|
|
299
|
+
# @return [Array<Symbol>] payload keys that cannot be read as a method
|
|
300
|
+
# because doing so would override Ruby's own semantics. Read them with
|
|
301
|
+
# +[]+ instead.
|
|
302
|
+
def reserved_keys
|
|
303
|
+
@raw.keys.map { |k| k.to_sym rescue nil }.compact & RESERVED
|
|
304
|
+
end
|
|
305
|
+
|
|
306
|
+
private
|
|
307
|
+
|
|
308
|
+
# Payload keys win over inherited methods.
|
|
309
|
+
#
|
|
310
|
+
# Reader access used to go through method_missing, which only fires when
|
|
311
|
+
# nothing else answers — so any key colliding with an Object or Enumerable
|
|
312
|
+
# method was silently shadowed. `contacts.bulk_marked_valid` really carries
|
|
313
|
+
# `count`, so `event.data.count` returned the NUMBER OF KEYS instead of the
|
|
314
|
+
# value, and respond_to?(:count) was true, giving the caller no signal.
|
|
315
|
+
# Defining a singleton reader per key makes the payload authoritative.
|
|
316
|
+
def define_payload_readers
|
|
317
|
+
singleton = singleton_class
|
|
318
|
+
@raw.each_key do |key|
|
|
319
|
+
name = begin
|
|
320
|
+
key.to_sym
|
|
321
|
+
rescue StandardError
|
|
322
|
+
next
|
|
323
|
+
end
|
|
324
|
+
next if RESERVED.include?(name)
|
|
325
|
+
next unless name.to_s.match?(/\A[A-Za-z_][A-Za-z0-9_]*[?!]?\z/)
|
|
326
|
+
|
|
327
|
+
singleton.define_method(name) { @raw[key] }
|
|
328
|
+
end
|
|
329
|
+
end
|
|
330
|
+
|
|
331
|
+
def resolve_key(key)
|
|
332
|
+
return key if @raw.key?(key)
|
|
333
|
+
|
|
334
|
+
case key
|
|
335
|
+
when Symbol then @raw.key?(key.to_s) ? key.to_s : nil
|
|
336
|
+
when String then @raw.key?(key.to_sym) ? key.to_sym : nil
|
|
337
|
+
end
|
|
338
|
+
end
|
|
339
|
+
|
|
340
|
+
def method_missing(name, *args, &block)
|
|
341
|
+
key = args.empty? && block.nil? ? resolve_key(name) : nil
|
|
342
|
+
return @raw[key] unless key.nil?
|
|
343
|
+
|
|
344
|
+
carries = @raw.empty? ? 'no fields' : @raw.keys.map(&:to_s).join(', ')
|
|
345
|
+
raise NoMethodError.new(
|
|
346
|
+
"undefined method '#{name}' for #{self.class.name}: " \
|
|
347
|
+
"this event's data.object carries #{carries}",
|
|
348
|
+
name
|
|
349
|
+
)
|
|
350
|
+
end
|
|
351
|
+
|
|
352
|
+
def respond_to_missing?(name, include_private = false)
|
|
353
|
+
!resolve_key(name).nil? || super
|
|
192
354
|
end
|
|
193
355
|
end
|
|
194
356
|
|
|
195
|
-
|
|
357
|
+
# The message view of +data.object+, built only for +message.*+ events.
|
|
358
|
+
#
|
|
359
|
+
# Readers return exactly what the payload carried. An absent field is +nil+:
|
|
360
|
+
# this class does not invent +segments+, +credits_used+, +direction+, +to+
|
|
361
|
+
# or +from+.
|
|
362
|
+
class WebhookMessageData < WebhookObject
|
|
196
363
|
attr_reader :id, :status, :to, :from, :direction, :organization_id,
|
|
197
364
|
:text, :error, :error_code, :delivered_at, :failed_at,
|
|
198
|
-
:created_at, :segments, :credits_used, :message_format,
|
|
199
|
-
:retry_count, :metadata, :batch_id
|
|
200
|
-
|
|
201
|
-
def initialize(data)
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
@
|
|
206
|
-
@
|
|
207
|
-
@
|
|
208
|
-
@
|
|
209
|
-
@
|
|
210
|
-
@
|
|
211
|
-
@
|
|
212
|
-
@
|
|
213
|
-
@
|
|
214
|
-
@
|
|
215
|
-
@
|
|
216
|
-
@
|
|
217
|
-
@
|
|
218
|
-
@
|
|
219
|
-
@
|
|
220
|
-
@
|
|
365
|
+
:created_at, :segments, :credits_used, :message_format,
|
|
366
|
+
:media_urls, :retry_count, :metadata, :batch_id
|
|
367
|
+
|
|
368
|
+
def initialize(data = {})
|
|
369
|
+
super
|
|
370
|
+
# Both spellings name the same message: the current payload shape uses
|
|
371
|
+
# `id`, the pre-`data.object` shape used `message_id`.
|
|
372
|
+
@id = self[:id] || self[:message_id]
|
|
373
|
+
@status = self[:status]
|
|
374
|
+
@to = self[:to]
|
|
375
|
+
@from = self[:from]
|
|
376
|
+
@direction = self[:direction]
|
|
377
|
+
@organization_id = self[:organization_id]
|
|
378
|
+
@text = self[:text]
|
|
379
|
+
@error = self[:error]
|
|
380
|
+
@error_code = self[:error_code]
|
|
381
|
+
@delivered_at = self[:delivered_at]
|
|
382
|
+
@failed_at = self[:failed_at]
|
|
383
|
+
@created_at = self[:created_at]
|
|
384
|
+
@segments = self[:segments]
|
|
385
|
+
@credits_used = self[:credits_used]
|
|
386
|
+
@message_format = self[:message_format]
|
|
387
|
+
@media_urls = self[:media_urls]
|
|
388
|
+
@retry_count = self[:retry_count]
|
|
389
|
+
@metadata = self[:metadata]
|
|
390
|
+
@batch_id = self[:batch_id]
|
|
221
391
|
end
|
|
222
392
|
|
|
393
|
+
# Backwards-compatible alias for {#id}.
|
|
223
394
|
def message_id
|
|
224
395
|
@id
|
|
225
396
|
end
|
|
226
|
-
|
|
227
|
-
def to_h
|
|
228
|
-
{
|
|
229
|
-
id: @id,
|
|
230
|
-
status: @status,
|
|
231
|
-
to: @to,
|
|
232
|
-
from: @from,
|
|
233
|
-
direction: @direction,
|
|
234
|
-
error: @error,
|
|
235
|
-
error_code: @error_code,
|
|
236
|
-
delivered_at: @delivered_at,
|
|
237
|
-
failed_at: @failed_at,
|
|
238
|
-
segments: @segments,
|
|
239
|
-
credits_used: @credits_used,
|
|
240
|
-
batch_id: @batch_id
|
|
241
|
-
}.compact
|
|
242
|
-
end
|
|
243
397
|
end
|
|
244
398
|
|
|
245
|
-
|
|
399
|
+
# The verification view of +data.object+, built only for +verification.*+
|
|
400
|
+
# events. Readers carry what the payload held and nothing more.
|
|
401
|
+
class WebhookVerificationData < WebhookObject
|
|
246
402
|
attr_reader :id, :organization_id, :phone, :status, :delivery_status,
|
|
247
403
|
:attempts, :max_attempts, :expires_at, :verified_at,
|
|
248
404
|
:created_at, :app_name, :template_id, :profile_id, :metadata
|
|
249
405
|
|
|
250
406
|
def initialize(data = {})
|
|
251
|
-
|
|
252
|
-
@
|
|
253
|
-
@
|
|
254
|
-
@
|
|
255
|
-
@
|
|
256
|
-
@
|
|
257
|
-
@
|
|
258
|
-
@
|
|
259
|
-
@
|
|
260
|
-
@
|
|
261
|
-
@
|
|
262
|
-
@
|
|
263
|
-
@
|
|
264
|
-
@
|
|
407
|
+
super
|
|
408
|
+
@id = self[:id]
|
|
409
|
+
@organization_id = self[:organization_id]
|
|
410
|
+
@phone = self[:phone]
|
|
411
|
+
@status = self[:status]
|
|
412
|
+
@delivery_status = self[:delivery_status]
|
|
413
|
+
@attempts = self[:attempts]
|
|
414
|
+
@max_attempts = self[:max_attempts]
|
|
415
|
+
@expires_at = self[:expires_at]
|
|
416
|
+
@verified_at = self[:verified_at]
|
|
417
|
+
@created_at = self[:created_at]
|
|
418
|
+
@app_name = self[:app_name]
|
|
419
|
+
@template_id = self[:template_id]
|
|
420
|
+
@profile_id = self[:profile_id]
|
|
421
|
+
@metadata = self[:metadata]
|
|
422
|
+
end
|
|
423
|
+
end
|
|
424
|
+
|
|
425
|
+
# A parsed webhook event.
|
|
426
|
+
#
|
|
427
|
+
# {#raw_object} is +data.object+ exactly as it arrived, for every event type.
|
|
428
|
+
# {#data} adds a typed view on top of it where one applies: a
|
|
429
|
+
# {WebhookMessageData} for +message.*+, a {WebhookVerificationData} for
|
|
430
|
+
# +verification.*+, and a plain {WebhookObject} for everything else —
|
|
431
|
+
# +rcs_*+, +whatsapp_*+, +call.*+, +brand.*+, +campaign.*+, +assignment.*+,
|
|
432
|
+
# +number.*+, +port*+, +contact*+, +conversation.*+ and +draft.*+, whose
|
|
433
|
+
# payloads are not message-shaped.
|
|
434
|
+
#
|
|
435
|
+
# @example Handling a lifecycle event
|
|
436
|
+
# case event.type
|
|
437
|
+
# when Sendly::Webhooks::EVENT_MESSAGE_DELIVERED
|
|
438
|
+
# puts event.message.id
|
|
439
|
+
# when Sendly::Webhooks::EVENT_RCS_AGENT_LIVE
|
|
440
|
+
# puts event.data[:agent_id]
|
|
441
|
+
# end
|
|
442
|
+
class WebhookEvent
|
|
443
|
+
MESSAGE_EVENT_PREFIX = 'message.'
|
|
444
|
+
|
|
445
|
+
# message.opt_in and message.opt_out share the message.* prefix but carry
|
|
446
|
+
# an opt-out record ({phone_number, keyword, from_number, timestamp}), not
|
|
447
|
+
# a message. Treating them as messages produced a message view with every
|
|
448
|
+
# field nil, which is the invented-value problem this class removes.
|
|
449
|
+
NON_MESSAGE_MESSAGE_EVENTS = ['message.opt_in', 'message.opt_out'].freeze
|
|
450
|
+
VERIFICATION_EVENT_PREFIX = 'verification.'
|
|
451
|
+
|
|
452
|
+
# @return [String] event id, for idempotency
|
|
453
|
+
attr_reader :id
|
|
454
|
+
|
|
455
|
+
# @return [String] event type, verbatim — including one this SDK predates
|
|
456
|
+
attr_reader :type
|
|
457
|
+
|
|
458
|
+
# @return [Hash] +data.object+ exactly as it arrived, for every event type
|
|
459
|
+
attr_reader :raw_object
|
|
460
|
+
|
|
461
|
+
# @return [WebhookObject] hash-like view of +data.object+. A
|
|
462
|
+
# {WebhookMessageData} for +message.*+ events and a
|
|
463
|
+
# {WebhookVerificationData} for +verification.*+ events; a plain
|
|
464
|
+
# {WebhookObject} otherwise.
|
|
465
|
+
attr_reader :data
|
|
466
|
+
|
|
467
|
+
# @return [WebhookMessageData, nil] the message view, or nil when this
|
|
468
|
+
# event is not a message. Lifecycle events return nil rather than a
|
|
469
|
+
# message struct full of invented values.
|
|
470
|
+
attr_reader :message
|
|
471
|
+
|
|
472
|
+
# @return [WebhookVerificationData, nil] the verification view, or nil
|
|
473
|
+
# when this event is not a +verification.*+ event
|
|
474
|
+
attr_reader :verification
|
|
475
|
+
|
|
476
|
+
attr_reader :created, :api_version, :livemode
|
|
477
|
+
|
|
478
|
+
def initialize(payload)
|
|
479
|
+
env = payload.is_a?(WebhookObject) ? payload : WebhookObject.new(payload)
|
|
480
|
+
|
|
481
|
+
@id = env[:id]
|
|
482
|
+
@type = env[:type]
|
|
483
|
+
@raw_object = extract_object(env[:data])
|
|
484
|
+
@created = env[:created] || env[:created_at] || 0
|
|
485
|
+
@api_version = env[:api_version] || '2024-01'
|
|
486
|
+
@livemode = env[:livemode] || false
|
|
487
|
+
|
|
488
|
+
@message = message? ? WebhookMessageData.new(@raw_object) : nil
|
|
489
|
+
@verification = verification? ? WebhookVerificationData.new(@raw_object) : nil
|
|
490
|
+
@data = @message || @verification || WebhookObject.new(@raw_object)
|
|
491
|
+
end
|
|
492
|
+
|
|
493
|
+
# @return [Hash] alias for {#raw_object}
|
|
494
|
+
def object
|
|
495
|
+
@raw_object
|
|
496
|
+
end
|
|
497
|
+
|
|
498
|
+
# Read +data.object+ as a type of your choosing — the supported way to
|
|
499
|
+
# handle a lifecycle payload with a typed object.
|
|
500
|
+
#
|
|
501
|
+
# A Struct or Data class is filled from the members it declares, and the
|
|
502
|
+
# rest of the payload is ignored, so a field added to the event later
|
|
503
|
+
# cannot break the call. Any other class is handed +raw_object+ itself.
|
|
504
|
+
#
|
|
505
|
+
# @example
|
|
506
|
+
# AgentLive = Struct.new(:agent_id, :name, :stage)
|
|
507
|
+
# agent = event.object_as(AgentLive) # => #<struct AgentLive ...>
|
|
508
|
+
#
|
|
509
|
+
# @param klass [Class] a Struct or Data class, or anything whose
|
|
510
|
+
# initializer takes a Hash
|
|
511
|
+
# @return [Object]
|
|
512
|
+
def object_as(klass)
|
|
513
|
+
return klass.new(@raw_object) unless klass.respond_to?(:members)
|
|
514
|
+
|
|
515
|
+
values = klass.members.map { |member| @data[member] }
|
|
516
|
+
begin
|
|
517
|
+
klass.new(*values)
|
|
518
|
+
rescue ArgumentError
|
|
519
|
+
klass.new(**klass.members.zip(values).to_h)
|
|
520
|
+
end
|
|
521
|
+
end
|
|
522
|
+
|
|
523
|
+
# @return [Boolean] whether this event carries a message
|
|
524
|
+
def message?
|
|
525
|
+
@type.to_s.start_with?(MESSAGE_EVENT_PREFIX) &&
|
|
526
|
+
!NON_MESSAGE_MESSAGE_EVENTS.include?(@type.to_s)
|
|
527
|
+
end
|
|
528
|
+
|
|
529
|
+
# @return [Boolean] whether this event carries a verification
|
|
530
|
+
def verification?
|
|
531
|
+
@type.to_s.start_with?(VERIFICATION_EVENT_PREFIX)
|
|
532
|
+
end
|
|
533
|
+
|
|
534
|
+
def created_at
|
|
535
|
+
@created
|
|
536
|
+
end
|
|
537
|
+
|
|
538
|
+
def to_h
|
|
539
|
+
{
|
|
540
|
+
id: @id,
|
|
541
|
+
type: @type,
|
|
542
|
+
data: @data.to_h,
|
|
543
|
+
created: @created,
|
|
544
|
+
api_version: @api_version,
|
|
545
|
+
livemode: @livemode
|
|
546
|
+
}
|
|
547
|
+
end
|
|
548
|
+
|
|
549
|
+
private
|
|
550
|
+
|
|
551
|
+
def extract_object(data)
|
|
552
|
+
return {} unless data.is_a?(Hash)
|
|
553
|
+
|
|
554
|
+
object = data.key?(:object) ? data[:object] : data['object']
|
|
555
|
+
object.is_a?(Hash) ? object : data
|
|
265
556
|
end
|
|
266
557
|
end
|
|
267
558
|
end
|
metadata
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: sendly
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 3.
|
|
4
|
+
version: 3.40.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Sendly
|
|
8
8
|
autorequire:
|
|
9
9
|
bindir: exe
|
|
10
10
|
cert_chain: []
|
|
11
|
-
date: 2026-
|
|
11
|
+
date: 2026-09-09 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: faraday
|