Files
chatwoot/app/services/tiktok/messaging_helpers.rb
b8108b71c1 fix(tiktok): Resolve media upload failures and gate attachments by conversation capability (#13643)
This PR fixes TikTok attachment send failures and adds a
capability-based guard so attachments are only enabled for conversations
that support media sending.

- Fixed TikTok media upload request formatting so TikTok accepts image
uploads reliably.
- Added TikTok capability check (IMAGE_SEND) during conversation
creation.
- Stored capability in
conversation.additional_attributes.tiktok_capabilities.
- Updated reply composer UI to disable/hide attachment upload for TikTok
conversations where image_send is false.

Fixes
https://linear.app/chatwoot/issue/CW-6532/enable-attachments-based-on-the-conversation-capability
and
https://linear.app/chatwoot/issue/CW-6996/unable-to-send-image-attachments-to-tiktok-customer-400-parsing-error

---------

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Muhsin <12408980+muhsin-k@users.noreply.github.com>
2026-05-06 11:21:15 +04:00

90 lines
3.2 KiB
Ruby

module Tiktok::MessagingHelpers
private
def create_contact_inbox(channel, tt_conversation_id, from, from_id)
::ContactInboxWithContactBuilder.new(
source_id: tt_conversation_id,
inbox: channel.inbox,
contact_attributes: contact_attributes(from, from_id)
).perform
end
def contact_attributes(from, from_id)
{
name: from,
additional_attributes: contact_additional_attributes(from, from_id)
}
end
def contact_additional_attributes(from, from_id)
{
# TODO: Remove this once we show the social_tiktok_user_name in the UI instead of the username
username: from,
social_profiles: { tiktok: from },
social_tiktok_user_id: from_id,
social_tiktok_user_name: from
}
end
def find_conversation(channel, tt_conversation_id)
contact_inbox = channel.inbox.contact_inboxes.find_by(source_id: tt_conversation_id)
return if contact_inbox.blank?
if channel.inbox.lock_to_single_conversation
contact_inbox.conversations.order(created_at: :desc).first
else
contact_inbox.conversations.where.not(status: :resolved).order(created_at: :desc).first ||
contact_inbox.conversations.order(created_at: :desc).first
end
end
def create_conversation(channel, contact_inbox, tt_conversation_id)
::Conversation.create!(conversation_params(channel, contact_inbox, tt_conversation_id))
end
def conversation_params(channel, contact_inbox, tt_conversation_id)
{
account_id: channel.inbox.account_id,
inbox_id: channel.inbox.id,
contact_id: contact_inbox.contact.id,
contact_inbox_id: contact_inbox.id,
additional_attributes: conversation_additional_attributes(channel, tt_conversation_id)
}
end
def conversation_additional_attributes(channel, tt_conversation_id)
attributes = { conversation_id: tt_conversation_id }
capabilities = tiktok_conversation_capabilities(channel, tt_conversation_id)
attributes[:tiktok_capabilities] = capabilities if capabilities.present?
attributes
end
def tiktok_conversation_capabilities(channel, tt_conversation_id)
image_send = tiktok_client(channel).image_send_capable?(tt_conversation_id)
{ image_send: image_send, updated_at: Time.current.iso8601 }
rescue StandardError => e
Rails.logger.error(
'Failed to fetch TikTok conversation capabilities ' \
"for tt_conversation_id=#{tt_conversation_id}, business_id=#{channel.business_id}: #{e.class}: #{e.message}"
)
{}
end
def find_message(tt_conversation_id, tt_message_id)
message = Message.find_by(source_id: tt_message_id)
message_conversation_id = message&.conversation&.[](:additional_attributes)&.[]('conversation_id')
return if message_conversation_id != tt_conversation_id
message
end
def fetch_attachment(channel, tt_conversation_id, tt_message_id, tt_image_media_id)
file_download_url = tiktok_client(channel).file_download_url(tt_conversation_id, tt_message_id, tt_image_media_id)
Down.download(file_download_url, headers: { 'x-user' => channel.validated_access_token })
end
def tiktok_client(channel)
Tiktok::Client.new(business_id: channel.business_id, access_token: channel.validated_access_token)
end
end