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