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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: ecce7836de0fe84c6c10ac40b1fa6da2623280863271baf3db46cade5c9cdcee
4
- data.tar.gz: a4ec369ee6ead60e8999e446d2be7945684b63b1fc680688720f25dc602567b1
3
+ metadata.gz: 2dc2c09c04faa2543613b476696a2106e1a7e2edd16a788bd0f16bb3ba8c3c56
4
+ data.tar.gz: d9a9c53078fb5a52272abaaf989cec2c95f59cc73d0c7046a3018371c1df3434
5
5
  SHA512:
6
- metadata.gz: '0682b8f55f344db0effff21d4ba7bdd5a501a522719af9fcd51968e1eb76e3b33ec1fa747a50219a4aab7ced7a32b879d7b6923e36bde1ce5d718e406ffcfa92'
7
- data.tar.gz: 424e836b39344a0e31e0861719d09f3784673c057763fdac923d1b41ab0840e3eb4c2652f25b9b35c2c3bc7ee89930f6e7291363e21647a7733f5a6c9c940367
6
+ metadata.gz: 00a1a0fbbcd66dc72009109dcb3dae50615c59a01cf1290461d1a0d95857f117fad8190e3ffaa7641810abb624c9eab145f2b1e942d19953ef02c9ef9df57a1b
7
+ data.tar.gz: b75222058daa05a0e408cf4f054e68c7dfb83203894daabfdf42e1fc1f5cd50fec806bc2fe875eb8199df042df1c2a14240779298387c69159bdd12129a58afd
data/Gemfile.lock CHANGED
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- sendly (3.21.1)
4
+ sendly (3.23.0)
5
5
  faraday (~> 2.0)
6
6
  faraday-retry (~> 2.0)
7
7
 
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] Telnyx message ID for tracking
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
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Sendly
4
- VERSION = "3.21.1"
4
+ VERSION = "3.23.0"
5
5
  end
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.21.1
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-13 00:00:00.000000000 Z
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