Merge branch 'develop' into feat/email-forwarding

This commit is contained in:
Sivin Varghese
2025-06-12 15:46:58 +05:30
committed by GitHub
736 changed files with 10702 additions and 1919 deletions
+48
View File
@@ -0,0 +1,48 @@
class CsatSurveyService
pattr_initialize [:conversation!]
def perform
return unless should_send_csat_survey?
if within_messaging_window?
::MessageTemplates::Template::CsatSurvey.new(conversation: conversation).perform
else
create_csat_not_sent_activity_message
end
end
private
delegate :inbox, :contact, to: :conversation
def should_send_csat_survey?
conversation_allows_csat? && csat_enabled? && !csat_already_sent?
end
def conversation_allows_csat?
conversation.resolved? && !conversation.tweet?
end
def csat_enabled?
inbox.csat_survey_enabled?
end
def csat_already_sent?
conversation.messages.where(content_type: :input_csat).present?
end
def within_messaging_window?
conversation.can_reply?
end
def create_csat_not_sent_activity_message
content = I18n.t('conversations.activity.csat.not_sent_due_to_messaging_window')
activity_message_params = {
account_id: conversation.account_id,
inbox_id: conversation.inbox_id,
message_type: :activity,
content: content
}
::Conversations::ActivityMessageJob.perform_later(conversation, activity_message_params) if content
end
end
@@ -47,12 +47,29 @@ class Facebook::SendOnFacebookService < Base::SendOnChannelService
def fb_text_message_params
{
recipient: { id: contact.get_source_id(inbox.id) },
message: { text: message.content },
message: fb_text_message_payload,
messaging_type: 'MESSAGE_TAG',
tag: 'ACCOUNT_UPDATE'
}
end
def fb_text_message_payload
if message.content_type == 'input_select' && message.content_attributes['items'].any?
{
text: message.content,
quick_replies: message.content_attributes['items'].map do |item|
{
content_type: 'text',
payload: item['title'],
title: item['title']
}
end
}
else
{ text: message.outgoing_content }
end
end
def external_error(response)
# https://developers.facebook.com/docs/graph-api/guides/error-handling/
error_message = response['error']['message']
+1 -1
View File
@@ -30,7 +30,7 @@ class Instagram::BaseSendService < Base::SendOnChannelService
params = {
recipient: { id: contact.get_source_id(inbox.id) },
message: {
text: message.content
text: message.outgoing_content
}
}
+46 -1
View File
@@ -22,6 +22,14 @@ class Line::SendOnLineService < Base::SendOnChannelService
end
def build_payload
if message.content_type == 'input_select' && message.content_attributes['items'].any?
build_input_select_payload
else
build_text_payload
end
end
def build_text_payload
if message.content && message.attachments.any?
[text_message, *attachments]
elsif message.content.nil? && message.attachments.any?
@@ -48,10 +56,47 @@ class Line::SendOnLineService < Base::SendOnChannelService
def text_message
{
type: 'text',
text: message.content
text: message.outgoing_content
}
end
# https://developers.line.biz/en/reference/messaging-api/#flex-message
def build_input_select_payload
{
type: 'flex',
altText: message.content,
contents: {
type: 'bubble',
body: {
type: 'box',
layout: 'vertical',
contents: [
{
type: 'text',
text: message.content
},
*input_select_to_button
]
}
}
}
end
def input_select_to_button
message.content_attributes['items'].map do |item|
{
type: 'button',
style: 'link',
height: 'sm',
action: {
type: 'message',
label: item['title'],
text: item['value']
}
}
end
end
# https://developers.line.biz/en/reference/messaging-api/#error-responses
def external_error(error)
# Message containing information about the error. See https://developers.line.biz/en/reference/messaging-api/#error-messages
@@ -0,0 +1,26 @@
class Liquid::CampaignTemplateService
pattr_initialize [:campaign!, :contact!]
def call(message)
process_liquid_in_content(message_drops, message)
end
private
def message_drops
{
'contact' => ContactDrop.new(contact),
'agent' => UserDrop.new(campaign.sender),
'inbox' => InboxDrop.new(campaign.inbox),
'account' => AccountDrop.new(campaign.account)
}
end
def process_liquid_in_content(drops, message)
message = message.gsub(/`(.*?)`/m, '{% raw %}`\\1`{% endraw %}')
template = Liquid::Template.parse(message)
template.render(drops)
rescue Liquid::Error
message
end
end
@@ -17,7 +17,6 @@ class MessageTemplates::HookExecutionService
::MessageTemplates::Template::OutOfOffice.new(conversation: conversation).perform if should_send_out_of_office_message?
::MessageTemplates::Template::Greeting.new(conversation: conversation).perform if should_send_greeting?
::MessageTemplates::Template::EmailCollect.new(conversation: conversation).perform if inbox.enable_email_collect && should_send_email_collect?
::MessageTemplates::Template::CsatSurvey.new(conversation: conversation).perform if should_send_csat_survey?
end
def should_send_out_of_office_message?
@@ -55,23 +54,5 @@ class MessageTemplates::HookExecutionService
def contact_has_email?
contact.email
end
def csat_enabled_conversation?
return false unless conversation.resolved?
# should not sent since the link will be public
return false if conversation.tweet?
return false unless inbox.csat_survey_enabled?
true
end
def should_send_csat_survey?
return unless csat_enabled_conversation?
# only send CSAT once in a conversation
return if conversation.messages.where(content_type: :input_csat).present?
true
end
end
MessageTemplates::HookExecutionService.prepend_mod_with('MessageTemplates::HookExecutionService')
@@ -22,7 +22,8 @@ class Sms::OneoffSmsCampaignService
campaign.account.contacts.tagged_with(audience_labels, any: true).each do |contact|
next if contact.phone_number.blank?
send_message(to: contact.phone_number, content: campaign.message)
content = Liquid::CampaignTemplateService.new(campaign: campaign, contact: contact).call(campaign.message)
send_message(to: contact.phone_number, content: content)
end
end
@@ -94,13 +94,25 @@ class Telegram::IncomingMessageService
end
def file_content_type
return :image if params[:message][:photo].present? || params.dig(:message, :sticker, :thumb).present?
return :audio if params[:message][:voice].present? || params[:message][:audio].present?
return :video if params[:message][:video].present?
return :image if image_message?
return :audio if audio_message?
return :video if video_message?
file_type(params[:message][:document][:mime_type])
end
def image_message?
params[:message][:photo].present? || params.dig(:message, :sticker, :thumb).present?
end
def audio_message?
params[:message][:voice].present? || params[:message][:audio].present?
end
def video_message?
params[:message][:video].present? || params[:message][:video_note].present?
end
def attach_files
return unless file
@@ -174,6 +186,9 @@ class Telegram::IncomingMessageService
end
def visual_media_params
params[:message][:photo].presence&.last || params.dig(:message, :sticker, :thumb).presence || params[:message][:video].presence
params[:message][:photo].presence&.last ||
params.dig(:message, :sticker, :thumb).presence ||
params[:message][:video].presence ||
params[:message][:video_note].presence
end
end
@@ -22,7 +22,8 @@ class Twilio::OneoffSmsCampaignService
campaign.account.contacts.tagged_with(audience_labels, any: true).each do |contact|
next if contact.phone_number.blank?
channel.send_message(to: contact.phone_number, body: campaign.message)
content = Liquid::CampaignTemplateService.new(campaign: campaign, contact: contact).call(campaign.message)
channel.send_message(to: contact.phone_number, body: content)
end
end
end
@@ -16,7 +16,7 @@ class Twilio::SendOnTwilioService < Base::SendOnChannelService
def message_params
{
body: message.content,
body: message.outgoing_content,
to: contact_inbox.source_id,
media_url: attachments
}
@@ -37,7 +37,7 @@ class Twitter::SendOnTwitterService < Base::SendOnChannelService
def send_direct_message
twitter_client.send_direct_message(
recipient_id: contact_inbox.source_id,
message: message.content
message: message.outgoing_content
)
end
@@ -52,7 +52,7 @@ class Twitter::SendOnTwitterService < Base::SendOnChannelService
def send_tweet_reply
response = twitter_client.send_tweet_reply(
reply_to_tweet_id: reply_to_message.source_id,
tweet: "#{screen_name} #{message.content}"
tweet: "#{screen_name} #{message.outgoing_content}"
)
if response.status == '200'
tweet_data = response.body
@@ -93,14 +93,14 @@ class Whatsapp::Providers::BaseService
def create_button_payload(message)
buttons = create_buttons(message.content_attributes['items'])
json_hash = { 'buttons' => buttons }
create_payload('button', message.content, JSON.generate(json_hash))
create_payload('button', message.outgoing_content, JSON.generate(json_hash))
end
def create_list_payload(message)
rows = create_rows(message.content_attributes['items'])
section1 = { 'rows' => rows }
sections = [section1]
json_hash = { :button => 'Choose an item', 'sections' => sections }
create_payload('list', message.content, JSON.generate(json_hash))
json_hash = { :button => I18n.t('conversations.messages.whatsapp.list_button_label'), 'sections' => sections }
create_payload('list', message.outgoing_content, JSON.generate(json_hash))
end
end
@@ -63,7 +63,7 @@ class Whatsapp::Providers::Whatsapp360DialogService < Whatsapp::Providers::BaseS
headers: api_headers,
body: {
to: phone_number,
text: { body: message.content },
text: { body: message.outgoing_content },
type: 'text'
}.to_json
)
@@ -77,7 +77,7 @@ class Whatsapp::Providers::Whatsapp360DialogService < Whatsapp::Providers::BaseS
type_content = {
'link': attachment.download_url
}
type_content['caption'] = message.content unless %w[audio sticker].include?(type)
type_content['caption'] = message.outgoing_content unless %w[audio sticker].include?(type)
type_content['filename'] = attachment.file.filename if type == 'document'
response = HTTParty.post(
@@ -82,7 +82,7 @@ class Whatsapp::Providers::WhatsappCloudService < Whatsapp::Providers::BaseServi
messaging_product: 'whatsapp',
context: whatsapp_reply_context(message),
to: phone_number,
text: { body: message.content },
text: { body: message.outgoing_content },
type: 'text'
}.to_json
)
@@ -96,7 +96,7 @@ class Whatsapp::Providers::WhatsappCloudService < Whatsapp::Providers::BaseServi
type_content = {
'link': attachment.download_url
}
type_content['caption'] = message.content unless %w[audio sticker].include?(type)
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",
@@ -61,7 +61,7 @@ class Whatsapp::SendOnWhatsappService < Base::SendOnChannelService
return if body_object.blank?
template_match_regex = build_template_match_regex(body_object['text'])
message.content.match(template_match_regex)
message.outgoing_content.match(template_match_regex)
end
def build_template_match_regex(template_text)