whatsapp-cloud-api-ruby 1.0.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 +7 -0
- data/.rubocop.yml +82 -0
- data/CHANGELOG.md +92 -0
- data/Gemfile +21 -0
- data/README.md +735 -0
- data/Rakefile +41 -0
- data/TEMPLATE_TOOLS_GUIDE.md +121 -0
- data/WHATSAPP_24_HOUR_GUIDE.md +134 -0
- data/examples/advanced_features.rb +350 -0
- data/examples/basic_messaging.rb +137 -0
- data/examples/media_management.rb +254 -0
- data/examples/template_management.rb +391 -0
- data/lib/whatsapp_cloud_api/client.rb +317 -0
- data/lib/whatsapp_cloud_api/errors.rb +330 -0
- data/lib/whatsapp_cloud_api/resources/calls.rb +173 -0
- data/lib/whatsapp_cloud_api/resources/contacts.rb +191 -0
- data/lib/whatsapp_cloud_api/resources/conversations.rb +104 -0
- data/lib/whatsapp_cloud_api/resources/media.rb +206 -0
- data/lib/whatsapp_cloud_api/resources/messages.rb +381 -0
- data/lib/whatsapp_cloud_api/resources/phone_numbers.rb +86 -0
- data/lib/whatsapp_cloud_api/resources/templates.rb +284 -0
- data/lib/whatsapp_cloud_api/types.rb +263 -0
- data/lib/whatsapp_cloud_api/version.rb +5 -0
- data/lib/whatsapp_cloud_api.rb +69 -0
- data/scripts/.env.example +18 -0
- data/scripts/kapso_template_finder.rb +91 -0
- data/scripts/sdk_setup.rb +405 -0
- data/scripts/test.rb +60 -0
- metadata +254 -0
@@ -0,0 +1,104 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module WhatsAppCloudApi
|
4
|
+
module Resources
|
5
|
+
class Conversations
|
6
|
+
def initialize(client)
|
7
|
+
@client = client
|
8
|
+
end
|
9
|
+
|
10
|
+
# List conversations (Kapso Proxy only)
|
11
|
+
def list(phone_number_id:, status: nil, last_active_since: nil,
|
12
|
+
last_active_until: nil, phone_number: nil, limit: nil,
|
13
|
+
after: nil, before: nil, fields: nil)
|
14
|
+
assert_kapso_proxy('Conversations API')
|
15
|
+
|
16
|
+
query_params = {
|
17
|
+
status: status,
|
18
|
+
last_active_since: last_active_since,
|
19
|
+
last_active_until: last_active_until,
|
20
|
+
phone_number: phone_number,
|
21
|
+
limit: limit,
|
22
|
+
after: after,
|
23
|
+
before: before,
|
24
|
+
fields: fields
|
25
|
+
}.compact
|
26
|
+
|
27
|
+
response = @client.request(:get, "#{phone_number_id}/conversations",
|
28
|
+
query: query_params, response_type: :json)
|
29
|
+
Types::PagedResponse.new(response, Types::ConversationRecord)
|
30
|
+
end
|
31
|
+
|
32
|
+
# Get conversation details (Kapso Proxy only)
|
33
|
+
def get(conversation_id:)
|
34
|
+
assert_kapso_proxy('Conversations API')
|
35
|
+
|
36
|
+
raise ArgumentError, 'conversation_id cannot be empty' if conversation_id.nil? || conversation_id.strip.empty?
|
37
|
+
|
38
|
+
response = @client.request(:get, "conversations/#{conversation_id}",
|
39
|
+
response_type: :json)
|
40
|
+
|
41
|
+
# Handle both single object and data envelope responses
|
42
|
+
if response.is_a?(Hash) && response.key?('data')
|
43
|
+
Types::ConversationRecord.new(response['data'])
|
44
|
+
else
|
45
|
+
Types::ConversationRecord.new(response)
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
49
|
+
# Update conversation status (Kapso Proxy only)
|
50
|
+
def update_status(conversation_id:, status:)
|
51
|
+
assert_kapso_proxy('Conversations API')
|
52
|
+
|
53
|
+
raise ArgumentError, 'conversation_id cannot be empty' if conversation_id.nil? || conversation_id.strip.empty?
|
54
|
+
raise ArgumentError, 'status cannot be empty' if status.nil? || status.strip.empty?
|
55
|
+
|
56
|
+
payload = { status: status }
|
57
|
+
|
58
|
+
response = @client.request(:patch, "conversations/#{conversation_id}",
|
59
|
+
body: payload.to_json, response_type: :json)
|
60
|
+
Types::GraphSuccessResponse.new(response)
|
61
|
+
end
|
62
|
+
|
63
|
+
# Archive conversation (Kapso Proxy only)
|
64
|
+
def archive(conversation_id:)
|
65
|
+
update_status(conversation_id: conversation_id, status: 'archived')
|
66
|
+
end
|
67
|
+
|
68
|
+
# Unarchive conversation (Kapso Proxy only)
|
69
|
+
def unarchive(conversation_id:)
|
70
|
+
update_status(conversation_id: conversation_id, status: 'active')
|
71
|
+
end
|
72
|
+
|
73
|
+
# End conversation (Kapso Proxy only)
|
74
|
+
def end_conversation(conversation_id:)
|
75
|
+
update_status(conversation_id: conversation_id, status: 'ended')
|
76
|
+
end
|
77
|
+
|
78
|
+
# Get conversation analytics (Kapso Proxy only)
|
79
|
+
def analytics(phone_number_id:, conversation_id: nil, since: nil,
|
80
|
+
until_time: nil, granularity: 'day')
|
81
|
+
assert_kapso_proxy('Conversation Analytics API')
|
82
|
+
|
83
|
+
query_params = {
|
84
|
+
conversation_id: conversation_id,
|
85
|
+
since: since,
|
86
|
+
until: until_time,
|
87
|
+
granularity: granularity
|
88
|
+
}.compact
|
89
|
+
|
90
|
+
response = @client.request(:get, "#{phone_number_id}/conversations/analytics",
|
91
|
+
query: query_params, response_type: :json)
|
92
|
+
response
|
93
|
+
end
|
94
|
+
|
95
|
+
private
|
96
|
+
|
97
|
+
def assert_kapso_proxy(feature)
|
98
|
+
unless @client.kapso_proxy?
|
99
|
+
raise Errors::KapsoProxyRequiredError.new(feature)
|
100
|
+
end
|
101
|
+
end
|
102
|
+
end
|
103
|
+
end
|
104
|
+
end
|
@@ -0,0 +1,206 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'mime/types'
|
4
|
+
|
5
|
+
module WhatsAppCloudApi
|
6
|
+
module Resources
|
7
|
+
class Media
|
8
|
+
def initialize(client)
|
9
|
+
@client = client
|
10
|
+
end
|
11
|
+
|
12
|
+
# Upload media file
|
13
|
+
def upload(phone_number_id:, type:, file:, filename: nil, messaging_product: 'whatsapp',
|
14
|
+
upload_strategy: nil)
|
15
|
+
validate_media_type(type)
|
16
|
+
|
17
|
+
# Build multipart form data
|
18
|
+
form_data = {
|
19
|
+
'messaging_product' => messaging_product,
|
20
|
+
'type' => type
|
21
|
+
}
|
22
|
+
|
23
|
+
# Handle file parameter - can be File, IO, or file path string
|
24
|
+
file_obj = case file
|
25
|
+
when String
|
26
|
+
# Assume it's a file path
|
27
|
+
File.open(file, 'rb')
|
28
|
+
when File, IO, StringIO
|
29
|
+
file
|
30
|
+
else
|
31
|
+
raise ArgumentError, 'file must be a File, IO object, or file path string'
|
32
|
+
end
|
33
|
+
|
34
|
+
# Determine filename and content type
|
35
|
+
if filename.nil? && file.is_a?(String)
|
36
|
+
filename = File.basename(file)
|
37
|
+
end
|
38
|
+
|
39
|
+
content_type = determine_content_type(file_obj, filename, type)
|
40
|
+
|
41
|
+
form_data['file'] = Faraday::UploadIO.new(file_obj, content_type, filename)
|
42
|
+
form_data['upload_strategy'] = upload_strategy if upload_strategy
|
43
|
+
|
44
|
+
# Set multipart content type header
|
45
|
+
headers = { 'Content-Type' => 'multipart/form-data' }
|
46
|
+
|
47
|
+
response = @client.request(:post, "#{phone_number_id}/media",
|
48
|
+
body: form_data, headers: headers, response_type: :json)
|
49
|
+
|
50
|
+
# Close file if we opened it
|
51
|
+
file_obj.close if file.is_a?(String) && file_obj.respond_to?(:close)
|
52
|
+
|
53
|
+
Types::MediaUploadResponse.new(response)
|
54
|
+
end
|
55
|
+
|
56
|
+
# Get media metadata
|
57
|
+
def get(media_id:, phone_number_id: nil)
|
58
|
+
# phone_number_id is required for Kapso proxy
|
59
|
+
if @client.kapso_proxy? && phone_number_id.nil?
|
60
|
+
raise ArgumentError, 'phone_number_id is required when using Kapso proxy'
|
61
|
+
end
|
62
|
+
|
63
|
+
query_params = {}
|
64
|
+
query_params[:phone_number_id] = phone_number_id if phone_number_id
|
65
|
+
|
66
|
+
response = @client.request(:get, media_id,
|
67
|
+
query: query_params, response_type: :json)
|
68
|
+
Types::MediaMetadataResponse.new(response)
|
69
|
+
end
|
70
|
+
|
71
|
+
# Delete media
|
72
|
+
def delete(media_id:, phone_number_id: nil)
|
73
|
+
# phone_number_id is required for Kapso proxy
|
74
|
+
if @client.kapso_proxy? && phone_number_id.nil?
|
75
|
+
raise ArgumentError, 'phone_number_id is required when using Kapso proxy'
|
76
|
+
end
|
77
|
+
|
78
|
+
query_params = {}
|
79
|
+
query_params[:phone_number_id] = phone_number_id if phone_number_id
|
80
|
+
|
81
|
+
response = @client.request(:delete, media_id,
|
82
|
+
query: query_params, response_type: :json)
|
83
|
+
Types::GraphSuccessResponse.new(response)
|
84
|
+
end
|
85
|
+
|
86
|
+
# Download media content
|
87
|
+
def download(media_id:, phone_number_id: nil, headers: {},
|
88
|
+
auth: :auto, as: :binary)
|
89
|
+
# First get the media metadata to get the download URL
|
90
|
+
metadata = get(media_id: media_id, phone_number_id: phone_number_id)
|
91
|
+
download_url = metadata.url
|
92
|
+
|
93
|
+
# Determine authentication strategy
|
94
|
+
use_auth = case auth
|
95
|
+
when :auto
|
96
|
+
# Auto-detect: use auth for graph.facebook.com URLs, no auth for CDNs
|
97
|
+
download_url.include?('graph.facebook.com')
|
98
|
+
when :always
|
99
|
+
true
|
100
|
+
when :never
|
101
|
+
false
|
102
|
+
else
|
103
|
+
raise ArgumentError, 'auth must be :auto, :always, or :never'
|
104
|
+
end
|
105
|
+
|
106
|
+
# Prepare headers
|
107
|
+
download_headers = headers.dup
|
108
|
+
|
109
|
+
# Make the download request
|
110
|
+
if use_auth
|
111
|
+
response = @client.fetch(download_url, headers: download_headers)
|
112
|
+
else
|
113
|
+
response = @client.raw_request(:get, download_url, headers: download_headers)
|
114
|
+
end
|
115
|
+
|
116
|
+
unless response.success?
|
117
|
+
raise Errors::GraphApiError.new(
|
118
|
+
message: "Failed to download media: #{response.status}",
|
119
|
+
http_status: response.status,
|
120
|
+
raw_response: response.body
|
121
|
+
)
|
122
|
+
end
|
123
|
+
|
124
|
+
# Return response based on requested format
|
125
|
+
case as
|
126
|
+
when :binary
|
127
|
+
response.body
|
128
|
+
when :response
|
129
|
+
response
|
130
|
+
when :base64
|
131
|
+
require 'base64'
|
132
|
+
Base64.strict_encode64(response.body)
|
133
|
+
else
|
134
|
+
raise ArgumentError, 'as must be :binary, :response, or :base64'
|
135
|
+
end
|
136
|
+
end
|
137
|
+
|
138
|
+
# Save media to file
|
139
|
+
def save_to_file(media_id:, filepath:, phone_number_id: nil, headers: {}, auth: :auto)
|
140
|
+
content = download(
|
141
|
+
media_id: media_id,
|
142
|
+
phone_number_id: phone_number_id,
|
143
|
+
headers: headers,
|
144
|
+
auth: auth,
|
145
|
+
as: :binary
|
146
|
+
)
|
147
|
+
|
148
|
+
File.binwrite(filepath, content)
|
149
|
+
filepath
|
150
|
+
end
|
151
|
+
|
152
|
+
# Get media info including size, type, and download URL
|
153
|
+
def info(media_id:, phone_number_id: nil)
|
154
|
+
metadata = get(media_id: media_id, phone_number_id: phone_number_id)
|
155
|
+
|
156
|
+
{
|
157
|
+
id: metadata.id,
|
158
|
+
url: metadata.url,
|
159
|
+
mime_type: metadata.mime_type,
|
160
|
+
sha256: metadata.sha256,
|
161
|
+
file_size: metadata.file_size.to_i,
|
162
|
+
messaging_product: metadata.messaging_product
|
163
|
+
}
|
164
|
+
end
|
165
|
+
|
166
|
+
private
|
167
|
+
|
168
|
+
def validate_media_type(type)
|
169
|
+
valid_types = %w[image audio video document sticker]
|
170
|
+
unless valid_types.include?(type.to_s)
|
171
|
+
raise ArgumentError, "Invalid media type '#{type}'. Must be one of: #{valid_types.join(', ')}"
|
172
|
+
end
|
173
|
+
end
|
174
|
+
|
175
|
+
def determine_content_type(file_obj, filename, media_type)
|
176
|
+
# First try to determine from filename
|
177
|
+
if filename
|
178
|
+
mime_types = MIME::Types.type_for(filename)
|
179
|
+
return mime_types.first.content_type unless mime_types.empty?
|
180
|
+
end
|
181
|
+
|
182
|
+
# Try to determine from file extension if file_obj responds to path
|
183
|
+
if file_obj.respond_to?(:path) && file_obj.path
|
184
|
+
mime_types = MIME::Types.type_for(file_obj.path)
|
185
|
+
return mime_types.first.content_type unless mime_types.empty?
|
186
|
+
end
|
187
|
+
|
188
|
+
# Fall back to generic types based on media_type
|
189
|
+
case media_type.to_s
|
190
|
+
when 'image'
|
191
|
+
'image/jpeg'
|
192
|
+
when 'audio'
|
193
|
+
'audio/mpeg'
|
194
|
+
when 'video'
|
195
|
+
'video/mp4'
|
196
|
+
when 'document'
|
197
|
+
'application/pdf'
|
198
|
+
when 'sticker'
|
199
|
+
'image/webp'
|
200
|
+
else
|
201
|
+
'application/octet-stream'
|
202
|
+
end
|
203
|
+
end
|
204
|
+
end
|
205
|
+
end
|
206
|
+
end
|
@@ -0,0 +1,381 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module WhatsAppCloudApi
|
4
|
+
module Resources
|
5
|
+
class Messages
|
6
|
+
def initialize(client)
|
7
|
+
@client = client
|
8
|
+
end
|
9
|
+
|
10
|
+
# Text Messages
|
11
|
+
def send_text(phone_number_id:, to:, body:, preview_url: nil, context_message_id: nil,
|
12
|
+
biz_opaque_callback_data: nil)
|
13
|
+
payload = build_base_payload(
|
14
|
+
phone_number_id: phone_number_id,
|
15
|
+
to: to,
|
16
|
+
type: 'text',
|
17
|
+
context_message_id: context_message_id,
|
18
|
+
biz_opaque_callback_data: biz_opaque_callback_data
|
19
|
+
)
|
20
|
+
|
21
|
+
payload[:text] = { body: body }
|
22
|
+
payload[:text][:preview_url] = preview_url unless preview_url.nil?
|
23
|
+
|
24
|
+
response = @client.request(:post, "#{phone_number_id}/messages",
|
25
|
+
body: payload.to_json, response_type: :json)
|
26
|
+
Types::SendMessageResponse.new(response)
|
27
|
+
end
|
28
|
+
|
29
|
+
# Image Messages
|
30
|
+
def send_image(phone_number_id:, to:, image:, caption: nil, context_message_id: nil,
|
31
|
+
biz_opaque_callback_data: nil)
|
32
|
+
payload = build_base_payload(
|
33
|
+
phone_number_id: phone_number_id,
|
34
|
+
to: to,
|
35
|
+
type: 'image',
|
36
|
+
context_message_id: context_message_id,
|
37
|
+
biz_opaque_callback_data: biz_opaque_callback_data
|
38
|
+
)
|
39
|
+
|
40
|
+
image_obj = build_media_object(image, caption)
|
41
|
+
payload[:image] = image_obj
|
42
|
+
|
43
|
+
response = @client.request(:post, "#{phone_number_id}/messages",
|
44
|
+
body: payload.to_json, response_type: :json)
|
45
|
+
Types::SendMessageResponse.new(response)
|
46
|
+
end
|
47
|
+
|
48
|
+
# Audio Messages
|
49
|
+
def send_audio(phone_number_id:, to:, audio:, context_message_id: nil,
|
50
|
+
biz_opaque_callback_data: nil)
|
51
|
+
payload = build_base_payload(
|
52
|
+
phone_number_id: phone_number_id,
|
53
|
+
to: to,
|
54
|
+
type: 'audio',
|
55
|
+
context_message_id: context_message_id,
|
56
|
+
biz_opaque_callback_data: biz_opaque_callback_data
|
57
|
+
)
|
58
|
+
|
59
|
+
payload[:audio] = build_media_object(audio)
|
60
|
+
|
61
|
+
response = @client.request(:post, "#{phone_number_id}/messages",
|
62
|
+
body: payload.to_json, response_type: :json)
|
63
|
+
Types::SendMessageResponse.new(response)
|
64
|
+
end
|
65
|
+
|
66
|
+
# Document Messages
|
67
|
+
def send_document(phone_number_id:, to:, document:, caption: nil, filename: nil,
|
68
|
+
context_message_id: nil, biz_opaque_callback_data: nil)
|
69
|
+
payload = build_base_payload(
|
70
|
+
phone_number_id: phone_number_id,
|
71
|
+
to: to,
|
72
|
+
type: 'document',
|
73
|
+
context_message_id: context_message_id,
|
74
|
+
biz_opaque_callback_data: biz_opaque_callback_data
|
75
|
+
)
|
76
|
+
|
77
|
+
document_obj = build_media_object(document, caption)
|
78
|
+
document_obj[:filename] = filename if filename
|
79
|
+
payload[:document] = document_obj
|
80
|
+
|
81
|
+
response = @client.request(:post, "#{phone_number_id}/messages",
|
82
|
+
body: payload.to_json, response_type: :json)
|
83
|
+
Types::SendMessageResponse.new(response)
|
84
|
+
end
|
85
|
+
|
86
|
+
# Video Messages
|
87
|
+
def send_video(phone_number_id:, to:, video:, caption: nil, context_message_id: nil,
|
88
|
+
biz_opaque_callback_data: nil)
|
89
|
+
payload = build_base_payload(
|
90
|
+
phone_number_id: phone_number_id,
|
91
|
+
to: to,
|
92
|
+
type: 'video',
|
93
|
+
context_message_id: context_message_id,
|
94
|
+
biz_opaque_callback_data: biz_opaque_callback_data
|
95
|
+
)
|
96
|
+
|
97
|
+
payload[:video] = build_media_object(video, caption)
|
98
|
+
|
99
|
+
response = @client.request(:post, "#{phone_number_id}/messages",
|
100
|
+
body: payload.to_json, response_type: :json)
|
101
|
+
Types::SendMessageResponse.new(response)
|
102
|
+
end
|
103
|
+
|
104
|
+
# Sticker Messages
|
105
|
+
def send_sticker(phone_number_id:, to:, sticker:, context_message_id: nil,
|
106
|
+
biz_opaque_callback_data: nil)
|
107
|
+
payload = build_base_payload(
|
108
|
+
phone_number_id: phone_number_id,
|
109
|
+
to: to,
|
110
|
+
type: 'sticker',
|
111
|
+
context_message_id: context_message_id,
|
112
|
+
biz_opaque_callback_data: biz_opaque_callback_data
|
113
|
+
)
|
114
|
+
|
115
|
+
payload[:sticker] = build_media_object(sticker)
|
116
|
+
|
117
|
+
response = @client.request(:post, "#{phone_number_id}/messages",
|
118
|
+
body: payload.to_json, response_type: :json)
|
119
|
+
Types::SendMessageResponse.new(response)
|
120
|
+
end
|
121
|
+
|
122
|
+
# Location Messages
|
123
|
+
def send_location(phone_number_id:, to:, latitude:, longitude:, name: nil,
|
124
|
+
address: nil, context_message_id: nil, biz_opaque_callback_data: nil)
|
125
|
+
payload = build_base_payload(
|
126
|
+
phone_number_id: phone_number_id,
|
127
|
+
to: to,
|
128
|
+
type: 'location',
|
129
|
+
context_message_id: context_message_id,
|
130
|
+
biz_opaque_callback_data: biz_opaque_callback_data
|
131
|
+
)
|
132
|
+
|
133
|
+
location_obj = {
|
134
|
+
latitude: latitude,
|
135
|
+
longitude: longitude
|
136
|
+
}
|
137
|
+
location_obj[:name] = name if name
|
138
|
+
location_obj[:address] = address if address
|
139
|
+
|
140
|
+
payload[:location] = location_obj
|
141
|
+
|
142
|
+
response = @client.request(:post, "#{phone_number_id}/messages",
|
143
|
+
body: payload.to_json, response_type: :json)
|
144
|
+
Types::SendMessageResponse.new(response)
|
145
|
+
end
|
146
|
+
|
147
|
+
# Contact Messages
|
148
|
+
def send_contacts(phone_number_id:, to:, contacts:, context_message_id: nil,
|
149
|
+
biz_opaque_callback_data: nil)
|
150
|
+
payload = build_base_payload(
|
151
|
+
phone_number_id: phone_number_id,
|
152
|
+
to: to,
|
153
|
+
type: 'contacts',
|
154
|
+
context_message_id: context_message_id,
|
155
|
+
biz_opaque_callback_data: biz_opaque_callback_data
|
156
|
+
)
|
157
|
+
|
158
|
+
payload[:contacts] = contacts
|
159
|
+
|
160
|
+
response = @client.request(:post, "#{phone_number_id}/messages",
|
161
|
+
body: payload.to_json, response_type: :json)
|
162
|
+
Types::SendMessageResponse.new(response)
|
163
|
+
end
|
164
|
+
|
165
|
+
# Template Messages
|
166
|
+
def send_template(phone_number_id:, to:, name:, language:, components: nil,
|
167
|
+
context_message_id: nil, biz_opaque_callback_data: nil)
|
168
|
+
payload = build_base_payload(
|
169
|
+
phone_number_id: phone_number_id,
|
170
|
+
to: to,
|
171
|
+
type: 'template',
|
172
|
+
context_message_id: context_message_id,
|
173
|
+
biz_opaque_callback_data: biz_opaque_callback_data
|
174
|
+
)
|
175
|
+
|
176
|
+
template_obj = {
|
177
|
+
name: name,
|
178
|
+
language: { code: language }
|
179
|
+
}
|
180
|
+
template_obj[:components] = components if components
|
181
|
+
|
182
|
+
payload[:template] = template_obj
|
183
|
+
|
184
|
+
response = @client.request(:post, "#{phone_number_id}/messages",
|
185
|
+
body: payload.to_json, response_type: :json)
|
186
|
+
Types::SendMessageResponse.new(response)
|
187
|
+
end
|
188
|
+
|
189
|
+
# Reaction Messages
|
190
|
+
def send_reaction(phone_number_id:, to:, message_id:, emoji: nil,
|
191
|
+
context_message_id: nil, biz_opaque_callback_data: nil)
|
192
|
+
payload = build_base_payload(
|
193
|
+
phone_number_id: phone_number_id,
|
194
|
+
to: to,
|
195
|
+
type: 'reaction',
|
196
|
+
context_message_id: context_message_id,
|
197
|
+
biz_opaque_callback_data: biz_opaque_callback_data
|
198
|
+
)
|
199
|
+
|
200
|
+
reaction_obj = { message_id: message_id }
|
201
|
+
reaction_obj[:emoji] = emoji if emoji # nil emoji removes reaction
|
202
|
+
|
203
|
+
payload[:reaction] = reaction_obj
|
204
|
+
|
205
|
+
response = @client.request(:post, "#{phone_number_id}/messages",
|
206
|
+
body: payload.to_json, response_type: :json)
|
207
|
+
Types::SendMessageResponse.new(response)
|
208
|
+
end
|
209
|
+
|
210
|
+
# Interactive Button Messages
|
211
|
+
def send_interactive_buttons(phone_number_id:, to:, body_text:, buttons:,
|
212
|
+
header: nil, footer: nil, context_message_id: nil,
|
213
|
+
biz_opaque_callback_data: nil)
|
214
|
+
payload = build_base_payload(
|
215
|
+
phone_number_id: phone_number_id,
|
216
|
+
to: to,
|
217
|
+
type: 'interactive',
|
218
|
+
context_message_id: context_message_id,
|
219
|
+
biz_opaque_callback_data: biz_opaque_callback_data
|
220
|
+
)
|
221
|
+
|
222
|
+
interactive_obj = {
|
223
|
+
type: 'button',
|
224
|
+
body: { text: body_text },
|
225
|
+
action: { buttons: buttons }
|
226
|
+
}
|
227
|
+
|
228
|
+
interactive_obj[:header] = header if header
|
229
|
+
interactive_obj[:footer] = footer if footer
|
230
|
+
|
231
|
+
payload[:interactive] = interactive_obj
|
232
|
+
|
233
|
+
response = @client.request(:post, "#{phone_number_id}/messages",
|
234
|
+
body: payload.to_json, response_type: :json)
|
235
|
+
Types::SendMessageResponse.new(response)
|
236
|
+
end
|
237
|
+
|
238
|
+
# Interactive List Messages
|
239
|
+
def send_interactive_list(phone_number_id:, to:, body_text:, button_text:, sections:,
|
240
|
+
header: nil, footer: nil, context_message_id: nil,
|
241
|
+
biz_opaque_callback_data: nil)
|
242
|
+
payload = build_base_payload(
|
243
|
+
phone_number_id: phone_number_id,
|
244
|
+
to: to,
|
245
|
+
type: 'interactive',
|
246
|
+
context_message_id: context_message_id,
|
247
|
+
biz_opaque_callback_data: biz_opaque_callback_data
|
248
|
+
)
|
249
|
+
|
250
|
+
interactive_obj = {
|
251
|
+
type: 'list',
|
252
|
+
body: { text: body_text },
|
253
|
+
action: {
|
254
|
+
button: button_text,
|
255
|
+
sections: sections
|
256
|
+
}
|
257
|
+
}
|
258
|
+
|
259
|
+
interactive_obj[:header] = header if header
|
260
|
+
interactive_obj[:footer] = footer if footer
|
261
|
+
|
262
|
+
payload[:interactive] = interactive_obj
|
263
|
+
|
264
|
+
response = @client.request(:post, "#{phone_number_id}/messages",
|
265
|
+
body: payload.to_json, response_type: :json)
|
266
|
+
Types::SendMessageResponse.new(response)
|
267
|
+
end
|
268
|
+
|
269
|
+
# Mark Message as Read
|
270
|
+
def mark_read(phone_number_id:, message_id:)
|
271
|
+
payload = {
|
272
|
+
messaging_product: 'whatsapp',
|
273
|
+
status: 'read',
|
274
|
+
message_id: message_id
|
275
|
+
}
|
276
|
+
|
277
|
+
response = @client.request(:post, "#{phone_number_id}/messages",
|
278
|
+
body: payload.to_json, response_type: :json)
|
279
|
+
Types::GraphSuccessResponse.new(response)
|
280
|
+
end
|
281
|
+
|
282
|
+
# Send Typing Indicator
|
283
|
+
def send_typing_indicator(phone_number_id:, to:)
|
284
|
+
payload = {
|
285
|
+
messaging_product: 'whatsapp',
|
286
|
+
recipient_type: 'individual',
|
287
|
+
to: to,
|
288
|
+
type: 'text',
|
289
|
+
text: { typing_indicator: { type: 'text' } }
|
290
|
+
}
|
291
|
+
|
292
|
+
response = @client.request(:post, "#{phone_number_id}/messages",
|
293
|
+
body: payload.to_json, response_type: :json)
|
294
|
+
Types::GraphSuccessResponse.new(response)
|
295
|
+
end
|
296
|
+
|
297
|
+
# Query Message History (Kapso Proxy only)
|
298
|
+
def query(phone_number_id:, direction: nil, status: nil, since: nil, until_time: nil,
|
299
|
+
conversation_id: nil, limit: nil, after: nil, before: nil, fields: nil)
|
300
|
+
assert_kapso_proxy('Message history API')
|
301
|
+
|
302
|
+
query_params = {
|
303
|
+
phone_number_id: phone_number_id,
|
304
|
+
direction: direction,
|
305
|
+
status: status,
|
306
|
+
since: since,
|
307
|
+
until: until_time,
|
308
|
+
conversation_id: conversation_id,
|
309
|
+
limit: limit,
|
310
|
+
after: after,
|
311
|
+
before: before,
|
312
|
+
fields: fields
|
313
|
+
}.compact
|
314
|
+
|
315
|
+
response = @client.request(:get, "#{phone_number_id}/messages",
|
316
|
+
query: query_params, response_type: :json)
|
317
|
+
Types::PagedResponse.new(response)
|
318
|
+
end
|
319
|
+
|
320
|
+
# List Messages by Conversation (Kapso Proxy only)
|
321
|
+
def list_by_conversation(phone_number_id:, conversation_id:, limit: nil,
|
322
|
+
after: nil, before: nil, fields: nil)
|
323
|
+
query(
|
324
|
+
phone_number_id: phone_number_id,
|
325
|
+
conversation_id: conversation_id,
|
326
|
+
limit: limit,
|
327
|
+
after: after,
|
328
|
+
before: before,
|
329
|
+
fields: fields
|
330
|
+
)
|
331
|
+
end
|
332
|
+
|
333
|
+
private
|
334
|
+
|
335
|
+
def build_base_payload(phone_number_id:, to:, type:, context_message_id: nil,
|
336
|
+
biz_opaque_callback_data: nil)
|
337
|
+
payload = {
|
338
|
+
messaging_product: 'whatsapp',
|
339
|
+
recipient_type: 'individual',
|
340
|
+
to: to,
|
341
|
+
type: type
|
342
|
+
}
|
343
|
+
|
344
|
+
if context_message_id
|
345
|
+
payload[:context] = { message_id: context_message_id }
|
346
|
+
end
|
347
|
+
|
348
|
+
if biz_opaque_callback_data
|
349
|
+
payload[:biz_opaque_callback_data] = biz_opaque_callback_data
|
350
|
+
end
|
351
|
+
|
352
|
+
payload
|
353
|
+
end
|
354
|
+
|
355
|
+
def build_media_object(media, caption = nil)
|
356
|
+
media_obj = case media
|
357
|
+
when Hash
|
358
|
+
media.dup
|
359
|
+
when String
|
360
|
+
# Assume it's either a media ID or URL
|
361
|
+
if media.match?(/\A\w+\z/) # Simple alphanumeric ID
|
362
|
+
{ id: media }
|
363
|
+
else
|
364
|
+
{ link: media }
|
365
|
+
end
|
366
|
+
else
|
367
|
+
raise ArgumentError, 'Media must be a Hash, media ID string, or URL string'
|
368
|
+
end
|
369
|
+
|
370
|
+
media_obj[:caption] = caption if caption
|
371
|
+
media_obj
|
372
|
+
end
|
373
|
+
|
374
|
+
def assert_kapso_proxy(feature)
|
375
|
+
unless @client.kapso_proxy?
|
376
|
+
raise Errors::KapsoProxyRequiredError.new(feature)
|
377
|
+
end
|
378
|
+
end
|
379
|
+
end
|
380
|
+
end
|
381
|
+
end
|