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,391 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'whatsapp_cloud_api'
|
4
|
+
|
5
|
+
puts "=== Template Management Examples ==="
|
6
|
+
|
7
|
+
# Initialize client
|
8
|
+
client = WhatsAppCloudApi::Client.new(
|
9
|
+
access_token: ENV['WHATSAPP_ACCESS_TOKEN']
|
10
|
+
)
|
11
|
+
|
12
|
+
business_account_id = ENV['BUSINESS_ACCOUNT_ID']
|
13
|
+
|
14
|
+
# Example 1: List Existing Templates
|
15
|
+
puts "\n--- List Existing Templates ---"
|
16
|
+
|
17
|
+
begin
|
18
|
+
templates = client.templates.list(business_account_id: business_account_id)
|
19
|
+
|
20
|
+
puts "Found #{templates.data.length} templates:"
|
21
|
+
templates.data.each do |template|
|
22
|
+
puts "- #{template.name} (#{template.language}) - Status: #{template.status}"
|
23
|
+
end
|
24
|
+
|
25
|
+
# Handle pagination if there are more results
|
26
|
+
if templates.paging.after
|
27
|
+
puts "\nMore templates available. Next cursor: #{templates.paging.after}"
|
28
|
+
end
|
29
|
+
|
30
|
+
rescue WhatsAppCloudApi::Errors::GraphApiError => e
|
31
|
+
puts "Error listing templates: #{e.message}"
|
32
|
+
end
|
33
|
+
|
34
|
+
# Example 2: Create Marketing Template
|
35
|
+
puts "\n--- Create Marketing Template ---"
|
36
|
+
|
37
|
+
begin
|
38
|
+
# Build a marketing template with the helper method
|
39
|
+
template_data = client.templates.build_marketing_template(
|
40
|
+
name: 'ruby_sdk_promo',
|
41
|
+
language: 'en_US',
|
42
|
+
header: {
|
43
|
+
type: 'HEADER',
|
44
|
+
format: 'TEXT',
|
45
|
+
text: 'Special Offer for {{1}}!'
|
46
|
+
},
|
47
|
+
body: 'Hi {{1}}, we have a special {{2}} discount just for you! Use code {{3}} to get {{4}} off your next purchase.',
|
48
|
+
footer: 'This offer expires in 24 hours',
|
49
|
+
buttons: [
|
50
|
+
{
|
51
|
+
type: 'URL',
|
52
|
+
text: 'Shop Now',
|
53
|
+
url: 'https://example.com/shop?code={{1}}'
|
54
|
+
},
|
55
|
+
{
|
56
|
+
type: 'QUICK_REPLY',
|
57
|
+
text: 'More Info'
|
58
|
+
}
|
59
|
+
],
|
60
|
+
body_example: {
|
61
|
+
body_text: [['John', 'exclusive', 'SAVE20', '20%']]
|
62
|
+
}
|
63
|
+
)
|
64
|
+
|
65
|
+
# Create the template
|
66
|
+
response = client.templates.create(
|
67
|
+
business_account_id: business_account_id,
|
68
|
+
**template_data
|
69
|
+
)
|
70
|
+
|
71
|
+
puts "Marketing template created!"
|
72
|
+
puts "Template ID: #{response.id}"
|
73
|
+
puts "Status: #{response.status}"
|
74
|
+
|
75
|
+
rescue WhatsAppCloudApi::Errors::GraphApiError => e
|
76
|
+
puts "Error creating marketing template: #{e.message}"
|
77
|
+
|
78
|
+
if e.template_error?
|
79
|
+
puts "Template-specific error - check template format and content"
|
80
|
+
end
|
81
|
+
end
|
82
|
+
|
83
|
+
# Example 3: Create Authentication Template
|
84
|
+
puts "\n--- Create Authentication Template ---"
|
85
|
+
|
86
|
+
begin
|
87
|
+
# Build authentication template using helper
|
88
|
+
auth_template = client.templates.build_authentication_template(
|
89
|
+
name: 'ruby_sdk_auth',
|
90
|
+
language: 'en_US',
|
91
|
+
ttl_seconds: 300, # 5 minutes
|
92
|
+
code_expiration_minutes: 5,
|
93
|
+
otp_type: 'COPY_CODE'
|
94
|
+
)
|
95
|
+
|
96
|
+
response = client.templates.create(
|
97
|
+
business_account_id: business_account_id,
|
98
|
+
**auth_template
|
99
|
+
)
|
100
|
+
|
101
|
+
puts "Authentication template created!"
|
102
|
+
puts "Template ID: #{response.id}"
|
103
|
+
puts "Status: #{response.status}"
|
104
|
+
|
105
|
+
rescue WhatsAppCloudApi::Errors::GraphApiError => e
|
106
|
+
puts "Error creating auth template: #{e.message}"
|
107
|
+
end
|
108
|
+
|
109
|
+
# Example 4: Create Utility Template
|
110
|
+
puts "\n--- Create Utility Template ---"
|
111
|
+
|
112
|
+
begin
|
113
|
+
utility_template = client.templates.build_utility_template(
|
114
|
+
name: 'ruby_sdk_notification',
|
115
|
+
language: 'en_US',
|
116
|
+
header: {
|
117
|
+
type: 'HEADER',
|
118
|
+
format: 'TEXT',
|
119
|
+
text: 'Order Update'
|
120
|
+
},
|
121
|
+
body: 'Your order #{{1}} has been {{2}}. Estimated delivery: {{3}}.',
|
122
|
+
footer: 'Thank you for choosing our service',
|
123
|
+
buttons: [
|
124
|
+
{
|
125
|
+
type: 'URL',
|
126
|
+
text: 'Track Order',
|
127
|
+
url: 'https://example.com/track/{{1}}'
|
128
|
+
}
|
129
|
+
],
|
130
|
+
body_example: {
|
131
|
+
body_text: [['12345', 'shipped', 'Tomorrow 2-4 PM']]
|
132
|
+
}
|
133
|
+
)
|
134
|
+
|
135
|
+
response = client.templates.create(
|
136
|
+
business_account_id: business_account_id,
|
137
|
+
**utility_template
|
138
|
+
)
|
139
|
+
|
140
|
+
puts "Utility template created!"
|
141
|
+
puts "Template ID: #{response.id}"
|
142
|
+
|
143
|
+
rescue WhatsAppCloudApi::Errors::GraphApiError => e
|
144
|
+
puts "Error creating utility template: #{e.message}"
|
145
|
+
end
|
146
|
+
|
147
|
+
# Example 5: Create Complex Template with All Components
|
148
|
+
puts "\n--- Create Complex Template ---"
|
149
|
+
|
150
|
+
begin
|
151
|
+
components = [
|
152
|
+
# Header with image
|
153
|
+
{
|
154
|
+
type: 'HEADER',
|
155
|
+
format: 'IMAGE',
|
156
|
+
example: {
|
157
|
+
header_handle: ['https://example.com/header-image.jpg']
|
158
|
+
}
|
159
|
+
},
|
160
|
+
# Body with variables
|
161
|
+
{
|
162
|
+
type: 'BODY',
|
163
|
+
text: 'Hello {{1}}! Your {{2}} order totaling {{3}} is ready for pickup. ' \
|
164
|
+
'Please bring your ID and order confirmation {{4}}.',
|
165
|
+
example: {
|
166
|
+
body_text: [['John Doe', 'premium', '$125.99', '#ORD12345']]
|
167
|
+
}
|
168
|
+
},
|
169
|
+
# Footer
|
170
|
+
{
|
171
|
+
type: 'FOOTER',
|
172
|
+
text: 'Reply STOP to unsubscribe'
|
173
|
+
},
|
174
|
+
# Multiple buttons
|
175
|
+
{
|
176
|
+
type: 'BUTTONS',
|
177
|
+
buttons: [
|
178
|
+
{
|
179
|
+
type: 'URL',
|
180
|
+
text: 'View Order',
|
181
|
+
url: 'https://example.com/orders/{{1}}'
|
182
|
+
},
|
183
|
+
{
|
184
|
+
type: 'PHONE_NUMBER',
|
185
|
+
text: 'Call Store',
|
186
|
+
phone_number: '+1234567890'
|
187
|
+
},
|
188
|
+
{
|
189
|
+
type: 'QUICK_REPLY',
|
190
|
+
text: 'Reschedule'
|
191
|
+
}
|
192
|
+
]
|
193
|
+
}
|
194
|
+
]
|
195
|
+
|
196
|
+
response = client.templates.create(
|
197
|
+
business_account_id: business_account_id,
|
198
|
+
name: 'ruby_sdk_complex',
|
199
|
+
language: 'en_US',
|
200
|
+
category: 'UTILITY',
|
201
|
+
components: components
|
202
|
+
)
|
203
|
+
|
204
|
+
puts "Complex template created!"
|
205
|
+
puts "Template ID: #{response.id}"
|
206
|
+
|
207
|
+
rescue WhatsAppCloudApi::Errors::GraphApiError => e
|
208
|
+
puts "Error creating complex template: #{e.message}"
|
209
|
+
end
|
210
|
+
|
211
|
+
# Example 6: Send Template Messages
|
212
|
+
puts "\n--- Send Template Messages ---"
|
213
|
+
|
214
|
+
begin
|
215
|
+
# Send simple template without variables
|
216
|
+
response1 = client.messages.send_template(
|
217
|
+
phone_number_id: ENV['PHONE_NUMBER_ID'],
|
218
|
+
to: '+1234567890',
|
219
|
+
name: 'hello_world', # Meta's sample template
|
220
|
+
language: 'en_US'
|
221
|
+
)
|
222
|
+
|
223
|
+
puts "Simple template sent: #{response1.messages.first.id}"
|
224
|
+
|
225
|
+
# Send template with parameters
|
226
|
+
response2 = client.messages.send_template(
|
227
|
+
phone_number_id: ENV['PHONE_NUMBER_ID'],
|
228
|
+
to: '+1234567890',
|
229
|
+
name: 'ruby_sdk_promo', # Our created template
|
230
|
+
language: 'en_US',
|
231
|
+
components: [
|
232
|
+
{
|
233
|
+
type: 'header',
|
234
|
+
parameters: [
|
235
|
+
{ type: 'text', text: 'John Doe' }
|
236
|
+
]
|
237
|
+
},
|
238
|
+
{
|
239
|
+
type: 'body',
|
240
|
+
parameters: [
|
241
|
+
{ type: 'text', text: 'John' },
|
242
|
+
{ type: 'text', text: 'exclusive' },
|
243
|
+
{ type: 'text', text: 'RUBY20' },
|
244
|
+
{ type: 'text', text: '20%' }
|
245
|
+
]
|
246
|
+
},
|
247
|
+
{
|
248
|
+
type: 'button',
|
249
|
+
sub_type: 'url',
|
250
|
+
index: '0',
|
251
|
+
parameters: [
|
252
|
+
{ type: 'text', text: 'RUBY20' }
|
253
|
+
]
|
254
|
+
}
|
255
|
+
]
|
256
|
+
)
|
257
|
+
|
258
|
+
puts "Parameterized template sent: #{response2.messages.first.id}"
|
259
|
+
|
260
|
+
rescue WhatsAppCloudApi::Errors::GraphApiError => e
|
261
|
+
puts "Error sending template: #{e.message}"
|
262
|
+
|
263
|
+
case e.category
|
264
|
+
when :template
|
265
|
+
puts "Template error - check template name, language, and parameters"
|
266
|
+
when :parameter
|
267
|
+
puts "Parameter error - check component parameters format"
|
268
|
+
end
|
269
|
+
end
|
270
|
+
|
271
|
+
# Example 7: Template Management Operations
|
272
|
+
puts "\n--- Template Management Operations ---"
|
273
|
+
|
274
|
+
begin
|
275
|
+
# Get specific template details
|
276
|
+
template_id = 'your_template_id' # Replace with actual template ID
|
277
|
+
|
278
|
+
template = client.templates.get(
|
279
|
+
business_account_id: business_account_id,
|
280
|
+
template_id: template_id
|
281
|
+
)
|
282
|
+
|
283
|
+
puts "Template Details:"
|
284
|
+
puts "Name: #{template.name}"
|
285
|
+
puts "Status: #{template.status}"
|
286
|
+
puts "Category: #{template.category}"
|
287
|
+
puts "Quality Score: #{template.quality_score_category}"
|
288
|
+
|
289
|
+
# Update template (if allowed)
|
290
|
+
if template.status == 'REJECTED'
|
291
|
+
puts "Attempting to update rejected template..."
|
292
|
+
|
293
|
+
client.templates.update(
|
294
|
+
business_account_id: business_account_id,
|
295
|
+
template_id: template_id,
|
296
|
+
category: 'UTILITY' # Change category if needed
|
297
|
+
)
|
298
|
+
|
299
|
+
puts "Template updated successfully"
|
300
|
+
end
|
301
|
+
|
302
|
+
rescue WhatsAppCloudApi::Errors::GraphApiError => e
|
303
|
+
puts "Template management error: #{e.message}"
|
304
|
+
end
|
305
|
+
|
306
|
+
# Example 8: Delete Templates
|
307
|
+
puts "\n--- Delete Templates ---"
|
308
|
+
|
309
|
+
begin
|
310
|
+
# Delete by template ID
|
311
|
+
client.templates.delete(
|
312
|
+
business_account_id: business_account_id,
|
313
|
+
template_id: 'template_id_to_delete'
|
314
|
+
)
|
315
|
+
|
316
|
+
puts "Template deleted by ID"
|
317
|
+
|
318
|
+
# Delete by name and language
|
319
|
+
client.templates.delete(
|
320
|
+
business_account_id: business_account_id,
|
321
|
+
name: 'ruby_sdk_test',
|
322
|
+
language: 'en_US'
|
323
|
+
)
|
324
|
+
|
325
|
+
puts "Template deleted by name"
|
326
|
+
|
327
|
+
rescue WhatsAppCloudApi::Errors::GraphApiError => e
|
328
|
+
puts "Delete error: #{e.message}"
|
329
|
+
|
330
|
+
if e.http_status == 404
|
331
|
+
puts "Template not found - may already be deleted"
|
332
|
+
end
|
333
|
+
end
|
334
|
+
|
335
|
+
# Example 9: Template Validation and Best Practices
|
336
|
+
puts "\n--- Template Validation Examples ---"
|
337
|
+
|
338
|
+
# Good template example
|
339
|
+
def create_good_template(client, business_account_id)
|
340
|
+
client.templates.create(
|
341
|
+
business_account_id: business_account_id,
|
342
|
+
name: 'good_template_example',
|
343
|
+
language: 'en_US',
|
344
|
+
category: 'UTILITY',
|
345
|
+
components: [
|
346
|
+
{
|
347
|
+
type: 'BODY',
|
348
|
+
text: 'Your appointment with {{1}} is confirmed for {{2}} at {{3}}.',
|
349
|
+
example: {
|
350
|
+
body_text: [['Dr. Smith', 'tomorrow', '2:00 PM']]
|
351
|
+
}
|
352
|
+
},
|
353
|
+
{
|
354
|
+
type: 'FOOTER',
|
355
|
+
text: 'Reply CANCEL to cancel this appointment'
|
356
|
+
}
|
357
|
+
]
|
358
|
+
)
|
359
|
+
end
|
360
|
+
|
361
|
+
# Bad template example (this will likely be rejected)
|
362
|
+
def create_bad_template_example(client, business_account_id)
|
363
|
+
begin
|
364
|
+
client.templates.create(
|
365
|
+
business_account_id: business_account_id,
|
366
|
+
name: 'bad_template_example',
|
367
|
+
language: 'en_US',
|
368
|
+
category: 'MARKETING',
|
369
|
+
components: [
|
370
|
+
{
|
371
|
+
type: 'BODY',
|
372
|
+
text: 'URGENT!!! Buy now or MISS OUT!!! Limited time offer!!!'
|
373
|
+
# No example provided, excessive caps, promotional language
|
374
|
+
}
|
375
|
+
]
|
376
|
+
)
|
377
|
+
rescue WhatsAppCloudApi::Errors::GraphApiError => e
|
378
|
+
puts "Bad template rejected (expected): #{e.message}"
|
379
|
+
end
|
380
|
+
end
|
381
|
+
|
382
|
+
begin
|
383
|
+
good_response = create_good_template(client, business_account_id)
|
384
|
+
puts "Good template created: #{good_response.id}"
|
385
|
+
rescue => e
|
386
|
+
puts "Error with good template: #{e.message}"
|
387
|
+
end
|
388
|
+
|
389
|
+
create_bad_template_example(client, business_account_id)
|
390
|
+
|
391
|
+
puts "\n=== Template Management Examples Completed ==="
|
@@ -0,0 +1,317 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'faraday'
|
4
|
+
require 'faraday/multipart'
|
5
|
+
require 'json'
|
6
|
+
require 'logger'
|
7
|
+
|
8
|
+
module WhatsAppCloudApi
|
9
|
+
class Client
|
10
|
+
DEFAULT_BASE_URL = 'https://graph.facebook.com'
|
11
|
+
DEFAULT_GRAPH_VERSION = 'v24.0'
|
12
|
+
KAPSO_PROXY_PATTERN = /kapso\.ai/
|
13
|
+
|
14
|
+
attr_reader :access_token, :kapso_api_key, :base_url, :graph_version,
|
15
|
+
:logger, :debug, :timeout, :open_timeout, :max_retries, :retry_delay
|
16
|
+
|
17
|
+
def initialize(access_token: nil, kapso_api_key: nil, base_url: nil,
|
18
|
+
graph_version: nil, logger: nil, debug: nil, timeout: nil,
|
19
|
+
open_timeout: nil, max_retries: nil, retry_delay: nil)
|
20
|
+
|
21
|
+
# Validation
|
22
|
+
unless access_token || kapso_api_key
|
23
|
+
raise Errors::ConfigurationError, 'Must provide either access_token or kapso_api_key'
|
24
|
+
end
|
25
|
+
|
26
|
+
@access_token = access_token
|
27
|
+
@kapso_api_key = kapso_api_key
|
28
|
+
@base_url = normalize_base_url(base_url || DEFAULT_BASE_URL)
|
29
|
+
@graph_version = graph_version || DEFAULT_GRAPH_VERSION
|
30
|
+
@kapso_proxy = detect_kapso_proxy(@base_url)
|
31
|
+
|
32
|
+
# Configuration with defaults
|
33
|
+
config = WhatsAppCloudApi.configuration
|
34
|
+
@logger = logger || WhatsAppCloudApi.logger
|
35
|
+
@debug = debug.nil? ? config.debug : debug
|
36
|
+
@timeout = timeout || config.timeout
|
37
|
+
@open_timeout = open_timeout || config.open_timeout
|
38
|
+
@max_retries = max_retries || config.max_retries
|
39
|
+
@retry_delay = retry_delay || config.retry_delay
|
40
|
+
|
41
|
+
# Initialize HTTP client
|
42
|
+
@http_client = build_http_client
|
43
|
+
|
44
|
+
# Initialize resource endpoints
|
45
|
+
@messages = nil
|
46
|
+
@media = nil
|
47
|
+
@templates = nil
|
48
|
+
@phone_numbers = nil
|
49
|
+
@calls = nil
|
50
|
+
@conversations = nil
|
51
|
+
@contacts = nil
|
52
|
+
end
|
53
|
+
|
54
|
+
# Resource accessors with lazy initialization
|
55
|
+
def messages
|
56
|
+
@messages ||= Resources::Messages.new(self)
|
57
|
+
end
|
58
|
+
|
59
|
+
def media
|
60
|
+
@media ||= Resources::Media.new(self)
|
61
|
+
end
|
62
|
+
|
63
|
+
def templates
|
64
|
+
@templates ||= Resources::Templates.new(self)
|
65
|
+
end
|
66
|
+
|
67
|
+
def phone_numbers
|
68
|
+
@phone_numbers ||= Resources::PhoneNumbers.new(self)
|
69
|
+
end
|
70
|
+
|
71
|
+
def calls
|
72
|
+
@calls ||= Resources::Calls.new(self)
|
73
|
+
end
|
74
|
+
|
75
|
+
def conversations
|
76
|
+
@conversations ||= Resources::Conversations.new(self)
|
77
|
+
end
|
78
|
+
|
79
|
+
def contacts
|
80
|
+
@contacts ||= Resources::Contacts.new(self)
|
81
|
+
end
|
82
|
+
|
83
|
+
def kapso_proxy?
|
84
|
+
@kapso_proxy
|
85
|
+
end
|
86
|
+
|
87
|
+
# Main request method with retry logic and error handling
|
88
|
+
def request(method, path, options = {})
|
89
|
+
method = method.to_s.upcase
|
90
|
+
body = options[:body]
|
91
|
+
query = options[:query]
|
92
|
+
custom_headers = options[:headers] || {}
|
93
|
+
response_type = options[:response_type] || :auto
|
94
|
+
|
95
|
+
url = build_url(path, query)
|
96
|
+
headers = build_headers(custom_headers)
|
97
|
+
|
98
|
+
# Log request if debugging
|
99
|
+
log_request(method, url, headers, body) if debug
|
100
|
+
|
101
|
+
retries = 0
|
102
|
+
begin
|
103
|
+
response = @http_client.run_request(method.downcase.to_sym, url, body, headers)
|
104
|
+
|
105
|
+
# Log response if debugging
|
106
|
+
log_response(response) if debug
|
107
|
+
|
108
|
+
# Handle response based on type requested
|
109
|
+
handle_response(response, response_type)
|
110
|
+
rescue Faraday::Error => e
|
111
|
+
retries += 1
|
112
|
+
if retries <= max_retries && retryable_error?(e)
|
113
|
+
sleep(retry_delay * retries)
|
114
|
+
retry
|
115
|
+
else
|
116
|
+
raise Errors::GraphApiError.new(
|
117
|
+
message: "Network error: #{e.message}",
|
118
|
+
http_status: 0,
|
119
|
+
category: :server
|
120
|
+
)
|
121
|
+
end
|
122
|
+
end
|
123
|
+
end
|
124
|
+
|
125
|
+
# Raw HTTP method without automatic error handling (for media downloads, etc.)
|
126
|
+
def raw_request(method, url, options = {})
|
127
|
+
headers = build_headers(options[:headers] || {})
|
128
|
+
|
129
|
+
log_request(method, url, headers, options[:body]) if debug
|
130
|
+
|
131
|
+
response = @http_client.run_request(method.to_sym, url, options[:body], headers)
|
132
|
+
|
133
|
+
log_response(response) if debug
|
134
|
+
|
135
|
+
response
|
136
|
+
end
|
137
|
+
|
138
|
+
# Fetch with automatic auth headers (for absolute URLs)
|
139
|
+
def fetch(url, options = {})
|
140
|
+
headers = build_headers(options[:headers] || {})
|
141
|
+
method = options[:method] || 'GET'
|
142
|
+
|
143
|
+
log_request(method, url, headers, options[:body]) if debug
|
144
|
+
|
145
|
+
response = @http_client.run_request(method.downcase.to_sym, url, options[:body], headers)
|
146
|
+
|
147
|
+
log_response(response) if debug
|
148
|
+
|
149
|
+
if response.success?
|
150
|
+
response
|
151
|
+
else
|
152
|
+
handle_error_response(response)
|
153
|
+
end
|
154
|
+
end
|
155
|
+
|
156
|
+
private
|
157
|
+
|
158
|
+
def build_http_client
|
159
|
+
Faraday.new do |f|
|
160
|
+
f.options.timeout = timeout
|
161
|
+
f.options.open_timeout = open_timeout
|
162
|
+
f.request :multipart
|
163
|
+
f.request :url_encoded
|
164
|
+
f.adapter Faraday.default_adapter
|
165
|
+
end
|
166
|
+
end
|
167
|
+
|
168
|
+
def build_headers(custom_headers = {})
|
169
|
+
headers = {}
|
170
|
+
|
171
|
+
# Authentication headers
|
172
|
+
if access_token
|
173
|
+
headers['Authorization'] = "Bearer #{access_token}"
|
174
|
+
end
|
175
|
+
|
176
|
+
if kapso_api_key
|
177
|
+
headers['X-API-Key'] = kapso_api_key
|
178
|
+
end
|
179
|
+
|
180
|
+
# Default content type for JSON requests
|
181
|
+
headers['Content-Type'] = 'application/json' unless custom_headers.key?('Content-Type')
|
182
|
+
|
183
|
+
headers.merge(custom_headers.compact)
|
184
|
+
end
|
185
|
+
|
186
|
+
def build_url(path, query = nil)
|
187
|
+
# Remove leading slash from path
|
188
|
+
clean_path = path.to_s.sub(%r{^/}, '')
|
189
|
+
|
190
|
+
# Build base URL with version
|
191
|
+
base = "#{base_url}/#{graph_version}/"
|
192
|
+
full_url = URI.join(base, clean_path).to_s
|
193
|
+
|
194
|
+
# Add query parameters if present
|
195
|
+
if query && !query.empty?
|
196
|
+
# Convert to snake_case for API (Meta expects snake_case)
|
197
|
+
snake_query = Types.deep_snake_case_keys(query)
|
198
|
+
query_string = URI.encode_www_form(flatten_query(snake_query))
|
199
|
+
separator = full_url.include?('?') ? '&' : '?'
|
200
|
+
full_url += "#{separator}#{query_string}"
|
201
|
+
end
|
202
|
+
|
203
|
+
full_url
|
204
|
+
end
|
205
|
+
|
206
|
+
def flatten_query(query, prefix = nil)
|
207
|
+
result = []
|
208
|
+
query.each do |key, value|
|
209
|
+
param_key = prefix ? "#{prefix}[#{key}]" : key.to_s
|
210
|
+
|
211
|
+
case value
|
212
|
+
when Hash
|
213
|
+
result.concat(flatten_query(value, param_key))
|
214
|
+
when Array
|
215
|
+
value.each { |v| result << [param_key, v] }
|
216
|
+
else
|
217
|
+
result << [param_key, value] unless value.nil?
|
218
|
+
end
|
219
|
+
end
|
220
|
+
result
|
221
|
+
end
|
222
|
+
|
223
|
+
def handle_response(response, response_type)
|
224
|
+
unless response.success?
|
225
|
+
handle_error_response(response)
|
226
|
+
end
|
227
|
+
|
228
|
+
case response_type
|
229
|
+
when :json
|
230
|
+
parse_json_response(response)
|
231
|
+
when :raw
|
232
|
+
response
|
233
|
+
when :auto
|
234
|
+
content_type = response.headers['content-type'] || ''
|
235
|
+
if content_type.include?('application/json')
|
236
|
+
parse_json_response(response)
|
237
|
+
elsif response.status == 204
|
238
|
+
Types::GraphSuccessResponse.new
|
239
|
+
else
|
240
|
+
response.body
|
241
|
+
end
|
242
|
+
else
|
243
|
+
response.body
|
244
|
+
end
|
245
|
+
end
|
246
|
+
|
247
|
+
def parse_json_response(response)
|
248
|
+
return Types::GraphSuccessResponse.new if response.body.nil? || response.body.strip.empty?
|
249
|
+
|
250
|
+
begin
|
251
|
+
json = JSON.parse(response.body)
|
252
|
+
# Convert camelCase keys to snake_case for Ruby conventions
|
253
|
+
Types.deep_snake_case_keys(json)
|
254
|
+
rescue JSON::ParserError => e
|
255
|
+
raise Errors::GraphApiError.new(
|
256
|
+
message: "Invalid JSON response: #{e.message}",
|
257
|
+
http_status: response.status,
|
258
|
+
raw_response: response.body
|
259
|
+
)
|
260
|
+
end
|
261
|
+
end
|
262
|
+
|
263
|
+
def handle_error_response(response)
|
264
|
+
body = response.body
|
265
|
+
|
266
|
+
# Try to parse JSON error
|
267
|
+
begin
|
268
|
+
json_body = JSON.parse(body) if body && !body.strip.empty?
|
269
|
+
rescue JSON::ParserError
|
270
|
+
json_body = nil
|
271
|
+
end
|
272
|
+
|
273
|
+
# Create error with proper parameters
|
274
|
+
raise Errors::GraphApiError.from_response(response, json_body || {}, body)
|
275
|
+
end
|
276
|
+
|
277
|
+
def retryable_error?(error)
|
278
|
+
# Only retry on network errors, not HTTP errors
|
279
|
+
error.is_a?(Faraday::TimeoutError) ||
|
280
|
+
error.is_a?(Faraday::ConnectionFailed) ||
|
281
|
+
error.is_a?(Faraday::ServerError)
|
282
|
+
end
|
283
|
+
|
284
|
+
def normalize_base_url(url)
|
285
|
+
url = url.to_s
|
286
|
+
url = "https://#{url}" unless url.match?(%r{^https?://})
|
287
|
+
url.chomp('/')
|
288
|
+
end
|
289
|
+
|
290
|
+
def detect_kapso_proxy(url)
|
291
|
+
url.match?(KAPSO_PROXY_PATTERN)
|
292
|
+
end
|
293
|
+
|
294
|
+
def log_request(method, url, headers, body)
|
295
|
+
logger.debug "WhatsApp API Request: #{method} #{url}"
|
296
|
+
logger.debug "Headers: #{headers.inspect}" if headers.any?
|
297
|
+
|
298
|
+
if body
|
299
|
+
if body.is_a?(String)
|
300
|
+
logger.debug "Body: #{body.length > 1000 ? "#{body[0..1000]}..." : body}"
|
301
|
+
else
|
302
|
+
logger.debug "Body: #{body.inspect}"
|
303
|
+
end
|
304
|
+
end
|
305
|
+
end
|
306
|
+
|
307
|
+
def log_response(response)
|
308
|
+
logger.debug "WhatsApp API Response: #{response.status}"
|
309
|
+
logger.debug "Response Headers: #{response.headers.to_h.inspect}"
|
310
|
+
|
311
|
+
if response.body
|
312
|
+
body_preview = response.body.length > 1000 ? "#{response.body[0..1000]}..." : response.body
|
313
|
+
logger.debug "Response Body: #{body_preview}"
|
314
|
+
end
|
315
|
+
end
|
316
|
+
end
|
317
|
+
end
|