sendly 4.1.0 → 4.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/CHANGELOG.md +98 -0
- data/Gemfile.lock +3 -3
- data/README.md +829 -150
- 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 +29 -14
- data/lib/sendly/campaigns_resource.rb +59 -8
- data/lib/sendly/client.rb +55 -21
- 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 -18
- data/lib/sendly/messages.rb +31 -11
- data/lib/sendly/numbers_resource.rb +2 -1
- data/lib/sendly/templates_resource.rb +0 -4
- data/lib/sendly/types.rb +165 -26
- data/lib/sendly/version.rb +1 -1
- data/lib/sendly/voice_resource.rb +721 -0
- data/lib/sendly/webhooks_resource.rb +30 -7
- data/lib/sendly/whatsapp_resource.rb +595 -44
- data/lib/sendly.rb +1 -0
- metadata +3 -2
data/lib/sendly/errors.rb
CHANGED
|
@@ -12,12 +12,30 @@ module Sendly
|
|
|
12
12
|
# @return [Integer, nil] HTTP status code
|
|
13
13
|
attr_reader :status_code
|
|
14
14
|
|
|
15
|
+
# @return [Hash, nil] The parsed JSON body of the API's error response,
|
|
16
|
+
# or +nil+ for errors raised before a request was sent. The API's own
|
|
17
|
+
# error code is under +"error"+, and some refusals carry more than a
|
|
18
|
+
# message: a 409 +agent_in_use+ lists the numbers the agent still
|
|
19
|
+
# answers under +"numbers"+, and a 422 +invalid_address+ carries a
|
|
20
|
+
# corrected address (or +nil+) under +"suggested"+.
|
|
21
|
+
attr_reader :response_body
|
|
22
|
+
|
|
15
23
|
def initialize(message = nil, code: nil, details: nil, status_code: nil)
|
|
16
24
|
@code = code
|
|
17
25
|
@details = details
|
|
18
26
|
@status_code = status_code
|
|
19
27
|
super(message)
|
|
20
28
|
end
|
|
29
|
+
|
|
30
|
+
# Attach the parsed body of the API response this error came from.
|
|
31
|
+
#
|
|
32
|
+
# @api private
|
|
33
|
+
# @param body [Hash, nil]
|
|
34
|
+
# @return [self]
|
|
35
|
+
def with_response_body(body)
|
|
36
|
+
@response_body = body
|
|
37
|
+
self
|
|
38
|
+
end
|
|
21
39
|
end
|
|
22
40
|
|
|
23
41
|
# Raised when the API key is invalid or missing
|
|
@@ -96,28 +114,38 @@ module Sendly
|
|
|
96
114
|
|
|
97
115
|
# Convert API response to appropriate error
|
|
98
116
|
class ErrorFactory
|
|
99
|
-
def self.from_response(status, body)
|
|
117
|
+
def self.from_response(status, body, retry_after_header: nil)
|
|
100
118
|
message = body["message"] || body["error"] || "Unknown error"
|
|
101
119
|
code = body["code"]
|
|
102
120
|
details = body["details"]
|
|
103
121
|
|
|
104
|
-
case status
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
122
|
+
error = case status
|
|
123
|
+
when 400, 422
|
|
124
|
+
ValidationError.new(message, details: details, field_errors: body["errors"])
|
|
125
|
+
when 401
|
|
126
|
+
AuthenticationError.new(message)
|
|
127
|
+
when 402
|
|
128
|
+
InsufficientCreditsError.new(message)
|
|
129
|
+
when 404
|
|
130
|
+
NotFoundError.new(message)
|
|
131
|
+
when 429
|
|
132
|
+
retry_after = parse_retry_after(retry_after_header) || body["retry_after"] || body["retryAfter"]
|
|
133
|
+
RateLimitError.new(message, retry_after: retry_after)
|
|
134
|
+
when 500..599
|
|
135
|
+
ServerError.new(message, status_code: status)
|
|
136
|
+
else
|
|
137
|
+
APIError.new(message, status_code: status, code: code, details: details)
|
|
138
|
+
end
|
|
139
|
+
error.with_response_body(body)
|
|
140
|
+
end
|
|
141
|
+
|
|
142
|
+
def self.parse_retry_after(value)
|
|
143
|
+
text = value.to_s.strip
|
|
144
|
+
return text.to_i if text.match?(/\A\d+\z/)
|
|
145
|
+
return text.to_f if text.match?(/\A\d+\.\d+\z/)
|
|
146
|
+
|
|
147
|
+
nil
|
|
121
148
|
end
|
|
149
|
+
private_class_method :parse_retry_after
|
|
122
150
|
end
|
|
123
151
|
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
|
|
@@ -71,7 +74,22 @@ module Sendly
|
|
|
71
74
|
# response instead of executing again.
|
|
72
75
|
# @return [Sendly::Message, Sendly::WhatsAppMessage, Sendly::RcsMessage] The sent message
|
|
73
76
|
#
|
|
74
|
-
# @raise [Sendly::ValidationError] If parameters are invalid
|
|
77
|
+
# @raise [Sendly::ValidationError] If parameters are invalid, or on
|
|
78
|
+
# WhatsApp the 422 +whatsapp_send_failed+ when WhatsApp refused the
|
|
79
|
+
# message (final, not retried, not charged; cached under the
|
|
80
|
+
# idempotency key and replayed for 24 hours)
|
|
81
|
+
# @raise [Sendly::ServerError] On WhatsApp, the 502 +whatsapp_send_failed+:
|
|
82
|
+
# the message provably never reached the carrier, so it was not sent
|
|
83
|
+
# and is safe to send again. It isn't charged and is never cached, so
|
|
84
|
+
# the client retries it like any 5xx first under the same idempotency
|
|
85
|
+
# key. No send returns 503 +whatsapp_unavailable+.
|
|
86
|
+
# @raise [Sendly::APIError] On WhatsApp, the 409
|
|
87
|
+
# +whatsapp_send_unconfirmed+ (+status_code+ 409): the outcome is
|
|
88
|
+
# unknown. The message was marked failed and refunded but may still be
|
|
89
|
+
# delivered, so check before sending it again (it could arrive twice).
|
|
90
|
+
# It is not retried automatically, and it is cached under the
|
|
91
|
+
# idempotency key, so repeating the request with the same key returns
|
|
92
|
+
# this answer again.
|
|
75
93
|
# @raise [Sendly::InsufficientCreditsError] If account has no credits
|
|
76
94
|
# @raise [Sendly::RateLimitError] If rate limit is exceeded
|
|
77
95
|
#
|
|
@@ -327,7 +345,7 @@ module Sendly
|
|
|
327
345
|
#
|
|
328
346
|
# @param status [String] Filter by status
|
|
329
347
|
# @param to [String] Filter by recipient
|
|
330
|
-
# @param batch_size [Integer] Number of messages per request
|
|
348
|
+
# @param batch_size [Integer] Number of messages per request (the API returns at most 100)
|
|
331
349
|
# @yield [Message] Each message
|
|
332
350
|
# @return [Enumerator] If no block given
|
|
333
351
|
#
|
|
@@ -343,9 +361,9 @@ module Sendly
|
|
|
343
361
|
page = list(limit: batch_size, offset: offset, status: status, to: to)
|
|
344
362
|
page.each(&block)
|
|
345
363
|
|
|
346
|
-
break
|
|
364
|
+
break if !page.has_more || page.count.zero?
|
|
347
365
|
|
|
348
|
-
offset +=
|
|
366
|
+
offset += page.count
|
|
349
367
|
end
|
|
350
368
|
end
|
|
351
369
|
|
|
@@ -446,7 +464,9 @@ module Sendly
|
|
|
446
464
|
# @param metadata [Hash] Shared metadata for all messages (max 4KB). Each message can also have its own metadata hash which takes priority.
|
|
447
465
|
# @param idempotency_key [String] Idempotency key for this operation
|
|
448
466
|
# (1-255 printable ASCII characters, optional)
|
|
449
|
-
# @return [Hash] Batch
|
|
467
|
+
# @return [Hash] Batch result: +batchId+, +status+ (processing, completed,
|
|
468
|
+
# partial_failure or failed), +total+, +sent+, +failed+, +creditsUsed+,
|
|
469
|
+
# +creditsRefunded+ and +messages+
|
|
450
470
|
#
|
|
451
471
|
# @raise [Sendly::ValidationError] If parameters are invalid
|
|
452
472
|
# @raise [Sendly::InsufficientCreditsError] If account has insufficient credits
|
|
@@ -458,7 +478,7 @@ module Sendly
|
|
|
458
478
|
# { to: "+15559876543", text: "Hello Bob!" }
|
|
459
479
|
# ]
|
|
460
480
|
# )
|
|
461
|
-
# puts "Batch #{result['batchId']}: #{result['
|
|
481
|
+
# puts "Batch #{result['batchId']}: #{result['total']} messages, #{result['status']}"
|
|
462
482
|
def send_batch(messages:, from: nil, message_type: nil, metadata: nil, idempotency_key: nil)
|
|
463
483
|
raise ValidationError, "Messages array is required" if messages.nil? || messages.empty?
|
|
464
484
|
|
|
@@ -505,12 +525,12 @@ module Sendly
|
|
|
505
525
|
#
|
|
506
526
|
# @param limit [Integer] Maximum batches to return (default: 20, max: 100)
|
|
507
527
|
# @param offset [Integer] Number of batches to skip
|
|
508
|
-
# @param status [String] Filter by status (processing, completed, failed)
|
|
528
|
+
# @param status [String] Filter by status (processing, completed, partial_failure, failed)
|
|
509
529
|
# @return [Hash] Paginated list of batches
|
|
510
530
|
#
|
|
511
531
|
# @example
|
|
512
532
|
# batches = client.messages.list_batches(limit: 10)
|
|
513
|
-
# batches["data"].each { |b| puts "#{b['
|
|
533
|
+
# batches["data"].each { |b| puts "#{b['id']}: #{b['status']}" }
|
|
514
534
|
def list_batches(limit: 20, offset: 0, status: nil)
|
|
515
535
|
params = {
|
|
516
536
|
limit: [limit, 100].min,
|
|
@@ -537,7 +557,7 @@ module Sendly
|
|
|
537
557
|
# { to: "+15559876543", text: "Hello Bob!" }
|
|
538
558
|
# ]
|
|
539
559
|
# )
|
|
540
|
-
# puts "
|
|
560
|
+
# puts "Sufficient credits: #{preview['hasSufficientCredits']}"
|
|
541
561
|
# puts "Credits needed: #{preview['creditsNeeded']}"
|
|
542
562
|
def preview_batch(messages:, from: nil, message_type: nil)
|
|
543
563
|
raise ValidationError, "Messages array is required" if messages.nil? || messages.empty?
|
|
@@ -55,7 +55,8 @@ module Sendly
|
|
|
55
55
|
# ISO-8601 timestamp string, or nil when no release is scheduled.
|
|
56
56
|
:scheduled_release_at,
|
|
57
57
|
# true when the number can place and receive phone calls
|
|
58
|
-
# (switched on
|
|
58
|
+
# (switched on with {VoiceNumbersResource#update} or in the
|
|
59
|
+
# dashboard under Calls, then Settings).
|
|
59
60
|
:voice_enabled,
|
|
60
61
|
# One of {VOICE_MODES}: "none" when voice is off, "ring_dashboard"
|
|
61
62
|
# when inbound calls ring the team, "agent" when an AI agent answers.
|
|
@@ -222,10 +222,6 @@ module Sendly
|
|
|
222
222
|
end
|
|
223
223
|
|
|
224
224
|
# Copy an existing template into a new draft.
|
|
225
|
-
#
|
|
226
|
-
# @note Not available yet: the versioned API serves no clone route, so
|
|
227
|
-
# this call fails with a 404. To copy a template today, read it with
|
|
228
|
-
# {#get} and pass its {Template#text} to {#create}.
|
|
229
225
|
def clone(id, name: nil)
|
|
230
226
|
body = {}
|
|
231
227
|
body[:name] = name if name
|
data/lib/sendly/types.rb
CHANGED
|
@@ -63,6 +63,12 @@ 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
|
+
|
|
66
72
|
# Message status constants (sending removed - doesn't exist in database)
|
|
67
73
|
STATUSES = %w[queued sent delivered failed bounced retrying].freeze
|
|
68
74
|
|
|
@@ -90,6 +96,8 @@ module Sendly
|
|
|
90
96
|
@retry_count = data["retryCount"] || 0
|
|
91
97
|
@metadata = data["metadata"]
|
|
92
98
|
@ai_metadata = data["aiMetadata"]
|
|
99
|
+
@media_urls = data["mediaUrls"] || data["media_urls"] || []
|
|
100
|
+
@message_format = data["messageFormat"] || data["message_format"] || "sms"
|
|
93
101
|
end
|
|
94
102
|
|
|
95
103
|
# Check if message was delivered
|
|
@@ -133,7 +141,9 @@ module Sendly
|
|
|
133
141
|
error_code: error_code,
|
|
134
142
|
retry_count: retry_count,
|
|
135
143
|
metadata: metadata,
|
|
136
|
-
ai_metadata: ai_metadata
|
|
144
|
+
ai_metadata: ai_metadata,
|
|
145
|
+
media_urls: media_urls,
|
|
146
|
+
message_format: message_format
|
|
137
147
|
}.compact
|
|
138
148
|
end
|
|
139
149
|
|
|
@@ -155,7 +165,7 @@ module Sendly
|
|
|
155
165
|
# @return [Array<Message>] Messages in this page
|
|
156
166
|
attr_reader :data
|
|
157
167
|
|
|
158
|
-
# @return [Integer] Total number of messages
|
|
168
|
+
# @return [Integer] Total number of messages that match the query, across all pages
|
|
159
169
|
attr_reader :total
|
|
160
170
|
|
|
161
171
|
# @return [Integer] Current limit
|
|
@@ -168,11 +178,16 @@ module Sendly
|
|
|
168
178
|
attr_reader :has_more
|
|
169
179
|
|
|
170
180
|
def initialize(response)
|
|
181
|
+
pagination = response["pagination"] || {}
|
|
171
182
|
@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 =
|
|
183
|
+
@total = pagination["total"] || response["total"] || response["count"] || @data.length
|
|
184
|
+
@limit = pagination["limit"] || response["limit"] || 20
|
|
185
|
+
@offset = pagination["offset"] || response["offset"] || 0
|
|
186
|
+
@has_more = if pagination.key?("hasMore")
|
|
187
|
+
pagination["hasMore"]
|
|
188
|
+
else
|
|
189
|
+
(@offset + @data.length) < @total
|
|
190
|
+
end
|
|
176
191
|
end
|
|
177
192
|
|
|
178
193
|
# Iterate over messages
|
|
@@ -433,14 +448,31 @@ module Sendly
|
|
|
433
448
|
end
|
|
434
449
|
|
|
435
450
|
# Result of testing a webhook
|
|
451
|
+
#
|
|
452
|
+
# Only a delivered test comes back as a result: when the test delivery
|
|
453
|
+
# fails, {Sendly::WebhooksResource#test} raises {Sendly::ValidationError}
|
|
454
|
+
# with the API's message.
|
|
436
455
|
class WebhookTestResult
|
|
437
456
|
attr_reader :success, :status_code, :response_time_ms, :error
|
|
438
457
|
|
|
458
|
+
# @return [String, nil] The API's summary of the test delivery
|
|
459
|
+
attr_reader :message
|
|
460
|
+
|
|
461
|
+
# @return [String, nil] ID of the test delivery
|
|
462
|
+
attr_reader :delivery_id
|
|
463
|
+
|
|
464
|
+
# @return [Hash] The raw parsed response
|
|
465
|
+
attr_reader :raw
|
|
466
|
+
|
|
439
467
|
def initialize(data)
|
|
468
|
+
delivery = data["delivery"] || {}
|
|
469
|
+
@raw = data
|
|
440
470
|
@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"]
|
|
471
|
+
@status_code = data["status_code"] || data["statusCode"] || delivery["status_code"]
|
|
472
|
+
@response_time_ms = data["response_time_ms"] || data["responseTimeMs"] || delivery["response_time"]
|
|
473
|
+
@error = data["error"] || delivery["error"]
|
|
474
|
+
@message = data["message"]
|
|
475
|
+
@delivery_id = delivery["id"] || delivery["delivery_id"]
|
|
444
476
|
end
|
|
445
477
|
|
|
446
478
|
def success?
|
|
@@ -449,14 +481,49 @@ module Sendly
|
|
|
449
481
|
end
|
|
450
482
|
|
|
451
483
|
# Result of rotating webhook secret
|
|
484
|
+
#
|
|
485
|
+
# The API signs deliveries with the new secret from the moment the rotation
|
|
486
|
+
# returns and does not keep the old one, so {#new_secret} is the only secret
|
|
487
|
+
# that verifies deliveries from then on.
|
|
452
488
|
class WebhookSecretRotation
|
|
453
|
-
|
|
489
|
+
# @return [Sendly::Webhook, nil] Always +nil+: the rotation response does not include the webhook
|
|
490
|
+
attr_reader :webhook
|
|
491
|
+
|
|
492
|
+
# @return [String] The new signing secret. It is shown only once.
|
|
493
|
+
attr_reader :new_secret
|
|
494
|
+
|
|
495
|
+
# @return [Time, nil] Always +nil+: the API does not keep the old secret, so it has no expiry
|
|
496
|
+
attr_reader :old_secret_expires_at
|
|
497
|
+
|
|
498
|
+
# @return [String, nil] Confirmation message
|
|
499
|
+
attr_reader :message
|
|
500
|
+
|
|
501
|
+
# @return [String, nil] The webhook's ID
|
|
502
|
+
attr_reader :id
|
|
503
|
+
|
|
504
|
+
# @return [Integer, nil] The webhook's secret version as the API reports it
|
|
505
|
+
attr_reader :new_secret_version
|
|
506
|
+
|
|
507
|
+
# @return [Integer, nil] The grace period the API reports (24). The old
|
|
508
|
+
# secret is not kept, so it does not verify deliveries during it.
|
|
509
|
+
attr_reader :grace_period_hours
|
|
510
|
+
|
|
511
|
+
# @return [Time, nil] When the secret was rotated
|
|
512
|
+
attr_reader :rotated_at
|
|
513
|
+
|
|
514
|
+
# @return [Hash] The raw parsed response
|
|
515
|
+
attr_reader :raw
|
|
454
516
|
|
|
455
517
|
def initialize(data)
|
|
456
|
-
@
|
|
457
|
-
@
|
|
518
|
+
@raw = data
|
|
519
|
+
@webhook = data["webhook"] ? Webhook.new(data["webhook"]) : nil
|
|
520
|
+
@new_secret = data["new_secret"] || data["newSecret"] || data["secret"]
|
|
458
521
|
@old_secret_expires_at = parse_time(data["old_secret_expires_at"] || data["oldSecretExpiresAt"])
|
|
459
522
|
@message = data["message"]
|
|
523
|
+
@id = data["id"]
|
|
524
|
+
@new_secret_version = data["new_secret_version"] || data["newSecretVersion"]
|
|
525
|
+
@grace_period_hours = data["grace_period_hours"] || data["gracePeriodHours"]
|
|
526
|
+
@rotated_at = parse_time(data["rotated_at"] || data["rotatedAt"])
|
|
460
527
|
end
|
|
461
528
|
|
|
462
529
|
private
|
|
@@ -475,13 +542,56 @@ module Sendly
|
|
|
475
542
|
|
|
476
543
|
# Represents account information
|
|
477
544
|
class Account
|
|
478
|
-
|
|
545
|
+
# @return [String, nil] The user's ID
|
|
546
|
+
attr_reader :id
|
|
547
|
+
|
|
548
|
+
# @return [String, nil] The user's email address
|
|
549
|
+
attr_reader :email
|
|
550
|
+
|
|
551
|
+
# @return [String, nil] The name of the workspace the API key belongs to,
|
|
552
|
+
# or +nil+ when the key is not bound to a workspace
|
|
553
|
+
attr_reader :name
|
|
554
|
+
|
|
555
|
+
# @return [Time, nil] When the user signed up
|
|
556
|
+
attr_reader :created_at
|
|
557
|
+
|
|
558
|
+
# @return [Hash, nil] The workspace the API key belongs to
|
|
559
|
+
# (+"id"+, +"name"+, +"isPersonal"+), or +nil+
|
|
560
|
+
attr_reader :organization
|
|
561
|
+
|
|
562
|
+
# @return [String, nil] The workspace's ID, the +organization_id+ in webhook payloads
|
|
563
|
+
attr_reader :organization_id
|
|
564
|
+
|
|
565
|
+
# @return [Hash, nil] Credit balances (+"balance"+, +"reservedBalance"+)
|
|
566
|
+
attr_reader :credits
|
|
567
|
+
|
|
568
|
+
# @return [Hash, nil] Business verification (+"status"+, +"type"+, +"region"+,
|
|
569
|
+
# +"submittedAt"+, +"updatedAt"+), or +nil+ when there is none
|
|
570
|
+
attr_reader :verification
|
|
571
|
+
|
|
572
|
+
# @return [Hash, nil] The API key making the call (+"id"+, +"name"+, +"type"+,
|
|
573
|
+
# +"scopes"+, +"createdAt"+, +"lastUsedAt"+)
|
|
574
|
+
attr_reader :api_key
|
|
575
|
+
|
|
576
|
+
# @return [Hash, nil] Sending limits (+"messagesPerMinute"+, +"messagesPerDay"+)
|
|
577
|
+
attr_reader :limits
|
|
578
|
+
|
|
579
|
+
# @return [Hash] The raw parsed response
|
|
580
|
+
attr_reader :raw
|
|
479
581
|
|
|
480
582
|
def initialize(data)
|
|
481
|
-
|
|
482
|
-
@
|
|
483
|
-
@
|
|
484
|
-
@
|
|
583
|
+
user = data["user"] || {}
|
|
584
|
+
@raw = data
|
|
585
|
+
@organization = data["organization"]
|
|
586
|
+
@organization_id = @organization && @organization["id"]
|
|
587
|
+
@id = user["id"] || data["id"]
|
|
588
|
+
@email = user["email"] || data["email"]
|
|
589
|
+
@name = (@organization && @organization["name"]) || data["name"]
|
|
590
|
+
@created_at = parse_time(user["createdAt"] || user["created_at"] || data["created_at"] || data["createdAt"])
|
|
591
|
+
@credits = data["credits"]
|
|
592
|
+
@verification = data["verification"]
|
|
593
|
+
@api_key = data["apiKey"] || data["api_key"]
|
|
594
|
+
@limits = data["limits"]
|
|
485
595
|
end
|
|
486
596
|
|
|
487
597
|
private
|
|
@@ -509,8 +619,9 @@ module Sendly
|
|
|
509
619
|
class CreditTransaction
|
|
510
620
|
attr_reader :id, :type, :amount, :balance_after, :description, :message_id, :created_at
|
|
511
621
|
|
|
512
|
-
# Transaction type constants
|
|
513
|
-
|
|
622
|
+
# Transaction type constants. Auto-recharges are recorded as +purchase+;
|
|
623
|
+
# +adjustment+ is never recorded.
|
|
624
|
+
TYPES = %w[purchase usage refund adjustment bonus transfer admin_grant admin_seed].freeze
|
|
514
625
|
|
|
515
626
|
def initialize(data)
|
|
516
627
|
@id = data["id"]
|
|
@@ -542,20 +653,41 @@ module Sendly
|
|
|
542
653
|
|
|
543
654
|
# Represents an API key
|
|
544
655
|
class ApiKey
|
|
545
|
-
attr_reader :id, :name, :type, :prefix, :
|
|
656
|
+
attr_reader :id, :name, :type, :prefix, :permissions,
|
|
546
657
|
:created_at, :last_used_at, :expires_at, :is_revoked
|
|
547
658
|
|
|
659
|
+
# @return [nil] Always +nil+: no API response carries the last four characters
|
|
660
|
+
attr_reader :last_four
|
|
661
|
+
|
|
662
|
+
# @return [Array<String>] The key's scopes (the same list as {#permissions})
|
|
663
|
+
attr_reader :scopes
|
|
664
|
+
|
|
665
|
+
# @return [Boolean, nil] Whether the key is active, or +nil+ when the response does not say
|
|
666
|
+
attr_reader :is_active
|
|
667
|
+
|
|
668
|
+
# @return [Time, nil] When the key was revoked
|
|
669
|
+
attr_reader :revoked_at
|
|
670
|
+
|
|
548
671
|
def initialize(data)
|
|
549
672
|
@id = data["id"]
|
|
550
673
|
@name = data["name"]
|
|
551
674
|
@type = data["type"]
|
|
552
675
|
@prefix = data["prefix"]
|
|
553
676
|
@last_four = data["last_four"] || data["lastFour"]
|
|
554
|
-
@permissions = data["permissions"] || []
|
|
677
|
+
@permissions = data["permissions"] || data["scopes"] || []
|
|
678
|
+
@scopes = data["scopes"] || data["permissions"] || []
|
|
555
679
|
@created_at = parse_time(data["created_at"] || data["createdAt"])
|
|
556
680
|
@last_used_at = parse_time(data["last_used_at"] || data["lastUsedAt"])
|
|
557
681
|
@expires_at = parse_time(data["expires_at"] || data["expiresAt"])
|
|
558
|
-
@
|
|
682
|
+
@revoked_at = parse_time(data["revoked_at"] || data["revokedAt"])
|
|
683
|
+
@is_active = data.key?("isActive") ? data["isActive"] : data["is_active"]
|
|
684
|
+
@is_revoked = if data.key?("isRevoked") || data.key?("is_revoked")
|
|
685
|
+
data["isRevoked"] || data["is_revoked"] || false
|
|
686
|
+
elsif !@is_active.nil?
|
|
687
|
+
!@is_active
|
|
688
|
+
else
|
|
689
|
+
!@revoked_at.nil?
|
|
690
|
+
end
|
|
559
691
|
end
|
|
560
692
|
|
|
561
693
|
def test?
|
|
@@ -842,13 +974,20 @@ module Sendly
|
|
|
842
974
|
|
|
843
975
|
attr_reader :data, :total, :limit, :offset, :has_more
|
|
844
976
|
|
|
845
|
-
|
|
977
|
+
# @param response [Hash] The parsed list response
|
|
978
|
+
# @param limit [Integer, nil] The page size the API used, when the response does not say
|
|
979
|
+
# @param offset [Integer, nil] The offset the API used, when the response does not say
|
|
980
|
+
def initialize(response, limit = nil, offset = nil)
|
|
846
981
|
@data = (response["data"] || []).map { |d| Draft.new(d) }
|
|
847
982
|
pagination = response["pagination"] || {}
|
|
848
983
|
@total = pagination["total"] || @data.length
|
|
849
|
-
@limit = pagination["limit"] || 20
|
|
850
|
-
@offset = pagination["offset"] || 0
|
|
851
|
-
@has_more = pagination
|
|
984
|
+
@limit = pagination["limit"] || limit || 20
|
|
985
|
+
@offset = pagination["offset"] || offset || 0
|
|
986
|
+
@has_more = if pagination.key?("hasMore") || pagination.key?("has_more")
|
|
987
|
+
pagination["hasMore"] || pagination["has_more"] || false
|
|
988
|
+
else
|
|
989
|
+
(@offset + @data.length) < @total
|
|
990
|
+
end
|
|
852
991
|
end
|
|
853
992
|
|
|
854
993
|
def each(&block)
|
data/lib/sendly/version.rb
CHANGED