sendly 3.37.0 → 3.38.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 +63 -1
- data/Gemfile.lock +2 -2
- data/README.md +180 -11
- data/lib/sendly/account_resource.rb +12 -8
- data/lib/sendly/business_upgrade_resource.rb +2 -0
- data/lib/sendly/client.rb +71 -4
- data/lib/sendly/enterprise.rb +2 -0
- data/lib/sendly/messages.rb +160 -13
- data/lib/sendly/rcs_resource.rb +222 -0
- data/lib/sendly/templates_resource.rb +169 -37
- data/lib/sendly/version.rb +1 -1
- data/lib/sendly/webhooks.rb +16 -0
- data/lib/sendly/whatsapp_resource.rb +655 -0
- data/lib/sendly.rb +2 -0
- data/sendly.gemspec +51 -0
- metadata +5 -2
data/lib/sendly/messages.rb
CHANGED
|
@@ -10,14 +10,66 @@ module Sendly
|
|
|
10
10
|
@client = client
|
|
11
11
|
end
|
|
12
12
|
|
|
13
|
-
# Send an SMS message
|
|
13
|
+
# Send an SMS, WhatsApp, or RCS message
|
|
14
|
+
#
|
|
15
|
+
# Pass +channel: "whatsapp"+ to send on WhatsApp. WhatsApp sends require
|
|
16
|
+
# a live API key and a +from+ number with an active WhatsApp connection
|
|
17
|
+
# (see +client.whatsapp.signup+). Provide exactly one of +text+
|
|
18
|
+
# (free-form, max 4096 bytes), +media_urls+ (a single attachment;
|
|
19
|
+
# optional +text+ becomes its caption, max 1024 bytes), or +template+
|
|
20
|
+
# (an approved template). Free-form text and media only deliver inside
|
|
21
|
+
# an open 24-hour customer-service window — outside it the API responds
|
|
22
|
+
# 422 +whatsapp_window_closed+; send a template instead (check with
|
|
23
|
+
# +client.whatsapp.window+).
|
|
24
|
+
#
|
|
25
|
+
# Pass +channel: "rcs"+ to send on RCS. RCS sends require a live API key
|
|
26
|
+
# and a sendable RCS agent on your workspace (see +client.rcs.agents+).
|
|
27
|
+
# Provide exactly one of +text+ (free-form, optionally with
|
|
28
|
+
# +suggestions+ — suggested replies and URL actions) or +card+ (a rich
|
|
29
|
+
# card). +agent_id+ picks the sending agent when your workspace has more
|
|
30
|
+
# than one. Recipients that can't receive RCS get +text+ delivered as
|
|
31
|
+
# plain SMS (billed as SMS) unless +fallback_to_sms+ is false — the
|
|
32
|
+
# returned {Sendly::RcsMessage} discloses which channel delivered.
|
|
14
33
|
#
|
|
15
34
|
# @param to [String] Recipient phone number in E.164 format
|
|
16
|
-
# @param text [String] Message content (max 1600 characters)
|
|
17
|
-
#
|
|
18
|
-
# @param
|
|
35
|
+
# @param text [String] Message content (max 1600 characters for SMS);
|
|
36
|
+
# for WhatsApp, optional free-form text or the media caption
|
|
37
|
+
# @param from [String] Sender ID or phone number (optional for SMS);
|
|
38
|
+
# required for WhatsApp — must be a WhatsApp-connected number
|
|
39
|
+
# @param message_type [String] Message type: "marketing" (default) or "transactional" (SMS only)
|
|
19
40
|
# @param metadata [Hash] Custom JSON metadata to attach to the message (max 4KB)
|
|
20
|
-
# @
|
|
41
|
+
# @param media_urls [Array<String>] Media URLs to attach (WhatsApp accepts exactly one)
|
|
42
|
+
# @param channel [String] Message channel: omit (or "sms") for SMS,
|
|
43
|
+
# "whatsapp" for WhatsApp, "rcs" for RCS
|
|
44
|
+
# @param template [Hash] WhatsApp only: approved template to send, with
|
|
45
|
+
# :name, :language, and optional :variables ({ "1" => "Acme" }) and
|
|
46
|
+
# :buttons ([{ index: 0, variables: { "1" => "4821" } }]). Works
|
|
47
|
+
# regardless of the 24-hour window.
|
|
48
|
+
# @param agent_id [String] RCS only: the agent to send from. Optional
|
|
49
|
+
# when your workspace has exactly one sendable agent; required (the
|
|
50
|
+
# API responds 400 +rcs_agent_ambiguous+) when it has more.
|
|
51
|
+
# @param card [Hash] RCS only: a rich card, with :title, :description,
|
|
52
|
+
# and optional :mediaUrl (a public JPEG, PNG, or GIF), :orientation
|
|
53
|
+
# ("vertical" or "horizontal"), and :suggestions. Passed through
|
|
54
|
+
# verbatim, so use the camelCase keys shown. Cards have no SMS form
|
|
55
|
+
# and only deliver to RCS-capable recipients.
|
|
56
|
+
# @param suggestions [Array<Hash>] RCS only, alongside +text+: suggested
|
|
57
|
+
# replies and actions, each either
|
|
58
|
+
# { reply: { text: ..., postbackData: ... } } or
|
|
59
|
+
# { action: { text: ..., postbackData: ..., url: ... } }. Passed
|
|
60
|
+
# through verbatim, so use the camelCase keys shown. Suggestions have
|
|
61
|
+
# no SMS form and are dropped on a fallback.
|
|
62
|
+
# @param fallback_to_sms [Boolean] RCS only: deliver +text+ as plain SMS
|
|
63
|
+
# when the recipient can't receive RCS (default true). Pass false to
|
|
64
|
+
# fail with 422 +rcs_not_supported_for_recipient+ instead.
|
|
65
|
+
# @param idempotency_key [String] Idempotency key for this operation
|
|
66
|
+
# (1-255 printable ASCII characters). The SDK already generates a key
|
|
67
|
+
# per logical request automatically, so the server can dedupe the
|
|
68
|
+
# SDK's own retries. Supply your own key when you need idempotency
|
|
69
|
+
# across process restarts or your own retry loops — repeating a
|
|
70
|
+
# request with the same key within 24 hours returns the original
|
|
71
|
+
# response instead of executing again.
|
|
72
|
+
# @return [Sendly::Message, Sendly::WhatsAppMessage, Sendly::RcsMessage] The sent message
|
|
21
73
|
#
|
|
22
74
|
# @raise [Sendly::ValidationError] If parameters are invalid
|
|
23
75
|
# @raise [Sendly::InsufficientCreditsError] If account has no credits
|
|
@@ -37,8 +89,93 @@ module Sendly
|
|
|
37
89
|
# text: "Your verification code is 123456",
|
|
38
90
|
# message_type: "transactional"
|
|
39
91
|
# )
|
|
40
|
-
|
|
92
|
+
#
|
|
93
|
+
# @example WhatsApp free-form reply inside an open 24h window
|
|
94
|
+
# message = client.messages.send(
|
|
95
|
+
# channel: "whatsapp",
|
|
96
|
+
# to: "+15551234567",
|
|
97
|
+
# from: "+15559876543",
|
|
98
|
+
# text: "Your table is ready!"
|
|
99
|
+
# )
|
|
100
|
+
#
|
|
101
|
+
# @example WhatsApp template send — works regardless of the window
|
|
102
|
+
# message = client.messages.send(
|
|
103
|
+
# channel: "whatsapp",
|
|
104
|
+
# to: "+15551234567",
|
|
105
|
+
# from: "+15559876543",
|
|
106
|
+
# template: {
|
|
107
|
+
# name: "order_shipped",
|
|
108
|
+
# language: "en_US",
|
|
109
|
+
# variables: { "1" => "Acme Inc", "2" => "#4821" }
|
|
110
|
+
# }
|
|
111
|
+
# )
|
|
112
|
+
# puts message.whatsapp.kind # "template"
|
|
113
|
+
# puts message.credits_used # priced by country + category
|
|
114
|
+
#
|
|
115
|
+
# @example RCS text with suggested replies and actions
|
|
116
|
+
# message = client.messages.send(
|
|
117
|
+
# channel: "rcs",
|
|
118
|
+
# to: "+15551234567",
|
|
119
|
+
# text: "Your order has shipped! Want live updates?",
|
|
120
|
+
# suggestions: [
|
|
121
|
+
# { reply: { text: "Yes, notify me", postbackData: "notify_yes" } },
|
|
122
|
+
# { action: { text: "Track order", postbackData: "track",
|
|
123
|
+
# url: "https://acme.example/orders/4821" } }
|
|
124
|
+
# ]
|
|
125
|
+
# )
|
|
126
|
+
# puts message.channel # "rcs", or "sms" when it fell back
|
|
127
|
+
# puts message.fell_back? # true when delivered as plain SMS
|
|
128
|
+
#
|
|
129
|
+
# @example RCS rich card (RCS-capable recipients only)
|
|
130
|
+
# client.messages.send(
|
|
131
|
+
# channel: "rcs",
|
|
132
|
+
# to: "+15551234567",
|
|
133
|
+
# card: {
|
|
134
|
+
# title: "Spring collection",
|
|
135
|
+
# description: "New arrivals are in - take a look.",
|
|
136
|
+
# mediaUrl: "https://example.com/spring.jpg",
|
|
137
|
+
# orientation: "vertical"
|
|
138
|
+
# }
|
|
139
|
+
# )
|
|
140
|
+
def send(to:, text: nil, from: nil, message_type: nil, metadata: nil, media_urls: nil,
|
|
141
|
+
channel: nil, template: nil, agent_id: nil, card: nil, suggestions: nil,
|
|
142
|
+
fallback_to_sms: nil, idempotency_key: nil)
|
|
41
143
|
validate_phone!(to)
|
|
144
|
+
|
|
145
|
+
if channel.to_s == "rcs"
|
|
146
|
+
has_text = !(text.nil? || text.to_s.empty?)
|
|
147
|
+
has_card = !card.nil?
|
|
148
|
+
raise ValidationError, "Provide exactly one of 'text' or 'card'" if has_text == has_card
|
|
149
|
+
|
|
150
|
+
body = { channel: "rcs", to: to }
|
|
151
|
+
body[:agentId] = agent_id if agent_id
|
|
152
|
+
body[:text] = text if has_text
|
|
153
|
+
body[:card] = card if has_card
|
|
154
|
+
body[:suggestions] = suggestions if suggestions
|
|
155
|
+
body[:fallbackToSms] = fallback_to_sms unless fallback_to_sms.nil?
|
|
156
|
+
body[:metadata] = metadata if metadata
|
|
157
|
+
|
|
158
|
+
response = client.post("/messages", body, idempotency_key: idempotency_key)
|
|
159
|
+
return RcsMessage.new(response)
|
|
160
|
+
end
|
|
161
|
+
|
|
162
|
+
if channel.to_s == "whatsapp"
|
|
163
|
+
validate_phone!(from)
|
|
164
|
+
has_media = media_urls.is_a?(Array) && !media_urls.empty?
|
|
165
|
+
if (text.nil? || text.empty?) && !has_media && template.nil?
|
|
166
|
+
raise ValidationError, "Provide 'text', 'media_urls', or 'template'"
|
|
167
|
+
end
|
|
168
|
+
|
|
169
|
+
body = { channel: "whatsapp", to: to, from: from }
|
|
170
|
+
body[:text] = text unless text.nil?
|
|
171
|
+
body[:mediaUrls] = media_urls if has_media
|
|
172
|
+
body[:template] = template if template
|
|
173
|
+
body[:metadata] = metadata if metadata
|
|
174
|
+
|
|
175
|
+
response = client.post("/messages", body, idempotency_key: idempotency_key)
|
|
176
|
+
return WhatsAppMessage.new(response)
|
|
177
|
+
end
|
|
178
|
+
|
|
42
179
|
validate_text!(text)
|
|
43
180
|
|
|
44
181
|
body = { to: to, text: text }
|
|
@@ -47,7 +184,7 @@ module Sendly
|
|
|
47
184
|
body[:metadata] = metadata if metadata
|
|
48
185
|
body[:mediaUrls] = media_urls if media_urls
|
|
49
186
|
|
|
50
|
-
response = client.post("/messages", body)
|
|
187
|
+
response = client.post("/messages", body, idempotency_key: idempotency_key)
|
|
51
188
|
# API returns message directly at top level
|
|
52
189
|
Message.new(response)
|
|
53
190
|
end
|
|
@@ -65,6 +202,8 @@ module Sendly
|
|
|
65
202
|
# @param from [String] Sender ID or phone number (optional)
|
|
66
203
|
# @param media_urls [Array<String>] Media URLs to attach (required unless text is provided)
|
|
67
204
|
# @param message_type [String] Message type: "transactional" (default) or "marketing"
|
|
205
|
+
# @param idempotency_key [String] Idempotency key for this operation
|
|
206
|
+
# (1-255 printable ASCII characters, optional)
|
|
68
207
|
# @return [Sendly::GroupMessage] The sent group message, including a group_message_id
|
|
69
208
|
#
|
|
70
209
|
# @raise [Sendly::ValidationError] If fewer than 2 / more than 8 recipients, or no body
|
|
@@ -77,7 +216,7 @@ module Sendly
|
|
|
77
216
|
# )
|
|
78
217
|
# puts group.id
|
|
79
218
|
# puts group.group_message_id
|
|
80
|
-
def send_group(to:, text: nil, from: nil, media_urls: nil, message_type: nil)
|
|
219
|
+
def send_group(to:, text: nil, from: nil, media_urls: nil, message_type: nil, idempotency_key: nil)
|
|
81
220
|
unless to.is_a?(Array) && to.length >= 2
|
|
82
221
|
raise ValidationError, "Group messaging requires at least 2 recipients in 'to'"
|
|
83
222
|
end
|
|
@@ -94,7 +233,7 @@ module Sendly
|
|
|
94
233
|
body[:mediaUrls] = media_urls if has_media
|
|
95
234
|
body[:messageType] = message_type if message_type
|
|
96
235
|
|
|
97
|
-
response = client.post("/messages/group", body)
|
|
236
|
+
response = client.post("/messages/group", body, idempotency_key: idempotency_key)
|
|
98
237
|
GroupMessage.new(response)
|
|
99
238
|
end
|
|
100
239
|
|
|
@@ -218,6 +357,8 @@ module Sendly
|
|
|
218
357
|
# @param from [String] Sender ID or phone number (optional)
|
|
219
358
|
# @param message_type [String] Message type: "marketing" (default) or "transactional"
|
|
220
359
|
# @param metadata [Hash] Custom JSON metadata to attach to the message (max 4KB)
|
|
360
|
+
# @param idempotency_key [String] Idempotency key for this operation
|
|
361
|
+
# (1-255 printable ASCII characters, optional)
|
|
221
362
|
# @return [Hash] The scheduled message
|
|
222
363
|
#
|
|
223
364
|
# @raise [Sendly::ValidationError] If parameters are invalid
|
|
@@ -229,7 +370,7 @@ module Sendly
|
|
|
229
370
|
# scheduled_at: "2025-01-20T10:00:00Z"
|
|
230
371
|
# )
|
|
231
372
|
# puts scheduled["id"]
|
|
232
|
-
def schedule(to:, text:, scheduled_at:, from: nil, message_type: nil, metadata: nil)
|
|
373
|
+
def schedule(to:, text:, scheduled_at:, from: nil, message_type: nil, metadata: nil, idempotency_key: nil)
|
|
233
374
|
validate_phone!(to)
|
|
234
375
|
validate_text!(text)
|
|
235
376
|
raise ValidationError, "scheduled_at is required" if scheduled_at.nil? || scheduled_at.empty?
|
|
@@ -239,7 +380,7 @@ module Sendly
|
|
|
239
380
|
body[:messageType] = message_type if message_type
|
|
240
381
|
body[:metadata] = metadata if metadata
|
|
241
382
|
|
|
242
|
-
client.post("/messages/schedule", body)
|
|
383
|
+
client.post("/messages/schedule", body, idempotency_key: idempotency_key)
|
|
243
384
|
end
|
|
244
385
|
|
|
245
386
|
# List scheduled messages
|
|
@@ -303,6 +444,8 @@ module Sendly
|
|
|
303
444
|
# @param from [String] Sender ID or phone number (optional, applies to all)
|
|
304
445
|
# @param message_type [String] Message type: "marketing" (default) or "transactional"
|
|
305
446
|
# @param metadata [Hash] Shared metadata for all messages (max 4KB). Each message can also have its own metadata hash which takes priority.
|
|
447
|
+
# @param idempotency_key [String] Idempotency key for this operation
|
|
448
|
+
# (1-255 printable ASCII characters, optional)
|
|
306
449
|
# @return [Hash] Batch response with batch_id and status
|
|
307
450
|
#
|
|
308
451
|
# @raise [Sendly::ValidationError] If parameters are invalid
|
|
@@ -316,7 +459,7 @@ module Sendly
|
|
|
316
459
|
# ]
|
|
317
460
|
# )
|
|
318
461
|
# puts "Batch #{result['batchId']}: #{result['queued']} queued"
|
|
319
|
-
def send_batch(messages:, from: nil, message_type: nil, metadata: nil)
|
|
462
|
+
def send_batch(messages:, from: nil, message_type: nil, metadata: nil, idempotency_key: nil)
|
|
320
463
|
raise ValidationError, "Messages array is required" if messages.nil? || messages.empty?
|
|
321
464
|
|
|
322
465
|
messages.each_with_index do |msg, i|
|
|
@@ -334,7 +477,11 @@ module Sendly
|
|
|
334
477
|
body[:messageType] = message_type if message_type
|
|
335
478
|
body[:metadata] = metadata if metadata
|
|
336
479
|
|
|
337
|
-
|
|
480
|
+
# The batch endpoint dedupes header-less retries server-side by hashing
|
|
481
|
+
# the request content; an auto-generated key would bypass that net for
|
|
482
|
+
# identical cross-process re-runs, so only caller-supplied keys are sent.
|
|
483
|
+
client.post("/messages/batch", body, idempotency_key: idempotency_key,
|
|
484
|
+
auto_idempotency_key: false)
|
|
338
485
|
end
|
|
339
486
|
|
|
340
487
|
# Get batch status by ID
|
|
@@ -0,0 +1,222 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Sendly
|
|
4
|
+
# An RCS agent registered for your workspace — the branded identity your
|
|
5
|
+
# RCS messages come from. +status+ moves +"draft"+ → +"submitted"+ →
|
|
6
|
+
# +"testing"+ (can send, but only to invited test devices) → +"approved"+
|
|
7
|
+
# (can send to everyone); +"suspended"+ agents cannot send. +sendable+ is
|
|
8
|
+
# true when the agent is fully provisioned and its status allows sending.
|
|
9
|
+
class RcsAgent
|
|
10
|
+
attr_reader :id, :name, :status, :use_case, :sendable, :created_at
|
|
11
|
+
|
|
12
|
+
STATUSES = %w[draft submitted testing approved suspended].freeze
|
|
13
|
+
|
|
14
|
+
def initialize(data)
|
|
15
|
+
@id = data["id"]
|
|
16
|
+
@name = data["name"]
|
|
17
|
+
@status = data["status"]
|
|
18
|
+
@use_case = data["useCase"] || data["use_case"]
|
|
19
|
+
@sendable = data["sendable"] || false
|
|
20
|
+
@created_at = data["createdAt"] || data["created_at"]
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def sendable?
|
|
24
|
+
sendable
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def approved?
|
|
28
|
+
status == "approved"
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def to_h
|
|
32
|
+
{
|
|
33
|
+
id: id, name: name, status: status, use_case: use_case,
|
|
34
|
+
sendable: sendable, created_at: created_at
|
|
35
|
+
}.compact
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
# Whether a recipient's device and network can receive RCS. When
|
|
40
|
+
# +capable+ is false, a text send to this recipient falls back to plain
|
|
41
|
+
# SMS (unless the fallback is disabled) and a card send fails. +features+
|
|
42
|
+
# lists the feature tags the device reports.
|
|
43
|
+
class RcsCapability
|
|
44
|
+
attr_reader :to, :agent_id, :capable, :features
|
|
45
|
+
|
|
46
|
+
def initialize(data)
|
|
47
|
+
@to = data["to"]
|
|
48
|
+
@agent_id = data["agentId"] || data["agent_id"]
|
|
49
|
+
@capable = data["capable"] || false
|
|
50
|
+
@features = data["features"] || []
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def capable?
|
|
54
|
+
capable
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def to_h
|
|
58
|
+
{ to: to, agent_id: agent_id, capable: capable, features: features }.compact
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
# RCS-specific details on a sent message. On a native RCS delivery +kind+
|
|
63
|
+
# is what was sent ("text" or "card") and +agent_name+ is the brand name
|
|
64
|
+
# recipients see. On an SMS fallback +requested_channel+ is "rcs" and
|
|
65
|
+
# +suggestions_dropped+ is true when suggested replies/actions were
|
|
66
|
+
# dropped (they have no SMS form).
|
|
67
|
+
class RcsMessageDetails
|
|
68
|
+
attr_reader :kind, :agent_id, :agent_name, :requested_channel,
|
|
69
|
+
:suggestions_dropped
|
|
70
|
+
|
|
71
|
+
KINDS = %w[text card].freeze
|
|
72
|
+
|
|
73
|
+
def initialize(data)
|
|
74
|
+
@kind = data["kind"]
|
|
75
|
+
@agent_id = data["agentId"] || data["agent_id"]
|
|
76
|
+
@agent_name = data["agentName"] || data["agent_name"]
|
|
77
|
+
@requested_channel = data["requestedChannel"] || data["requested_channel"]
|
|
78
|
+
@suggestions_dropped = data["suggestionsDropped"] || data["suggestions_dropped"] || false
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
def to_h
|
|
82
|
+
{
|
|
83
|
+
kind: kind, agent_id: agent_id, agent_name: agent_name,
|
|
84
|
+
requested_channel: requested_channel,
|
|
85
|
+
suggestions_dropped: suggestions_dropped
|
|
86
|
+
}.compact
|
|
87
|
+
end
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
# A message sent through the RCS channel. +channel+ is "rcs" when it was
|
|
91
|
+
# delivered as RCS, or "sms" when the recipient couldn't receive RCS and
|
|
92
|
+
# the message fell back to plain SMS — check with {#fell_back?}. On a
|
|
93
|
+
# fallback +fell_back_to+ is "sms" and +segments+/+credits_used+ are
|
|
94
|
+
# billed as SMS; +rcs+ carries the channel-specific details either way.
|
|
95
|
+
class RcsMessage
|
|
96
|
+
attr_reader :id, :channel, :fell_back_to, :message_format, :to, :from,
|
|
97
|
+
:text, :status, :segments, :credits_used, :rcs, :created_at,
|
|
98
|
+
:metadata
|
|
99
|
+
|
|
100
|
+
def initialize(data)
|
|
101
|
+
@id = data["id"]
|
|
102
|
+
@channel = data["channel"] || "rcs"
|
|
103
|
+
@fell_back_to = data["fellBackTo"] || data["fell_back_to"]
|
|
104
|
+
@message_format = data["message_format"] || data["messageFormat"] || @channel
|
|
105
|
+
@to = data["to"]
|
|
106
|
+
@from = data["from"]
|
|
107
|
+
@text = data["text"]
|
|
108
|
+
@status = data["status"]
|
|
109
|
+
@segments = data["segments"] || 1
|
|
110
|
+
@credits_used = data["creditsUsed"] || data["credits_used"] || 0
|
|
111
|
+
@rcs = data["rcs"] ? RcsMessageDetails.new(data["rcs"]) : nil
|
|
112
|
+
@created_at = data["createdAt"] || data["created_at"]
|
|
113
|
+
@metadata = data["metadata"]
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
def fell_back?
|
|
117
|
+
fell_back_to == "sms"
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
def delivered?
|
|
121
|
+
status == "delivered"
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
def failed?
|
|
125
|
+
status == "failed"
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
def to_h
|
|
129
|
+
{
|
|
130
|
+
id: id, channel: channel, fell_back_to: fell_back_to,
|
|
131
|
+
message_format: message_format, to: to, from: from, text: text,
|
|
132
|
+
status: status, segments: segments, credits_used: credits_used,
|
|
133
|
+
rcs: rcs&.to_h, created_at: created_at, metadata: metadata
|
|
134
|
+
}.compact
|
|
135
|
+
end
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
# List the RCS agents registered for your workspace.
|
|
139
|
+
class RcsAgentsResource
|
|
140
|
+
def initialize(client)
|
|
141
|
+
@client = client
|
|
142
|
+
end
|
|
143
|
+
|
|
144
|
+
# List your RCS agents, newest first. An empty list means no agent is
|
|
145
|
+
# set up yet — contact support to register an RCS agent for your brand.
|
|
146
|
+
#
|
|
147
|
+
# @return [Hash] +{ agents: Array<RcsAgent> }+
|
|
148
|
+
#
|
|
149
|
+
# @example
|
|
150
|
+
# client.rcs.agents.list[:agents].each do |a|
|
|
151
|
+
# puts "#{a.name} — #{a.status}#{a.sendable? ? ' (sendable)' : ''}"
|
|
152
|
+
# end
|
|
153
|
+
def list
|
|
154
|
+
response = @client.get("/rcs/agents")
|
|
155
|
+
agents = (response["agents"] || []).map { |a| RcsAgent.new(a) }
|
|
156
|
+
{ agents: agents }
|
|
157
|
+
end
|
|
158
|
+
end
|
|
159
|
+
|
|
160
|
+
# RCS resource — discover your RCS agents and pre-flight recipient
|
|
161
|
+
# capability.
|
|
162
|
+
#
|
|
163
|
+
# RCS is a first-class Sendly channel: send branded rich messages — text
|
|
164
|
+
# with suggested replies and actions, or rich cards — via
|
|
165
|
+
# +client.messages.send(channel: "rcs", ...)+. RCS agents are registered
|
|
166
|
+
# by Sendly for your brand; contact support to set one up, then discover
|
|
167
|
+
# it with {RcsAgentsResource#list}.
|
|
168
|
+
#
|
|
169
|
+
# Delivery is per-recipient: not every device or network supports RCS.
|
|
170
|
+
# Text messages fall back to plain SMS automatically (billed as SMS)
|
|
171
|
+
# unless you pass +fallback_to_sms: false+; rich cards have no SMS form
|
|
172
|
+
# and only deliver to RCS-capable recipients. Use {#capability} to check
|
|
173
|
+
# a recipient before sending.
|
|
174
|
+
#
|
|
175
|
+
# RCS sends and capability checks require a live API key
|
|
176
|
+
# (+sk_live_v1_xxx+).
|
|
177
|
+
#
|
|
178
|
+
# @example Check capability, then send
|
|
179
|
+
# cap = client.rcs.capability(to: "+15551234567")
|
|
180
|
+
# if cap.capable?
|
|
181
|
+
# client.messages.send(
|
|
182
|
+
# channel: "rcs",
|
|
183
|
+
# to: "+15551234567",
|
|
184
|
+
# text: "Your order has shipped!"
|
|
185
|
+
# )
|
|
186
|
+
# end
|
|
187
|
+
class RcsResource
|
|
188
|
+
# @return [RcsAgentsResource] List registered agents
|
|
189
|
+
attr_reader :agents
|
|
190
|
+
|
|
191
|
+
def initialize(client)
|
|
192
|
+
@client = client
|
|
193
|
+
@agents = RcsAgentsResource.new(client)
|
|
194
|
+
end
|
|
195
|
+
|
|
196
|
+
# Check whether a recipient can receive RCS.
|
|
197
|
+
#
|
|
198
|
+
# Probes the recipient's device and network. When the recipient is not
|
|
199
|
+
# capable, a text send falls back to plain SMS (unless the fallback is
|
|
200
|
+
# disabled) and a card send fails with
|
|
201
|
+
# +rcs_not_supported_for_recipient+.
|
|
202
|
+
#
|
|
203
|
+
# @param to [String] The recipient's number, in E.164 format
|
|
204
|
+
# @param agent_id [String, nil] The agent to probe with. Optional when
|
|
205
|
+
# your workspace has exactly one sendable agent; required (the API
|
|
206
|
+
# responds 400 +rcs_agent_ambiguous+) when it has more.
|
|
207
|
+
# @return [RcsCapability]
|
|
208
|
+
#
|
|
209
|
+
# @example
|
|
210
|
+
# cap = client.rcs.capability(to: "+15551234567")
|
|
211
|
+
# puts cap.capable?
|
|
212
|
+
# puts cap.features.inspect
|
|
213
|
+
def capability(to:, agent_id: nil)
|
|
214
|
+
raise ValidationError, "to is required" if to.nil? || to.to_s.empty?
|
|
215
|
+
|
|
216
|
+
params = { to: to }
|
|
217
|
+
params[:agentId] = agent_id if agent_id
|
|
218
|
+
response = @client.get("/rcs/capability", params)
|
|
219
|
+
RcsCapability.new(response)
|
|
220
|
+
end
|
|
221
|
+
end
|
|
222
|
+
end
|