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.
@@ -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