213 lines
6.0 KiB
Ruby
213 lines
6.0 KiB
Ruby
class Twilio::IncomingMessageService
|
|
include ::FileTypeHelper
|
|
|
|
pattr_initialize [:params!]
|
|
|
|
def perform
|
|
return if twilio_channel.blank?
|
|
|
|
set_contact
|
|
set_conversation
|
|
@message = @conversation.messages.build(
|
|
content: message_body,
|
|
account_id: @inbox.account_id,
|
|
inbox_id: @inbox.id,
|
|
message_type: :incoming,
|
|
sender: @contact,
|
|
source_id: params[:SmsSid]
|
|
)
|
|
attach_files
|
|
attach_location if location_message?
|
|
@message.save!
|
|
end
|
|
|
|
private
|
|
|
|
def twilio_channel
|
|
@twilio_channel ||= ::Channel::TwilioSms.find_by(messaging_service_sid: params[:MessagingServiceSid]) if params[:MessagingServiceSid].present?
|
|
if params[:AccountSid].present? && params[:To].present?
|
|
@twilio_channel ||= ::Channel::TwilioSms.find_by(account_sid: params[:AccountSid],
|
|
phone_number: params[:To])
|
|
end
|
|
log_channel_not_found if @twilio_channel.blank?
|
|
@twilio_channel
|
|
end
|
|
|
|
def log_channel_not_found
|
|
Rails.logger.warn(
|
|
'[TWILIO] Incoming message channel lookup failed ' \
|
|
"account_sid=#{params[:AccountSid]} " \
|
|
"to=#{params[:To]} " \
|
|
"messaging_service_sid=#{params[:MessagingServiceSid]} " \
|
|
"sms_sid=#{params[:SmsSid]}"
|
|
)
|
|
end
|
|
|
|
def inbox
|
|
@inbox ||= twilio_channel.inbox
|
|
end
|
|
|
|
def account
|
|
@account ||= inbox.account
|
|
end
|
|
|
|
def phone_number
|
|
twilio_channel.sms? ? params[:From] : params[:From].gsub('whatsapp:', '')
|
|
end
|
|
|
|
def normalized_phone_number
|
|
return phone_number unless twilio_channel.whatsapp?
|
|
|
|
Whatsapp::PhoneNumberNormalizationService.new(inbox).normalize_and_find_contact_by_provider("whatsapp:#{phone_number}", :twilio)
|
|
end
|
|
|
|
def formatted_phone_number
|
|
TelephoneNumber.parse(phone_number).international_number
|
|
end
|
|
|
|
def message_body
|
|
params[:Body]&.delete("\u0000")
|
|
end
|
|
|
|
def set_contact
|
|
source_id = twilio_channel.whatsapp? ? normalized_phone_number : params[:From]
|
|
|
|
contact_inbox = ::ContactInboxWithContactBuilder.new(
|
|
source_id: source_id,
|
|
inbox: inbox,
|
|
contact_attributes: contact_attributes
|
|
).perform
|
|
|
|
@contact_inbox = contact_inbox
|
|
@contact = contact_inbox.contact
|
|
|
|
# Update existing contact name if ProfileName is available and current name is just phone number
|
|
update_contact_name_if_needed
|
|
end
|
|
|
|
def conversation_params
|
|
{
|
|
account_id: @inbox.account_id,
|
|
inbox_id: @inbox.id,
|
|
contact_id: @contact.id,
|
|
contact_inbox_id: @contact_inbox.id,
|
|
additional_attributes: additional_attributes
|
|
}
|
|
end
|
|
|
|
def set_conversation
|
|
# if lock to single conversation is disabled, we will create a new conversation if previous conversation is resolved
|
|
@conversation = if @inbox.lock_to_single_conversation
|
|
@contact_inbox.conversations.last
|
|
else
|
|
@contact_inbox.conversations.where
|
|
.not(status: :resolved).last
|
|
end
|
|
return if @conversation
|
|
|
|
@conversation = ::Conversation.create!(conversation_params)
|
|
end
|
|
|
|
def contact_attributes
|
|
{
|
|
name: contact_name,
|
|
phone_number: phone_number,
|
|
additional_attributes: additional_attributes
|
|
}
|
|
end
|
|
|
|
def contact_name
|
|
params[:ProfileName].presence || formatted_phone_number
|
|
end
|
|
|
|
def additional_attributes
|
|
if twilio_channel.sms?
|
|
{
|
|
from_zip_code: params[:FromZip],
|
|
from_country: params[:FromCountry],
|
|
from_state: params[:FromState]
|
|
}
|
|
else
|
|
{}
|
|
end
|
|
end
|
|
|
|
def attach_files
|
|
num_media = params[:NumMedia].to_i
|
|
return if num_media.zero?
|
|
|
|
num_media.times do |i|
|
|
media_url = params[:"MediaUrl#{i}"]
|
|
attach_single_file(media_url) if media_url.present?
|
|
end
|
|
end
|
|
|
|
def attach_single_file(media_url)
|
|
attachment_file = download_attachment_file(media_url)
|
|
return if attachment_file.blank?
|
|
|
|
@message.attachments.new(
|
|
account_id: @message.account_id,
|
|
file_type: file_type(attachment_file.content_type),
|
|
file: {
|
|
io: attachment_file,
|
|
filename: attachment_file.original_filename,
|
|
content_type: attachment_file.content_type
|
|
}
|
|
)
|
|
end
|
|
|
|
def download_attachment_file(media_url)
|
|
download_with_auth(media_url)
|
|
rescue Down::Error, Down::ClientError => e
|
|
handle_download_attachment_error(e, media_url)
|
|
end
|
|
|
|
def download_with_auth(media_url)
|
|
auth_credentials = if twilio_channel.api_key_sid.present?
|
|
# When using api_key_sid, the auth token should be the api_secret_key
|
|
[twilio_channel.api_key_sid, twilio_channel.auth_token]
|
|
else
|
|
# When using account_sid, the auth token is the account's auth token
|
|
[twilio_channel.account_sid, twilio_channel.auth_token]
|
|
end
|
|
|
|
Down.download(media_url, http_basic_authentication: auth_credentials)
|
|
end
|
|
|
|
def handle_download_attachment_error(error, media_url)
|
|
Rails.logger.info "Error downloading attachment from Twilio: #{error.message}: Retrying without auth"
|
|
Down.download(media_url)
|
|
rescue StandardError => e
|
|
Rails.logger.info "Error downloading attachment from Twilio: #{e.message}: Skipping"
|
|
nil
|
|
end
|
|
|
|
def location_message?
|
|
params[:MessageType] == 'location' && params[:Latitude].present? && params[:Longitude].present?
|
|
end
|
|
|
|
def attach_location
|
|
@message.attachments.new(
|
|
account_id: @message.account_id,
|
|
file_type: :location,
|
|
coordinates_lat: params[:Latitude].to_f,
|
|
coordinates_long: params[:Longitude].to_f
|
|
)
|
|
end
|
|
|
|
def update_contact_name_if_needed
|
|
return if params[:ProfileName].blank?
|
|
return if @contact.name == params[:ProfileName]
|
|
|
|
# Only update if current name exactly matches the phone number or formatted phone number
|
|
return unless contact_name_matches_phone_number?
|
|
|
|
@contact.update!(name: params[:ProfileName])
|
|
end
|
|
|
|
def contact_name_matches_phone_number?
|
|
@contact.name == phone_number || @contact.name == formatted_phone_number
|
|
end
|
|
end
|