sendly 3.12.3 → 3.13.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 3411e6ff88aa6e65422703ce8c630a1f114ad43e9b8ebbcae67e0f26b82d599c
4
- data.tar.gz: 502f2fac031f7f9d9af111020d10d4fe5ea1628d90a8c3b9ad9b889afc86f13e
3
+ metadata.gz: a1468fb0c87212a7d6f930ad557fc6b9546eca11a292ac8c7873c9db1d209ac1
4
+ data.tar.gz: bf62a3866fa8c02557d5925d175fe06c0de554c00d0bb371b1178d2eff84f5a0
5
5
  SHA512:
6
- metadata.gz: c2bef1c5b6835f20201cb236236fbcaff13f6e84b64bb0781ec9f6259f394473851fd7ec33d271c1250fcb4895e3390481574bf129f3b0b4f91e201dcc13b627
7
- data.tar.gz: 76d95f0e6397731868ba8d38d4a557fbce5dce849125631aac794739b918da28da2dde502724d5f24d34dfaf55fb9261fec5d2ac78fe45b533afdc77821bb08c
6
+ metadata.gz: acc02947f862c3de0dd2122a44438fee340398d091a9066eab33e8929e153f545b385f906c3831185760d90afefe6e0c2f6464f5357c8ba185e617833f1ff6d8
7
+ data.tar.gz: 97843f126f57e6ce43e75c3d7f9a6658acd2b3b74b805fee08561f0e57ad55664bcb1f5e95cbad2f920dd927ec0d04e71b20ebde5e628ec6ba28ff729010ba01
data/Gemfile.lock CHANGED
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- sendly (3.12.3)
4
+ sendly (3.13.0)
5
5
  faraday (~> 2.0)
6
6
  faraday-retry (~> 2.0)
7
7
 
@@ -0,0 +1,176 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Sendly
4
+ class Campaign
5
+ attr_reader :id, :name, :text, :template_id, :contact_list_ids, :status,
6
+ :recipient_count, :sent_count, :delivered_count, :failed_count,
7
+ :estimated_credits, :credits_used, :scheduled_at, :timezone,
8
+ :started_at, :completed_at, :created_at, :updated_at
9
+
10
+ STATUSES = %w[draft scheduled sending sent paused cancelled failed].freeze
11
+
12
+ def initialize(data)
13
+ @id = data["id"]
14
+ @name = data["name"]
15
+ @text = data["text"]
16
+ @template_id = data["template_id"] || data["templateId"]
17
+ @contact_list_ids = data["contact_list_ids"] || data["contactListIds"] || []
18
+ @status = data["status"]
19
+ @recipient_count = data["recipient_count"] || data["recipientCount"] || 0
20
+ @sent_count = data["sent_count"] || data["sentCount"] || 0
21
+ @delivered_count = data["delivered_count"] || data["deliveredCount"] || 0
22
+ @failed_count = data["failed_count"] || data["failedCount"] || 0
23
+ @estimated_credits = data["estimated_credits"] || data["estimatedCredits"] || 0
24
+ @credits_used = data["credits_used"] || data["creditsUsed"] || 0
25
+ @scheduled_at = parse_time(data["scheduled_at"] || data["scheduledAt"])
26
+ @timezone = data["timezone"]
27
+ @started_at = parse_time(data["started_at"] || data["startedAt"])
28
+ @completed_at = parse_time(data["completed_at"] || data["completedAt"])
29
+ @created_at = parse_time(data["created_at"] || data["createdAt"])
30
+ @updated_at = parse_time(data["updated_at"] || data["updatedAt"])
31
+ end
32
+
33
+ def draft?
34
+ status == "draft"
35
+ end
36
+
37
+ def scheduled?
38
+ status == "scheduled"
39
+ end
40
+
41
+ def sending?
42
+ status == "sending"
43
+ end
44
+
45
+ def sent?
46
+ status == "sent"
47
+ end
48
+
49
+ def cancelled?
50
+ status == "cancelled"
51
+ end
52
+
53
+ def to_h
54
+ {
55
+ id: id, name: name, text: text, template_id: template_id,
56
+ contact_list_ids: contact_list_ids, status: status,
57
+ recipient_count: recipient_count, sent_count: sent_count,
58
+ delivered_count: delivered_count, failed_count: failed_count,
59
+ estimated_credits: estimated_credits, credits_used: credits_used,
60
+ scheduled_at: scheduled_at&.iso8601, timezone: timezone,
61
+ started_at: started_at&.iso8601, completed_at: completed_at&.iso8601,
62
+ created_at: created_at&.iso8601, updated_at: updated_at&.iso8601
63
+ }.compact
64
+ end
65
+
66
+ private
67
+
68
+ def parse_time(value)
69
+ return nil if value.nil?
70
+ Time.parse(value)
71
+ rescue ArgumentError
72
+ nil
73
+ end
74
+ end
75
+
76
+ class CampaignPreview
77
+ attr_reader :id, :recipient_count, :estimated_segments, :estimated_credits,
78
+ :current_balance, :has_enough_credits, :breakdown
79
+
80
+ def initialize(data)
81
+ @id = data["id"]
82
+ @recipient_count = data["recipient_count"] || data["recipientCount"] || 0
83
+ @estimated_segments = data["estimated_segments"] || data["estimatedSegments"] || 0
84
+ @estimated_credits = data["estimated_credits"] || data["estimatedCredits"] || 0
85
+ @current_balance = data["current_balance"] || data["currentBalance"] || 0
86
+ @has_enough_credits = data["has_enough_credits"] || data["hasEnoughCredits"] || false
87
+ @breakdown = data["breakdown"]
88
+ end
89
+
90
+ def enough_credits?
91
+ has_enough_credits
92
+ end
93
+ end
94
+
95
+ class CampaignsResource
96
+ def initialize(client)
97
+ @client = client
98
+ end
99
+
100
+ def create(name:, text:, contact_list_ids:, template_id: nil)
101
+ body = {
102
+ name: name,
103
+ text: text,
104
+ contactListIds: contact_list_ids
105
+ }
106
+ body[:templateId] = template_id if template_id
107
+
108
+ response = @client.post("/campaigns", body)
109
+ Campaign.new(response)
110
+ end
111
+
112
+ def list(limit: nil, offset: nil, status: nil)
113
+ params = {}
114
+ params[:limit] = limit if limit
115
+ params[:offset] = offset if offset
116
+ params[:status] = status if status
117
+
118
+ response = @client.get("/campaigns", params)
119
+ campaigns = (response["campaigns"] || []).map { |c| Campaign.new(c) }
120
+ {
121
+ campaigns: campaigns,
122
+ total: response["total"],
123
+ limit: response["limit"],
124
+ offset: response["offset"]
125
+ }
126
+ end
127
+
128
+ def get(id)
129
+ response = @client.get("/campaigns/#{id}")
130
+ Campaign.new(response)
131
+ end
132
+
133
+ def update(id, name: nil, text: nil, template_id: nil, contact_list_ids: nil)
134
+ body = {}
135
+ body[:name] = name if name
136
+ body[:text] = text if text
137
+ body[:templateId] = template_id unless template_id.nil?
138
+ body[:contactListIds] = contact_list_ids if contact_list_ids
139
+
140
+ response = @client.patch("/campaigns/#{id}", body)
141
+ Campaign.new(response)
142
+ end
143
+
144
+ def delete(id)
145
+ @client.delete("/campaigns/#{id}")
146
+ end
147
+
148
+ def preview(id)
149
+ response = @client.get("/campaigns/#{id}/preview")
150
+ CampaignPreview.new(response)
151
+ end
152
+
153
+ def send_campaign(id)
154
+ response = @client.post("/campaigns/#{id}/send")
155
+ Campaign.new(response)
156
+ end
157
+
158
+ def schedule(id, scheduled_at:, timezone: nil)
159
+ body = { scheduledAt: scheduled_at }
160
+ body[:timezone] = timezone if timezone
161
+
162
+ response = @client.post("/campaigns/#{id}/schedule", body)
163
+ Campaign.new(response)
164
+ end
165
+
166
+ def cancel(id)
167
+ response = @client.post("/campaigns/#{id}/cancel")
168
+ Campaign.new(response)
169
+ end
170
+
171
+ def clone(id)
172
+ response = @client.post("/campaigns/#{id}/clone")
173
+ Campaign.new(response)
174
+ end
175
+ end
176
+ end
data/lib/sendly/client.rb CHANGED
@@ -73,6 +73,20 @@ module Sendly
73
73
  @templates ||= TemplatesResource.new(self)
74
74
  end
75
75
 
76
+ # Access the Campaigns resource
77
+ #
78
+ # @return [Sendly::CampaignsResource]
79
+ def campaigns
80
+ @campaigns ||= CampaignsResource.new(self)
81
+ end
82
+
83
+ # Access the Contacts resource
84
+ #
85
+ # @return [Sendly::ContactsResource]
86
+ def contacts
87
+ @contacts ||= ContactsResource.new(self)
88
+ end
89
+
76
90
  # Make a GET request
77
91
  #
78
92
  # @param path [String] API path
@@ -0,0 +1,189 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Sendly
4
+ class Contact
5
+ attr_reader :id, :phone_number, :name, :email, :metadata, :opted_out,
6
+ :created_at, :updated_at, :lists
7
+
8
+ def initialize(data)
9
+ @id = data["id"]
10
+ @phone_number = data["phone_number"] || data["phoneNumber"]
11
+ @name = data["name"]
12
+ @email = data["email"]
13
+ @metadata = data["metadata"] || {}
14
+ @opted_out = data["opted_out"] || data["optedOut"] || false
15
+ @created_at = parse_time(data["created_at"] || data["createdAt"])
16
+ @updated_at = parse_time(data["updated_at"] || data["updatedAt"])
17
+ @lists = data["lists"]&.map { |l| { id: l["id"], name: l["name"] } }
18
+ end
19
+
20
+ def opted_out?
21
+ opted_out
22
+ end
23
+
24
+ def to_h
25
+ {
26
+ id: id, phone_number: phone_number, name: name, email: email,
27
+ metadata: metadata, opted_out: opted_out,
28
+ created_at: created_at&.iso8601, updated_at: updated_at&.iso8601,
29
+ lists: lists
30
+ }.compact
31
+ end
32
+
33
+ private
34
+
35
+ def parse_time(value)
36
+ return nil if value.nil?
37
+ Time.parse(value)
38
+ rescue ArgumentError
39
+ nil
40
+ end
41
+ end
42
+
43
+ class ContactList
44
+ attr_reader :id, :name, :description, :contact_count, :created_at,
45
+ :updated_at, :contacts, :contacts_total
46
+
47
+ def initialize(data)
48
+ @id = data["id"]
49
+ @name = data["name"]
50
+ @description = data["description"]
51
+ @contact_count = data["contact_count"] || data["contactCount"] || 0
52
+ @created_at = parse_time(data["created_at"] || data["createdAt"])
53
+ @updated_at = parse_time(data["updated_at"] || data["updatedAt"])
54
+ @contacts = data["contacts"]&.map do |c|
55
+ {
56
+ id: c["id"],
57
+ phone_number: c["phone_number"] || c["phoneNumber"],
58
+ name: c["name"],
59
+ email: c["email"]
60
+ }
61
+ end
62
+ @contacts_total = data["contacts_total"] || data["contactsTotal"]
63
+ end
64
+
65
+ def to_h
66
+ {
67
+ id: id, name: name, description: description,
68
+ contact_count: contact_count, created_at: created_at&.iso8601,
69
+ updated_at: updated_at&.iso8601, contacts: contacts,
70
+ contacts_total: contacts_total
71
+ }.compact
72
+ end
73
+
74
+ private
75
+
76
+ def parse_time(value)
77
+ return nil if value.nil?
78
+ Time.parse(value)
79
+ rescue ArgumentError
80
+ nil
81
+ end
82
+ end
83
+
84
+ class ContactListsResource
85
+ def initialize(client)
86
+ @client = client
87
+ end
88
+
89
+ def list
90
+ response = @client.get("/contact-lists")
91
+ lists = (response["lists"] || []).map { |l| ContactList.new(l) }
92
+ { lists: lists }
93
+ end
94
+
95
+ def get(id, limit: nil, offset: nil)
96
+ params = {}
97
+ params[:limit] = limit if limit
98
+ params[:offset] = offset if offset
99
+
100
+ response = @client.get("/contact-lists/#{id}", params)
101
+ ContactList.new(response)
102
+ end
103
+
104
+ def create(name:, description: nil)
105
+ body = { name: name }
106
+ body[:description] = description if description
107
+
108
+ response = @client.post("/contact-lists", body)
109
+ ContactList.new(response)
110
+ end
111
+
112
+ def update(id, name: nil, description: nil)
113
+ body = {}
114
+ body[:name] = name if name
115
+ body[:description] = description unless description.nil?
116
+
117
+ response = @client.patch("/contact-lists/#{id}", body)
118
+ ContactList.new(response)
119
+ end
120
+
121
+ def delete(id)
122
+ @client.delete("/contact-lists/#{id}")
123
+ end
124
+
125
+ def add_contacts(list_id, contact_ids)
126
+ response = @client.post("/contact-lists/#{list_id}/contacts", { contact_ids: contact_ids })
127
+ { added_count: response["added_count"] || response["addedCount"] }
128
+ end
129
+
130
+ def remove_contact(list_id, contact_id)
131
+ @client.delete("/contact-lists/#{list_id}/contacts/#{contact_id}")
132
+ end
133
+ end
134
+
135
+ class ContactsResource
136
+ attr_reader :lists
137
+
138
+ def initialize(client)
139
+ @client = client
140
+ @lists = ContactListsResource.new(client)
141
+ end
142
+
143
+ def list(limit: nil, offset: nil, search: nil, list_id: nil)
144
+ params = {}
145
+ params[:limit] = limit if limit
146
+ params[:offset] = offset if offset
147
+ params[:search] = search if search
148
+ params[:list_id] = list_id if list_id
149
+
150
+ response = @client.get("/contacts", params)
151
+ contacts = (response["contacts"] || []).map { |c| Contact.new(c) }
152
+ {
153
+ contacts: contacts,
154
+ total: response["total"],
155
+ limit: response["limit"],
156
+ offset: response["offset"]
157
+ }
158
+ end
159
+
160
+ def get(id)
161
+ response = @client.get("/contacts/#{id}")
162
+ Contact.new(response)
163
+ end
164
+
165
+ def create(phone_number:, name: nil, email: nil, metadata: nil)
166
+ body = { phone_number: phone_number }
167
+ body[:name] = name if name
168
+ body[:email] = email if email
169
+ body[:metadata] = metadata if metadata
170
+
171
+ response = @client.post("/contacts", body)
172
+ Contact.new(response)
173
+ end
174
+
175
+ def update(id, name: nil, email: nil, metadata: nil)
176
+ body = {}
177
+ body[:name] = name unless name.nil?
178
+ body[:email] = email unless email.nil?
179
+ body[:metadata] = metadata unless metadata.nil?
180
+
181
+ response = @client.patch("/contacts/#{id}", body)
182
+ Contact.new(response)
183
+ end
184
+
185
+ def delete(id)
186
+ @client.delete("/contacts/#{id}")
187
+ end
188
+ end
189
+ end
@@ -104,5 +104,12 @@ module Sendly
104
104
  response = @client.post("/verify/templates/#{id}/unpublish")
105
105
  Template.new(response)
106
106
  end
107
+
108
+ def clone(id, name: nil)
109
+ body = {}
110
+ body[:name] = name if name
111
+ response = @client.post("/templates/#{id}/clone", body)
112
+ Template.new(response)
113
+ end
107
114
  end
108
115
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Sendly
4
- VERSION = "3.12.3"
4
+ VERSION = "3.13.0"
5
5
  end
data/lib/sendly.rb CHANGED
@@ -13,6 +13,8 @@ require_relative "sendly/webhooks_resource"
13
13
  require_relative "sendly/account_resource"
14
14
  require_relative "sendly/verify"
15
15
  require_relative "sendly/templates_resource"
16
+ require_relative "sendly/campaigns_resource"
17
+ require_relative "sendly/contacts_resource"
16
18
 
17
19
  # Sendly Ruby SDK
18
20
  #
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.12.3
4
+ version: 3.13.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-01-15 00:00:00.000000000 Z
11
+ date: 2026-01-29 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: faraday
@@ -124,7 +124,9 @@ files:
124
124
  - examples/send_sms.rb
125
125
  - lib/sendly.rb
126
126
  - lib/sendly/account_resource.rb
127
+ - lib/sendly/campaigns_resource.rb
127
128
  - lib/sendly/client.rb
129
+ - lib/sendly/contacts_resource.rb
128
130
  - lib/sendly/errors.rb
129
131
  - lib/sendly/messages.rb
130
132
  - lib/sendly/templates_resource.rb