sendly 3.34.0 → 3.37.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/Gemfile.lock +1 -1
- data/README.md +413 -0
- data/lib/sendly/account_resource.rb +31 -0
- data/lib/sendly/client.rb +57 -4
- data/lib/sendly/links_resource.rb +240 -0
- data/lib/sendly/messages.rb +86 -1
- data/lib/sendly/numbers_resource.rb +121 -2
- data/lib/sendly/tendlc_resource.rb +397 -0
- data/lib/sendly/types.rb +79 -0
- data/lib/sendly/version.rb +1 -1
- data/lib/sendly.rb +2 -0
- metadata +4 -2
|
@@ -0,0 +1,397 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Sendly
|
|
4
|
+
# A business identity registered for carrier review. The status starts
|
|
5
|
+
# +"pending"+ and moves to +"verified"+ (campaigns can be created) or
|
|
6
|
+
# +"failed"+ (see +failure_reasons+).
|
|
7
|
+
class TenDlcBrand
|
|
8
|
+
attr_reader :id, :legal_name, :dba, :entity_type, :ein, :vertical,
|
|
9
|
+
:website, :status, :identity_status, :failure_reasons,
|
|
10
|
+
:created_at, :updated_at
|
|
11
|
+
|
|
12
|
+
def initialize(data)
|
|
13
|
+
@id = data["id"]
|
|
14
|
+
@legal_name = data["legalName"] || data["legal_name"]
|
|
15
|
+
@dba = data["dba"]
|
|
16
|
+
@entity_type = data["entityType"] || data["entity_type"]
|
|
17
|
+
@ein = data["ein"]
|
|
18
|
+
@vertical = data["vertical"]
|
|
19
|
+
@website = data["website"]
|
|
20
|
+
@status = data["status"]
|
|
21
|
+
@identity_status = data["identityStatus"] || data["identity_status"]
|
|
22
|
+
@failure_reasons = data["failureReasons"] || data["failure_reasons"]
|
|
23
|
+
@created_at = data["createdAt"] || data["created_at"]
|
|
24
|
+
@updated_at = data["updatedAt"] || data["updated_at"]
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def pending?
|
|
28
|
+
status == "pending"
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def verified?
|
|
32
|
+
status == "verified"
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def failed?
|
|
36
|
+
status == "failed"
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def to_h
|
|
40
|
+
{
|
|
41
|
+
id: id, legal_name: legal_name, dba: dba, entity_type: entity_type,
|
|
42
|
+
ein: ein, vertical: vertical, website: website, status: status,
|
|
43
|
+
identity_status: identity_status, failure_reasons: failure_reasons,
|
|
44
|
+
created_at: created_at, updated_at: updated_at
|
|
45
|
+
}.compact
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
# Messaging throughput granted by the carrier network.
|
|
50
|
+
class TenDlcThroughput
|
|
51
|
+
attr_reader :tier, :carriers_ready
|
|
52
|
+
|
|
53
|
+
def initialize(data)
|
|
54
|
+
@tier = data["tier"]
|
|
55
|
+
@carriers_ready = data["carriersReady"] || data["carriers_ready"]
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def to_h
|
|
59
|
+
{ tier: tier, carriers_ready: carriers_ready }.compact
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
# The result of a use-case qualification pre-check. When +qualified+ is
|
|
64
|
+
# false, +reason+ explains why; +throughput+ carries the expected tier
|
|
65
|
+
# when the carrier network reports it.
|
|
66
|
+
class TenDlcQualifyResult
|
|
67
|
+
attr_reader :use_case, :qualified, :reason, :throughput
|
|
68
|
+
|
|
69
|
+
def initialize(data)
|
|
70
|
+
@use_case = data["useCase"] || data["use_case"]
|
|
71
|
+
@qualified = data["qualified"]
|
|
72
|
+
@reason = data["reason"]
|
|
73
|
+
@throughput = data["throughput"] ? TenDlcThroughput.new(data["throughput"]) : nil
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
def qualified?
|
|
77
|
+
qualified == true
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
def to_h
|
|
81
|
+
{
|
|
82
|
+
use_case: use_case, qualified: qualified, reason: reason,
|
|
83
|
+
throughput: throughput&.to_h
|
|
84
|
+
}.compact
|
|
85
|
+
end
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
# A messaging campaign registered for carrier review. The status starts
|
|
89
|
+
# +"pending"+ and moves to +"active"+ (numbers can be assigned) or
|
|
90
|
+
# +"failed"+ (see +failure_reasons+); the carrier network may later mark
|
|
91
|
+
# it +"suspended"+ or +"expired"+.
|
|
92
|
+
class TenDlcCampaign
|
|
93
|
+
attr_reader :id, :brand_id, :use_case, :sub_use_cases, :description,
|
|
94
|
+
:status, :sample_messages, :throughput, :failure_reasons,
|
|
95
|
+
:created_at, :updated_at
|
|
96
|
+
|
|
97
|
+
def initialize(data)
|
|
98
|
+
@id = data["id"]
|
|
99
|
+
@brand_id = data["brandId"] || data["brand_id"]
|
|
100
|
+
@use_case = data["useCase"] || data["use_case"]
|
|
101
|
+
@sub_use_cases = data["subUseCases"] || data["sub_use_cases"] || []
|
|
102
|
+
@description = data["description"]
|
|
103
|
+
@status = data["status"]
|
|
104
|
+
@sample_messages = data["sampleMessages"] || data["sample_messages"] || []
|
|
105
|
+
@throughput = data["throughput"] ? TenDlcThroughput.new(data["throughput"]) : nil
|
|
106
|
+
@failure_reasons = data["failureReasons"] || data["failure_reasons"]
|
|
107
|
+
@created_at = data["createdAt"] || data["created_at"]
|
|
108
|
+
@updated_at = data["updatedAt"] || data["updated_at"]
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
def pending?
|
|
112
|
+
status == "pending"
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
def active?
|
|
116
|
+
status == "active"
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
def failed?
|
|
120
|
+
status == "failed"
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
def to_h
|
|
124
|
+
{
|
|
125
|
+
id: id, brand_id: brand_id, use_case: use_case,
|
|
126
|
+
sub_use_cases: sub_use_cases, description: description,
|
|
127
|
+
status: status, sample_messages: sample_messages,
|
|
128
|
+
throughput: throughput&.to_h, failure_reasons: failure_reasons,
|
|
129
|
+
created_at: created_at, updated_at: updated_at
|
|
130
|
+
}.compact
|
|
131
|
+
end
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
# A phone number assigned to a campaign. The number can send once the
|
|
135
|
+
# status is +"Active"+.
|
|
136
|
+
class TenDlcAssignment
|
|
137
|
+
attr_reader :id, :campaign_id, :phone_number, :status, :assigned_at
|
|
138
|
+
|
|
139
|
+
def initialize(data)
|
|
140
|
+
@id = data["id"]
|
|
141
|
+
@campaign_id = data["campaignId"] || data["campaign_id"]
|
|
142
|
+
@phone_number = data["phoneNumber"] || data["phone_number"]
|
|
143
|
+
@status = data["status"]
|
|
144
|
+
@assigned_at = data["assignedAt"] || data["assigned_at"]
|
|
145
|
+
end
|
|
146
|
+
|
|
147
|
+
def active?
|
|
148
|
+
status == "Active"
|
|
149
|
+
end
|
|
150
|
+
|
|
151
|
+
def to_h
|
|
152
|
+
{
|
|
153
|
+
id: id, campaign_id: campaign_id, phone_number: phone_number,
|
|
154
|
+
status: status, assigned_at: assigned_at
|
|
155
|
+
}.compact
|
|
156
|
+
end
|
|
157
|
+
end
|
|
158
|
+
|
|
159
|
+
# 10DLC resource — register your business for carrier review so you can
|
|
160
|
+
# text from local (10-digit) US numbers. The flow has three steps:
|
|
161
|
+
#
|
|
162
|
+
# 1. Register a brand with {#create_brand}, then poll {#get_brand} until
|
|
163
|
+
# it is verified.
|
|
164
|
+
# 2. Create a campaign under the verified brand with {#create_campaign},
|
|
165
|
+
# then poll {#get_campaign} until it is active. {#qualify} pre-checks
|
|
166
|
+
# a use case before you create the campaign.
|
|
167
|
+
# 3. Attach a number you own with {#assign_number}. Once the assignment
|
|
168
|
+
# is "Active", the number can send.
|
|
169
|
+
#
|
|
170
|
+
# Writes require a live API key with the +tendlc:write+ scope.
|
|
171
|
+
#
|
|
172
|
+
# @example Register a brand and poll until it's verified
|
|
173
|
+
# brand = client.ten_dlc.create_brand(
|
|
174
|
+
# legal_name: "Acme Holdings LLC",
|
|
175
|
+
# ein: "12-3456789",
|
|
176
|
+
# website: "https://acme.example",
|
|
177
|
+
# email: "ops@acme.example"
|
|
178
|
+
# )
|
|
179
|
+
# # ...poll client.ten_dlc.get_brand(brand.id) until brand.verified?
|
|
180
|
+
#
|
|
181
|
+
# @example Pre-check the use case, then create a campaign
|
|
182
|
+
# check = client.ten_dlc.qualify(brand.id, "MIXED")
|
|
183
|
+
# if check.qualified?
|
|
184
|
+
# campaign = client.ten_dlc.create_campaign(
|
|
185
|
+
# brand_id: brand.id,
|
|
186
|
+
# use_case: "MIXED",
|
|
187
|
+
# description: "Order updates and support replies for Acme customers",
|
|
188
|
+
# message_flow: "Customers opt in at checkout on acme.example",
|
|
189
|
+
# sample_messages: ["Your order #123 has shipped!"]
|
|
190
|
+
# )
|
|
191
|
+
# # ...poll client.ten_dlc.get_campaign(campaign.id) until campaign.active?
|
|
192
|
+
# end
|
|
193
|
+
#
|
|
194
|
+
# @example Assign a number you own
|
|
195
|
+
# client.ten_dlc.assign_number(campaign.id, phone_number: "+15551234567")
|
|
196
|
+
class TenDlcResource
|
|
197
|
+
def initialize(client)
|
|
198
|
+
@client = client
|
|
199
|
+
end
|
|
200
|
+
|
|
201
|
+
# List the brands registered for carrier review.
|
|
202
|
+
#
|
|
203
|
+
# @return [Hash] +{ brands: Array<TenDlcBrand> }+
|
|
204
|
+
def list_brands
|
|
205
|
+
response = @client.get("/tendlc/brands")
|
|
206
|
+
brands = (response["data"] || []).map { |b| TenDlcBrand.new(b) }
|
|
207
|
+
{ brands: brands }
|
|
208
|
+
end
|
|
209
|
+
|
|
210
|
+
# Register a brand for carrier review — step 1 of enabling local-number
|
|
211
|
+
# texting. Requires a live API key.
|
|
212
|
+
#
|
|
213
|
+
# The brand starts pending. Poll {#get_brand} until it is verified
|
|
214
|
+
# before creating a campaign.
|
|
215
|
+
#
|
|
216
|
+
# @param legal_name [String] Legal business name
|
|
217
|
+
# @param dba [String, nil] "Doing business as" name, if different from the legal name
|
|
218
|
+
# @param ein [String, nil] Business registration number (e.g. EIN)
|
|
219
|
+
# @param entity_type [String, nil] Business entity type (e.g. "PRIVATE_PROFIT",
|
|
220
|
+
# "SOLE_PROPRIETOR"); the API defaults to "PRIVATE_PROFIT"
|
|
221
|
+
# @param vertical [String, nil] Industry vertical
|
|
222
|
+
# @param website [String, nil] Business website URL
|
|
223
|
+
# @param email [String, nil] Business contact email
|
|
224
|
+
# @param phone [String, nil] Business phone number
|
|
225
|
+
# @param mobile_phone [String, nil] Business mobile phone number
|
|
226
|
+
# @param street [String, nil] Street address
|
|
227
|
+
# @param city [String, nil] City
|
|
228
|
+
# @param state [String, nil] State or region
|
|
229
|
+
# @param postal_code [String, nil] Postal code
|
|
230
|
+
# @param country [String, nil] ISO 3166-1 alpha-2 country code; the API defaults to "US"
|
|
231
|
+
# @param verification_id [String, nil] Existing Sendly verification to prefill
|
|
232
|
+
# business details from
|
|
233
|
+
# @return [TenDlcBrand]
|
|
234
|
+
def create_brand(legal_name:, dba: nil, ein: nil, entity_type: nil,
|
|
235
|
+
vertical: nil, website: nil, email: nil, phone: nil,
|
|
236
|
+
mobile_phone: nil, street: nil, city: nil, state: nil,
|
|
237
|
+
postal_code: nil, country: nil, verification_id: nil)
|
|
238
|
+
raise ValidationError, "legal_name is required" if legal_name.nil? || legal_name.to_s.empty?
|
|
239
|
+
|
|
240
|
+
body = { legalName: legal_name }
|
|
241
|
+
body[:dba] = dba if dba
|
|
242
|
+
body[:ein] = ein if ein
|
|
243
|
+
body[:entityType] = entity_type if entity_type
|
|
244
|
+
body[:vertical] = vertical if vertical
|
|
245
|
+
body[:website] = website if website
|
|
246
|
+
body[:email] = email if email
|
|
247
|
+
body[:phone] = phone if phone
|
|
248
|
+
body[:mobilePhone] = mobile_phone if mobile_phone
|
|
249
|
+
body[:street] = street if street
|
|
250
|
+
body[:city] = city if city
|
|
251
|
+
body[:state] = state if state
|
|
252
|
+
body[:postalCode] = postal_code if postal_code
|
|
253
|
+
body[:country] = country if country
|
|
254
|
+
body[:verificationId] = verification_id if verification_id
|
|
255
|
+
|
|
256
|
+
response = @client.post("/tendlc/brands", body)
|
|
257
|
+
TenDlcBrand.new(response["data"] || {})
|
|
258
|
+
end
|
|
259
|
+
|
|
260
|
+
# Fetch one brand. Also refreshes its carrier-review status, so polling
|
|
261
|
+
# this method shows progress (pending -> verified/failed).
|
|
262
|
+
#
|
|
263
|
+
# @param id [String] Brand identifier
|
|
264
|
+
# @return [TenDlcBrand]
|
|
265
|
+
def get_brand(id)
|
|
266
|
+
raise ValidationError, "Brand ID is required" if id.nil? || id.to_s.empty?
|
|
267
|
+
|
|
268
|
+
encoded_id = URI.encode_www_form_component(id)
|
|
269
|
+
response = @client.get("/tendlc/brands/#{encoded_id}")
|
|
270
|
+
TenDlcBrand.new(response["data"] || {})
|
|
271
|
+
end
|
|
272
|
+
|
|
273
|
+
# Pre-check whether a use case qualifies for a brand on the carrier
|
|
274
|
+
# network before creating a campaign.
|
|
275
|
+
#
|
|
276
|
+
# @param brand_id [String] Brand identifier
|
|
277
|
+
# @param use_case [String] Use-case code (e.g. "MIXED", "MARKETING",
|
|
278
|
+
# "ACCOUNT_NOTIFICATION", "2FA")
|
|
279
|
+
# @return [TenDlcQualifyResult]
|
|
280
|
+
def qualify(brand_id, use_case)
|
|
281
|
+
raise ValidationError, "Brand ID is required" if brand_id.nil? || brand_id.to_s.empty?
|
|
282
|
+
raise ValidationError, "use_case is required" if use_case.nil? || use_case.to_s.empty?
|
|
283
|
+
|
|
284
|
+
encoded_id = URI.encode_www_form_component(brand_id)
|
|
285
|
+
encoded_use_case = URI.encode_www_form_component(use_case)
|
|
286
|
+
response = @client.get("/tendlc/brands/#{encoded_id}/qualify/#{encoded_use_case}")
|
|
287
|
+
TenDlcQualifyResult.new(response["data"] || {})
|
|
288
|
+
end
|
|
289
|
+
|
|
290
|
+
# List your messaging campaigns.
|
|
291
|
+
#
|
|
292
|
+
# @return [Hash] +{ campaigns: Array<TenDlcCampaign> }+
|
|
293
|
+
def list_campaigns
|
|
294
|
+
response = @client.get("/tendlc/campaigns")
|
|
295
|
+
campaigns = (response["data"] || []).map { |c| TenDlcCampaign.new(c) }
|
|
296
|
+
{ campaigns: campaigns }
|
|
297
|
+
end
|
|
298
|
+
|
|
299
|
+
# Create a messaging campaign under a verified brand and submit it for
|
|
300
|
+
# carrier review. Requires a live API key.
|
|
301
|
+
#
|
|
302
|
+
# The campaign starts pending. Poll {#get_campaign} until it is active
|
|
303
|
+
# before assigning numbers.
|
|
304
|
+
#
|
|
305
|
+
# @param brand_id [String] The verified brand to create the campaign under
|
|
306
|
+
# @param use_case [String] Primary use-case code (e.g. "MIXED", "MARKETING")
|
|
307
|
+
# @param description [String] What the campaign sends and why
|
|
308
|
+
# @param message_flow [String] How recipients opt in to receive messages
|
|
309
|
+
# @param sample_messages [Array<String>] Example messages the campaign sends
|
|
310
|
+
# (the first 5 are used)
|
|
311
|
+
# @param sub_use_cases [Array<String>, nil] Sub-use-case codes
|
|
312
|
+
# @param opt_in_keywords [String, nil] Comma-separated keywords that opt a recipient in
|
|
313
|
+
# @param opt_out_keywords [String, nil] Comma-separated keywords that opt a recipient out
|
|
314
|
+
# @param help_keywords [String, nil] Comma-separated keywords that request help
|
|
315
|
+
# @param opt_in_message [String, nil] Auto-reply sent on opt-in
|
|
316
|
+
# @param opt_out_message [String, nil] Auto-reply sent on opt-out
|
|
317
|
+
# @param help_message [String, nil] Auto-reply sent on a help request
|
|
318
|
+
# @param embedded_link [Boolean, nil] Whether messages may contain links;
|
|
319
|
+
# the API defaults to true
|
|
320
|
+
# @param embedded_phone [Boolean, nil] Whether messages may contain phone
|
|
321
|
+
# numbers; the API defaults to false
|
|
322
|
+
# @return [TenDlcCampaign]
|
|
323
|
+
def create_campaign(brand_id:, use_case:, description:, message_flow:,
|
|
324
|
+
sample_messages:, sub_use_cases: nil,
|
|
325
|
+
opt_in_keywords: nil, opt_out_keywords: nil,
|
|
326
|
+
help_keywords: nil, opt_in_message: nil,
|
|
327
|
+
opt_out_message: nil, help_message: nil,
|
|
328
|
+
embedded_link: nil, embedded_phone: nil)
|
|
329
|
+
raise ValidationError, "brand_id is required" if brand_id.nil? || brand_id.to_s.empty?
|
|
330
|
+
raise ValidationError, "use_case is required" if use_case.nil? || use_case.to_s.empty?
|
|
331
|
+
raise ValidationError, "description is required" if description.nil? || description.to_s.empty?
|
|
332
|
+
raise ValidationError, "message_flow is required" if message_flow.nil? || message_flow.to_s.empty?
|
|
333
|
+
raise ValidationError, "sample_messages is required" if sample_messages.nil? || sample_messages.empty?
|
|
334
|
+
|
|
335
|
+
body = {
|
|
336
|
+
brandId: brand_id,
|
|
337
|
+
useCase: use_case,
|
|
338
|
+
description: description,
|
|
339
|
+
messageFlow: message_flow,
|
|
340
|
+
sampleMessages: sample_messages
|
|
341
|
+
}
|
|
342
|
+
body[:subUseCases] = sub_use_cases if sub_use_cases
|
|
343
|
+
body[:optInKeywords] = opt_in_keywords if opt_in_keywords
|
|
344
|
+
body[:optOutKeywords] = opt_out_keywords if opt_out_keywords
|
|
345
|
+
body[:helpKeywords] = help_keywords if help_keywords
|
|
346
|
+
body[:optInMessage] = opt_in_message if opt_in_message
|
|
347
|
+
body[:optOutMessage] = opt_out_message if opt_out_message
|
|
348
|
+
body[:helpMessage] = help_message if help_message
|
|
349
|
+
body[:embeddedLink] = embedded_link unless embedded_link.nil?
|
|
350
|
+
body[:embeddedPhone] = embedded_phone unless embedded_phone.nil?
|
|
351
|
+
|
|
352
|
+
response = @client.post("/tendlc/campaigns", body)
|
|
353
|
+
TenDlcCampaign.new(response["data"] || {})
|
|
354
|
+
end
|
|
355
|
+
|
|
356
|
+
# Fetch one campaign. Also refreshes its carrier-review status, so
|
|
357
|
+
# polling this method shows progress (pending -> active) including
|
|
358
|
+
# throughput once carriers approve.
|
|
359
|
+
#
|
|
360
|
+
# @param id [String] Campaign identifier
|
|
361
|
+
# @return [TenDlcCampaign]
|
|
362
|
+
def get_campaign(id)
|
|
363
|
+
raise ValidationError, "Campaign ID is required" if id.nil? || id.to_s.empty?
|
|
364
|
+
|
|
365
|
+
encoded_id = URI.encode_www_form_component(id)
|
|
366
|
+
response = @client.get("/tendlc/campaigns/#{encoded_id}")
|
|
367
|
+
TenDlcCampaign.new(response["data"] || {})
|
|
368
|
+
end
|
|
369
|
+
|
|
370
|
+
# Assign a phone number you own to an active (carrier-approved)
|
|
371
|
+
# campaign, making the number sendable. Requires a live API key.
|
|
372
|
+
#
|
|
373
|
+
# Idempotent — re-assigning the same number to the same campaign
|
|
374
|
+
# returns the existing assignment.
|
|
375
|
+
#
|
|
376
|
+
# @param campaign_id [String] Campaign identifier
|
|
377
|
+
# @param phone_number [String] E.164 number the workspace already owns
|
|
378
|
+
# @return [TenDlcAssignment] The number can send once its status is "Active"
|
|
379
|
+
def assign_number(campaign_id, phone_number:)
|
|
380
|
+
raise ValidationError, "Campaign ID is required" if campaign_id.nil? || campaign_id.to_s.empty?
|
|
381
|
+
raise ValidationError, "phone_number is required" if phone_number.nil? || phone_number.to_s.empty?
|
|
382
|
+
|
|
383
|
+
encoded_id = URI.encode_www_form_component(campaign_id)
|
|
384
|
+
response = @client.post("/tendlc/campaigns/#{encoded_id}/assign", { phoneNumber: phone_number })
|
|
385
|
+
TenDlcAssignment.new(response["data"] || {})
|
|
386
|
+
end
|
|
387
|
+
|
|
388
|
+
# List your number-to-campaign assignments.
|
|
389
|
+
#
|
|
390
|
+
# @return [Hash] +{ assignments: Array<TenDlcAssignment> }+
|
|
391
|
+
def list_assignments
|
|
392
|
+
response = @client.get("/tendlc/assignments")
|
|
393
|
+
assignments = (response["data"] || []).map { |a| TenDlcAssignment.new(a) }
|
|
394
|
+
{ assignments: assignments }
|
|
395
|
+
end
|
|
396
|
+
end
|
|
397
|
+
end
|
data/lib/sendly/types.rb
CHANGED
|
@@ -208,6 +208,85 @@ module Sendly
|
|
|
208
208
|
end
|
|
209
209
|
end
|
|
210
210
|
|
|
211
|
+
# Represents the result of sending a group MMS to 2-8 recipients.
|
|
212
|
+
#
|
|
213
|
+
# Unlike {Message}, +to+ is an array of recipients and the response carries a
|
|
214
|
+
# +group_message_id+ identifying the shared conversation. The raw parsed
|
|
215
|
+
# response is preserved on +#raw+ so callers can read any field the server
|
|
216
|
+
# adds.
|
|
217
|
+
class GroupMessage
|
|
218
|
+
# @return [String] Message id — matches the id in delivery webhooks
|
|
219
|
+
attr_reader :id
|
|
220
|
+
|
|
221
|
+
# @return [String] Delivery status ("sent" on a live send, "delivered" when simulated)
|
|
222
|
+
attr_reader :status
|
|
223
|
+
|
|
224
|
+
# @return [Array<String>] The recipients the group message was sent to
|
|
225
|
+
attr_reader :to
|
|
226
|
+
|
|
227
|
+
# @return [String, nil] Identifier for the group conversation (present on live sends)
|
|
228
|
+
attr_reader :group_message_id
|
|
229
|
+
|
|
230
|
+
# @return [Boolean] True when the send was simulated and nothing reached the carrier
|
|
231
|
+
attr_reader :simulated
|
|
232
|
+
|
|
233
|
+
# @return [String, nil] Human-readable note, present on simulated sends
|
|
234
|
+
attr_reader :message
|
|
235
|
+
|
|
236
|
+
# @return [Hash] The raw parsed response
|
|
237
|
+
attr_reader :raw
|
|
238
|
+
|
|
239
|
+
def initialize(data)
|
|
240
|
+
@raw = data
|
|
241
|
+
@id = data["id"]
|
|
242
|
+
@status = data["status"]
|
|
243
|
+
@to = data["to"] || []
|
|
244
|
+
@group_message_id = data["group_message_id"] || data["groupMessageId"]
|
|
245
|
+
@simulated = data["simulated"] || false
|
|
246
|
+
@message = data["message"]
|
|
247
|
+
end
|
|
248
|
+
|
|
249
|
+
# @return [Boolean] Whether the send was simulated
|
|
250
|
+
def simulated?
|
|
251
|
+
simulated
|
|
252
|
+
end
|
|
253
|
+
|
|
254
|
+
def to_h
|
|
255
|
+
{
|
|
256
|
+
id: id, status: status, to: to,
|
|
257
|
+
group_message_id: group_message_id,
|
|
258
|
+
simulated: simulated, message: message
|
|
259
|
+
}.compact
|
|
260
|
+
end
|
|
261
|
+
end
|
|
262
|
+
|
|
263
|
+
# Represents the result of an AI message enhancement.
|
|
264
|
+
class EnhancedMessage
|
|
265
|
+
# @return [String] The rewritten message, capped at 160 characters (one SMS
|
|
266
|
+
# segment). Falls back to the original text when AI is unavailable.
|
|
267
|
+
attr_reader :enhanced
|
|
268
|
+
|
|
269
|
+
# @return [String] Short explanation of what changed (empty on the fallback path)
|
|
270
|
+
attr_reader :explanation
|
|
271
|
+
|
|
272
|
+
# @return [String, nil] The model that produced the enhancement, when available
|
|
273
|
+
attr_reader :model
|
|
274
|
+
|
|
275
|
+
# @return [Hash] The raw parsed response
|
|
276
|
+
attr_reader :raw
|
|
277
|
+
|
|
278
|
+
def initialize(data)
|
|
279
|
+
@raw = data
|
|
280
|
+
@enhanced = data["enhanced"]
|
|
281
|
+
@explanation = data["explanation"] || ""
|
|
282
|
+
@model = data["model"]
|
|
283
|
+
end
|
|
284
|
+
|
|
285
|
+
def to_h
|
|
286
|
+
{ enhanced: enhanced, explanation: explanation, model: model }.compact
|
|
287
|
+
end
|
|
288
|
+
end
|
|
289
|
+
|
|
211
290
|
# ============================================================================
|
|
212
291
|
# Media
|
|
213
292
|
# ============================================================================
|
data/lib/sendly/version.rb
CHANGED
data/lib/sendly.rb
CHANGED
|
@@ -23,6 +23,8 @@ require_relative "sendly/rules_resource"
|
|
|
23
23
|
require_relative "sendly/enterprise"
|
|
24
24
|
require_relative "sendly/business_upgrade_resource"
|
|
25
25
|
require_relative "sendly/numbers_resource"
|
|
26
|
+
require_relative "sendly/tendlc_resource"
|
|
27
|
+
require_relative "sendly/links_resource"
|
|
26
28
|
|
|
27
29
|
# Sendly Ruby SDK
|
|
28
30
|
#
|
metadata
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: sendly
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 3.
|
|
4
|
+
version: 3.37.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Sendly
|
|
8
8
|
autorequire:
|
|
9
9
|
bindir: exe
|
|
10
10
|
cert_chain: []
|
|
11
|
-
date: 2026-
|
|
11
|
+
date: 2026-07-09 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: faraday
|
|
@@ -134,11 +134,13 @@ files:
|
|
|
134
134
|
- lib/sendly/enterprise.rb
|
|
135
135
|
- lib/sendly/errors.rb
|
|
136
136
|
- lib/sendly/labels_resource.rb
|
|
137
|
+
- lib/sendly/links_resource.rb
|
|
137
138
|
- lib/sendly/media.rb
|
|
138
139
|
- lib/sendly/messages.rb
|
|
139
140
|
- lib/sendly/numbers_resource.rb
|
|
140
141
|
- lib/sendly/rules_resource.rb
|
|
141
142
|
- lib/sendly/templates_resource.rb
|
|
143
|
+
- lib/sendly/tendlc_resource.rb
|
|
142
144
|
- lib/sendly/types.rb
|
|
143
145
|
- lib/sendly/verify.rb
|
|
144
146
|
- lib/sendly/version.rb
|