feat: support reply to for Telegram (#8105)
Co-authored-by: Muhsin Keloth <muhsinkeramam@gmail.com>
This commit is contained in:
@@ -9,11 +9,11 @@ class Messages::MessageBuilder
|
|||||||
@user = user
|
@user = user
|
||||||
@message_type = params[:message_type] || 'outgoing'
|
@message_type = params[:message_type] || 'outgoing'
|
||||||
@attachments = params[:attachments]
|
@attachments = params[:attachments]
|
||||||
@automation_rule = @params&.dig(:content_attributes, :automation_rule_id)
|
@automation_rule = content_attributes&.dig(:automation_rule_id)
|
||||||
return unless params.instance_of?(ActionController::Parameters)
|
return unless params.instance_of?(ActionController::Parameters)
|
||||||
|
|
||||||
@in_reply_to = params.to_unsafe_h&.dig(:content_attributes, :in_reply_to)
|
@in_reply_to = content_attributes&.dig(:in_reply_to)
|
||||||
@items = params.to_unsafe_h&.dig(:content_attributes, :items)
|
@items = content_attributes&.dig(:items)
|
||||||
end
|
end
|
||||||
|
|
||||||
def perform
|
def perform
|
||||||
@@ -26,6 +26,38 @@ class Messages::MessageBuilder
|
|||||||
|
|
||||||
private
|
private
|
||||||
|
|
||||||
|
# Extracts content attributes from the given params.
|
||||||
|
# - Converts ActionController::Parameters to a regular hash if needed.
|
||||||
|
# - Attempts to parse a JSON string if content is a string.
|
||||||
|
# - Returns an empty hash if content is not present, if there's a parsing error, or if it's an unexpected type.
|
||||||
|
def content_attributes
|
||||||
|
params = convert_to_hash(@params)
|
||||||
|
content_attributes = params.fetch(:content_attributes, {})
|
||||||
|
|
||||||
|
return parse_json(content_attributes) if content_attributes.is_a?(String)
|
||||||
|
return content_attributes if content_attributes.is_a?(Hash)
|
||||||
|
|
||||||
|
{}
|
||||||
|
end
|
||||||
|
|
||||||
|
# Converts the given object to a hash.
|
||||||
|
# If it's an instance of ActionController::Parameters, converts it to an unsafe hash.
|
||||||
|
# Otherwise, returns the object as-is.
|
||||||
|
def convert_to_hash(obj)
|
||||||
|
return obj.to_unsafe_h if obj.instance_of?(ActionController::Parameters)
|
||||||
|
|
||||||
|
obj
|
||||||
|
end
|
||||||
|
|
||||||
|
# Attempts to parse a string as JSON.
|
||||||
|
# If successful, returns the parsed hash with symbolized names.
|
||||||
|
# If unsuccessful, returns nil.
|
||||||
|
def parse_json(content)
|
||||||
|
JSON.parse(content, symbolize_names: true)
|
||||||
|
rescue JSON::ParserError
|
||||||
|
{}
|
||||||
|
end
|
||||||
|
|
||||||
def process_attachments
|
def process_attachments
|
||||||
return if @attachments.blank?
|
return if @attachments.blank?
|
||||||
|
|
||||||
|
|||||||
@@ -26,9 +26,13 @@ export const buildCreatePayload = ({
|
|||||||
payload.append('echo_id', echoId);
|
payload.append('echo_id', echoId);
|
||||||
payload.append('cc_emails', ccEmails);
|
payload.append('cc_emails', ccEmails);
|
||||||
payload.append('bcc_emails', bccEmails);
|
payload.append('bcc_emails', bccEmails);
|
||||||
|
|
||||||
if (toEmails) {
|
if (toEmails) {
|
||||||
payload.append('to_emails', toEmails);
|
payload.append('to_emails', toEmails);
|
||||||
}
|
}
|
||||||
|
if (contentAttributes) {
|
||||||
|
payload.append('content_attributes', JSON.stringify(contentAttributes));
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
payload = {
|
payload = {
|
||||||
content: message,
|
content: message,
|
||||||
|
|||||||
@@ -50,6 +50,7 @@ describe('#ConversationAPI', () => {
|
|||||||
message: 'test content',
|
message: 'test content',
|
||||||
echoId: 12,
|
echoId: 12,
|
||||||
isPrivate: true,
|
isPrivate: true,
|
||||||
|
contentAttributes: { in_reply_to: 12 },
|
||||||
files: [new Blob(['test-content'], { type: 'application/pdf' })],
|
files: [new Blob(['test-content'], { type: 'application/pdf' })],
|
||||||
});
|
});
|
||||||
expect(formPayload).toBeInstanceOf(FormData);
|
expect(formPayload).toBeInstanceOf(FormData);
|
||||||
@@ -57,6 +58,10 @@ describe('#ConversationAPI', () => {
|
|||||||
expect(formPayload.get('echo_id')).toEqual('12');
|
expect(formPayload.get('echo_id')).toEqual('12');
|
||||||
expect(formPayload.get('private')).toEqual('true');
|
expect(formPayload.get('private')).toEqual('true');
|
||||||
expect(formPayload.get('cc_emails')).toEqual('');
|
expect(formPayload.get('cc_emails')).toEqual('');
|
||||||
|
expect(formPayload.get('bcc_emails')).toEqual('');
|
||||||
|
expect(formPayload.get('content_attributes')).toEqual(
|
||||||
|
'{"in_reply_to":12}'
|
||||||
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('builds object payload if file is not available', () => {
|
it('builds object payload if file is not available', () => {
|
||||||
|
|||||||
@@ -38,6 +38,7 @@
|
|||||||
v-if="inReplyToMessageId && inboxSupportsReplyTo"
|
v-if="inReplyToMessageId && inboxSupportsReplyTo"
|
||||||
:message="inReplyTo"
|
:message="inReplyTo"
|
||||||
:message-type="data.message_type"
|
:message-type="data.message_type"
|
||||||
|
:parent-has-attachments="hasAttachments"
|
||||||
/>
|
/>
|
||||||
<bubble-text
|
<bubble-text
|
||||||
v-if="data.content"
|
v-if="data.content"
|
||||||
|
|||||||
@@ -5,7 +5,7 @@
|
|||||||
'bg-slate-100 dark:bg-slate-600 dark:text-slate-50':
|
'bg-slate-100 dark:bg-slate-600 dark:text-slate-50':
|
||||||
messageType === MESSAGE_TYPE.INCOMING,
|
messageType === MESSAGE_TYPE.INCOMING,
|
||||||
'bg-woot-600 text-woot-50': messageType === MESSAGE_TYPE.OUTGOING,
|
'bg-woot-600 text-woot-50': messageType === MESSAGE_TYPE.OUTGOING,
|
||||||
'-mx-2': message.content,
|
'-mx-2': !parentHasAttachments,
|
||||||
}"
|
}"
|
||||||
>
|
>
|
||||||
<message-preview
|
<message-preview
|
||||||
@@ -34,6 +34,10 @@ export default {
|
|||||||
type: Number,
|
type: Number,
|
||||||
required: true,
|
required: true,
|
||||||
},
|
},
|
||||||
|
parentHasAttachments: {
|
||||||
|
type: Boolean,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
data() {
|
data() {
|
||||||
return { MESSAGE_TYPE };
|
return { MESSAGE_TYPE };
|
||||||
|
|||||||
@@ -77,8 +77,16 @@ class Channel::Telegram < ApplicationRecord
|
|||||||
errors.add(:bot_token, 'error setting up the webook') unless response.success?
|
errors.add(:bot_token, 'error setting up the webook') unless response.success?
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def chat_id(message)
|
||||||
|
message.conversation[:additional_attributes]['chat_id']
|
||||||
|
end
|
||||||
|
|
||||||
|
def reply_to_message_id(message)
|
||||||
|
message.content_attributes['in_reply_to_external_id']
|
||||||
|
end
|
||||||
|
|
||||||
def send_message(message)
|
def send_message(message)
|
||||||
response = message_request(message.conversation[:additional_attributes]['chat_id'], message.content, reply_markup(message))
|
response = message_request(chat_id(message), message.content, reply_markup(message), reply_to_message_id(message))
|
||||||
response.parsed_response['result']['message_id'] if response.success?
|
response.parsed_response['result']['message_id'] if response.success?
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -115,24 +123,26 @@ class Channel::Telegram < ApplicationRecord
|
|||||||
telegram_attachments << telegram_attachment
|
telegram_attachments << telegram_attachment
|
||||||
end
|
end
|
||||||
|
|
||||||
response = attachments_request(message.conversation[:additional_attributes]['chat_id'], telegram_attachments)
|
response = attachments_request(chat_id(message), telegram_attachments, reply_to_message_id(message))
|
||||||
response.parsed_response['result'].first['message_id'] if response.success?
|
response.parsed_response['result'].first['message_id'] if response.success?
|
||||||
end
|
end
|
||||||
|
|
||||||
def attachments_request(chat_id, attachments)
|
def attachments_request(chat_id, attachments, reply_to_message_id)
|
||||||
HTTParty.post("#{telegram_api_url}/sendMediaGroup",
|
HTTParty.post("#{telegram_api_url}/sendMediaGroup",
|
||||||
body: {
|
body: {
|
||||||
chat_id: chat_id,
|
chat_id: chat_id,
|
||||||
media: attachments.to_json
|
media: attachments.to_json,
|
||||||
|
reply_to_message_id: reply_to_message_id
|
||||||
})
|
})
|
||||||
end
|
end
|
||||||
|
|
||||||
def message_request(chat_id, text, reply_markup = nil)
|
def message_request(chat_id, text, reply_markup = nil, reply_to_message_id = nil)
|
||||||
HTTParty.post("#{telegram_api_url}/sendMessage",
|
HTTParty.post("#{telegram_api_url}/sendMessage",
|
||||||
body: {
|
body: {
|
||||||
chat_id: chat_id,
|
chat_id: chat_id,
|
||||||
text: text,
|
text: text,
|
||||||
reply_markup: reply_markup
|
reply_markup: reply_markup,
|
||||||
|
reply_to_message_id: reply_to_message_id
|
||||||
})
|
})
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ class Telegram::IncomingMessageService
|
|||||||
inbox_id: @inbox.id,
|
inbox_id: @inbox.id,
|
||||||
message_type: :incoming,
|
message_type: :incoming,
|
||||||
sender: @contact,
|
sender: @contact,
|
||||||
|
content_attributes: telegram_params_content_attributes,
|
||||||
source_id: telegram_params_message_id.to_s
|
source_id: telegram_params_message_id.to_s
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -6,6 +6,13 @@ module Telegram::ParamHelpers
|
|||||||
params.dig(:message, :chat, :type) == 'private'
|
params.dig(:message, :chat, :type) == 'private'
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def telegram_params_content_attributes
|
||||||
|
reply_to = params.dig(:message, :reply_to_message, :message_id)
|
||||||
|
return { 'in_reply_to_external_id' => reply_to } if reply_to
|
||||||
|
|
||||||
|
{}
|
||||||
|
end
|
||||||
|
|
||||||
def message_params?
|
def message_params?
|
||||||
params[:message].present?
|
params[:message].present?
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ describe Messages::MessageBuilder do
|
|||||||
let(:inbox) { create(:inbox, account: account) }
|
let(:inbox) { create(:inbox, account: account) }
|
||||||
let(:inbox_member) { create(:inbox_member, inbox: inbox, account: account) }
|
let(:inbox_member) { create(:inbox_member, inbox: inbox, account: account) }
|
||||||
let(:conversation) { create(:conversation, inbox: inbox, account: account) }
|
let(:conversation) { create(:conversation, inbox: inbox, account: account) }
|
||||||
|
let(:message_for_reply) { create(:message, conversation: conversation) }
|
||||||
let(:params) do
|
let(:params) do
|
||||||
ActionController::Parameters.new({
|
ActionController::Parameters.new({
|
||||||
content: 'test'
|
content: 'test'
|
||||||
@@ -21,6 +22,75 @@ describe Messages::MessageBuilder do
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
describe '#content_attributes' do
|
||||||
|
context 'when content_attributes is a JSON string' do
|
||||||
|
let(:params) do
|
||||||
|
ActionController::Parameters.new({
|
||||||
|
content: 'test',
|
||||||
|
content_attributes: "{\"in_reply_to\":#{message_for_reply.id}}"
|
||||||
|
})
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'parses content_attributes from JSON string' do
|
||||||
|
message = described_class.new(user, conversation, params).perform
|
||||||
|
expect(message.content_attributes).to include(in_reply_to: message_for_reply.id)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'when content_attributes is a hash' do
|
||||||
|
let(:params) do
|
||||||
|
ActionController::Parameters.new({
|
||||||
|
content: 'test',
|
||||||
|
content_attributes: { in_reply_to: message_for_reply.id }
|
||||||
|
})
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'uses content_attributes as provided' do
|
||||||
|
message = described_class.new(user, conversation, params).perform
|
||||||
|
expect(message.content_attributes).to include(in_reply_to: message_for_reply.id)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'when content_attributes is absent' do
|
||||||
|
let(:params) do
|
||||||
|
ActionController::Parameters.new({ content: 'test' })
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'defaults to an empty hash' do
|
||||||
|
message = message_builder
|
||||||
|
expect(message.content_attributes).to eq({})
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'when content_attributes is nil' do
|
||||||
|
let(:params) do
|
||||||
|
ActionController::Parameters.new({
|
||||||
|
content: 'test',
|
||||||
|
content_attributes: nil
|
||||||
|
})
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'defaults to an empty hash' do
|
||||||
|
message = message_builder
|
||||||
|
expect(message.content_attributes).to eq({})
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'when content_attributes is an invalid JSON string' do
|
||||||
|
let(:params) do
|
||||||
|
ActionController::Parameters.new({
|
||||||
|
content: 'test',
|
||||||
|
content_attributes: 'invalid_json'
|
||||||
|
})
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'defaults to an empty hash' do
|
||||||
|
message = message_builder
|
||||||
|
expect(message.content_attributes).to eq({})
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
describe '#perform when message_type is incoming' do
|
describe '#perform when message_type is incoming' do
|
||||||
context 'when channel is not api' do
|
context 'when channel is not api' do
|
||||||
let(:params) do
|
let(:params) do
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ RSpec.describe Channel::Telegram do
|
|||||||
|
|
||||||
stub_request(:post, "https://api.telegram.org/bot#{telegram_channel.bot_token}/sendMessage")
|
stub_request(:post, "https://api.telegram.org/bot#{telegram_channel.bot_token}/sendMessage")
|
||||||
.with(
|
.with(
|
||||||
body: 'chat_id=123&text=test&reply_markup='
|
body: 'chat_id=123&text=test&reply_markup=&reply_to_message_id='
|
||||||
)
|
)
|
||||||
.to_return(
|
.to_return(
|
||||||
status: 200,
|
status: 200,
|
||||||
@@ -32,7 +32,7 @@ RSpec.describe Channel::Telegram do
|
|||||||
.with(
|
.with(
|
||||||
body: 'chat_id=123&text=test' \
|
body: 'chat_id=123&text=test' \
|
||||||
'&reply_markup=%7B%22one_time_keyboard%22%3Atrue%2C%22inline_keyboard%22%3A%5B%5B%7B%22text%22%3A%22test%22%2C%22' \
|
'&reply_markup=%7B%22one_time_keyboard%22%3Atrue%2C%22inline_keyboard%22%3A%5B%5B%7B%22text%22%3A%22test%22%2C%22' \
|
||||||
'callback_data%22%3A%22test%22%7D%5D%5D%7D'
|
'callback_data%22%3A%22test%22%7D%5D%5D%7D&reply_to_message_id='
|
||||||
)
|
)
|
||||||
.to_return(
|
.to_return(
|
||||||
status: 200,
|
status: 200,
|
||||||
|
|||||||
Reference in New Issue
Block a user