## Description This pull request introduces an optional parameter, `phone_number_id`, to the WhatsApp API call responsible for retrieving media. The addition of this parameter allows for greater flexibility when interacting with the WhatsApp API, as it can now accommodate scenarios where specifying a particular phone number ID is necessary. This change is backward compatible and does not affect existing functionality if the parameter is not provided. Fixes # (issue) ## Type of change - [x] New feature (non-breaking change which adds functionality) ## How Has This Been Tested? The changes were tested locally by invoking the WhatsApp media retrieval API with and without the `phone_number_id` parameter. Both scenarios were verified to ensure that: - When `phone_number_id` is provided, the API call includes the parameter and functions as expected. - When `phone_number_id` is omitted, the API call continues to work as before, maintaining backward compatibility. No errors or warnings were observed during testing, and all relevant unit tests passed successfully. ## Checklist - [x] My code follows the style guidelines of this project - [x] I have performed a self-review of my code - [x] I have commented on my code, particularly in hard-to-understand areas - [x] I have made corresponding changes to the documentation - [x] My changes generate no new warnings - [x] I have added tests that prove my fix is effective or that my feature works - [x] New and existing unit tests pass locally with my changes - [x] Any dependent changes have been merged and published in downstream modules --------- Co-authored-by: Muhsin Keloth <muhsinkeramam@gmail.com>
191 lines
5.8 KiB
Ruby
191 lines
5.8 KiB
Ruby
class Whatsapp::Providers::WhatsappCloudService < Whatsapp::Providers::BaseService
|
|
def send_message(phone_number, message)
|
|
@message = message
|
|
|
|
if message.attachments.present?
|
|
send_attachment_message(phone_number, message)
|
|
elsif message.content_type == 'input_select'
|
|
send_interactive_text_message(phone_number, message)
|
|
else
|
|
send_text_message(phone_number, message)
|
|
end
|
|
end
|
|
|
|
def send_template(phone_number, template_info, message)
|
|
template_body = template_body_parameters(template_info)
|
|
|
|
request_body = {
|
|
messaging_product: 'whatsapp',
|
|
recipient_type: 'individual', # Only individual messages supported (not group messages)
|
|
to: phone_number,
|
|
type: 'template',
|
|
template: template_body
|
|
}
|
|
|
|
response = HTTParty.post(
|
|
"#{phone_id_path}/messages",
|
|
headers: api_headers,
|
|
body: request_body.to_json
|
|
)
|
|
|
|
process_response(response, message)
|
|
end
|
|
|
|
def sync_templates
|
|
# ensuring that channels with wrong provider config wouldn't keep trying to sync templates
|
|
whatsapp_channel.mark_message_templates_updated
|
|
templates = fetch_whatsapp_templates("#{business_account_path}/message_templates?access_token=#{whatsapp_channel.provider_config['api_key']}")
|
|
whatsapp_channel.update(message_templates: templates, message_templates_last_updated: Time.now.utc) if templates.present?
|
|
end
|
|
|
|
def fetch_whatsapp_templates(url)
|
|
response = HTTParty.get(url)
|
|
return [] unless response.success?
|
|
|
|
next_url = next_url(response)
|
|
|
|
return response['data'] + fetch_whatsapp_templates(next_url) if next_url.present?
|
|
|
|
response['data']
|
|
end
|
|
|
|
def next_url(response)
|
|
response['paging'] ? response['paging']['next'] : ''
|
|
end
|
|
|
|
def validate_provider_config?
|
|
response = HTTParty.get("#{business_account_path}/message_templates?access_token=#{whatsapp_channel.provider_config['api_key']}")
|
|
response.success?
|
|
end
|
|
|
|
def api_headers
|
|
{ 'Authorization' => "Bearer #{whatsapp_channel.provider_config['api_key']}", 'Content-Type' => 'application/json' }
|
|
end
|
|
|
|
def media_url(media_id, phone_number_id = nil)
|
|
url = "#{api_base_path}/v13.0/#{media_id}"
|
|
url += "?phone_number_id=#{phone_number_id}" if phone_number_id
|
|
url
|
|
end
|
|
|
|
def api_base_path
|
|
ENV.fetch('WHATSAPP_CLOUD_BASE_URL', 'https://graph.facebook.com')
|
|
end
|
|
|
|
# TODO: See if we can unify the API versions and for both paths and make it consistent with out facebook app API versions
|
|
def phone_id_path
|
|
"#{api_base_path}/v13.0/#{whatsapp_channel.provider_config['phone_number_id']}"
|
|
end
|
|
|
|
def business_account_path
|
|
"#{api_base_path}/v14.0/#{whatsapp_channel.provider_config['business_account_id']}"
|
|
end
|
|
|
|
def send_text_message(phone_number, message)
|
|
response = HTTParty.post(
|
|
"#{phone_id_path}/messages",
|
|
headers: api_headers,
|
|
body: {
|
|
messaging_product: 'whatsapp',
|
|
context: whatsapp_reply_context(message),
|
|
to: phone_number,
|
|
text: { body: message.outgoing_content },
|
|
type: 'text'
|
|
}.to_json
|
|
)
|
|
|
|
process_response(response, message)
|
|
end
|
|
|
|
def send_attachment_message(phone_number, message)
|
|
attachment = message.attachments.first
|
|
type = %w[image audio video].include?(attachment.file_type) ? attachment.file_type : 'document'
|
|
type_content = {
|
|
'link': attachment.download_url
|
|
}
|
|
type_content['caption'] = message.outgoing_content unless %w[audio sticker].include?(type)
|
|
type_content['filename'] = attachment.file.filename if type == 'document'
|
|
response = HTTParty.post(
|
|
"#{phone_id_path}/messages",
|
|
headers: api_headers,
|
|
body: {
|
|
:messaging_product => 'whatsapp',
|
|
:context => whatsapp_reply_context(message),
|
|
'to' => phone_number,
|
|
'type' => type,
|
|
type.to_s => type_content
|
|
}.to_json
|
|
)
|
|
|
|
process_response(response, message)
|
|
end
|
|
|
|
def error_message(response)
|
|
# https://developers.facebook.com/docs/whatsapp/cloud-api/support/error-codes/#sample-response
|
|
response.parsed_response&.dig('error', 'message')
|
|
end
|
|
|
|
def template_body_parameters(template_info)
|
|
template_body = {
|
|
name: template_info[:name],
|
|
language: {
|
|
policy: 'deterministic',
|
|
code: template_info[:lang_code]
|
|
}
|
|
}
|
|
|
|
# Enhanced template parameters structure
|
|
# Note: Legacy format support (simple parameter arrays) has been removed
|
|
# in favor of the enhanced component-based structure that supports
|
|
# headers, buttons, and authentication templates.
|
|
#
|
|
# Expected payload format from frontend:
|
|
# {
|
|
# processed_params: {
|
|
# body: { '1': 'John', '2': '123 Main St' },
|
|
# header: {
|
|
# media_url: 'https://...',
|
|
# media_type: 'image',
|
|
# media_name: 'filename.pdf' # Optional, for document templates only
|
|
# },
|
|
# buttons: [{ type: 'url', parameter: 'otp123456' }]
|
|
# }
|
|
# }
|
|
# This gets transformed into WhatsApp API component format:
|
|
# [
|
|
# { type: 'body', parameters: [...] },
|
|
# { type: 'header', parameters: [...] },
|
|
# { type: 'button', sub_type: 'url', parameters: [...] }
|
|
# ]
|
|
template_body[:components] = template_info[:parameters] || []
|
|
|
|
template_body
|
|
end
|
|
|
|
def whatsapp_reply_context(message)
|
|
reply_to = message.content_attributes[:in_reply_to_external_id]
|
|
return nil if reply_to.blank?
|
|
|
|
{
|
|
message_id: reply_to
|
|
}
|
|
end
|
|
|
|
def send_interactive_text_message(phone_number, message)
|
|
payload = create_payload_based_on_items(message)
|
|
|
|
response = HTTParty.post(
|
|
"#{phone_id_path}/messages",
|
|
headers: api_headers,
|
|
body: {
|
|
messaging_product: 'whatsapp',
|
|
to: phone_number,
|
|
interactive: payload,
|
|
type: 'interactive'
|
|
}.to_json
|
|
)
|
|
|
|
process_response(response, message)
|
|
end
|
|
end
|