Restructure omni services and add Chatwoot research snapshot

This commit is contained in:
Ruslan Bakiev
2026-02-21 11:11:27 +07:00
parent edea7a0034
commit b73babbbf6
7732 changed files with 978203 additions and 32 deletions

View File

@@ -0,0 +1,52 @@
class Sms::DeliveryStatusService
pattr_initialize [:inbox!, :params!]
def perform
return unless supported_status?
process_status if message.present?
end
private
def process_status
@message.status = status
@message.external_error = external_error if error_occurred?
@message.save!
end
def supported_status?
%w[message-delivered message-failed].include?(params[:type])
end
# Relevant documentation:
# https://dev.bandwidth.com/docs/mfa/webhooks/international/message-delivered
# https://dev.bandwidth.com/docs/mfa/webhooks/international/message-failed
def status
type_mapping = {
'message-delivered' => 'delivered',
'message-failed' => 'failed'
}
type_mapping[params[:type]]
end
def external_error
return nil unless error_occurred?
error_message = params[:description]
error_code = params[:errorCode]
"#{error_code} - #{error_message}"
end
def error_occurred?
params[:errorCode] && params[:type] == 'message-failed'
end
def message
return unless params[:message][:id]
@message ||= inbox.messages.find_by(source_id: params[:message][:id])
end
end

View File

@@ -0,0 +1,102 @@
class Sms::IncomingMessageService
include ::FileTypeHelper
pattr_initialize [:inbox!, :params!]
def perform
set_contact
set_conversation
@message = @conversation.messages.create!(
content: params[:text],
account_id: @inbox.account_id,
inbox_id: @inbox.id,
message_type: :incoming,
sender: @contact,
source_id: params[:id]
)
attach_files
@message.save!
end
private
def account
@account ||= @inbox.account
end
def channel
@channel ||= @inbox.channel
end
def phone_number
params[:from]
end
def formatted_phone_number
TelephoneNumber.parse(phone_number).international_number
end
def set_contact
contact_inbox = ::ContactInboxWithContactBuilder.new(
source_id: params[:from],
inbox: @inbox,
contact_attributes: contact_attributes
).perform
@contact_inbox = contact_inbox
@contact = contact_inbox.contact
end
def conversation_params
{
account_id: @inbox.account_id,
inbox_id: @inbox.id,
contact_id: @contact.id,
contact_inbox_id: @contact_inbox.id
}
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: formatted_phone_number,
phone_number: phone_number
}
end
def attach_files
return if params[:media].blank?
params[:media].each do |media_url|
# we don't need to process this files since chatwoot doesn't support it
next if media_url.end_with?('.smil', '.xml')
attachment_file = Down.download(
media_url,
http_basic_authentication: [channel.provider_config['api_key'], channel.provider_config['api_secret']]
)
@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
end
end

View File

@@ -0,0 +1,35 @@
class Sms::OneoffSmsCampaignService
pattr_initialize [:campaign!]
def perform
raise "Invalid campaign #{campaign.id}" if campaign.inbox.inbox_type != 'Sms' || !campaign.one_off?
raise 'Completed Campaign' if campaign.completed?
# marks campaign completed so that other jobs won't pick it up
campaign.completed!
audience_label_ids = campaign.audience.select { |audience| audience['type'] == 'Label' }.pluck('id')
audience_labels = campaign.account.labels.where(id: audience_label_ids).pluck(:title)
process_audience(audience_labels)
end
private
delegate :inbox, to: :campaign
delegate :channel, to: :inbox
def process_audience(audience_labels)
campaign.account.contacts.tagged_with(audience_labels, any: true).each do |contact|
next if contact.phone_number.blank?
content = Liquid::CampaignTemplateService.new(campaign: campaign, contact: contact).call(campaign.message)
send_message(to: contact.phone_number, content: content)
end
end
def send_message(to:, content:)
channel.send_text_message(to, content)
rescue StandardError => e
Rails.logger.error("[SMS Campaign #{campaign.id}] Failed to send to #{to}: #{e.message}")
end
end

View File

@@ -0,0 +1,16 @@
class Sms::SendOnSmsService < Base::SendOnChannelService
private
def channel_class
Channel::Sms
end
def perform_reply
send_on_sms
end
def send_on_sms
message_id = channel.send_message(message.conversation.contact_inbox.source_id, message)
message.update!(source_id: message_id) if message_id.present?
end
end