feat(channel): add support for Telegram Business bots (#10181) (#11663)

Added support for Telegram Business bots. Telegram webhooks from such bots include the business_message field, which we transform into a standard message for Chatwoot. This PR also modifies how we handle replies, attachments, and image uploads when working with Telegram Business bots.

demo: https://drive.google.com/file/d/1Yz82wXBVRtb-mxjXogkUju4hlJbt3qyh/view?usp=sharing&t=4

Fixes #10181
This commit is contained in:
ruslan
2025-06-17 06:35:23 +03:00
committed by GitHub
parent 149dab239a
commit b87b7972c1
10 changed files with 203 additions and 11 deletions

View File

@@ -35,7 +35,7 @@ class Webhooks::TelegramEventsJob < ApplicationJob
def process_event_params(channel, params)
return unless params[:telegram]
if params.dig(:telegram, :edited_message).present?
if params.dig(:telegram, :edited_message).present? || params.dig(:telegram, :edited_business_message).present?
Telegram::UpdateMessageService.new(inbox: channel.inbox, params: params['telegram'].with_indifferent_access).perform
else
Telegram::IncomingMessageService.new(inbox: channel.inbox, params: params['telegram'].with_indifferent_access).perform

View File

@@ -69,6 +69,10 @@ class Channel::Telegram < ApplicationRecord
message.conversation[:additional_attributes]['chat_id']
end
def business_connection_id(message)
message.conversation[:additional_attributes]['business_connection_id']
end
def reply_to_message_id(message)
message.content_attributes['in_reply_to_external_id']
end
@@ -95,7 +99,13 @@ class Channel::Telegram < ApplicationRecord
end
def send_message(message)
response = message_request(chat_id(message), message.outgoing_content, reply_markup(message), reply_to_message_id(message))
response = message_request(
chat_id(message),
message.outgoing_content,
reply_markup(message),
reply_to_message_id(message),
business_connection_id: business_connection_id(message)
)
process_error(message, response)
response.parsed_response['result']['message_id'] if response.success?
end
@@ -131,9 +141,12 @@ class Channel::Telegram < ApplicationRecord
stripped_html.gsub('&lt;br&gt;', "\n")
end
def message_request(chat_id, text, reply_markup = nil, reply_to_message_id = nil)
def message_request(chat_id, text, reply_markup = nil, reply_to_message_id = nil, business_connection_id: nil)
text_payload = convert_markdown_to_telegram_html(text)
business_body = {}
business_body[:business_connection_id] = business_connection_id if business_connection_id
HTTParty.post("#{telegram_api_url}/sendMessage",
body: {
chat_id: chat_id,
@@ -141,6 +154,6 @@ class Channel::Telegram < ApplicationRecord
reply_markup: reply_markup,
parse_mode: 'HTML',
reply_to_message_id: reply_to_message_id
})
}.merge(business_body))
end
end

View File

@@ -8,17 +8,25 @@ class Telegram::IncomingMessageService
def perform
# chatwoot doesn't support group conversations at the moment
transform_business_message!
return unless private_message?
set_contact
update_contact_avatar
set_conversation
# TODO: Since the recent Telegram Business update, we need to explicitly mark messages as read using an additional request.
# Otherwise, the client will see their messages as unread.
# Chatwoot defines a 'read' status in its enum but does not currently update this status for Telegram conversations.
# We have two options:
# 1. Send the read request to Telegram here, immediately when the message is created.
# 2. Properly update the read status in the Chatwoot UI and trigger the Telegram request when the agent actually reads the message.
# See: https://core.telegram.org/bots/api#readbusinessmessage
@message = @conversation.messages.build(
content: telegram_params_message_content,
account_id: @inbox.account_id,
inbox_id: @inbox.id,
message_type: :incoming,
sender: @contact,
message_type: message_type,
sender: message_sender,
content_attributes: telegram_params_content_attributes,
source_id: telegram_params_message_id.to_s
)
@@ -36,6 +44,11 @@ class Telegram::IncomingMessageService
contact_attributes: contact_attributes
).perform
# TODO: Should we update contact_attributes when the user changes their first or last name?
# In business chats, when our Telegram bot initiates the conversation,
# the message does not include a language code.
# This is critical for AI assistants and translation plugins.
@contact_inbox = contact_inbox
@contact = contact_inbox.contact
end
@@ -89,10 +102,19 @@ class Telegram::IncomingMessageService
def conversation_additional_attributes
{
chat_id: telegram_params_chat_id
chat_id: telegram_params_chat_id,
business_connection_id: telegram_params_business_connection_id
}
end
def message_type
business_message_outgoing? ? :outgoing : :incoming
end
def message_sender
business_message_outgoing? ? nil : @contact
end
def file_content_type
return :image if image_message?
return :audio if audio_message?
@@ -191,4 +213,8 @@ class Telegram::IncomingMessageService
params[:message][:video].presence ||
params[:message][:video_note].presence
end
def transform_business_message!
params[:message] = params[:business_message] if params[:business_message] && !params[:message]
end
end

View File

@@ -13,6 +13,17 @@ module Telegram::ParamHelpers
{}
end
def business_message?
telegram_params_business_connection_id.present?
end
# In business bot mode we will receive messages from our telegram.
# This is our messages posted via telegram client.
# Such messages should be outgoing (from us to client)
def business_message_outgoing?
business_message? && telegram_params_base_object[:chat][:id] != telegram_params_base_object[:from][:id]
end
def message_params?
params[:message].present?
end
@@ -29,24 +40,34 @@ module Telegram::ParamHelpers
end
end
def contact_params
if business_message_outgoing?
telegram_params_base_object[:chat]
else
telegram_params_base_object[:from]
end
end
def telegram_params_from_id
return telegram_params_base_object[:chat][:id] if business_message?
telegram_params_base_object[:from][:id]
end
def telegram_params_first_name
telegram_params_base_object[:from][:first_name]
contact_params[:first_name]
end
def telegram_params_last_name
telegram_params_base_object[:from][:last_name]
contact_params[:last_name]
end
def telegram_params_username
telegram_params_base_object[:from][:username]
contact_params[:username]
end
def telegram_params_language_code
telegram_params_base_object[:from][:language_code]
contact_params[:language_code]
end
def telegram_params_chat_id
@@ -57,6 +78,14 @@ module Telegram::ParamHelpers
end
end
def telegram_params_business_connection_id
if callback_query_params?
params[:callback_query][:message][:business_connection_id]
else
telegram_params_base_object[:business_connection_id]
end
end
def telegram_params_message_content
if callback_query_params?
params[:callback_query][:data]

View File

@@ -71,6 +71,7 @@ class Telegram::SendAttachmentsService
HTTParty.post("#{channel.telegram_api_url}/sendMediaGroup",
body: {
chat_id: chat_id,
**business_connection_body,
media: attachments.map { |hash| hash.except(:attachment) }.to_json,
reply_to_message_id: reply_to_message_id
})
@@ -108,6 +109,7 @@ class Telegram::SendAttachmentsService
HTTParty.post("#{channel.telegram_api_url}/sendDocument",
body: {
chat_id: chat_id,
**business_connection_body,
document: file,
reply_to_message_id: reply_to_message_id
},
@@ -135,4 +137,14 @@ class Telegram::SendAttachmentsService
def channel
@channel ||= message.inbox.channel
end
def business_connection_id
@business_connection_id ||= channel.business_connection_id(message)
end
def business_connection_body
body = {}
body[:business_connection_id] = business_connection_id if business_connection_id
body
end
end

View File

@@ -5,6 +5,7 @@ class Telegram::UpdateMessageService
pattr_initialize [:inbox!, :params!]
def perform
transform_business_message!
find_contact_inbox
find_conversation
find_message
@@ -36,4 +37,8 @@ class Telegram::UpdateMessageService
@message.update!(content: edited_message[:caption])
end
end
def transform_business_message!
params[:edited_message] = params[:edited_business_message] if params[:edited_business_message].present?
end
end