Merge remote-tracking branch 'origin/feat/captain-image-support' into feat/captain-image-support
This commit is contained in:
@@ -31,7 +31,7 @@ class V2::Reports::LabelSummaryBuilder < V2::Reports::BaseSummaryBuilder
|
||||
resolved_counts: fetch_resolved_counts(conversation_filter),
|
||||
resolution_metrics: fetch_metrics(conversation_filter, 'conversation_resolved', use_business_hours),
|
||||
first_response_metrics: fetch_metrics(conversation_filter, 'first_response', use_business_hours),
|
||||
reply_metrics: fetch_metrics(conversation_filter, 'reply', use_business_hours)
|
||||
reply_metrics: fetch_metrics(conversation_filter, 'reply_time', use_business_hours)
|
||||
}
|
||||
end
|
||||
|
||||
@@ -63,7 +63,9 @@ class V2::Reports::LabelSummaryBuilder < V2::Reports::BaseSummaryBuilder
|
||||
end
|
||||
|
||||
def fetch_resolved_counts(conversation_filter)
|
||||
fetch_counts(conversation_filter.merge(status: :resolved))
|
||||
# since the base query is ActsAsTaggableOn,
|
||||
# the status :resolved won't automatically be converted to integer status
|
||||
fetch_counts(conversation_filter.merge(status: Conversation.statuses[:resolved]))
|
||||
end
|
||||
|
||||
def fetch_counts(conversation_filter)
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
class Api::V1::Accounts::Integrations::LinearController < Api::V1::Accounts::BaseController
|
||||
before_action :fetch_conversation, only: [:link_issue, :linked_issues]
|
||||
before_action :fetch_conversation, only: [:create_issue, :link_issue, :unlink_issue, :linked_issues]
|
||||
before_action :fetch_hook, only: [:destroy]
|
||||
|
||||
def destroy
|
||||
@@ -31,6 +31,12 @@ class Api::V1::Accounts::Integrations::LinearController < Api::V1::Accounts::Bas
|
||||
if issue[:error]
|
||||
render json: { error: issue[:error] }, status: :unprocessable_entity
|
||||
else
|
||||
Linear::ActivityMessageService.new(
|
||||
conversation: @conversation,
|
||||
action_type: :issue_created,
|
||||
issue_data: { id: issue[:data][:identifier] },
|
||||
user: Current.user
|
||||
).perform
|
||||
render json: issue[:data], status: :ok
|
||||
end
|
||||
end
|
||||
@@ -42,17 +48,30 @@ class Api::V1::Accounts::Integrations::LinearController < Api::V1::Accounts::Bas
|
||||
if issue[:error]
|
||||
render json: { error: issue[:error] }, status: :unprocessable_entity
|
||||
else
|
||||
Linear::ActivityMessageService.new(
|
||||
conversation: @conversation,
|
||||
action_type: :issue_linked,
|
||||
issue_data: { id: issue_id },
|
||||
user: Current.user
|
||||
).perform
|
||||
render json: issue[:data], status: :ok
|
||||
end
|
||||
end
|
||||
|
||||
def unlink_issue
|
||||
link_id = permitted_params[:link_id]
|
||||
issue_id = permitted_params[:issue_id]
|
||||
issue = linear_processor_service.unlink_issue(link_id)
|
||||
|
||||
if issue[:error]
|
||||
render json: { error: issue[:error] }, status: :unprocessable_entity
|
||||
else
|
||||
Linear::ActivityMessageService.new(
|
||||
conversation: @conversation,
|
||||
action_type: :issue_unlinked,
|
||||
issue_data: { id: issue_id },
|
||||
user: Current.user
|
||||
).perform
|
||||
render json: issue[:data], status: :ok
|
||||
end
|
||||
end
|
||||
|
||||
@@ -33,9 +33,11 @@ class LinearAPI extends ApiClient {
|
||||
);
|
||||
}
|
||||
|
||||
unlinkIssue(linkId) {
|
||||
unlinkIssue(linkId, issueIdentifier, conversationId) {
|
||||
return axios.post(`${this.url}/unlink_issue`, {
|
||||
link_id: linkId,
|
||||
issue_id: issueIdentifier,
|
||||
conversation_id: conversationId,
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -91,6 +91,19 @@ describe('#linearAPI', () => {
|
||||
issueData
|
||||
);
|
||||
});
|
||||
|
||||
it('creates a valid request with conversation_id', () => {
|
||||
const issueData = {
|
||||
title: 'New Issue',
|
||||
description: 'Issue description',
|
||||
conversation_id: 123,
|
||||
};
|
||||
LinearAPIClient.createIssue(issueData);
|
||||
expect(axiosMock.post).toHaveBeenCalledWith(
|
||||
'/api/v1/integrations/linear/create_issue',
|
||||
issueData
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('link_issue', () => {
|
||||
@@ -120,6 +133,18 @@ describe('#linearAPI', () => {
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
it('creates a valid request with title', () => {
|
||||
LinearAPIClient.link_issue(1, 'ENG-123', 'Sample Issue');
|
||||
expect(axiosMock.post).toHaveBeenCalledWith(
|
||||
'/api/v1/integrations/linear/link_issue',
|
||||
{
|
||||
issue_id: 'ENG-123',
|
||||
conversation_id: 1,
|
||||
title: 'Sample Issue',
|
||||
}
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getLinkedIssue', () => {
|
||||
@@ -164,12 +189,26 @@ describe('#linearAPI', () => {
|
||||
window.axios = originalAxios;
|
||||
});
|
||||
|
||||
it('creates a valid request', () => {
|
||||
LinearAPIClient.unlinkIssue(1);
|
||||
it('creates a valid request with link_id only', () => {
|
||||
LinearAPIClient.unlinkIssue('link123');
|
||||
expect(axiosMock.post).toHaveBeenCalledWith(
|
||||
'/api/v1/integrations/linear/unlink_issue',
|
||||
{
|
||||
link_id: 1,
|
||||
link_id: 'link123',
|
||||
issue_id: undefined,
|
||||
conversation_id: undefined,
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
it('creates a valid request with all parameters', () => {
|
||||
LinearAPIClient.unlinkIssue('link123', 'ENG-456', 789);
|
||||
expect(axiosMock.post).toHaveBeenCalledWith(
|
||||
'/api/v1/integrations/linear/unlink_issue',
|
||||
{
|
||||
link_id: 'link123',
|
||||
issue_id: 'ENG-456',
|
||||
conversation_id: 789,
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
@@ -19,6 +19,7 @@ const isConversationRoute = computed(() => {
|
||||
'conversation_through_mentions',
|
||||
'conversation_through_unattended',
|
||||
'conversation_through_participating',
|
||||
'inbox_view_conversation',
|
||||
];
|
||||
return CONVERSATION_ROUTES.includes(route.name);
|
||||
});
|
||||
|
||||
@@ -14,7 +14,8 @@ const fromEmail = computed(() => {
|
||||
});
|
||||
|
||||
const toEmail = computed(() => {
|
||||
return contentAttributes.value?.email?.to ?? [];
|
||||
const { toEmails, email } = contentAttributes.value;
|
||||
return email?.to ?? toEmails ?? [];
|
||||
});
|
||||
|
||||
const ccEmail = computed(() => {
|
||||
|
||||
@@ -109,7 +109,6 @@ const advancedFilterTypes = ref(
|
||||
attributeName: t(`FILTER.ATTRIBUTES.${filter.attributeI18nKey}`),
|
||||
}))
|
||||
);
|
||||
const isInitialLoad = ref(false);
|
||||
|
||||
const currentUser = useMapGetter('getCurrentUser');
|
||||
const chatLists = useMapGetter('getFilteredConversations');
|
||||
@@ -377,7 +376,6 @@ function setFiltersFromUISettings() {
|
||||
|
||||
function emitConversationLoaded() {
|
||||
emit('conversationLoad');
|
||||
isInitialLoad.value = false;
|
||||
// [VITE] removing this since the library has changed
|
||||
// nextTick(() => {
|
||||
// // Addressing a known issue in the virtual list library where dynamically added items
|
||||
@@ -422,7 +420,6 @@ function onApplyFilter(payload) {
|
||||
foldersQuery.value = filterQueryGenerator(payload);
|
||||
store.dispatch('conversationPage/reset');
|
||||
store.dispatch('emptyAllConversations');
|
||||
isInitialLoad.value = true;
|
||||
fetchFilteredConversations(payload);
|
||||
}
|
||||
|
||||
@@ -577,7 +574,6 @@ function resetAndFetchData() {
|
||||
store.dispatch('conversationPage/reset');
|
||||
store.dispatch('emptyAllConversations');
|
||||
store.dispatch('clearConversationFilters');
|
||||
isInitialLoad.value = true;
|
||||
if (hasActiveFolders.value) {
|
||||
const payload = activeFolder.value.query;
|
||||
fetchSavedFilteredConversations(payload);
|
||||
@@ -858,7 +854,7 @@ watch(conversationFilters, (newVal, oldVal) => {
|
||||
:active-status="activeStatus"
|
||||
:is-on-expanded-layout="isOnExpandedLayout"
|
||||
:conversation-stats="conversationStats"
|
||||
:is-list-loading="isInitialLoad"
|
||||
:is-list-loading="chatListLoading && !conversationList.length"
|
||||
@add-folders="onClickOpenAddFoldersModal"
|
||||
@delete-folders="onClickOpenDeleteFoldersModal"
|
||||
@filters-modal="onToggleAdvanceFiltersModal"
|
||||
|
||||
@@ -183,13 +183,18 @@ const createIssue = async () => {
|
||||
state_id: formState.stateId || undefined,
|
||||
priority: formState.priority || undefined,
|
||||
label_ids: formState.labelId ? [formState.labelId] : undefined,
|
||||
conversation_id: props.conversationId,
|
||||
};
|
||||
|
||||
try {
|
||||
isCreating.value = true;
|
||||
const response = await LinearAPI.createIssue(payload);
|
||||
const { id: issueId } = response.data;
|
||||
await LinearAPI.link_issue(props.conversationId, issueId, props.title);
|
||||
const { identifier: issueIdentifier } = response.data;
|
||||
await LinearAPI.link_issue(
|
||||
props.conversationId,
|
||||
issueIdentifier,
|
||||
props.title
|
||||
);
|
||||
useAlert(t('INTEGRATION_SETTINGS.LINEAR.ADD_OR_LINK.CREATE_SUCCESS'));
|
||||
useTrack(LINEAR_EVENTS.CREATE_ISSUE);
|
||||
onClose();
|
||||
|
||||
@@ -46,9 +46,9 @@ const loadLinkedIssues = async () => {
|
||||
}
|
||||
};
|
||||
|
||||
const unlinkIssue = async linkId => {
|
||||
const unlinkIssue = async (linkId, issueIdentifier) => {
|
||||
try {
|
||||
await LinearAPI.unlinkIssue(linkId);
|
||||
await LinearAPI.unlinkIssue(linkId, issueIdentifier, props.conversationId);
|
||||
useTrack(LINEAR_EVENTS.UNLINK_ISSUE);
|
||||
linkedIssues.value = linkedIssues.value.filter(
|
||||
issue => issue.id !== linkId
|
||||
@@ -110,7 +110,7 @@ onMounted(() => {
|
||||
<LinearIssueItem
|
||||
v-for="linkedIssue in linkedIssues"
|
||||
:key="linkedIssue.id"
|
||||
class="pt-3 px-4 pb-4 border-b border-n-weak last:border-b-0"
|
||||
class="px-4 pt-3 pb-4 border-b border-n-weak last:border-b-0"
|
||||
:linked-issue="linkedIssue"
|
||||
@unlink-issue="unlinkIssue"
|
||||
/>
|
||||
|
||||
@@ -14,6 +14,8 @@ const props = defineProps({
|
||||
|
||||
const emit = defineEmits(['unlinkIssue']);
|
||||
|
||||
const { linkedIssue } = props;
|
||||
|
||||
const priorityMap = {
|
||||
1: 'Urgent',
|
||||
2: 'High',
|
||||
@@ -21,7 +23,7 @@ const priorityMap = {
|
||||
4: 'Low',
|
||||
};
|
||||
|
||||
const issue = computed(() => props.linkedIssue.issue);
|
||||
const issue = computed(() => linkedIssue.issue);
|
||||
|
||||
const assignee = computed(() => {
|
||||
const assigneeDetails = issue.value.assignee;
|
||||
@@ -37,7 +39,7 @@ const labels = computed(() => issue.value.labels?.nodes || []);
|
||||
const priorityLabel = computed(() => priorityMap[issue.value.priority]);
|
||||
|
||||
const unlinkIssue = () => {
|
||||
emit('unlinkIssue', props.linkedIssue.id);
|
||||
emit('unlinkIssue', linkedIssue.id, linkedIssue.issue.identifier);
|
||||
};
|
||||
</script>
|
||||
|
||||
|
||||
@@ -63,7 +63,7 @@ const onSearch = async value => {
|
||||
isFetching.value = true;
|
||||
const response = await LinearAPI.searchIssues(value);
|
||||
issues.value = response.data.map(issue => ({
|
||||
id: issue.id,
|
||||
id: issue.identifier,
|
||||
name: `${issue.identifier} ${issue.title}`,
|
||||
icon: 'status',
|
||||
iconColor: issue.state.color,
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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('<br>', "\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
|
||||
|
||||
@@ -0,0 +1,41 @@
|
||||
class Linear::ActivityMessageService
|
||||
attr_reader :conversation, :action_type, :issue_data, :user
|
||||
|
||||
def initialize(conversation:, action_type:, user:, issue_data: {})
|
||||
@conversation = conversation
|
||||
@action_type = action_type
|
||||
@issue_data = issue_data
|
||||
@user = user
|
||||
end
|
||||
|
||||
def perform
|
||||
return unless conversation && issue_data[:id] && user
|
||||
|
||||
content = generate_activity_content
|
||||
return unless content
|
||||
|
||||
::Conversations::ActivityMessageJob.perform_later(conversation, activity_message_params(content))
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def generate_activity_content
|
||||
case action_type.to_sym
|
||||
when :issue_created
|
||||
I18n.t('conversations.activity.linear.issue_created', user_name: user.name, issue_id: issue_data[:id])
|
||||
when :issue_linked
|
||||
I18n.t('conversations.activity.linear.issue_linked', user_name: user.name, issue_id: issue_data[:id])
|
||||
when :issue_unlinked
|
||||
I18n.t('conversations.activity.linear.issue_unlinked', user_name: user.name, issue_id: issue_data[:id])
|
||||
end
|
||||
end
|
||||
|
||||
def activity_message_params(content)
|
||||
{
|
||||
account_id: conversation.account_id,
|
||||
inbox_id: conversation.inbox_id,
|
||||
message_type: :activity,
|
||||
content: content
|
||||
}
|
||||
end
|
||||
end
|
||||
@@ -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
|
||||
|
||||
@@ -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]
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -4,7 +4,9 @@
|
||||
I18n.t('reports.label_csv.label_title'),
|
||||
I18n.t('reports.label_csv.conversations_count'),
|
||||
I18n.t('reports.label_csv.avg_first_response_time'),
|
||||
I18n.t('reports.label_csv.avg_resolution_time')
|
||||
I18n.t('reports.label_csv.avg_resolution_time'),
|
||||
I18n.t('reports.label_csv.avg_reply_time'),
|
||||
I18n.t('reports.label_csv.resolution_count'),
|
||||
]
|
||||
%>
|
||||
<%= CSVSafe.generate_line headers -%>
|
||||
|
||||
@@ -1,18 +1,18 @@
|
||||
<header class="sticky top-0 z-50 w-full bg-white shadow-sm dark:bg-slate-900">
|
||||
<nav class="hidden sm:flex max-w-5xl px-4 mx-auto md:px-8" aria-label="Top">
|
||||
<div class="flex items-center w-full py-5 overflow-hidden">
|
||||
<a href="<%= generate_home_link(@portal.slug, @portal.config['default_locale'] || params[:locale], @theme_from_params, @is_plain_layout_enabled) %>" class="flex items-center h-10">
|
||||
<a href="<%= generate_home_link(@portal.slug, @portal.config['default_locale'] || params[:locale], @theme_from_params, @is_plain_layout_enabled) %>" class="flex items-center min-w-0 h-10">
|
||||
<% if @portal.logo.present? %>
|
||||
<img src="<%= url_for(@portal.logo) %>" class="w-auto h-10 ltr:mr-2 rtl:ml-2" />
|
||||
<img src="<%= url_for(@portal.logo) %>" class="w-auto h-10 ltr:mr-2 rtl:ml-2" draggable="false" />
|
||||
<% end %>
|
||||
<span class="text-lg font-semibold text-slate-900 dark:text-white <%= @portal.logo.present? ? 'hidden' : 'hidden sm:block' %>"><%= @portal.name %></span>
|
||||
<span class="text-lg font-semibold text-slate-900 truncate dark:text-white <%= @portal.logo.present? ? 'hidden lg:block' : 'hidden sm:block' %>"><%= @portal.name %></span>
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<%# Go to homepage link section %>
|
||||
<div class="flex items-center justify-between gap-2 sm:gap-5">
|
||||
<% if @portal.homepage_link %>
|
||||
<div class="hidden px-1 py-2 ltr:ml-8 rtl:mr-8 cursor-pointer md:block">
|
||||
<div class="px-1 py-2 ltr:ml-8 rtl:mr-8 cursor-pointer block">
|
||||
<div class="flex-grow flex-shrink-0">
|
||||
<a id="header-action-button" target="_blank" rel="noopener noreferrer nofollow" href="<%= @portal.homepage_link %>" class="flex flex-row items-center gap-1 text-sm font-medium whitespace-nowrap text-slate-800 dark:text-slate-100 stroke-slate-700 dark:stroke-slate-200">
|
||||
<%= render partial: 'icons/redirect' %>
|
||||
@@ -99,11 +99,11 @@
|
||||
|
||||
<nav class="flex sm:hidden max-w-5xl px-4 mx-auto" aria-label="Mobile Top">
|
||||
<div class="flex items-center justify-between w-full py-5">
|
||||
<a href="<%= generate_home_link(@portal.slug, @portal.config['default_locale'] || params[:locale], @theme_from_params, @is_plain_layout_enabled) %>" class="flex items-center h-10 text-lg font-semibold text-slate-900 dark:text-white">
|
||||
<a href="<%= generate_home_link(@portal.slug, @portal.config['default_locale'] || params[:locale], @theme_from_params, @is_plain_layout_enabled) %>" class="flex items-center min-w-0 h-10 text-lg font-semibold text-slate-900 dark:text-white">
|
||||
<% if @portal.logo.present? %>
|
||||
<img src="<%= url_for(@portal.logo) %>" class="w-auto h-10 ltr:mr-2 rtl:ml-2" />
|
||||
<img src="<%= url_for(@portal.logo) %>" class="w-auto h-10 ltr:mr-2 rtl:ml-2" draggable="false" />
|
||||
<% end %>
|
||||
<span class="text-lg font-semibold text-slate-900 dark:text-white <%= @portal.logo.present? ? 'hidden' : 'sm:hidden block' %>"><%= @portal.name %></span>
|
||||
<span class="text-lg font-semibold text-slate-900 truncate dark:text-white <%= @portal.logo.present? ? 'hidden' : 'sm:hidden block' %>"><%= @portal.name %></span>
|
||||
</a>
|
||||
|
||||
<!-- Mobile Menu Component -->
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
<section id="portal-bg" class="w-full bg-white dark:bg-slate-900 shadow-inner">
|
||||
<div id="portal-bg-gradient" class="pt-8 pb-8 md:pt-14 md:pb-6 min-h-[240px] md:min-h-[260px]">
|
||||
<div class="mx-auto max-w-5xl px-4 md:px-8 flex flex-col items-start">
|
||||
<span class="text-sm leading-[24px] font-semibold text-slate-600 dark:text-slate-300 mb-1 <%= @portal.logo.present? ? 'block' : 'hidden' %>"><%= @portal.name %></span>
|
||||
<span class="text-sm leading-[24px] font-semibold text-slate-600 dark:text-slate-300 mb-1 <%= @portal.logo.present? ? 'block lg:hidden' : 'hidden' %>"><%= @portal.name %></span>
|
||||
<h1 class="text-2xl md:text-4xl text-slate-900 dark:text-white font-semibold leading-normal">
|
||||
<%= portal.header_text %>
|
||||
</h1>
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
<% has_multiple_locales = @portal.config["allowed_locales"].length > 1 %>
|
||||
|
||||
<input type="checkbox" id="mobile-menu-toggle" class="peer/menu hidden" />
|
||||
|
||||
@@ -46,10 +47,12 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<span class="h-px bg-slate-100/70 dark:bg-slate-800/70 w-full"></span>
|
||||
<% if has_multiple_locales %>
|
||||
<span class="h-px bg-slate-100/70 dark:bg-slate-800/70 w-full"></span>
|
||||
<% end %>
|
||||
|
||||
<!-- Locale Switcher -->
|
||||
<% if @portal.config["allowed_locales"].length > 1 %>
|
||||
<% if has_multiple_locales %>
|
||||
<div id="header-action-button" class="flex flex-col gap-2">
|
||||
<h3 class="text-base font-medium text-slate-700 dark:text-slate-300 my-2">
|
||||
<%= I18n.t('public_portal.header.language', default: 'Language') %>
|
||||
|
||||
@@ -105,6 +105,8 @@ en:
|
||||
conversations_count: No. of conversations
|
||||
avg_first_response_time: Avg first response time
|
||||
avg_resolution_time: Avg resolution time
|
||||
avg_reply_time: Avg reply time
|
||||
resolution_count: Resolution Count
|
||||
team_csv:
|
||||
team_name: Team name
|
||||
conversations_count: Conversations count
|
||||
@@ -188,6 +190,10 @@ en:
|
||||
sla:
|
||||
added: '%{user_name} added SLA policy %{sla_name}'
|
||||
removed: '%{user_name} removed SLA policy %{sla_name}'
|
||||
linear:
|
||||
issue_created: 'Linear issue %{issue_id} was created by %{user_name}'
|
||||
issue_linked: 'Linear issue %{issue_id} was linked by %{user_name}'
|
||||
issue_unlinked: 'Linear issue %{issue_id} was unlinked by %{user_name}'
|
||||
csat:
|
||||
not_sent_due_to_messaging_window: 'CSAT survey not sent due to outgoing message restrictions'
|
||||
muted: '%{user_name} has muted the conversation'
|
||||
|
||||
@@ -28,7 +28,8 @@ class Integrations::Linear::ProcessorService
|
||||
|
||||
{
|
||||
data: { id: response['issueCreate']['issue']['id'],
|
||||
title: response['issueCreate']['issue']['title'] }
|
||||
title: response['issueCreate']['issue']['title'],
|
||||
identifier: response['issueCreate']['issue']['identifier'] }
|
||||
}
|
||||
end
|
||||
|
||||
|
||||
@@ -25,6 +25,7 @@ module Linear::Mutations
|
||||
issue {
|
||||
id
|
||||
title
|
||||
identifier
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -140,7 +140,7 @@ RSpec.describe V2::Reports::LabelSummaryBuilder do
|
||||
create(:reporting_event,
|
||||
account: account,
|
||||
conversation: conv,
|
||||
name: 'reply',
|
||||
name: 'reply_time',
|
||||
value: (15 + (idx * 5)) * 60,
|
||||
value_in_business_hours: (10 + (idx * 3)) * 60,
|
||||
created_at: Time.zone.today)
|
||||
|
||||
@@ -93,6 +93,8 @@ RSpec.describe 'Linear Integration API', type: :request do
|
||||
end
|
||||
|
||||
describe 'POST /api/v1/accounts/:account_id/integrations/linear/create_issue' do
|
||||
let(:inbox) { create(:inbox, account: account) }
|
||||
let(:conversation) { create(:conversation, account: account, inbox: inbox) }
|
||||
let(:issue_params) do
|
||||
{
|
||||
team_id: 'team1',
|
||||
@@ -101,32 +103,56 @@ RSpec.describe 'Linear Integration API', type: :request do
|
||||
assignee_id: 'user1',
|
||||
priority: 'high',
|
||||
state_id: 'state1',
|
||||
label_ids: ['label1']
|
||||
label_ids: ['label1'],
|
||||
conversation_id: conversation.display_id
|
||||
}
|
||||
end
|
||||
|
||||
context 'when it is an authenticated user' do
|
||||
context 'when the issue is created successfully' do
|
||||
let(:created_issue) { { data: { 'id' => 'issue1', 'title' => 'Sample Issue' } } }
|
||||
let(:created_issue) { { data: { identifier: 'ENG-123', title: 'Sample Issue' } } }
|
||||
|
||||
it 'returns the created issue' do
|
||||
allow(processor_service).to receive(:create_issue).with(issue_params.stringify_keys).and_return(created_issue)
|
||||
|
||||
post "/api/v1/accounts/#{account.id}/integrations/linear/create_issue",
|
||||
params: issue_params,
|
||||
headers: agent.create_new_auth_token,
|
||||
as: :json
|
||||
|
||||
expect(response).to have_http_status(:ok)
|
||||
expect(response.body).to include('Sample Issue')
|
||||
end
|
||||
|
||||
it 'creates activity message when conversation is provided' do
|
||||
allow(processor_service).to receive(:create_issue).with(issue_params.stringify_keys).and_return(created_issue)
|
||||
|
||||
expect do
|
||||
post "/api/v1/accounts/#{account.id}/integrations/linear/create_issue",
|
||||
params: issue_params,
|
||||
headers: agent.create_new_auth_token,
|
||||
as: :json
|
||||
end.to have_enqueued_job(Conversations::ActivityMessageJob)
|
||||
.with(conversation, {
|
||||
account_id: conversation.account_id,
|
||||
inbox_id: conversation.inbox_id,
|
||||
message_type: :activity,
|
||||
content: "Linear issue ENG-123 was created by #{agent.name}"
|
||||
})
|
||||
end
|
||||
end
|
||||
|
||||
context 'when issue creation fails' do
|
||||
it 'returns error message' do
|
||||
it 'returns error message and does not create activity message' do
|
||||
allow(processor_service).to receive(:create_issue).with(issue_params.stringify_keys).and_return(error: 'error message')
|
||||
post "/api/v1/accounts/#{account.id}/integrations/linear/create_issue",
|
||||
params: issue_params,
|
||||
headers: agent.create_new_auth_token,
|
||||
as: :json
|
||||
|
||||
expect do
|
||||
post "/api/v1/accounts/#{account.id}/integrations/linear/create_issue",
|
||||
params: issue_params,
|
||||
headers: agent.create_new_auth_token,
|
||||
as: :json
|
||||
end.not_to have_enqueued_job(Conversations::ActivityMessageJob)
|
||||
|
||||
expect(response).to have_http_status(:unprocessable_entity)
|
||||
expect(response.body).to include('error message')
|
||||
end
|
||||
@@ -135,7 +161,7 @@ RSpec.describe 'Linear Integration API', type: :request do
|
||||
end
|
||||
|
||||
describe 'POST /api/v1/accounts/:account_id/integrations/linear/link_issue' do
|
||||
let(:issue_id) { 'issue1' }
|
||||
let(:issue_id) { 'ENG-456' }
|
||||
let(:conversation) { create(:conversation, account: account) }
|
||||
let(:link) { "#{ENV.fetch('FRONTEND_URL', nil)}/app/accounts/#{account.id}/conversations/#{conversation.display_id}" }
|
||||
let(:title) { 'Sample Issue' }
|
||||
@@ -144,24 +170,38 @@ RSpec.describe 'Linear Integration API', type: :request do
|
||||
context 'when the issue is linked successfully' do
|
||||
let(:linked_issue) { { data: { 'id' => 'issue1', 'link' => 'https://linear.app/issue1' } } }
|
||||
|
||||
it 'returns the linked issue' do
|
||||
it 'returns the linked issue and creates activity message' do
|
||||
allow(processor_service).to receive(:link_issue).with(link, issue_id, title).and_return(linked_issue)
|
||||
post "/api/v1/accounts/#{account.id}/integrations/linear/link_issue",
|
||||
params: { conversation_id: conversation.display_id, issue_id: issue_id, title: title },
|
||||
headers: agent.create_new_auth_token,
|
||||
as: :json
|
||||
|
||||
expect do
|
||||
post "/api/v1/accounts/#{account.id}/integrations/linear/link_issue",
|
||||
params: { conversation_id: conversation.display_id, issue_id: issue_id, title: title },
|
||||
headers: agent.create_new_auth_token,
|
||||
as: :json
|
||||
end.to have_enqueued_job(Conversations::ActivityMessageJob)
|
||||
.with(conversation, {
|
||||
account_id: conversation.account_id,
|
||||
inbox_id: conversation.inbox_id,
|
||||
message_type: :activity,
|
||||
content: "Linear issue ENG-456 was linked by #{agent.name}"
|
||||
})
|
||||
|
||||
expect(response).to have_http_status(:ok)
|
||||
expect(response.body).to include('https://linear.app/issue1')
|
||||
end
|
||||
end
|
||||
|
||||
context 'when issue linking fails' do
|
||||
it 'returns error message' do
|
||||
it 'returns error message and does not create activity message' do
|
||||
allow(processor_service).to receive(:link_issue).with(link, issue_id, title).and_return(error: 'error message')
|
||||
post "/api/v1/accounts/#{account.id}/integrations/linear/link_issue",
|
||||
params: { conversation_id: conversation.display_id, issue_id: issue_id, title: title },
|
||||
headers: agent.create_new_auth_token,
|
||||
as: :json
|
||||
|
||||
expect do
|
||||
post "/api/v1/accounts/#{account.id}/integrations/linear/link_issue",
|
||||
params: { conversation_id: conversation.display_id, issue_id: issue_id, title: title },
|
||||
headers: agent.create_new_auth_token,
|
||||
as: :json
|
||||
end.not_to have_enqueued_job(Conversations::ActivityMessageJob)
|
||||
|
||||
expect(response).to have_http_status(:unprocessable_entity)
|
||||
expect(response.body).to include('error message')
|
||||
end
|
||||
@@ -171,29 +211,45 @@ RSpec.describe 'Linear Integration API', type: :request do
|
||||
|
||||
describe 'POST /api/v1/accounts/:account_id/integrations/linear/unlink_issue' do
|
||||
let(:link_id) { 'attachment1' }
|
||||
let(:issue_id) { 'ENG-789' }
|
||||
let(:conversation) { create(:conversation, account: account) }
|
||||
|
||||
context 'when it is an authenticated user' do
|
||||
context 'when the issue is unlinked successfully' do
|
||||
let(:unlinked_issue) { { data: { 'id' => 'issue1', 'link' => 'https://linear.app/issue1' } } }
|
||||
|
||||
it 'returns the unlinked issue' do
|
||||
it 'returns the unlinked issue and creates activity message' do
|
||||
allow(processor_service).to receive(:unlink_issue).with(link_id).and_return(unlinked_issue)
|
||||
post "/api/v1/accounts/#{account.id}/integrations/linear/unlink_issue",
|
||||
params: { link_id: link_id },
|
||||
headers: agent.create_new_auth_token,
|
||||
as: :json
|
||||
|
||||
expect do
|
||||
post "/api/v1/accounts/#{account.id}/integrations/linear/unlink_issue",
|
||||
params: { link_id: link_id, issue_id: issue_id, conversation_id: conversation.display_id },
|
||||
headers: agent.create_new_auth_token,
|
||||
as: :json
|
||||
end.to have_enqueued_job(Conversations::ActivityMessageJob)
|
||||
.with(conversation, {
|
||||
account_id: conversation.account_id,
|
||||
inbox_id: conversation.inbox_id,
|
||||
message_type: :activity,
|
||||
content: "Linear issue ENG-789 was unlinked by #{agent.name}"
|
||||
})
|
||||
|
||||
expect(response).to have_http_status(:ok)
|
||||
expect(response.body).to include('https://linear.app/issue1')
|
||||
end
|
||||
end
|
||||
|
||||
context 'when issue unlinking fails' do
|
||||
it 'returns error message' do
|
||||
it 'returns error message and does not create activity message' do
|
||||
allow(processor_service).to receive(:unlink_issue).with(link_id).and_return(error: 'error message')
|
||||
post "/api/v1/accounts/#{account.id}/integrations/linear/unlink_issue",
|
||||
params: { link_id: link_id },
|
||||
headers: agent.create_new_auth_token,
|
||||
as: :json
|
||||
|
||||
expect do
|
||||
post "/api/v1/accounts/#{account.id}/integrations/linear/unlink_issue",
|
||||
params: { link_id: link_id, issue_id: issue_id, conversation_id: conversation.display_id },
|
||||
headers: agent.create_new_auth_token,
|
||||
as: :json
|
||||
end.not_to have_enqueued_job(Conversations::ActivityMessageJob)
|
||||
|
||||
expect(response).to have_http_status(:unprocessable_entity)
|
||||
expect(response.body).to include('error message')
|
||||
end
|
||||
|
||||
@@ -82,15 +82,27 @@ describe Integrations::Linear::ProcessorService do
|
||||
end
|
||||
let(:issue_response) do
|
||||
{
|
||||
'issueCreate' => { 'issue' => { 'id' => 'issue1', 'title' => 'Issue title' } }
|
||||
'issueCreate' => {
|
||||
'issue' => {
|
||||
'id' => 'issue1',
|
||||
'title' => 'Issue title',
|
||||
'identifier' => 'ENG-123'
|
||||
}
|
||||
}
|
||||
}
|
||||
end
|
||||
|
||||
context 'when Linear client returns valid data' do
|
||||
it 'returns parsed issue data' do
|
||||
it 'returns parsed issue data with identifier' do
|
||||
allow(linear_client).to receive(:create_issue).with(params).and_return(issue_response)
|
||||
result = service.create_issue(params)
|
||||
expect(result).to eq({ data: { id: 'issue1', title: 'Issue title' } })
|
||||
expect(result).to eq({
|
||||
data: {
|
||||
id: 'issue1',
|
||||
title: 'Issue title',
|
||||
identifier: 'ENG-123'
|
||||
}
|
||||
})
|
||||
end
|
||||
end
|
||||
|
||||
@@ -133,13 +145,13 @@ describe Integrations::Linear::ProcessorService do
|
||||
|
||||
describe '#unlink_issue' do
|
||||
let(:link_id) { 'attachment1' }
|
||||
let(:unlink_response) { { data: { link_id: link_id } } }
|
||||
let(:linear_client_response) { { success: true } }
|
||||
|
||||
context 'when Linear client returns valid data' do
|
||||
it 'returns parsed unlink data' do
|
||||
allow(linear_client).to receive(:unlink_issue).with(link_id).and_return(unlink_response)
|
||||
it 'returns unlink data with link_id' do
|
||||
allow(linear_client).to receive(:unlink_issue).with(link_id).and_return(linear_client_response)
|
||||
result = service.unlink_issue(link_id)
|
||||
expect(result).to eq(unlink_response)
|
||||
expect(result).to eq({ data: { link_id: link_id } })
|
||||
end
|
||||
end
|
||||
|
||||
@@ -207,4 +219,59 @@ describe Integrations::Linear::ProcessorService do
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
# Tests specifically for activity message integration
|
||||
describe 'activity message data compatibility' do
|
||||
let(:linear_client_response) { { success: true } }
|
||||
|
||||
describe '#create_issue' do
|
||||
it 'includes identifier field needed for activity messages' do
|
||||
params = { title: 'Test Issue', team_id: 'team1' }
|
||||
response = {
|
||||
'issueCreate' => {
|
||||
'issue' => {
|
||||
'id' => 'internal_id_123',
|
||||
'title' => 'Test Issue',
|
||||
'identifier' => 'ENG-456'
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
allow(linear_client).to receive(:create_issue).with(params).and_return(response)
|
||||
result = service.create_issue(params)
|
||||
|
||||
expect(result[:data]).to have_key(:identifier)
|
||||
expect(result[:data][:identifier]).to eq('ENG-456')
|
||||
end
|
||||
end
|
||||
|
||||
describe '#link_issue' do
|
||||
it 'returns issue_id in response for activity messages' do
|
||||
link = 'https://example.com'
|
||||
issue_id = 'ENG-789'
|
||||
title = 'Test Issue'
|
||||
response = {
|
||||
'attachmentLinkURL' => {
|
||||
'attachment' => { 'id' => 'attachment123' }
|
||||
}
|
||||
}
|
||||
|
||||
allow(linear_client).to receive(:link_issue).with(link, issue_id, title).and_return(response)
|
||||
result = service.link_issue(link, issue_id, title)
|
||||
|
||||
expect(result[:data][:id]).to eq(issue_id)
|
||||
end
|
||||
end
|
||||
|
||||
describe '#unlink_issue' do
|
||||
it 'returns structured data for activity messages' do
|
||||
link_id = 'attachment456'
|
||||
|
||||
allow(linear_client).to receive(:unlink_issue).with(link_id).and_return(linear_client_response)
|
||||
result = service.unlink_issue(link_id)
|
||||
|
||||
expect(result).to eq({ data: { link_id: link_id } })
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -114,6 +114,24 @@ RSpec.describe Channel::Telegram do
|
||||
expect(telegram_channel.send_message_on_telegram(message)).to eq('telegram_123')
|
||||
end
|
||||
|
||||
it 'sends message with business_connection_id' do
|
||||
additional_attributes = { 'chat_id' => '123', 'business_connection_id' => 'eooW3KF5WB5HxTD7T826' }
|
||||
message = create(:message, message_type: :outgoing, content: 'test',
|
||||
conversation: create(:conversation, inbox: telegram_channel.inbox, additional_attributes: additional_attributes))
|
||||
|
||||
stub_request(:post, "https://api.telegram.org/bot#{telegram_channel.bot_token}/sendMessage")
|
||||
.with(
|
||||
body: 'chat_id=123&text=test&reply_markup=&parse_mode=HTML&reply_to_message_id=&business_connection_id=eooW3KF5WB5HxTD7T826'
|
||||
)
|
||||
.to_return(
|
||||
status: 200,
|
||||
body: { result: { message_id: 'telegram_123' } }.to_json,
|
||||
headers: { 'Content-Type' => 'application/json' }
|
||||
)
|
||||
|
||||
expect(telegram_channel.send_message_on_telegram(message)).to eq('telegram_123')
|
||||
end
|
||||
|
||||
it 'send text message failed' do
|
||||
message = create(:message, message_type: :outgoing, content: 'test',
|
||||
conversation: create(:conversation, inbox: telegram_channel.inbox, additional_attributes: { 'chat_id' => '123' }))
|
||||
|
||||
@@ -0,0 +1,174 @@
|
||||
require 'rails_helper'
|
||||
|
||||
RSpec.describe Linear::ActivityMessageService, type: :service do
|
||||
let(:account) { create(:account) }
|
||||
let(:inbox) { create(:inbox, account: account) }
|
||||
let(:conversation) { create(:conversation, account: account, inbox: inbox) }
|
||||
let(:user) { create(:user, account: account) }
|
||||
|
||||
describe '#perform' do
|
||||
context 'when action_type is issue_created' do
|
||||
let(:service) do
|
||||
described_class.new(
|
||||
conversation: conversation,
|
||||
action_type: :issue_created,
|
||||
issue_data: { id: 'ENG-123' },
|
||||
user: user
|
||||
)
|
||||
end
|
||||
|
||||
it 'enqueues an activity message job' do
|
||||
expect do
|
||||
service.perform
|
||||
end.to have_enqueued_job(Conversations::ActivityMessageJob)
|
||||
.with(conversation, {
|
||||
account_id: conversation.account_id,
|
||||
inbox_id: conversation.inbox_id,
|
||||
message_type: :activity,
|
||||
content: "Linear issue ENG-123 was created by #{user.name}"
|
||||
})
|
||||
end
|
||||
|
||||
it 'does not enqueue job when issue data lacks id' do
|
||||
service = described_class.new(
|
||||
conversation: conversation,
|
||||
action_type: :issue_created,
|
||||
issue_data: { title: 'Some issue' },
|
||||
user: user
|
||||
)
|
||||
|
||||
expect do
|
||||
service.perform
|
||||
end.not_to have_enqueued_job(Conversations::ActivityMessageJob)
|
||||
end
|
||||
|
||||
it 'does not enqueue job when issue_data is empty' do
|
||||
service = described_class.new(
|
||||
conversation: conversation,
|
||||
action_type: :issue_created,
|
||||
issue_data: {},
|
||||
user: user
|
||||
)
|
||||
|
||||
expect do
|
||||
service.perform
|
||||
end.not_to have_enqueued_job(Conversations::ActivityMessageJob)
|
||||
end
|
||||
|
||||
it 'does not enqueue job when conversation is nil' do
|
||||
service = described_class.new(
|
||||
conversation: nil,
|
||||
action_type: :issue_created,
|
||||
issue_data: { id: 'ENG-123' },
|
||||
user: user
|
||||
)
|
||||
|
||||
expect do
|
||||
service.perform
|
||||
end.not_to have_enqueued_job(Conversations::ActivityMessageJob)
|
||||
end
|
||||
|
||||
it 'does not enqueue job when user is nil' do
|
||||
service = described_class.new(
|
||||
conversation: conversation,
|
||||
action_type: :issue_created,
|
||||
issue_data: { id: 'ENG-123' },
|
||||
user: nil
|
||||
)
|
||||
|
||||
expect do
|
||||
service.perform
|
||||
end.not_to have_enqueued_job(Conversations::ActivityMessageJob)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when action_type is issue_linked' do
|
||||
let(:service) do
|
||||
described_class.new(
|
||||
conversation: conversation,
|
||||
action_type: :issue_linked,
|
||||
issue_data: { id: 'ENG-456' },
|
||||
user: user
|
||||
)
|
||||
end
|
||||
|
||||
it 'enqueues an activity message job' do
|
||||
expect do
|
||||
service.perform
|
||||
end.to have_enqueued_job(Conversations::ActivityMessageJob)
|
||||
.with(conversation, {
|
||||
account_id: conversation.account_id,
|
||||
inbox_id: conversation.inbox_id,
|
||||
message_type: :activity,
|
||||
content: "Linear issue ENG-456 was linked by #{user.name}"
|
||||
})
|
||||
end
|
||||
|
||||
it 'does not enqueue job when issue data lacks id' do
|
||||
service = described_class.new(
|
||||
conversation: conversation,
|
||||
action_type: :issue_linked,
|
||||
issue_data: { title: 'Some issue' },
|
||||
user: user
|
||||
)
|
||||
|
||||
expect do
|
||||
service.perform
|
||||
end.not_to have_enqueued_job(Conversations::ActivityMessageJob)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when action_type is issue_unlinked' do
|
||||
let(:service) do
|
||||
described_class.new(
|
||||
conversation: conversation,
|
||||
action_type: :issue_unlinked,
|
||||
issue_data: { id: 'ENG-789' },
|
||||
user: user
|
||||
)
|
||||
end
|
||||
|
||||
it 'enqueues an activity message job' do
|
||||
expect do
|
||||
service.perform
|
||||
end.to have_enqueued_job(Conversations::ActivityMessageJob)
|
||||
.with(conversation, {
|
||||
account_id: conversation.account_id,
|
||||
inbox_id: conversation.inbox_id,
|
||||
message_type: :activity,
|
||||
content: "Linear issue ENG-789 was unlinked by #{user.name}"
|
||||
})
|
||||
end
|
||||
|
||||
it 'does not enqueue job when issue data lacks id' do
|
||||
service = described_class.new(
|
||||
conversation: conversation,
|
||||
action_type: :issue_unlinked,
|
||||
issue_data: { title: 'Some issue' },
|
||||
user: user
|
||||
)
|
||||
|
||||
expect do
|
||||
service.perform
|
||||
end.not_to have_enqueued_job(Conversations::ActivityMessageJob)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when action_type is unknown' do
|
||||
let(:service) do
|
||||
described_class.new(
|
||||
conversation: conversation,
|
||||
action_type: :unknown_action,
|
||||
issue_data: { id: 'ENG-999' },
|
||||
user: user
|
||||
)
|
||||
end
|
||||
|
||||
it 'does not enqueue job for unknown action types' do
|
||||
expect do
|
||||
service.perform
|
||||
end.not_to have_enqueued_job(Conversations::ActivityMessageJob)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -89,6 +89,61 @@ describe Telegram::IncomingMessageService do
|
||||
end
|
||||
end
|
||||
|
||||
context 'when business connection messages' do
|
||||
subject do
|
||||
described_class.new(inbox: telegram_channel.inbox, params: params).perform
|
||||
end
|
||||
|
||||
let(:business_message_params) { message_params.merge('business_connection_id' => 'eooW3KF5WB5HxTD7T826') }
|
||||
let(:params) do
|
||||
{
|
||||
'update_id' => 2_342_342_343_242,
|
||||
'business_message' => { 'text' => 'test' }.deep_merge(business_message_params)
|
||||
}.with_indifferent_access
|
||||
end
|
||||
|
||||
it 'creates appropriate conversations, message and contacts' do
|
||||
subject
|
||||
expect(telegram_channel.inbox.conversations.count).not_to eq(0)
|
||||
expect(telegram_channel.inbox.conversations.last.additional_attributes).to include({ 'chat_id' => 23,
|
||||
'business_connection_id' => 'eooW3KF5WB5HxTD7T826' })
|
||||
contact = Contact.all.first
|
||||
expect(contact.name).to eq('Sojan Jose')
|
||||
expect(contact.additional_attributes['language_code']).to eq('en')
|
||||
message = telegram_channel.inbox.messages.first
|
||||
expect(message.content).to eq('test')
|
||||
expect(message.message_type).to eq('incoming')
|
||||
expect(message.sender).to eq(contact)
|
||||
end
|
||||
|
||||
context 'when sender is your business account' do
|
||||
let(:business_message_params) do
|
||||
message_params.merge(
|
||||
'business_connection_id' => 'eooW3KF5WB5HxTD7T826',
|
||||
'from' => {
|
||||
'id' => 42, 'is_bot' => false, 'first_name' => 'John', 'last_name' => 'Doe', 'username' => 'johndoe', 'language_code' => 'en'
|
||||
}
|
||||
)
|
||||
end
|
||||
|
||||
it 'creates appropriate conversations, message and contacts' do
|
||||
subject
|
||||
expect(telegram_channel.inbox.conversations.count).not_to eq(0)
|
||||
expect(telegram_channel.inbox.conversations.last.additional_attributes).to include({ 'chat_id' => 23,
|
||||
'business_connection_id' => 'eooW3KF5WB5HxTD7T826' })
|
||||
contact = Contact.all.first
|
||||
expect(contact.name).to eq('Sojan Jose')
|
||||
# TODO: The language code is not present when we send the first message to the client.
|
||||
# Should we update it when the user replies?
|
||||
expect(contact.additional_attributes['language_code']).to be_nil
|
||||
message = telegram_channel.inbox.messages.first
|
||||
expect(message.content).to eq('test')
|
||||
expect(message.message_type).to eq('outgoing')
|
||||
expect(message.sender).to be_nil
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'when valid audio messages params' do
|
||||
it 'creates appropriate conversations, message and contacts' do
|
||||
allow(telegram_channel.inbox.channel).to receive(:get_telegram_file_path).and_return('https://chatwoot-assets.local/sample.mp3')
|
||||
|
||||
@@ -40,6 +40,22 @@ RSpec.describe Telegram::SendAttachmentsService do
|
||||
end
|
||||
end
|
||||
|
||||
context 'when this is business chat' do
|
||||
before { allow(channel).to receive(:business_connection_id).and_return('eooW3KF5WB5HxTD7T826') }
|
||||
|
||||
it 'sends all types of attachments in seperate groups and returns the last successful message ID from the batch' do
|
||||
attach_files(message)
|
||||
service.perform
|
||||
expect(a_request(:post, "#{telegram_api_url}/sendMediaGroup")
|
||||
.with { |req| req.body =~ /business_connection_id.+eooW3KF5WB5HxTD7T826/m })
|
||||
.to have_been_made.times(2)
|
||||
|
||||
expect(a_request(:post, "#{telegram_api_url}/sendDocument")
|
||||
.with { |req| req.body =~ /business_connection_id.+eooW3KF5WB5HxTD7T826/m })
|
||||
.to have_been_made.once
|
||||
end
|
||||
end
|
||||
|
||||
context 'when all attachments are photo and video' do
|
||||
before do
|
||||
2.times { attach_file_to_message(message, 'image', 'sample.png', 'image/png') }
|
||||
|
||||
@@ -53,6 +53,24 @@ describe Telegram::UpdateMessageService do
|
||||
described_class.new(inbox: telegram_channel.inbox, params: caption_update_params.with_indifferent_access).perform
|
||||
expect(message.reload.content).to eq('updated caption')
|
||||
end
|
||||
|
||||
context 'when business message' do
|
||||
let(:text_update_params) do
|
||||
{
|
||||
'update_id': 1,
|
||||
'edited_business_message': common_message_params.merge(
|
||||
'message_id': 48,
|
||||
'text': 'updated message'
|
||||
)
|
||||
}
|
||||
end
|
||||
|
||||
it 'updates the message text when text is present' do
|
||||
message = create(:message, conversation: conversation, source_id: text_update_params[:edited_business_message][:message_id])
|
||||
described_class.new(inbox: telegram_channel.inbox, params: text_update_params.with_indifferent_access).perform
|
||||
expect(message.reload.content).to eq('updated message')
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'when invalid update message params' do
|
||||
|
||||
Reference in New Issue
Block a user