sendly 3.21.1 → 3.23.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/lib/sendly/client.rb +21 -0
- data/lib/sendly/conversations_resource.rb +113 -0
- data/lib/sendly/drafts_resource.rb +66 -0
- data/lib/sendly/labels_resource.rb +29 -0
- data/lib/sendly/types.rb +253 -2
- data/lib/sendly/version.rb +1 -1
- data/lib/sendly.rb +3 -0
- metadata +5 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 2dc2c09c04faa2543613b476696a2106e1a7e2edd16a788bd0f16bb3ba8c3c56
|
|
4
|
+
data.tar.gz: d9a9c53078fb5a52272abaaf989cec2c95f59cc73d0c7046a3018371c1df3434
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 00a1a0fbbcd66dc72009109dcb3dae50615c59a01cf1290461d1a0d95857f117fad8190e3ffaa7641810abb624c9eab145f2b1e942d19953ef02c9ef9df57a1b
|
|
7
|
+
data.tar.gz: b75222058daa05a0e408cf4f054e68c7dfb83203894daabfdf42e1fc1f5cd50fec806bc2fe875eb8199df042df1c2a14240779298387c69159bdd12129a58afd
|
data/Gemfile.lock
CHANGED
data/lib/sendly/client.rb
CHANGED
|
@@ -100,6 +100,27 @@ module Sendly
|
|
|
100
100
|
@contacts ||= ContactsResource.new(self)
|
|
101
101
|
end
|
|
102
102
|
|
|
103
|
+
# Access the Conversations resource
|
|
104
|
+
#
|
|
105
|
+
# @return [Sendly::ConversationsResource]
|
|
106
|
+
def conversations
|
|
107
|
+
@conversations ||= ConversationsResource.new(self)
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
# Access the Labels resource
|
|
111
|
+
#
|
|
112
|
+
# @return [Sendly::LabelsResource]
|
|
113
|
+
def labels
|
|
114
|
+
@labels ||= LabelsResource.new(self)
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
# Access the Drafts resource
|
|
118
|
+
#
|
|
119
|
+
# @return [Sendly::DraftsResource]
|
|
120
|
+
def drafts
|
|
121
|
+
@drafts ||= DraftsResource.new(self)
|
|
122
|
+
end
|
|
123
|
+
|
|
103
124
|
# Access the Enterprise resource
|
|
104
125
|
#
|
|
105
126
|
# @return [Sendly::EnterpriseResource]
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Sendly
|
|
4
|
+
class ConversationsResource
|
|
5
|
+
def initialize(client)
|
|
6
|
+
@client = client
|
|
7
|
+
end
|
|
8
|
+
|
|
9
|
+
def list(limit: 20, offset: 0, status: nil)
|
|
10
|
+
params = {
|
|
11
|
+
limit: [limit, 100].min,
|
|
12
|
+
offset: offset
|
|
13
|
+
}
|
|
14
|
+
params[:status] = status if status
|
|
15
|
+
|
|
16
|
+
response = @client.get("/conversations", params.compact)
|
|
17
|
+
ConversationList.new(response)
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def get(id, include_messages: false, message_limit: nil, message_offset: nil)
|
|
21
|
+
raise ValidationError, "Conversation ID is required" if id.nil? || id.empty?
|
|
22
|
+
|
|
23
|
+
params = {}
|
|
24
|
+
params[:include_messages] = true if include_messages
|
|
25
|
+
params[:message_limit] = message_limit if message_limit
|
|
26
|
+
params[:message_offset] = message_offset if message_offset
|
|
27
|
+
|
|
28
|
+
encoded_id = URI.encode_www_form_component(id)
|
|
29
|
+
response = @client.get("/conversations/#{encoded_id}", params.compact)
|
|
30
|
+
ConversationWithMessages.new(response)
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def reply(id, text:, media_urls: nil, metadata: nil)
|
|
34
|
+
raise ValidationError, "Conversation ID is required" if id.nil? || id.empty?
|
|
35
|
+
raise ValidationError, "Message text is required" if text.nil? || text.empty?
|
|
36
|
+
|
|
37
|
+
body = { text: text }
|
|
38
|
+
body[:mediaUrls] = media_urls if media_urls
|
|
39
|
+
body[:metadata] = metadata if metadata
|
|
40
|
+
|
|
41
|
+
encoded_id = URI.encode_www_form_component(id)
|
|
42
|
+
response = @client.post("/conversations/#{encoded_id}/messages", body)
|
|
43
|
+
Message.new(response)
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def update(id, metadata: nil, tags: nil)
|
|
47
|
+
raise ValidationError, "Conversation ID is required" if id.nil? || id.empty?
|
|
48
|
+
|
|
49
|
+
body = {}
|
|
50
|
+
body[:metadata] = metadata unless metadata.nil?
|
|
51
|
+
body[:tags] = tags unless tags.nil?
|
|
52
|
+
|
|
53
|
+
encoded_id = URI.encode_www_form_component(id)
|
|
54
|
+
response = @client.patch("/conversations/#{encoded_id}", body)
|
|
55
|
+
Conversation.new(response)
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def close(id)
|
|
59
|
+
raise ValidationError, "Conversation ID is required" if id.nil? || id.empty?
|
|
60
|
+
|
|
61
|
+
encoded_id = URI.encode_www_form_component(id)
|
|
62
|
+
response = @client.post("/conversations/#{encoded_id}/close")
|
|
63
|
+
Conversation.new(response)
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
def reopen(id)
|
|
67
|
+
raise ValidationError, "Conversation ID is required" if id.nil? || id.empty?
|
|
68
|
+
|
|
69
|
+
encoded_id = URI.encode_www_form_component(id)
|
|
70
|
+
response = @client.post("/conversations/#{encoded_id}/reopen")
|
|
71
|
+
Conversation.new(response)
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
def mark_read(id)
|
|
75
|
+
raise ValidationError, "Conversation ID is required" if id.nil? || id.empty?
|
|
76
|
+
|
|
77
|
+
encoded_id = URI.encode_www_form_component(id)
|
|
78
|
+
response = @client.post("/conversations/#{encoded_id}/mark-read")
|
|
79
|
+
Conversation.new(response)
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
def add_labels(id, label_ids:)
|
|
83
|
+
raise ValidationError, "Conversation ID is required" if id.nil? || id.empty?
|
|
84
|
+
raise ValidationError, "Label IDs are required" if label_ids.nil? || label_ids.empty?
|
|
85
|
+
|
|
86
|
+
encoded_id = URI.encode_www_form_component(id)
|
|
87
|
+
@client.post("/conversations/#{encoded_id}/labels", { labelIds: label_ids })
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
def remove_label(id, label_id:)
|
|
91
|
+
raise ValidationError, "Conversation ID is required" if id.nil? || id.empty?
|
|
92
|
+
raise ValidationError, "Label ID is required" if label_id.nil? || label_id.empty?
|
|
93
|
+
|
|
94
|
+
encoded_id = URI.encode_www_form_component(id)
|
|
95
|
+
encoded_label_id = URI.encode_www_form_component(label_id)
|
|
96
|
+
@client.delete("/conversations/#{encoded_id}/labels/#{encoded_label_id}")
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
def each(status: nil, batch_size: 100, &block)
|
|
100
|
+
return enum_for(:each, status: status, batch_size: batch_size) unless block_given?
|
|
101
|
+
|
|
102
|
+
offset = 0
|
|
103
|
+
loop do
|
|
104
|
+
page = list(limit: batch_size, offset: offset, status: status)
|
|
105
|
+
page.each(&block)
|
|
106
|
+
|
|
107
|
+
break unless page.has_more
|
|
108
|
+
|
|
109
|
+
offset += batch_size
|
|
110
|
+
end
|
|
111
|
+
end
|
|
112
|
+
end
|
|
113
|
+
end
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Sendly
|
|
4
|
+
class DraftsResource
|
|
5
|
+
def initialize(client)
|
|
6
|
+
@client = client
|
|
7
|
+
end
|
|
8
|
+
|
|
9
|
+
def create(conversation_id:, text:, media_urls: nil, metadata: nil, source: nil)
|
|
10
|
+
body = { conversationId: conversation_id, text: text }
|
|
11
|
+
body[:mediaUrls] = media_urls if media_urls
|
|
12
|
+
body[:metadata] = metadata if metadata
|
|
13
|
+
body[:source] = source if source
|
|
14
|
+
|
|
15
|
+
response = @client.post("/drafts", body)
|
|
16
|
+
Draft.new(response)
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def list(conversation_id: nil, status: nil, limit: nil, offset: nil)
|
|
20
|
+
params = {}
|
|
21
|
+
params[:conversation_id] = conversation_id if conversation_id
|
|
22
|
+
params[:status] = status if status
|
|
23
|
+
params[:limit] = limit if limit
|
|
24
|
+
params[:offset] = offset if offset
|
|
25
|
+
|
|
26
|
+
response = @client.get("/drafts", params.compact)
|
|
27
|
+
DraftList.new(response)
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def get(id)
|
|
31
|
+
raise ValidationError, "Draft ID is required" if id.nil? || id.empty?
|
|
32
|
+
|
|
33
|
+
response = @client.get("/drafts/#{URI.encode_www_form_component(id)}")
|
|
34
|
+
Draft.new(response)
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def update(id, text: nil, media_urls: nil, metadata: nil)
|
|
38
|
+
raise ValidationError, "Draft ID is required" if id.nil? || id.empty?
|
|
39
|
+
|
|
40
|
+
body = {}
|
|
41
|
+
body[:text] = text if text
|
|
42
|
+
body[:mediaUrls] = media_urls if media_urls
|
|
43
|
+
body[:metadata] = metadata unless metadata.nil?
|
|
44
|
+
|
|
45
|
+
response = @client.patch("/drafts/#{URI.encode_www_form_component(id)}", body)
|
|
46
|
+
Draft.new(response)
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def approve(id)
|
|
50
|
+
raise ValidationError, "Draft ID is required" if id.nil? || id.empty?
|
|
51
|
+
|
|
52
|
+
response = @client.post("/drafts/#{URI.encode_www_form_component(id)}/approve")
|
|
53
|
+
Draft.new(response)
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def reject(id, reason: nil)
|
|
57
|
+
raise ValidationError, "Draft ID is required" if id.nil? || id.empty?
|
|
58
|
+
|
|
59
|
+
body = {}
|
|
60
|
+
body[:reason] = reason if reason
|
|
61
|
+
|
|
62
|
+
response = @client.post("/drafts/#{URI.encode_www_form_component(id)}/reject", body)
|
|
63
|
+
Draft.new(response)
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
end
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Sendly
|
|
4
|
+
class LabelsResource
|
|
5
|
+
def initialize(client)
|
|
6
|
+
@client = client
|
|
7
|
+
end
|
|
8
|
+
|
|
9
|
+
def create(name:, color: nil, description: nil)
|
|
10
|
+
body = { name: name }
|
|
11
|
+
body[:color] = color if color
|
|
12
|
+
body[:description] = description if description
|
|
13
|
+
|
|
14
|
+
response = @client.post("/labels", body)
|
|
15
|
+
Label.new(response)
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def list
|
|
19
|
+
response = @client.get("/labels")
|
|
20
|
+
(response["data"] || []).map { |l| Label.new(l) }
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def delete(id)
|
|
24
|
+
raise ValidationError, "Label ID is required" if id.nil? || id.empty?
|
|
25
|
+
|
|
26
|
+
@client.delete("/labels/#{URI.encode_www_form_component(id)}")
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
end
|
data/lib/sendly/types.rb
CHANGED
|
@@ -36,7 +36,7 @@ module Sendly
|
|
|
36
36
|
# @return [String, nil] How the message was sent (number_pool, alphanumeric, sandbox)
|
|
37
37
|
attr_reader :sender_type
|
|
38
38
|
|
|
39
|
-
# @return [String, nil]
|
|
39
|
+
# @return [String, nil] Carrier message ID for tracking
|
|
40
40
|
attr_reader :telnyx_message_id
|
|
41
41
|
|
|
42
42
|
# @return [String, nil] Warning message
|
|
@@ -60,6 +60,9 @@ module Sendly
|
|
|
60
60
|
# @return [Hash, nil] Custom metadata attached to the message
|
|
61
61
|
attr_reader :metadata
|
|
62
62
|
|
|
63
|
+
# @return [Hash, nil] AI classification metadata for inbound messages
|
|
64
|
+
attr_reader :ai_metadata
|
|
65
|
+
|
|
63
66
|
# Message status constants (sending removed - doesn't exist in database)
|
|
64
67
|
STATUSES = %w[queued sent delivered failed bounced retrying].freeze
|
|
65
68
|
|
|
@@ -86,6 +89,7 @@ module Sendly
|
|
|
86
89
|
@error_code = data["errorCode"]
|
|
87
90
|
@retry_count = data["retryCount"] || 0
|
|
88
91
|
@metadata = data["metadata"]
|
|
92
|
+
@ai_metadata = data["aiMetadata"]
|
|
89
93
|
end
|
|
90
94
|
|
|
91
95
|
# Check if message was delivered
|
|
@@ -128,7 +132,8 @@ module Sendly
|
|
|
128
132
|
delivered_at: delivered_at&.iso8601,
|
|
129
133
|
error_code: error_code,
|
|
130
134
|
retry_count: retry_count,
|
|
131
|
-
metadata: metadata
|
|
135
|
+
metadata: metadata,
|
|
136
|
+
ai_metadata: ai_metadata
|
|
132
137
|
}.compact
|
|
133
138
|
end
|
|
134
139
|
|
|
@@ -495,4 +500,250 @@ module Sendly
|
|
|
495
500
|
nil
|
|
496
501
|
end
|
|
497
502
|
end
|
|
503
|
+
|
|
504
|
+
# ============================================================================
|
|
505
|
+
# Conversations
|
|
506
|
+
# ============================================================================
|
|
507
|
+
|
|
508
|
+
class Conversation
|
|
509
|
+
attr_reader :id, :phone_number, :status, :unread_count, :message_count,
|
|
510
|
+
:last_message_text, :last_message_at, :last_message_direction,
|
|
511
|
+
:metadata, :tags, :contact_id, :created_at, :updated_at
|
|
512
|
+
|
|
513
|
+
STATUSES = %w[active closed].freeze
|
|
514
|
+
|
|
515
|
+
def initialize(data)
|
|
516
|
+
@id = data["id"]
|
|
517
|
+
@phone_number = data["phoneNumber"] || data["phone_number"]
|
|
518
|
+
@status = data["status"]
|
|
519
|
+
@unread_count = data["unreadCount"] || data["unread_count"] || 0
|
|
520
|
+
@message_count = data["messageCount"] || data["message_count"] || 0
|
|
521
|
+
@last_message_text = data["lastMessageText"] || data["last_message_text"]
|
|
522
|
+
@last_message_at = parse_time(data["lastMessageAt"] || data["last_message_at"])
|
|
523
|
+
@last_message_direction = data["lastMessageDirection"] || data["last_message_direction"]
|
|
524
|
+
@metadata = data["metadata"] || {}
|
|
525
|
+
@tags = data["tags"] || []
|
|
526
|
+
@contact_id = data["contactId"] || data["contact_id"]
|
|
527
|
+
@created_at = parse_time(data["createdAt"] || data["created_at"])
|
|
528
|
+
@updated_at = parse_time(data["updatedAt"] || data["updated_at"])
|
|
529
|
+
end
|
|
530
|
+
|
|
531
|
+
def active?
|
|
532
|
+
status == "active"
|
|
533
|
+
end
|
|
534
|
+
|
|
535
|
+
def closed?
|
|
536
|
+
status == "closed"
|
|
537
|
+
end
|
|
538
|
+
|
|
539
|
+
def to_h
|
|
540
|
+
{
|
|
541
|
+
id: id, phone_number: phone_number, status: status,
|
|
542
|
+
unread_count: unread_count, message_count: message_count,
|
|
543
|
+
last_message_text: last_message_text,
|
|
544
|
+
last_message_at: last_message_at&.iso8601,
|
|
545
|
+
last_message_direction: last_message_direction,
|
|
546
|
+
metadata: metadata, tags: tags, contact_id: contact_id,
|
|
547
|
+
created_at: created_at&.iso8601, updated_at: updated_at&.iso8601
|
|
548
|
+
}.compact
|
|
549
|
+
end
|
|
550
|
+
|
|
551
|
+
private
|
|
552
|
+
|
|
553
|
+
def parse_time(value)
|
|
554
|
+
return nil if value.nil?
|
|
555
|
+
Time.parse(value)
|
|
556
|
+
rescue ArgumentError
|
|
557
|
+
nil
|
|
558
|
+
end
|
|
559
|
+
end
|
|
560
|
+
|
|
561
|
+
class ConversationList
|
|
562
|
+
include Enumerable
|
|
563
|
+
|
|
564
|
+
attr_reader :data, :total, :limit, :offset, :has_more
|
|
565
|
+
|
|
566
|
+
def initialize(response)
|
|
567
|
+
@data = (response["data"] || []).map { |c| Conversation.new(c) }
|
|
568
|
+
pagination = response["pagination"] || {}
|
|
569
|
+
@total = pagination["total"] || @data.length
|
|
570
|
+
@limit = pagination["limit"] || 20
|
|
571
|
+
@offset = pagination["offset"] || 0
|
|
572
|
+
@has_more = pagination["hasMore"] || pagination["has_more"] || false
|
|
573
|
+
end
|
|
574
|
+
|
|
575
|
+
def each(&block)
|
|
576
|
+
data.each(&block)
|
|
577
|
+
end
|
|
578
|
+
|
|
579
|
+
def count
|
|
580
|
+
data.length
|
|
581
|
+
end
|
|
582
|
+
|
|
583
|
+
alias size count
|
|
584
|
+
alias length count
|
|
585
|
+
|
|
586
|
+
def empty?
|
|
587
|
+
data.empty?
|
|
588
|
+
end
|
|
589
|
+
|
|
590
|
+
def first
|
|
591
|
+
data.first
|
|
592
|
+
end
|
|
593
|
+
|
|
594
|
+
def last
|
|
595
|
+
data.last
|
|
596
|
+
end
|
|
597
|
+
end
|
|
598
|
+
|
|
599
|
+
# ============================================================================
|
|
600
|
+
# Labels
|
|
601
|
+
# ============================================================================
|
|
602
|
+
|
|
603
|
+
class Label
|
|
604
|
+
attr_reader :id, :name, :color, :description, :created_at
|
|
605
|
+
|
|
606
|
+
def initialize(data)
|
|
607
|
+
@id = data["id"]
|
|
608
|
+
@name = data["name"]
|
|
609
|
+
@color = data["color"]
|
|
610
|
+
@description = data["description"]
|
|
611
|
+
@created_at = parse_time(data["createdAt"] || data["created_at"])
|
|
612
|
+
end
|
|
613
|
+
|
|
614
|
+
def to_h
|
|
615
|
+
{
|
|
616
|
+
id: id, name: name, color: color, description: description,
|
|
617
|
+
created_at: created_at&.iso8601
|
|
618
|
+
}.compact
|
|
619
|
+
end
|
|
620
|
+
|
|
621
|
+
private
|
|
622
|
+
|
|
623
|
+
def parse_time(value)
|
|
624
|
+
return nil if value.nil?
|
|
625
|
+
Time.parse(value)
|
|
626
|
+
rescue ArgumentError
|
|
627
|
+
nil
|
|
628
|
+
end
|
|
629
|
+
end
|
|
630
|
+
|
|
631
|
+
# ============================================================================
|
|
632
|
+
# Drafts
|
|
633
|
+
# ============================================================================
|
|
634
|
+
|
|
635
|
+
class Draft
|
|
636
|
+
attr_reader :id, :conversation_id, :text, :media_urls, :metadata, :status,
|
|
637
|
+
:source, :created_by, :reviewed_by, :reviewed_at,
|
|
638
|
+
:rejection_reason, :message_id, :created_at, :updated_at
|
|
639
|
+
|
|
640
|
+
STATUSES = %w[pending approved rejected sent failed].freeze
|
|
641
|
+
|
|
642
|
+
def initialize(data)
|
|
643
|
+
@id = data["id"]
|
|
644
|
+
@conversation_id = data["conversationId"] || data["conversation_id"]
|
|
645
|
+
@text = data["text"]
|
|
646
|
+
@media_urls = data["mediaUrls"] || data["media_urls"] || []
|
|
647
|
+
@metadata = data["metadata"] || {}
|
|
648
|
+
@status = data["status"]
|
|
649
|
+
@source = data["source"]
|
|
650
|
+
@created_by = data["createdBy"] || data["created_by"]
|
|
651
|
+
@reviewed_by = data["reviewedBy"] || data["reviewed_by"]
|
|
652
|
+
@reviewed_at = parse_time(data["reviewedAt"] || data["reviewed_at"])
|
|
653
|
+
@rejection_reason = data["rejectionReason"] || data["rejection_reason"]
|
|
654
|
+
@message_id = data["messageId"] || data["message_id"]
|
|
655
|
+
@created_at = parse_time(data["createdAt"] || data["created_at"])
|
|
656
|
+
@updated_at = parse_time(data["updatedAt"] || data["updated_at"])
|
|
657
|
+
end
|
|
658
|
+
|
|
659
|
+
def pending?
|
|
660
|
+
status == "pending"
|
|
661
|
+
end
|
|
662
|
+
|
|
663
|
+
def approved?
|
|
664
|
+
status == "approved"
|
|
665
|
+
end
|
|
666
|
+
|
|
667
|
+
def rejected?
|
|
668
|
+
status == "rejected"
|
|
669
|
+
end
|
|
670
|
+
|
|
671
|
+
def to_h
|
|
672
|
+
{
|
|
673
|
+
id: id, conversation_id: conversation_id, text: text,
|
|
674
|
+
media_urls: media_urls, metadata: metadata, status: status,
|
|
675
|
+
source: source, created_by: created_by, reviewed_by: reviewed_by,
|
|
676
|
+
reviewed_at: reviewed_at&.iso8601, rejection_reason: rejection_reason,
|
|
677
|
+
message_id: message_id, created_at: created_at&.iso8601,
|
|
678
|
+
updated_at: updated_at&.iso8601
|
|
679
|
+
}.compact
|
|
680
|
+
end
|
|
681
|
+
|
|
682
|
+
private
|
|
683
|
+
|
|
684
|
+
def parse_time(value)
|
|
685
|
+
return nil if value.nil?
|
|
686
|
+
Time.parse(value)
|
|
687
|
+
rescue ArgumentError
|
|
688
|
+
nil
|
|
689
|
+
end
|
|
690
|
+
end
|
|
691
|
+
|
|
692
|
+
class DraftList
|
|
693
|
+
include Enumerable
|
|
694
|
+
|
|
695
|
+
attr_reader :data, :total, :limit, :offset, :has_more
|
|
696
|
+
|
|
697
|
+
def initialize(response)
|
|
698
|
+
@data = (response["data"] || []).map { |d| Draft.new(d) }
|
|
699
|
+
pagination = response["pagination"] || {}
|
|
700
|
+
@total = pagination["total"] || @data.length
|
|
701
|
+
@limit = pagination["limit"] || 20
|
|
702
|
+
@offset = pagination["offset"] || 0
|
|
703
|
+
@has_more = pagination["hasMore"] || pagination["has_more"] || false
|
|
704
|
+
end
|
|
705
|
+
|
|
706
|
+
def each(&block)
|
|
707
|
+
data.each(&block)
|
|
708
|
+
end
|
|
709
|
+
|
|
710
|
+
def count
|
|
711
|
+
data.length
|
|
712
|
+
end
|
|
713
|
+
|
|
714
|
+
alias size count
|
|
715
|
+
alias length count
|
|
716
|
+
|
|
717
|
+
def empty?
|
|
718
|
+
data.empty?
|
|
719
|
+
end
|
|
720
|
+
|
|
721
|
+
def first
|
|
722
|
+
data.first
|
|
723
|
+
end
|
|
724
|
+
|
|
725
|
+
def last
|
|
726
|
+
data.last
|
|
727
|
+
end
|
|
728
|
+
end
|
|
729
|
+
|
|
730
|
+
class ConversationWithMessages < Conversation
|
|
731
|
+
attr_reader :messages
|
|
732
|
+
|
|
733
|
+
def initialize(data)
|
|
734
|
+
super(data)
|
|
735
|
+
if data["messages"]
|
|
736
|
+
msgs = data["messages"]
|
|
737
|
+
@messages = {
|
|
738
|
+
data: (msgs["data"] || []).map { |m| Message.new(m) },
|
|
739
|
+
pagination: {
|
|
740
|
+
total: msgs.dig("pagination", "total") || 0,
|
|
741
|
+
limit: msgs.dig("pagination", "limit") || 20,
|
|
742
|
+
offset: msgs.dig("pagination", "offset") || 0,
|
|
743
|
+
has_more: msgs.dig("pagination", "hasMore") || msgs.dig("pagination", "has_more") || false
|
|
744
|
+
}
|
|
745
|
+
}
|
|
746
|
+
end
|
|
747
|
+
end
|
|
748
|
+
end
|
|
498
749
|
end
|
data/lib/sendly/version.rb
CHANGED
data/lib/sendly.rb
CHANGED
|
@@ -16,6 +16,9 @@ require_relative "sendly/verify"
|
|
|
16
16
|
require_relative "sendly/templates_resource"
|
|
17
17
|
require_relative "sendly/campaigns_resource"
|
|
18
18
|
require_relative "sendly/contacts_resource"
|
|
19
|
+
require_relative "sendly/conversations_resource"
|
|
20
|
+
require_relative "sendly/labels_resource"
|
|
21
|
+
require_relative "sendly/drafts_resource"
|
|
19
22
|
require_relative "sendly/enterprise"
|
|
20
23
|
|
|
21
24
|
# Sendly Ruby SDK
|
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.23.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-03-
|
|
11
|
+
date: 2026-03-17 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: faraday
|
|
@@ -128,8 +128,11 @@ files:
|
|
|
128
128
|
- lib/sendly/campaigns_resource.rb
|
|
129
129
|
- lib/sendly/client.rb
|
|
130
130
|
- lib/sendly/contacts_resource.rb
|
|
131
|
+
- lib/sendly/conversations_resource.rb
|
|
132
|
+
- lib/sendly/drafts_resource.rb
|
|
131
133
|
- lib/sendly/enterprise.rb
|
|
132
134
|
- lib/sendly/errors.rb
|
|
135
|
+
- lib/sendly/labels_resource.rb
|
|
133
136
|
- lib/sendly/media.rb
|
|
134
137
|
- lib/sendly/messages.rb
|
|
135
138
|
- lib/sendly/templates_resource.rb
|