sendly 4.2.0 → 4.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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +90 -0
- data/Gemfile.lock +5 -5
- data/README.md +760 -147
- data/examples/list_messages.rb +1 -1
- data/examples/send_sms.rb +6 -2
- data/lib/sendly/account_resource.rb +16 -4
- data/lib/sendly/business_upgrade_resource.rb +3 -2
- data/lib/sendly/calls_resource.rb +20 -8
- data/lib/sendly/campaigns_resource.rb +59 -8
- data/lib/sendly/client.rb +38 -19
- data/lib/sendly/conversations_resource.rb +17 -5
- data/lib/sendly/drafts_resource.rb +2 -1
- data/lib/sendly/enterprise.rb +21 -10
- data/lib/sendly/errors.rb +46 -22
- data/lib/sendly/messages.rb +53 -13
- data/lib/sendly/types.rb +177 -26
- data/lib/sendly/version.rb +1 -1
- data/lib/sendly/webhooks.rb +5 -0
- data/lib/sendly/webhooks_resource.rb +30 -7
- data/lib/sendly/whatsapp_resource.rb +651 -46
- metadata +6 -6
data/lib/sendly/errors.rb
CHANGED
|
@@ -3,7 +3,10 @@
|
|
|
3
3
|
module Sendly
|
|
4
4
|
# Base error class for all Sendly errors
|
|
5
5
|
class Error < StandardError
|
|
6
|
-
# @return [String, nil]
|
|
6
|
+
# @return [String, nil] The API's error code, such as
|
|
7
|
+
# +"whatsapp_window_closed"+ or +"sms_fallback_unavailable"+, for an
|
|
8
|
+
# error built from an API response that names one. Otherwise the
|
|
9
|
+
# class's own code, such as +"VALIDATION_ERROR"+.
|
|
7
10
|
attr_reader :code
|
|
8
11
|
|
|
9
12
|
# @return [Hash, nil] Additional error details
|
|
@@ -40,8 +43,8 @@ module Sendly
|
|
|
40
43
|
|
|
41
44
|
# Raised when the API key is invalid or missing
|
|
42
45
|
class AuthenticationError < Error
|
|
43
|
-
def initialize(message = "Invalid or missing API key")
|
|
44
|
-
super(message, code: "AUTHENTICATION_ERROR", status_code: 401)
|
|
46
|
+
def initialize(message = "Invalid or missing API key", code: nil)
|
|
47
|
+
super(message, code: code || "AUTHENTICATION_ERROR", status_code: 401)
|
|
45
48
|
end
|
|
46
49
|
end
|
|
47
50
|
|
|
@@ -50,16 +53,16 @@ module Sendly
|
|
|
50
53
|
# @return [Integer, nil] Seconds to wait before retrying
|
|
51
54
|
attr_reader :retry_after
|
|
52
55
|
|
|
53
|
-
def initialize(message = "Rate limit exceeded", retry_after: nil)
|
|
56
|
+
def initialize(message = "Rate limit exceeded", retry_after: nil, code: nil)
|
|
54
57
|
@retry_after = retry_after
|
|
55
|
-
super(message, code: "RATE_LIMIT_EXCEEDED", status_code: 429)
|
|
58
|
+
super(message, code: code || "RATE_LIMIT_EXCEEDED", status_code: 429)
|
|
56
59
|
end
|
|
57
60
|
end
|
|
58
61
|
|
|
59
62
|
# Raised when the account has insufficient credits
|
|
60
63
|
class InsufficientCreditsError < Error
|
|
61
|
-
def initialize(message = "Insufficient credits")
|
|
62
|
-
super(message, code: "INSUFFICIENT_CREDITS", status_code: 402)
|
|
64
|
+
def initialize(message = "Insufficient credits", code: nil)
|
|
65
|
+
super(message, code: code || "INSUFFICIENT_CREDITS", status_code: 402)
|
|
63
66
|
end
|
|
64
67
|
end
|
|
65
68
|
|
|
@@ -70,16 +73,16 @@ module Sendly
|
|
|
70
73
|
# e.g. +[{ "path" => "brand.ein", "message" => "Enter a 9-digit EIN" }]+
|
|
71
74
|
attr_reader :field_errors
|
|
72
75
|
|
|
73
|
-
def initialize(message = "Validation failed", field_errors: nil, details: nil)
|
|
76
|
+
def initialize(message = "Validation failed", field_errors: nil, details: nil, code: nil)
|
|
74
77
|
@field_errors = field_errors
|
|
75
|
-
super(message, code: "VALIDATION_ERROR", details: details, status_code: 400)
|
|
78
|
+
super(message, code: code || "VALIDATION_ERROR", details: details, status_code: 400)
|
|
76
79
|
end
|
|
77
80
|
end
|
|
78
81
|
|
|
79
82
|
# Raised when the requested resource is not found
|
|
80
83
|
class NotFoundError < Error
|
|
81
|
-
def initialize(message = "Resource not found")
|
|
82
|
-
super(message, code: "NOT_FOUND", status_code: 404)
|
|
84
|
+
def initialize(message = "Resource not found", code: nil)
|
|
85
|
+
super(message, code: code || "NOT_FOUND", status_code: 404)
|
|
83
86
|
end
|
|
84
87
|
end
|
|
85
88
|
|
|
@@ -107,36 +110,57 @@ module Sendly
|
|
|
107
110
|
|
|
108
111
|
# Raised for server errors (5xx)
|
|
109
112
|
class ServerError < Error
|
|
110
|
-
def initialize(message = "Server error occurred", status_code: 500)
|
|
111
|
-
super(message, code: "SERVER_ERROR", status_code: status_code)
|
|
113
|
+
def initialize(message = "Server error occurred", status_code: 500, code: nil)
|
|
114
|
+
super(message, code: code || "SERVER_ERROR", status_code: status_code)
|
|
112
115
|
end
|
|
113
116
|
end
|
|
114
117
|
|
|
115
118
|
# Convert API response to appropriate error
|
|
116
119
|
class ErrorFactory
|
|
117
|
-
|
|
120
|
+
API_ERROR_CODE = /\A[a-z][a-z0-9_]*\z/
|
|
121
|
+
private_constant :API_ERROR_CODE
|
|
122
|
+
|
|
123
|
+
def self.from_response(status, body, retry_after_header: nil)
|
|
118
124
|
message = body["message"] || body["error"] || "Unknown error"
|
|
119
|
-
code = body
|
|
125
|
+
code = api_error_code(body)
|
|
120
126
|
details = body["details"]
|
|
121
127
|
|
|
122
128
|
error = case status
|
|
123
129
|
when 400, 422
|
|
124
|
-
ValidationError.new(message, details: details, field_errors: body["errors"])
|
|
130
|
+
ValidationError.new(message, details: details, field_errors: body["errors"], code: code)
|
|
125
131
|
when 401
|
|
126
|
-
AuthenticationError.new(message)
|
|
132
|
+
AuthenticationError.new(message, code: code)
|
|
127
133
|
when 402
|
|
128
|
-
InsufficientCreditsError.new(message)
|
|
134
|
+
InsufficientCreditsError.new(message, code: code)
|
|
129
135
|
when 404
|
|
130
|
-
NotFoundError.new(message)
|
|
136
|
+
NotFoundError.new(message, code: code)
|
|
131
137
|
when 429
|
|
132
|
-
retry_after = body["retry_after"] || body["retryAfter"]
|
|
133
|
-
RateLimitError.new(message, retry_after: retry_after)
|
|
138
|
+
retry_after = parse_retry_after(retry_after_header) || body["retry_after"] || body["retryAfter"]
|
|
139
|
+
RateLimitError.new(message, retry_after: retry_after, code: code)
|
|
134
140
|
when 500..599
|
|
135
|
-
ServerError.new(message, status_code: status)
|
|
141
|
+
ServerError.new(message, status_code: status, code: code)
|
|
136
142
|
else
|
|
137
143
|
APIError.new(message, status_code: status, code: code, details: details)
|
|
138
144
|
end
|
|
139
145
|
error.with_response_body(body)
|
|
140
146
|
end
|
|
147
|
+
|
|
148
|
+
def self.api_error_code(body)
|
|
149
|
+
error = body["error"]
|
|
150
|
+
return error.strip if error.is_a?(String) && error.strip.match?(API_ERROR_CODE)
|
|
151
|
+
|
|
152
|
+
code = body["code"]
|
|
153
|
+
code.is_a?(String) && !code.strip.empty? ? code : nil
|
|
154
|
+
end
|
|
155
|
+
private_class_method :api_error_code
|
|
156
|
+
|
|
157
|
+
def self.parse_retry_after(value)
|
|
158
|
+
text = value.to_s.strip
|
|
159
|
+
return text.to_i if text.match?(/\A\d+\z/)
|
|
160
|
+
return text.to_f if text.match?(/\A\d+\.\d+\z/)
|
|
161
|
+
|
|
162
|
+
nil
|
|
163
|
+
end
|
|
164
|
+
private_class_method :parse_retry_after
|
|
141
165
|
end
|
|
142
166
|
end
|
data/lib/sendly/messages.rb
CHANGED
|
@@ -13,8 +13,11 @@ module Sendly
|
|
|
13
13
|
# Send an SMS, WhatsApp, or RCS message
|
|
14
14
|
#
|
|
15
15
|
# Pass +channel: "whatsapp"+ to send on WhatsApp. WhatsApp sends require
|
|
16
|
-
#
|
|
17
|
-
#
|
|
16
|
+
# the +sms:send+ scope (not +whatsapp:write+), a live API key and a
|
|
17
|
+
# +from+ number with an active WhatsApp connection (see
|
|
18
|
+
# +client.whatsapp.signup+). WhatsApp is enabled per person (the user who
|
|
19
|
+
# owns the API key, not the workspace); while it is off the API responds
|
|
20
|
+
# 403 +whatsapp_not_enabled+. Provide exactly one of +text+
|
|
18
21
|
# (free-form, max 4096 bytes), +media_urls+ (a single attachment;
|
|
19
22
|
# optional +text+ becomes its caption, max 1024 bytes), or +template+
|
|
20
23
|
# (an approved template). Free-form text and media only deliver inside
|
|
@@ -59,9 +62,17 @@ module Sendly
|
|
|
59
62
|
# { action: { text: ..., postbackData: ..., url: ... } }. Passed
|
|
60
63
|
# through verbatim, so use the camelCase keys shown. Suggestions have
|
|
61
64
|
# no SMS form and are dropped on a fallback.
|
|
62
|
-
# @param fallback_to_sms [Boolean] RCS
|
|
65
|
+
# @param fallback_to_sms [Boolean] RCS: deliver +text+ as plain SMS
|
|
63
66
|
# when the recipient can't receive RCS (default true). Pass false to
|
|
64
|
-
# fail with 422 +rcs_not_supported_for_recipient+ instead.
|
|
67
|
+
# fail with 422 +rcs_not_supported_for_recipient+ instead. WhatsApp:
|
|
68
|
+
# default false. With true, a message WhatsApp can't deliver (the
|
|
69
|
+
# 24-hour window is closed, WhatsApp refuses it, or reports it
|
|
70
|
+
# undeliverable later) goes as an SMS from the same number, billed as
|
|
71
|
+
# an SMS; +message.fallback+ ({Sendly::WhatsAppSmsFallback}) says what
|
|
72
|
+
# happened. The number must already be able to send SMS to the
|
|
73
|
+
# recipient (US and Canada), or the API answers 403
|
|
74
|
+
# +sms_fallback_unavailable+; marketing and authentication templates,
|
|
75
|
+
# and media without a caption, get 422 +sms_fallback_not_supported+.
|
|
65
76
|
# @param idempotency_key [String] Idempotency key for this operation
|
|
66
77
|
# (1-255 printable ASCII characters). The SDK already generates a key
|
|
67
78
|
# per logical request automatically, so the server can dedupe the
|
|
@@ -71,7 +82,22 @@ module Sendly
|
|
|
71
82
|
# response instead of executing again.
|
|
72
83
|
# @return [Sendly::Message, Sendly::WhatsAppMessage, Sendly::RcsMessage] The sent message
|
|
73
84
|
#
|
|
74
|
-
# @raise [Sendly::ValidationError] If parameters are invalid
|
|
85
|
+
# @raise [Sendly::ValidationError] If parameters are invalid, or on
|
|
86
|
+
# WhatsApp the 422 +whatsapp_send_failed+ when WhatsApp refused the
|
|
87
|
+
# message (final, not retried, not charged; cached under the
|
|
88
|
+
# idempotency key and replayed for 24 hours)
|
|
89
|
+
# @raise [Sendly::ServerError] On WhatsApp, the 502 +whatsapp_send_failed+:
|
|
90
|
+
# the message provably never reached the carrier, so it was not sent
|
|
91
|
+
# and is safe to send again. It isn't charged and is never cached, so
|
|
92
|
+
# the client retries it like any 5xx first under the same idempotency
|
|
93
|
+
# key. No send returns 503 +whatsapp_unavailable+.
|
|
94
|
+
# @raise [Sendly::APIError] On WhatsApp, the 409
|
|
95
|
+
# +whatsapp_send_unconfirmed+ (+status_code+ 409): the outcome is
|
|
96
|
+
# unknown. The message was marked failed and refunded but may still be
|
|
97
|
+
# delivered, so check before sending it again (it could arrive twice).
|
|
98
|
+
# It is not retried automatically, and it is cached under the
|
|
99
|
+
# idempotency key, so repeating the request with the same key returns
|
|
100
|
+
# this answer again.
|
|
75
101
|
# @raise [Sendly::InsufficientCreditsError] If account has no credits
|
|
76
102
|
# @raise [Sendly::RateLimitError] If rate limit is exceeded
|
|
77
103
|
#
|
|
@@ -112,6 +138,17 @@ module Sendly
|
|
|
112
138
|
# puts message.whatsapp.kind # "template"
|
|
113
139
|
# puts message.credits_used # priced by country + category
|
|
114
140
|
#
|
|
141
|
+
# @example WhatsApp with an SMS fallback
|
|
142
|
+
# message = client.messages.send(
|
|
143
|
+
# channel: "whatsapp",
|
|
144
|
+
# to: "+15551234567",
|
|
145
|
+
# from: "+15559876543",
|
|
146
|
+
# text: "Your table is ready!",
|
|
147
|
+
# fallback_to_sms: true
|
|
148
|
+
# )
|
|
149
|
+
# puts message.fallback&.status # "sent" when it went as an SMS
|
|
150
|
+
# puts message.fell_back?
|
|
151
|
+
#
|
|
115
152
|
# @example RCS text with suggested replies and actions
|
|
116
153
|
# message = client.messages.send(
|
|
117
154
|
# channel: "rcs",
|
|
@@ -171,6 +208,7 @@ module Sendly
|
|
|
171
208
|
body[:mediaUrls] = media_urls if has_media
|
|
172
209
|
body[:template] = template if template
|
|
173
210
|
body[:metadata] = metadata if metadata
|
|
211
|
+
body[:fallbackToSms] = fallback_to_sms unless fallback_to_sms.nil?
|
|
174
212
|
|
|
175
213
|
response = client.post("/messages", body, idempotency_key: idempotency_key)
|
|
176
214
|
return WhatsAppMessage.new(response)
|
|
@@ -327,7 +365,7 @@ module Sendly
|
|
|
327
365
|
#
|
|
328
366
|
# @param status [String] Filter by status
|
|
329
367
|
# @param to [String] Filter by recipient
|
|
330
|
-
# @param batch_size [Integer] Number of messages per request
|
|
368
|
+
# @param batch_size [Integer] Number of messages per request (the API returns at most 100)
|
|
331
369
|
# @yield [Message] Each message
|
|
332
370
|
# @return [Enumerator] If no block given
|
|
333
371
|
#
|
|
@@ -343,9 +381,9 @@ module Sendly
|
|
|
343
381
|
page = list(limit: batch_size, offset: offset, status: status, to: to)
|
|
344
382
|
page.each(&block)
|
|
345
383
|
|
|
346
|
-
break
|
|
384
|
+
break if !page.has_more || page.count.zero?
|
|
347
385
|
|
|
348
|
-
offset +=
|
|
386
|
+
offset += page.count
|
|
349
387
|
end
|
|
350
388
|
end
|
|
351
389
|
|
|
@@ -446,7 +484,9 @@ module Sendly
|
|
|
446
484
|
# @param metadata [Hash] Shared metadata for all messages (max 4KB). Each message can also have its own metadata hash which takes priority.
|
|
447
485
|
# @param idempotency_key [String] Idempotency key for this operation
|
|
448
486
|
# (1-255 printable ASCII characters, optional)
|
|
449
|
-
# @return [Hash] Batch
|
|
487
|
+
# @return [Hash] Batch result: +batchId+, +status+ (processing, completed,
|
|
488
|
+
# partial_failure or failed), +total+, +sent+, +failed+, +creditsUsed+,
|
|
489
|
+
# +creditsRefunded+ and +messages+
|
|
450
490
|
#
|
|
451
491
|
# @raise [Sendly::ValidationError] If parameters are invalid
|
|
452
492
|
# @raise [Sendly::InsufficientCreditsError] If account has insufficient credits
|
|
@@ -458,7 +498,7 @@ module Sendly
|
|
|
458
498
|
# { to: "+15559876543", text: "Hello Bob!" }
|
|
459
499
|
# ]
|
|
460
500
|
# )
|
|
461
|
-
# puts "Batch #{result['batchId']}: #{result['
|
|
501
|
+
# puts "Batch #{result['batchId']}: #{result['total']} messages, #{result['status']}"
|
|
462
502
|
def send_batch(messages:, from: nil, message_type: nil, metadata: nil, idempotency_key: nil)
|
|
463
503
|
raise ValidationError, "Messages array is required" if messages.nil? || messages.empty?
|
|
464
504
|
|
|
@@ -505,12 +545,12 @@ module Sendly
|
|
|
505
545
|
#
|
|
506
546
|
# @param limit [Integer] Maximum batches to return (default: 20, max: 100)
|
|
507
547
|
# @param offset [Integer] Number of batches to skip
|
|
508
|
-
# @param status [String] Filter by status (processing, completed, failed)
|
|
548
|
+
# @param status [String] Filter by status (processing, completed, partial_failure, failed)
|
|
509
549
|
# @return [Hash] Paginated list of batches
|
|
510
550
|
#
|
|
511
551
|
# @example
|
|
512
552
|
# batches = client.messages.list_batches(limit: 10)
|
|
513
|
-
# batches["data"].each { |b| puts "#{b['
|
|
553
|
+
# batches["data"].each { |b| puts "#{b['id']}: #{b['status']}" }
|
|
514
554
|
def list_batches(limit: 20, offset: 0, status: nil)
|
|
515
555
|
params = {
|
|
516
556
|
limit: [limit, 100].min,
|
|
@@ -537,7 +577,7 @@ module Sendly
|
|
|
537
577
|
# { to: "+15559876543", text: "Hello Bob!" }
|
|
538
578
|
# ]
|
|
539
579
|
# )
|
|
540
|
-
# puts "
|
|
580
|
+
# puts "Sufficient credits: #{preview['hasSufficientCredits']}"
|
|
541
581
|
# puts "Credits needed: #{preview['creditsNeeded']}"
|
|
542
582
|
def preview_batch(messages:, from: nil, message_type: nil)
|
|
543
583
|
raise ValidationError, "Messages array is required" if messages.nil? || messages.empty?
|
data/lib/sendly/types.rb
CHANGED
|
@@ -63,6 +63,20 @@ module Sendly
|
|
|
63
63
|
# @return [Hash, nil] AI classification metadata for inbound messages
|
|
64
64
|
attr_reader :ai_metadata
|
|
65
65
|
|
|
66
|
+
# @return [Array<String>] Media attached to the message; empty when there is none
|
|
67
|
+
attr_reader :media_urls
|
|
68
|
+
|
|
69
|
+
# @return [String] "sms", "mms", "rcs" or "whatsapp"; "sms" when the response does not say
|
|
70
|
+
attr_reader :message_format
|
|
71
|
+
|
|
72
|
+
# @return [Sendly::WhatsAppSmsFallback, nil] On a WhatsApp message sent
|
|
73
|
+
# with +fallback_to_sms+, what happened to its SMS fallback
|
|
74
|
+
attr_reader :fallback
|
|
75
|
+
|
|
76
|
+
# @return [String, nil] On an SMS fallback, the id of the WhatsApp
|
|
77
|
+
# message it replaced
|
|
78
|
+
attr_reader :fallback_of
|
|
79
|
+
|
|
66
80
|
# Message status constants (sending removed - doesn't exist in database)
|
|
67
81
|
STATUSES = %w[queued sent delivered failed bounced retrying].freeze
|
|
68
82
|
|
|
@@ -90,6 +104,10 @@ module Sendly
|
|
|
90
104
|
@retry_count = data["retryCount"] || 0
|
|
91
105
|
@metadata = data["metadata"]
|
|
92
106
|
@ai_metadata = data["aiMetadata"]
|
|
107
|
+
@media_urls = data["mediaUrls"] || data["media_urls"] || []
|
|
108
|
+
@message_format = data["messageFormat"] || data["message_format"] || "sms"
|
|
109
|
+
@fallback = data["fallback"].is_a?(Hash) ? WhatsAppSmsFallback.new(data["fallback"]) : nil
|
|
110
|
+
@fallback_of = data["fallbackOf"] || data["fallback_of"]
|
|
93
111
|
end
|
|
94
112
|
|
|
95
113
|
# Check if message was delivered
|
|
@@ -133,7 +151,11 @@ module Sendly
|
|
|
133
151
|
error_code: error_code,
|
|
134
152
|
retry_count: retry_count,
|
|
135
153
|
metadata: metadata,
|
|
136
|
-
ai_metadata: ai_metadata
|
|
154
|
+
ai_metadata: ai_metadata,
|
|
155
|
+
media_urls: media_urls,
|
|
156
|
+
message_format: message_format,
|
|
157
|
+
fallback: fallback&.to_h,
|
|
158
|
+
fallback_of: fallback_of
|
|
137
159
|
}.compact
|
|
138
160
|
end
|
|
139
161
|
|
|
@@ -155,7 +177,7 @@ module Sendly
|
|
|
155
177
|
# @return [Array<Message>] Messages in this page
|
|
156
178
|
attr_reader :data
|
|
157
179
|
|
|
158
|
-
# @return [Integer] Total number of messages
|
|
180
|
+
# @return [Integer] Total number of messages that match the query, across all pages
|
|
159
181
|
attr_reader :total
|
|
160
182
|
|
|
161
183
|
# @return [Integer] Current limit
|
|
@@ -168,11 +190,16 @@ module Sendly
|
|
|
168
190
|
attr_reader :has_more
|
|
169
191
|
|
|
170
192
|
def initialize(response)
|
|
193
|
+
pagination = response["pagination"] || {}
|
|
171
194
|
@data = (response["data"] || []).map { |m| Message.new(m) }
|
|
172
|
-
@total = response["count"] || @data.length
|
|
173
|
-
@limit = response["limit"] || 20
|
|
174
|
-
@offset = response["offset"] || 0
|
|
175
|
-
@has_more =
|
|
195
|
+
@total = pagination["total"] || response["total"] || response["count"] || @data.length
|
|
196
|
+
@limit = pagination["limit"] || response["limit"] || 20
|
|
197
|
+
@offset = pagination["offset"] || response["offset"] || 0
|
|
198
|
+
@has_more = if pagination.key?("hasMore")
|
|
199
|
+
pagination["hasMore"]
|
|
200
|
+
else
|
|
201
|
+
(@offset + @data.length) < @total
|
|
202
|
+
end
|
|
176
203
|
end
|
|
177
204
|
|
|
178
205
|
# Iterate over messages
|
|
@@ -433,14 +460,31 @@ module Sendly
|
|
|
433
460
|
end
|
|
434
461
|
|
|
435
462
|
# Result of testing a webhook
|
|
463
|
+
#
|
|
464
|
+
# Only a delivered test comes back as a result: when the test delivery
|
|
465
|
+
# fails, {Sendly::WebhooksResource#test} raises {Sendly::ValidationError}
|
|
466
|
+
# with the API's message.
|
|
436
467
|
class WebhookTestResult
|
|
437
468
|
attr_reader :success, :status_code, :response_time_ms, :error
|
|
438
469
|
|
|
470
|
+
# @return [String, nil] The API's summary of the test delivery
|
|
471
|
+
attr_reader :message
|
|
472
|
+
|
|
473
|
+
# @return [String, nil] ID of the test delivery
|
|
474
|
+
attr_reader :delivery_id
|
|
475
|
+
|
|
476
|
+
# @return [Hash] The raw parsed response
|
|
477
|
+
attr_reader :raw
|
|
478
|
+
|
|
439
479
|
def initialize(data)
|
|
480
|
+
delivery = data["delivery"] || {}
|
|
481
|
+
@raw = data
|
|
440
482
|
@success = data["success"]
|
|
441
|
-
@status_code = data["status_code"] || data["statusCode"]
|
|
442
|
-
@response_time_ms = data["response_time_ms"] || data["responseTimeMs"]
|
|
443
|
-
@error = data["error"]
|
|
483
|
+
@status_code = data["status_code"] || data["statusCode"] || delivery["status_code"]
|
|
484
|
+
@response_time_ms = data["response_time_ms"] || data["responseTimeMs"] || delivery["response_time"]
|
|
485
|
+
@error = data["error"] || delivery["error"]
|
|
486
|
+
@message = data["message"]
|
|
487
|
+
@delivery_id = delivery["id"] || delivery["delivery_id"]
|
|
444
488
|
end
|
|
445
489
|
|
|
446
490
|
def success?
|
|
@@ -449,14 +493,49 @@ module Sendly
|
|
|
449
493
|
end
|
|
450
494
|
|
|
451
495
|
# Result of rotating webhook secret
|
|
496
|
+
#
|
|
497
|
+
# The API signs deliveries with the new secret from the moment the rotation
|
|
498
|
+
# returns and does not keep the old one, so {#new_secret} is the only secret
|
|
499
|
+
# that verifies deliveries from then on.
|
|
452
500
|
class WebhookSecretRotation
|
|
453
|
-
|
|
501
|
+
# @return [Sendly::Webhook, nil] Always +nil+: the rotation response does not include the webhook
|
|
502
|
+
attr_reader :webhook
|
|
503
|
+
|
|
504
|
+
# @return [String] The new signing secret. It is shown only once.
|
|
505
|
+
attr_reader :new_secret
|
|
506
|
+
|
|
507
|
+
# @return [Time, nil] Always +nil+: the API does not keep the old secret, so it has no expiry
|
|
508
|
+
attr_reader :old_secret_expires_at
|
|
509
|
+
|
|
510
|
+
# @return [String, nil] Confirmation message
|
|
511
|
+
attr_reader :message
|
|
512
|
+
|
|
513
|
+
# @return [String, nil] The webhook's ID
|
|
514
|
+
attr_reader :id
|
|
515
|
+
|
|
516
|
+
# @return [Integer, nil] The webhook's secret version as the API reports it
|
|
517
|
+
attr_reader :new_secret_version
|
|
518
|
+
|
|
519
|
+
# @return [Integer, nil] The grace period the API reports (24). The old
|
|
520
|
+
# secret is not kept, so it does not verify deliveries during it.
|
|
521
|
+
attr_reader :grace_period_hours
|
|
522
|
+
|
|
523
|
+
# @return [Time, nil] When the secret was rotated
|
|
524
|
+
attr_reader :rotated_at
|
|
525
|
+
|
|
526
|
+
# @return [Hash] The raw parsed response
|
|
527
|
+
attr_reader :raw
|
|
454
528
|
|
|
455
529
|
def initialize(data)
|
|
456
|
-
@
|
|
457
|
-
@
|
|
530
|
+
@raw = data
|
|
531
|
+
@webhook = data["webhook"] ? Webhook.new(data["webhook"]) : nil
|
|
532
|
+
@new_secret = data["new_secret"] || data["newSecret"] || data["secret"]
|
|
458
533
|
@old_secret_expires_at = parse_time(data["old_secret_expires_at"] || data["oldSecretExpiresAt"])
|
|
459
534
|
@message = data["message"]
|
|
535
|
+
@id = data["id"]
|
|
536
|
+
@new_secret_version = data["new_secret_version"] || data["newSecretVersion"]
|
|
537
|
+
@grace_period_hours = data["grace_period_hours"] || data["gracePeriodHours"]
|
|
538
|
+
@rotated_at = parse_time(data["rotated_at"] || data["rotatedAt"])
|
|
460
539
|
end
|
|
461
540
|
|
|
462
541
|
private
|
|
@@ -475,13 +554,56 @@ module Sendly
|
|
|
475
554
|
|
|
476
555
|
# Represents account information
|
|
477
556
|
class Account
|
|
478
|
-
|
|
557
|
+
# @return [String, nil] The user's ID
|
|
558
|
+
attr_reader :id
|
|
559
|
+
|
|
560
|
+
# @return [String, nil] The user's email address
|
|
561
|
+
attr_reader :email
|
|
562
|
+
|
|
563
|
+
# @return [String, nil] The name of the workspace the API key belongs to,
|
|
564
|
+
# or +nil+ when the key is not bound to a workspace
|
|
565
|
+
attr_reader :name
|
|
566
|
+
|
|
567
|
+
# @return [Time, nil] When the user signed up
|
|
568
|
+
attr_reader :created_at
|
|
569
|
+
|
|
570
|
+
# @return [Hash, nil] The workspace the API key belongs to
|
|
571
|
+
# (+"id"+, +"name"+, +"isPersonal"+), or +nil+
|
|
572
|
+
attr_reader :organization
|
|
573
|
+
|
|
574
|
+
# @return [String, nil] The workspace's ID, the +organization_id+ in webhook payloads
|
|
575
|
+
attr_reader :organization_id
|
|
576
|
+
|
|
577
|
+
# @return [Hash, nil] Credit balances (+"balance"+, +"reservedBalance"+)
|
|
578
|
+
attr_reader :credits
|
|
579
|
+
|
|
580
|
+
# @return [Hash, nil] Business verification (+"status"+, +"type"+, +"region"+,
|
|
581
|
+
# +"submittedAt"+, +"updatedAt"+), or +nil+ when there is none
|
|
582
|
+
attr_reader :verification
|
|
583
|
+
|
|
584
|
+
# @return [Hash, nil] The API key making the call (+"id"+, +"name"+, +"type"+,
|
|
585
|
+
# +"scopes"+, +"createdAt"+, +"lastUsedAt"+)
|
|
586
|
+
attr_reader :api_key
|
|
587
|
+
|
|
588
|
+
# @return [Hash, nil] Sending limits (+"messagesPerMinute"+, +"messagesPerDay"+)
|
|
589
|
+
attr_reader :limits
|
|
590
|
+
|
|
591
|
+
# @return [Hash] The raw parsed response
|
|
592
|
+
attr_reader :raw
|
|
479
593
|
|
|
480
594
|
def initialize(data)
|
|
481
|
-
|
|
482
|
-
@
|
|
483
|
-
@
|
|
484
|
-
@
|
|
595
|
+
user = data["user"] || {}
|
|
596
|
+
@raw = data
|
|
597
|
+
@organization = data["organization"]
|
|
598
|
+
@organization_id = @organization && @organization["id"]
|
|
599
|
+
@id = user["id"] || data["id"]
|
|
600
|
+
@email = user["email"] || data["email"]
|
|
601
|
+
@name = (@organization && @organization["name"]) || data["name"]
|
|
602
|
+
@created_at = parse_time(user["createdAt"] || user["created_at"] || data["created_at"] || data["createdAt"])
|
|
603
|
+
@credits = data["credits"]
|
|
604
|
+
@verification = data["verification"]
|
|
605
|
+
@api_key = data["apiKey"] || data["api_key"]
|
|
606
|
+
@limits = data["limits"]
|
|
485
607
|
end
|
|
486
608
|
|
|
487
609
|
private
|
|
@@ -509,8 +631,9 @@ module Sendly
|
|
|
509
631
|
class CreditTransaction
|
|
510
632
|
attr_reader :id, :type, :amount, :balance_after, :description, :message_id, :created_at
|
|
511
633
|
|
|
512
|
-
# Transaction type constants
|
|
513
|
-
|
|
634
|
+
# Transaction type constants. Auto-recharges are recorded as +purchase+;
|
|
635
|
+
# +adjustment+ is never recorded.
|
|
636
|
+
TYPES = %w[purchase usage refund adjustment bonus transfer admin_grant admin_seed].freeze
|
|
514
637
|
|
|
515
638
|
def initialize(data)
|
|
516
639
|
@id = data["id"]
|
|
@@ -542,20 +665,41 @@ module Sendly
|
|
|
542
665
|
|
|
543
666
|
# Represents an API key
|
|
544
667
|
class ApiKey
|
|
545
|
-
attr_reader :id, :name, :type, :prefix, :
|
|
668
|
+
attr_reader :id, :name, :type, :prefix, :permissions,
|
|
546
669
|
:created_at, :last_used_at, :expires_at, :is_revoked
|
|
547
670
|
|
|
671
|
+
# @return [nil] Always +nil+: no API response carries the last four characters
|
|
672
|
+
attr_reader :last_four
|
|
673
|
+
|
|
674
|
+
# @return [Array<String>] The key's scopes (the same list as {#permissions})
|
|
675
|
+
attr_reader :scopes
|
|
676
|
+
|
|
677
|
+
# @return [Boolean, nil] Whether the key is active, or +nil+ when the response does not say
|
|
678
|
+
attr_reader :is_active
|
|
679
|
+
|
|
680
|
+
# @return [Time, nil] When the key was revoked
|
|
681
|
+
attr_reader :revoked_at
|
|
682
|
+
|
|
548
683
|
def initialize(data)
|
|
549
684
|
@id = data["id"]
|
|
550
685
|
@name = data["name"]
|
|
551
686
|
@type = data["type"]
|
|
552
687
|
@prefix = data["prefix"]
|
|
553
688
|
@last_four = data["last_four"] || data["lastFour"]
|
|
554
|
-
@permissions = data["permissions"] || []
|
|
689
|
+
@permissions = data["permissions"] || data["scopes"] || []
|
|
690
|
+
@scopes = data["scopes"] || data["permissions"] || []
|
|
555
691
|
@created_at = parse_time(data["created_at"] || data["createdAt"])
|
|
556
692
|
@last_used_at = parse_time(data["last_used_at"] || data["lastUsedAt"])
|
|
557
693
|
@expires_at = parse_time(data["expires_at"] || data["expiresAt"])
|
|
558
|
-
@
|
|
694
|
+
@revoked_at = parse_time(data["revoked_at"] || data["revokedAt"])
|
|
695
|
+
@is_active = data.key?("isActive") ? data["isActive"] : data["is_active"]
|
|
696
|
+
@is_revoked = if data.key?("isRevoked") || data.key?("is_revoked")
|
|
697
|
+
data["isRevoked"] || data["is_revoked"] || false
|
|
698
|
+
elsif !@is_active.nil?
|
|
699
|
+
!@is_active
|
|
700
|
+
else
|
|
701
|
+
!@revoked_at.nil?
|
|
702
|
+
end
|
|
559
703
|
end
|
|
560
704
|
|
|
561
705
|
def test?
|
|
@@ -842,13 +986,20 @@ module Sendly
|
|
|
842
986
|
|
|
843
987
|
attr_reader :data, :total, :limit, :offset, :has_more
|
|
844
988
|
|
|
845
|
-
|
|
989
|
+
# @param response [Hash] The parsed list response
|
|
990
|
+
# @param limit [Integer, nil] The page size the API used, when the response does not say
|
|
991
|
+
# @param offset [Integer, nil] The offset the API used, when the response does not say
|
|
992
|
+
def initialize(response, limit = nil, offset = nil)
|
|
846
993
|
@data = (response["data"] || []).map { |d| Draft.new(d) }
|
|
847
994
|
pagination = response["pagination"] || {}
|
|
848
995
|
@total = pagination["total"] || @data.length
|
|
849
|
-
@limit = pagination["limit"] || 20
|
|
850
|
-
@offset = pagination["offset"] || 0
|
|
851
|
-
@has_more = pagination
|
|
996
|
+
@limit = pagination["limit"] || limit || 20
|
|
997
|
+
@offset = pagination["offset"] || offset || 0
|
|
998
|
+
@has_more = if pagination.key?("hasMore") || pagination.key?("has_more")
|
|
999
|
+
pagination["hasMore"] || pagination["has_more"] || false
|
|
1000
|
+
else
|
|
1001
|
+
(@offset + @data.length) < @total
|
|
1002
|
+
end
|
|
852
1003
|
end
|
|
853
1004
|
|
|
854
1005
|
def each(&block)
|
data/lib/sendly/version.rb
CHANGED
data/lib/sendly/webhooks.rb
CHANGED
|
@@ -103,6 +103,11 @@ module Sendly
|
|
|
103
103
|
EVENT_SHORT_CODE_REJECTED = "short_code.rejected"
|
|
104
104
|
EVENT_SHORT_CODE_FILED = "short_code.filed"
|
|
105
105
|
EVENT_SHORT_CODE_LIVE = "short_code.live"
|
|
106
|
+
EVENT_SHORT_CODE_SUSPENDED = "short_code.suspended"
|
|
107
|
+
EVENT_SHORT_CODE_REACTIVATED = "short_code.reactivated"
|
|
108
|
+
EVENT_SHORT_CODE_CANCELLED = "short_code.cancelled"
|
|
109
|
+
EVENT_SHORT_CODE_PAYMENT_SUCCEEDED = "short_code.payment_succeeded"
|
|
110
|
+
EVENT_SHORT_CODE_PAYMENT_FAILED = "short_code.payment_failed"
|
|
106
111
|
|
|
107
112
|
# Source of a list-health event. Frozen enum — new values will be
|
|
108
113
|
# added in minor SDK versions, never removed.
|