sendly 4.3.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 +10 -0
- data/Gemfile.lock +5 -5
- data/README.md +28 -9
- data/lib/sendly/calls_resource.rb +1 -0
- data/lib/sendly/errors.rb +35 -20
- data/lib/sendly/messages.rb +22 -2
- data/lib/sendly/types.rb +13 -1
- data/lib/sendly/version.rb +1 -1
- data/lib/sendly/webhooks.rb +5 -0
- data/lib/sendly/whatsapp_resource.rb +57 -3
- metadata +6 -6
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 5a69a00fe63398e0ad342ed03ad9ddf38d668fe0d2ddd85533a2aa43e534eacb
|
|
4
|
+
data.tar.gz: 8488579a38c0f09720a8d9e67a5084819eb884dfe6cc99afc66be5d143ae5323
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: caa9cd2bb16dc70c5eea42f760fe9295587e0f3b8c7a83bb4de6af903b5ba29ff6ec89c6c53ab211fc630c68d8139648516a95dccd27412382be08012ba922ed
|
|
7
|
+
data.tar.gz: b5ef93d9ce46c42dd53ef375b23aa66a5027e6534bf93425ab507d37f4c2f17311cdefbda52269e2fbad9b167808ad96abae255fd0b216942d5af1873d5f8337
|
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,15 @@
|
|
|
1
1
|
# sendly (Ruby)
|
|
2
2
|
|
|
3
|
+
## 4.4.0
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- **WhatsApp to SMS fallback.** `messages.send(channel: "whatsapp", fallback_to_sms: true)` sends `fallbackToSms`; it was dropped before. When WhatsApp can't deliver the message (the 24-hour window is closed, WhatsApp refuses it, or reports it undeliverable later), it goes as an SMS from the same number, billed as an SMS. The new `Sendly::WhatsAppSmsFallback` (`channel`, `status`, `reason`, `message_id`, `credits_used`, `error`, `content_dropped`, with `pending?` and `sent?`, and `STATUSES` and `REASONS`) says what happened: it is `WhatsAppMessage#fallback`, and `WhatsAppMessage#fell_back?` is true when the SMS went. When the SMS went in place of the WhatsApp message, `status` is `"failed"` and `fallback.status` is `"sent"`. `Message` gains `fallback` (on a WhatsApp message from `messages.get` or `messages.list`) and `fallback_of` (on the SMS, the id of the WhatsApp message it replaced), and `to_h` includes them when present. The number must already be able to send SMS to the recipient (US and Canada), or the API answers 403 `sms_fallback_unavailable`; marketing and authentication templates, and media without a caption, get 422 `sms_fallback_not_supported`.
|
|
8
|
+
|
|
9
|
+
### Patch Changes
|
|
10
|
+
|
|
11
|
+
- **Errors carry the API's error code.** `e.code` is the code the API sent in `error`, such as `"whatsapp_window_closed"`, `"sms_fallback_unavailable"` or `"invalid_api_key"`, on every error class, so refusals with the same status can be told apart. Before, only an unrecognised status read a code, and from a `code` field the API doesn't send, so `e.code` was the class's own code (`"VALIDATION_ERROR"`, `"API_ERROR"` and so on). That code is still used when the body names none (an `error` that is a sentence, or no `error` at all); a body's `code` field is read when `error` isn't a code. Code that compared `e.code` with a class code for an API error should rescue the class instead, or compare with the API's code.
|
|
12
|
+
|
|
3
13
|
## 4.3.0
|
|
4
14
|
|
|
5
15
|
### Minor Changes
|
data/Gemfile.lock
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
PATH
|
|
2
2
|
remote: .
|
|
3
3
|
specs:
|
|
4
|
-
sendly (4.
|
|
4
|
+
sendly (4.4.0)
|
|
5
5
|
faraday (~> 2.0)
|
|
6
6
|
faraday-retry (~> 2.0)
|
|
7
7
|
|
|
8
8
|
GEM
|
|
9
9
|
remote: https://rubygems.org/
|
|
10
10
|
specs:
|
|
11
|
-
addressable (2.
|
|
11
|
+
addressable (2.8.8)
|
|
12
12
|
public_suffix (>= 2.0.2, < 8.0)
|
|
13
13
|
ast (2.4.3)
|
|
14
14
|
bigdecimal (3.3.1)
|
|
@@ -36,7 +36,7 @@ GEM
|
|
|
36
36
|
ast (~> 2.4.1)
|
|
37
37
|
racc
|
|
38
38
|
prism (1.6.0)
|
|
39
|
-
public_suffix (
|
|
39
|
+
public_suffix (7.0.0)
|
|
40
40
|
racc (1.8.1)
|
|
41
41
|
rainbow (3.1.1)
|
|
42
42
|
rake (13.3.1)
|
|
@@ -74,7 +74,7 @@ GEM
|
|
|
74
74
|
unicode-emoji (~> 4.1)
|
|
75
75
|
unicode-emoji (4.1.0)
|
|
76
76
|
uri (1.1.1)
|
|
77
|
-
webmock (3.26.
|
|
77
|
+
webmock (3.26.1)
|
|
78
78
|
addressable (>= 2.8.0)
|
|
79
79
|
crack (>= 0.3.2)
|
|
80
80
|
hashdiff (>= 0.4.0, < 2.0.0)
|
|
@@ -92,4 +92,4 @@ DEPENDENCIES
|
|
|
92
92
|
webmock (~> 3.0)
|
|
93
93
|
|
|
94
94
|
BUNDLED WITH
|
|
95
|
-
2.
|
|
95
|
+
2.6.9
|
data/README.md
CHANGED
|
@@ -1316,6 +1316,25 @@ client.messages.send(
|
|
|
1316
1316
|
text: "Here is your receipt",
|
|
1317
1317
|
media_urls: ["https://acme.example/receipt.pdf"]
|
|
1318
1318
|
)
|
|
1319
|
+
|
|
1320
|
+
# Fall back to SMS from the same number when WhatsApp can't deliver: the
|
|
1321
|
+
# window is closed, WhatsApp refuses it, or reports it undeliverable later.
|
|
1322
|
+
# The SMS is billed as an SMS. The number must already be able to send SMS to
|
|
1323
|
+
# the recipient (US and Canada), or this raises 403 sms_fallback_unavailable;
|
|
1324
|
+
# marketing and authentication templates, and media without a caption, raise
|
|
1325
|
+
# 422 sms_fallback_not_supported.
|
|
1326
|
+
message = client.messages.send(
|
|
1327
|
+
channel: "whatsapp",
|
|
1328
|
+
to: "+15555550100",
|
|
1329
|
+
from: "+15125550123",
|
|
1330
|
+
text: "Your table is ready!",
|
|
1331
|
+
fallback_to_sms: true
|
|
1332
|
+
)
|
|
1333
|
+
message.fallback.status # "pending", "sent", "failed", "unavailable" or "skipped"
|
|
1334
|
+
message.fallback.reason # "window_closed", "whatsapp_refused" or "whatsapp_failed"
|
|
1335
|
+
message.fell_back? # true when the SMS went (message.status is then "failed")
|
|
1336
|
+
sms = client.messages.get(message.fallback.message_id) if message.fallback.message_id
|
|
1337
|
+
sms&.fallback_of # the WhatsApp message's id
|
|
1319
1338
|
```
|
|
1320
1339
|
|
|
1321
1340
|
WhatsApp errors map onto the usual classes:
|
|
@@ -1770,12 +1789,12 @@ begin
|
|
|
1770
1789
|
rescue Sendly::AuthenticationError => e
|
|
1771
1790
|
puts "Invalid API key"
|
|
1772
1791
|
rescue Sendly::RateLimitError => e
|
|
1773
|
-
if e.
|
|
1792
|
+
if e.code == "too_many_failed_key_attempts"
|
|
1774
1793
|
# Repeated wrong API keys from this address locked it out for up to
|
|
1775
1794
|
# 5 minutes. Fix the key; do not retry until the lockout ends.
|
|
1776
1795
|
puts "Locked out for #{e.retry_after}s: check the API key"
|
|
1777
1796
|
else
|
|
1778
|
-
puts "Rate limited (#{e.
|
|
1797
|
+
puts "Rate limited (#{e.code}), retry after #{e.retry_after} seconds"
|
|
1779
1798
|
end
|
|
1780
1799
|
rescue Sendly::InsufficientCreditsError => e
|
|
1781
1800
|
puts "Add more credits to your account"
|
|
@@ -1786,17 +1805,18 @@ rescue Sendly::NotFoundError => e
|
|
|
1786
1805
|
rescue Sendly::NetworkError => e
|
|
1787
1806
|
puts "Network error: #{e.message}"
|
|
1788
1807
|
rescue Sendly::Error => e
|
|
1789
|
-
puts "Error: #{e.message} (#{e.
|
|
1808
|
+
puts "Error: #{e.message} (#{e.code})"
|
|
1790
1809
|
end
|
|
1791
1810
|
```
|
|
1792
1811
|
|
|
1793
1812
|
Every error is a `Sendly::Error`, which carries `message`, `code`,
|
|
1794
1813
|
`status_code`, `details` and `response_body` — the parsed JSON body of the
|
|
1795
1814
|
API's response, or `nil` for an error raised before the request went out.
|
|
1796
|
-
|
|
1797
|
-
|
|
1798
|
-
|
|
1799
|
-
|
|
1815
|
+
`code` is the API's own error code, such as `"whatsapp_window_closed"` or
|
|
1816
|
+
`"sms_fallback_unavailable"` (the body's `"error"`), so refusals with the same
|
|
1817
|
+
status can be told apart. When the body names no code, `code` is the SDK's
|
|
1818
|
+
name for the class (`"VALIDATION_ERROR"`, `"RATE_LIMIT_EXCEEDED"` and so on).
|
|
1819
|
+
Some refusals put more in that body than a message: a 409 `agent_in_use` lists the
|
|
1800
1820
|
numbers under `"numbers"`, a 422 `invalid_address` carries a corrected
|
|
1801
1821
|
address under `"suggested"`.
|
|
1802
1822
|
|
|
@@ -1809,7 +1829,7 @@ endpoint.
|
|
|
1809
1829
|
| Class | Raised for |
|
|
1810
1830
|
|-------|-----------|
|
|
1811
1831
|
| `Sendly::AuthenticationError` | 401, and a missing or malformed API key at construction |
|
|
1812
|
-
| `Sendly::ValidationError` | 400 and 422; `field_errors` holds the API's `errors` list. `status_code` reads 400 for both, so tell them apart by `
|
|
1832
|
+
| `Sendly::ValidationError` | 400 and 422; `field_errors` holds the API's `errors` list. `status_code` reads 400 for both, so tell them apart by `code` |
|
|
1813
1833
|
| `Sendly::InsufficientCreditsError` | 402 |
|
|
1814
1834
|
| `Sendly::NotFoundError` | 404, including a feature that is not enabled for you |
|
|
1815
1835
|
| `Sendly::RateLimitError` | 429; `retry_after` in seconds when the API sent one |
|
|
@@ -2066,7 +2086,6 @@ client.enterprise.workspaces.list_keys("ws_xxx")
|
|
|
2066
2086
|
# Enterprise-level account, credits and billing
|
|
2067
2087
|
client.enterprise.get_account
|
|
2068
2088
|
client.enterprise.credits.get
|
|
2069
|
-
client.enterprise.credits.deposit(amount: 100_000, description: "Q4 top-up")
|
|
2070
2089
|
client.enterprise.settings.get_auto_top_up
|
|
2071
2090
|
client.enterprise.settings.update_auto_top_up(enabled: true, threshold: 1000, amount: 10_000)
|
|
2072
2091
|
client.enterprise.billing.get_breakdown(period: "30d")
|
|
@@ -55,6 +55,7 @@ module Sendly
|
|
|
55
55
|
ring_timeout callee_declined callee_busy caller_cancelled room_closed_unanswered
|
|
56
56
|
agent_left_unanswered agent_caller_never_joined invalid_number destination_rejected
|
|
57
57
|
max_duration credits_exhausted media_aborted peer_connection_lost room_closed agent_left
|
|
58
|
+
transferred voicemail voicemail_unavailable
|
|
58
59
|
setup_failed agent_dispatch_failed agent_api_unreachable agent_already_ended
|
|
59
60
|
ended
|
|
60
61
|
].freeze
|
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,38 +110,50 @@ 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
|
|
120
|
+
API_ERROR_CODE = /\A[a-z][a-z0-9_]*\z/
|
|
121
|
+
private_constant :API_ERROR_CODE
|
|
122
|
+
|
|
117
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
138
|
retry_after = parse_retry_after(retry_after_header) || body["retry_after"] || body["retryAfter"]
|
|
133
|
-
RateLimitError.new(message, retry_after: retry_after)
|
|
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
|
|
141
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
|
+
|
|
142
157
|
def self.parse_retry_after(value)
|
|
143
158
|
text = value.to_s.strip
|
|
144
159
|
return text.to_i if text.match?(/\A\d+\z/)
|
data/lib/sendly/messages.rb
CHANGED
|
@@ -62,9 +62,17 @@ module Sendly
|
|
|
62
62
|
# { action: { text: ..., postbackData: ..., url: ... } }. Passed
|
|
63
63
|
# through verbatim, so use the camelCase keys shown. Suggestions have
|
|
64
64
|
# no SMS form and are dropped on a fallback.
|
|
65
|
-
# @param fallback_to_sms [Boolean] RCS
|
|
65
|
+
# @param fallback_to_sms [Boolean] RCS: deliver +text+ as plain SMS
|
|
66
66
|
# when the recipient can't receive RCS (default true). Pass false to
|
|
67
|
-
# 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+.
|
|
68
76
|
# @param idempotency_key [String] Idempotency key for this operation
|
|
69
77
|
# (1-255 printable ASCII characters). The SDK already generates a key
|
|
70
78
|
# per logical request automatically, so the server can dedupe the
|
|
@@ -130,6 +138,17 @@ module Sendly
|
|
|
130
138
|
# puts message.whatsapp.kind # "template"
|
|
131
139
|
# puts message.credits_used # priced by country + category
|
|
132
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
|
+
#
|
|
133
152
|
# @example RCS text with suggested replies and actions
|
|
134
153
|
# message = client.messages.send(
|
|
135
154
|
# channel: "rcs",
|
|
@@ -189,6 +208,7 @@ module Sendly
|
|
|
189
208
|
body[:mediaUrls] = media_urls if has_media
|
|
190
209
|
body[:template] = template if template
|
|
191
210
|
body[:metadata] = metadata if metadata
|
|
211
|
+
body[:fallbackToSms] = fallback_to_sms unless fallback_to_sms.nil?
|
|
192
212
|
|
|
193
213
|
response = client.post("/messages", body, idempotency_key: idempotency_key)
|
|
194
214
|
return WhatsAppMessage.new(response)
|
data/lib/sendly/types.rb
CHANGED
|
@@ -69,6 +69,14 @@ module Sendly
|
|
|
69
69
|
# @return [String] "sms", "mms", "rcs" or "whatsapp"; "sms" when the response does not say
|
|
70
70
|
attr_reader :message_format
|
|
71
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
|
+
|
|
72
80
|
# Message status constants (sending removed - doesn't exist in database)
|
|
73
81
|
STATUSES = %w[queued sent delivered failed bounced retrying].freeze
|
|
74
82
|
|
|
@@ -98,6 +106,8 @@ module Sendly
|
|
|
98
106
|
@ai_metadata = data["aiMetadata"]
|
|
99
107
|
@media_urls = data["mediaUrls"] || data["media_urls"] || []
|
|
100
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"]
|
|
101
111
|
end
|
|
102
112
|
|
|
103
113
|
# Check if message was delivered
|
|
@@ -143,7 +153,9 @@ module Sendly
|
|
|
143
153
|
metadata: metadata,
|
|
144
154
|
ai_metadata: ai_metadata,
|
|
145
155
|
media_urls: media_urls,
|
|
146
|
-
message_format: message_format
|
|
156
|
+
message_format: message_format,
|
|
157
|
+
fallback: fallback&.to_h,
|
|
158
|
+
fallback_of: fallback_of
|
|
147
159
|
}.compact
|
|
148
160
|
end
|
|
149
161
|
|
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.
|
|
@@ -420,6 +420,51 @@ module Sendly
|
|
|
420
420
|
end
|
|
421
421
|
end
|
|
422
422
|
|
|
423
|
+
# The SMS fallback of a WhatsApp message sent with +fallback_to_sms: true+.
|
|
424
|
+
# +status+ is +"pending"+ until WhatsApp's outcome is known, +"sent"+ when
|
|
425
|
+
# the SMS went, +"failed"+ when the SMS was refused or credits ran out,
|
|
426
|
+
# +"unavailable"+ when the SMS could no longer be sent from this number,
|
|
427
|
+
# and +"skipped"+ when the WhatsApp failure isn't one an SMS fixes.
|
|
428
|
+
# +reason+ is what made the message fall back (+"window_closed"+,
|
|
429
|
+
# +"whatsapp_refused"+ or +"whatsapp_failed"+). +message_id+ is the SMS
|
|
430
|
+
# message's id once it exists, +credits_used+ what the SMS cost, +error+
|
|
431
|
+
# why the SMS didn't go, in plain English, and +content_dropped+ the
|
|
432
|
+
# template parts with no SMS form, such as +"quick_replies"+. Values the
|
|
433
|
+
# API adds later come through unchanged.
|
|
434
|
+
class WhatsAppSmsFallback
|
|
435
|
+
attr_reader :channel, :status, :reason, :message_id, :credits_used,
|
|
436
|
+
:error, :content_dropped
|
|
437
|
+
|
|
438
|
+
STATUSES = %w[pending sent failed unavailable skipped].freeze
|
|
439
|
+
REASONS = %w[window_closed whatsapp_refused whatsapp_failed].freeze
|
|
440
|
+
|
|
441
|
+
def initialize(data)
|
|
442
|
+
@channel = data["channel"] || "sms"
|
|
443
|
+
@status = data["status"]
|
|
444
|
+
@reason = data["reason"]
|
|
445
|
+
@message_id = data["messageId"] || data["message_id"]
|
|
446
|
+
@credits_used = data["creditsUsed"] || data["credits_used"]
|
|
447
|
+
@error = data["error"]
|
|
448
|
+
@content_dropped = data["contentDropped"] || data["content_dropped"]
|
|
449
|
+
end
|
|
450
|
+
|
|
451
|
+
def pending?
|
|
452
|
+
status == "pending"
|
|
453
|
+
end
|
|
454
|
+
|
|
455
|
+
def sent?
|
|
456
|
+
status == "sent"
|
|
457
|
+
end
|
|
458
|
+
|
|
459
|
+
def to_h
|
|
460
|
+
{
|
|
461
|
+
channel: channel, status: status, reason: reason,
|
|
462
|
+
message_id: message_id, credits_used: credits_used, error: error,
|
|
463
|
+
content_dropped: content_dropped
|
|
464
|
+
}.compact
|
|
465
|
+
end
|
|
466
|
+
end
|
|
467
|
+
|
|
423
468
|
# A sent WhatsApp message. Unlike {Message}, +segments+ is always 1
|
|
424
469
|
# (WhatsApp has no segment concept), +text+ is the caption for a media
|
|
425
470
|
# send (pass it as +text+ with +media_urls+) and nil for template sends
|
|
@@ -431,10 +476,14 @@ module Sendly
|
|
|
431
476
|
# utility price of 12 credits. Templates are priced by category and
|
|
432
477
|
# destination country; countries without a listed price use 33
|
|
433
478
|
# (marketing), 12 (utility) and 12 (authentication) credits. A failed
|
|
434
|
-
# send gives its slot back.
|
|
479
|
+
# send gives its slot back. +fallback+ is a {WhatsAppSmsFallback} when the
|
|
480
|
+
# send asked for +fallback_to_sms+: when WhatsApp couldn't take the
|
|
481
|
+
# message and it went as an SMS instead, +status+ is +"failed"+ and
|
|
482
|
+
# +fallback.status+ is +"sent"+ (check with {#fell_back?}).
|
|
435
483
|
class WhatsAppMessage
|
|
436
484
|
attr_reader :id, :channel, :message_format, :to, :from, :text, :status,
|
|
437
|
-
:segments, :credits_used, :whatsapp, :created_at, :metadata
|
|
485
|
+
:segments, :credits_used, :whatsapp, :created_at, :metadata,
|
|
486
|
+
:fallback
|
|
438
487
|
|
|
439
488
|
def initialize(data)
|
|
440
489
|
@id = data["id"]
|
|
@@ -449,6 +498,11 @@ module Sendly
|
|
|
449
498
|
@whatsapp = data["whatsapp"] ? WhatsAppMessageDetails.new(data["whatsapp"]) : nil
|
|
450
499
|
@created_at = data["createdAt"] || data["created_at"]
|
|
451
500
|
@metadata = data["metadata"]
|
|
501
|
+
@fallback = data["fallback"].is_a?(Hash) ? WhatsAppSmsFallback.new(data["fallback"]) : nil
|
|
502
|
+
end
|
|
503
|
+
|
|
504
|
+
def fell_back?
|
|
505
|
+
fallback&.sent? || false
|
|
452
506
|
end
|
|
453
507
|
|
|
454
508
|
def delivered?
|
|
@@ -464,7 +518,7 @@ module Sendly
|
|
|
464
518
|
id: id, channel: channel, message_format: message_format,
|
|
465
519
|
to: to, from: from, text: text, status: status, segments: segments,
|
|
466
520
|
credits_used: credits_used, whatsapp: whatsapp&.to_h,
|
|
467
|
-
created_at: created_at, metadata: metadata
|
|
521
|
+
created_at: created_at, metadata: metadata, fallback: fallback&.to_h
|
|
468
522
|
}.compact
|
|
469
523
|
end
|
|
470
524
|
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: 4.
|
|
4
|
+
version: 4.4.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Sendly
|
|
8
|
-
autorequire:
|
|
8
|
+
autorequire:
|
|
9
9
|
bindir: exe
|
|
10
10
|
cert_chain: []
|
|
11
|
-
date: 2026-10-
|
|
11
|
+
date: 2026-10-17 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: faraday
|
|
@@ -159,7 +159,7 @@ metadata:
|
|
|
159
159
|
source_code_uri: https://github.com/SendlyHQ/sendly-ruby
|
|
160
160
|
changelog_uri: https://github.com/SendlyHQ/sendly-ruby/blob/main/CHANGELOG.md
|
|
161
161
|
documentation_uri: https://sendly.live/docs
|
|
162
|
-
post_install_message:
|
|
162
|
+
post_install_message:
|
|
163
163
|
rdoc_options: []
|
|
164
164
|
require_paths:
|
|
165
165
|
- lib
|
|
@@ -174,8 +174,8 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
|
174
174
|
- !ruby/object:Gem::Version
|
|
175
175
|
version: '0'
|
|
176
176
|
requirements: []
|
|
177
|
-
rubygems_version: 3.
|
|
178
|
-
signing_key:
|
|
177
|
+
rubygems_version: 3.5.22
|
|
178
|
+
signing_key:
|
|
179
179
|
specification_version: 4
|
|
180
180
|
summary: Official Ruby SDK for the Sendly SMS API
|
|
181
181
|
test_files: []
|