## Summary This PR enables and surfaces **conversation workflow** for social-style channels that should support either: - `Create new conversations` after resolve, or - `Reopen same conversation` ## What is included - Adds the conversation workflow setting UI as card-based options in Inbox Settings. - Expands channel availability in settings to include channels like: - Telegram - TikTok - Instagram - Line - WhatsApp - Facebook - Updates conversation selection behavior for Line incoming messages to respect the workflow (reopen vs create-new-after-resolved). - Updates TikTok conversation selection behavior to respect the workflow (reopen vs create-new-after-resolved). - Keeps email behavior unchanged (always starts a new thread). Fixes: https://github.com/chatwoot/chatwoot/issues/8426 ## Screenshot <img width="1400" height="900" alt="pr11079-workflow-sender-clear-tight" src="https://github.com/user-attachments/assets/9456821f-8d83-4924-8dcf-7503c811a7b1" /> ## How To Reproduce 1. Open `Settings -> Inboxes -> <Telegram/TikTok/Instagram/Line/Facebook/WhatsApp inbox> -> Settings`. 2. Verify **Conversation workflow** is visible with the two card options. 3. Toggle between both options and save. 4. For Line and TikTok, verify resolved-conversation behavior follows the selected workflow. ## Testing - `RAILS_ENV=test bundle exec rspec spec/builders/messages/instagram/message_builder_spec.rb:213 spec/builders/messages/instagram/message_builder_spec.rb:255 spec/builders/messages/instagram/messenger/message_builder_spec.rb:228 spec/builders/messages/instagram/messenger/message_builder_spec.rb:293 spec/services/tiktok/message_service_spec.rb` - Result: `16 examples, 0 failures` ## Follow-up - Migrate Website Live Chat workflow settings into this same conversation-workflow settings model. - Add Voice channel support for this workflow setting. --------- Co-authored-by: Muhsin Keloth <muhsinkeramam@gmail.com> Co-authored-by: Sivin Varghese <64252451+iamsivin@users.noreply.github.com> Co-authored-by: iamsivin <iamsivin@gmail.com>
177 lines
4.9 KiB
Ruby
177 lines
4.9 KiB
Ruby
# ref : https://developers.line.biz/en/docs/messaging-api/receiving-messages/#webhook-event-types
|
|
# https://developers.line.biz/en/reference/messaging-api/#message-event
|
|
|
|
class Line::IncomingMessageService
|
|
include ::FileTypeHelper
|
|
pattr_initialize [:inbox!, :params!]
|
|
LINE_STICKER_IMAGE_URL = 'https://stickershop.line-scdn.net/stickershop/v1/sticker/%s/android/sticker.png'.freeze
|
|
|
|
def perform
|
|
# probably test events
|
|
return if params[:events].blank?
|
|
|
|
parse_events
|
|
end
|
|
|
|
private
|
|
|
|
def parse_events
|
|
params[:events].each do |event|
|
|
next unless event_type_message?(event)
|
|
|
|
get_line_contact_info(event)
|
|
next if @line_contact_info['userId'].blank?
|
|
|
|
set_contact
|
|
set_conversation
|
|
|
|
next unless message_created? event
|
|
|
|
attach_files event['message']
|
|
@message.save!
|
|
end
|
|
end
|
|
|
|
def message_created?(event)
|
|
@message = @conversation.messages.build(
|
|
content: message_content(event),
|
|
account_id: @inbox.account_id,
|
|
content_type: message_content_type(event),
|
|
inbox_id: @inbox.id,
|
|
message_type: :incoming,
|
|
sender: @contact,
|
|
source_id: event['message']['id'].to_s
|
|
)
|
|
@message
|
|
end
|
|
|
|
def message_content(event)
|
|
message_type = event.dig('message', 'type')
|
|
case message_type
|
|
when 'text'
|
|
event.dig('message', 'text')
|
|
when 'sticker'
|
|
sticker_id = event.dig('message', 'stickerId')
|
|
sticker_image_url(sticker_id)
|
|
end
|
|
end
|
|
|
|
# Currently, Chatwoot doesn't support stickers. As a temporary solution,
|
|
# we're displaying stickers as images using the sticker ID in markdown format.
|
|
# This is subject to change in the future. We've chosen not to download and display the sticker as an image because the sticker's information
|
|
# and images are the property of the creator or legal owner. We aim to avoid storing it on our server without their consent.
|
|
# If there are any permission or rendering issues, the URL may break, and we'll display the sticker ID as text instead.
|
|
# Ref: https://developers.line.biz/en/reference/messaging-api/#wh-sticker
|
|
def sticker_image_url(sticker_id)
|
|
""
|
|
end
|
|
|
|
def message_content_type(event)
|
|
return 'sticker' if event['message']['type'] == 'sticker'
|
|
|
|
'text'
|
|
end
|
|
|
|
def attach_files(message)
|
|
return unless message_type_non_text?(message['type'])
|
|
|
|
response = inbox.channel.client.get_message_content(message['id'])
|
|
|
|
extension = get_file_extension(response)
|
|
file_name = message['fileName'] || "media-#{message['id']}.#{extension}"
|
|
temp_file = Tempfile.new(file_name)
|
|
temp_file.binmode
|
|
temp_file << response.body
|
|
temp_file.rewind
|
|
|
|
@message.attachments.new(
|
|
account_id: @message.account_id,
|
|
file_type: file_content_type(response),
|
|
file: {
|
|
io: temp_file,
|
|
filename: file_name,
|
|
content_type: response.content_type
|
|
}
|
|
)
|
|
end
|
|
|
|
def get_file_extension(response)
|
|
if response.content_type&.include?('/')
|
|
response.content_type.split('/')[1]
|
|
else
|
|
'bin'
|
|
end
|
|
end
|
|
|
|
def event_type_message?(event)
|
|
event['type'] == 'message' || event['type'] == 'sticker'
|
|
end
|
|
|
|
def message_type_non_text?(type)
|
|
[
|
|
Line::Bot::Event::MessageType::Video,
|
|
Line::Bot::Event::MessageType::Audio,
|
|
Line::Bot::Event::MessageType::Image,
|
|
Line::Bot::Event::MessageType::File
|
|
].include?(type)
|
|
end
|
|
|
|
def account
|
|
@account ||= inbox.account
|
|
end
|
|
|
|
def get_line_contact_info(event)
|
|
@line_contact_info = JSON.parse(inbox.channel.client.get_profile(event['source']['userId']).body)
|
|
end
|
|
|
|
def set_contact
|
|
contact_inbox = ::ContactInboxWithContactBuilder.new(
|
|
source_id: @line_contact_info['userId'],
|
|
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: @line_contact_info['displayName'],
|
|
avatar_url: @line_contact_info['pictureUrl'],
|
|
additional_attributes: additional_attributes
|
|
}
|
|
end
|
|
|
|
def additional_attributes
|
|
{
|
|
social_line_user_id: @line_contact_info['userId']
|
|
}
|
|
end
|
|
|
|
def file_content_type(file_content)
|
|
file_type(file_content.content_type)
|
|
end
|
|
end
|