Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
950ebcacd4 | ||
|
|
b9834c3a18 |
@@ -1,5 +1,6 @@
|
|||||||
class Api::V1::Accounts::Conversations::MessagesController < Api::V1::Accounts::Conversations::BaseController
|
class Api::V1::Accounts::Conversations::MessagesController < Api::V1::Accounts::Conversations::BaseController
|
||||||
before_action :ensure_api_inbox, only: :update
|
before_action :ensure_api_inbox, only: :update
|
||||||
|
before_action :ensure_agent_bot_takeover, only: :create
|
||||||
|
|
||||||
def index
|
def index
|
||||||
@messages = message_finder.perform
|
@messages = message_finder.perform
|
||||||
@@ -72,6 +73,21 @@ class Api::V1::Accounts::Conversations::MessagesController < Api::V1::Accounts::
|
|||||||
message.translations.present? && message.translations[permitted_params[:target_language]].present?
|
message.translations.present? && message.translations[permitted_params[:target_language]].present?
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def ensure_agent_bot_takeover
|
||||||
|
return unless Current.user.is_a?(User) && @conversation.assignee_agent_bot_id.present?
|
||||||
|
return if private_message? || incoming_message?
|
||||||
|
|
||||||
|
render json: { error: 'Conversation is assigned to an Agent Bot. Take over the conversation before replying.' }, status: :unprocessable_entity
|
||||||
|
end
|
||||||
|
|
||||||
|
def private_message?
|
||||||
|
ActiveModel::Type::Boolean.new.cast(params[:private])
|
||||||
|
end
|
||||||
|
|
||||||
|
def incoming_message?
|
||||||
|
params[:message_type] == 'incoming'
|
||||||
|
end
|
||||||
|
|
||||||
# API inbox check
|
# API inbox check
|
||||||
def ensure_api_inbox
|
def ensure_api_inbox
|
||||||
# Only API inboxes can update messages
|
# Only API inboxes can update messages
|
||||||
|
|||||||
@@ -171,6 +171,9 @@ export default {
|
|||||||
return this.isATwilioWhatsAppChannel && !this.isPrivate;
|
return this.isATwilioWhatsAppChannel && !this.isPrivate;
|
||||||
},
|
},
|
||||||
isPrivate() {
|
isPrivate() {
|
||||||
|
if (this.isAgentBotOwned) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
if (
|
if (
|
||||||
this.currentChat.can_reply ||
|
this.currentChat.can_reply ||
|
||||||
this.isAWhatsAppChannel ||
|
this.isAWhatsAppChannel ||
|
||||||
@@ -197,10 +200,14 @@ export default {
|
|||||||
},
|
},
|
||||||
isReplyRestricted() {
|
isReplyRestricted() {
|
||||||
return (
|
return (
|
||||||
!this.currentChat?.can_reply &&
|
this.isAgentBotOwned ||
|
||||||
!(this.isAWhatsAppChannel || this.isAPIInbox)
|
(!this.currentChat?.can_reply &&
|
||||||
|
!(this.isAWhatsAppChannel || this.isAPIInbox))
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
isAgentBotOwned() {
|
||||||
|
return this.currentChat?.meta?.assignee_type === 'AgentBot';
|
||||||
|
},
|
||||||
inboxId() {
|
inboxId() {
|
||||||
return this.currentChat.inbox_id;
|
return this.currentChat.inbox_id;
|
||||||
},
|
},
|
||||||
@@ -462,6 +469,11 @@ export default {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (this.isAgentBotOwned) {
|
||||||
|
this.replyType = REPLY_EDITOR_MODES.NOTE;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (canReply || this.isAWhatsAppChannel || this.isAPIInbox) {
|
if (canReply || this.isAWhatsAppChannel || this.isAPIInbox) {
|
||||||
this.replyType = REPLY_EDITOR_MODES.REPLY;
|
this.replyType = REPLY_EDITOR_MODES.REPLY;
|
||||||
} else {
|
} else {
|
||||||
@@ -501,6 +513,9 @@ export default {
|
|||||||
|
|
||||||
mounted() {
|
mounted() {
|
||||||
this.getFromDraft();
|
this.getFromDraft();
|
||||||
|
if (this.isAgentBotOwned) {
|
||||||
|
this.replyType = REPLY_EDITOR_MODES.NOTE;
|
||||||
|
}
|
||||||
// Don't use the keyboard listener mixin here as the events here are supposed to be
|
// Don't use the keyboard listener mixin here as the events here are supposed to be
|
||||||
// working even if the editor is focussed.
|
// working even if the editor is focussed.
|
||||||
document.addEventListener('paste', this.onPaste);
|
document.addEventListener('paste', this.onPaste);
|
||||||
@@ -921,6 +936,9 @@ export default {
|
|||||||
this.hideContentTemplatesModal();
|
this.hideContentTemplatesModal();
|
||||||
},
|
},
|
||||||
setReplyMode(mode = REPLY_EDITOR_MODES.REPLY) {
|
setReplyMode(mode = REPLY_EDITOR_MODES.REPLY) {
|
||||||
|
if (this.isAgentBotOwned && mode === REPLY_EDITOR_MODES.REPLY) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
// Clear attachments when switching between private note and reply modes
|
// Clear attachments when switching between private note and reply modes
|
||||||
// This is to prevent from breaking the upload rules
|
// This is to prevent from breaking the upload rules
|
||||||
if (this.attachedFiles.length > 0) this.attachedFiles = [];
|
if (this.attachedFiles.length > 0) this.attachedFiles = [];
|
||||||
|
|||||||
@@ -4,7 +4,6 @@ import { useStore } from 'vuex';
|
|||||||
import { useMapGetter } from 'dashboard/composables/store';
|
import { useMapGetter } from 'dashboard/composables/store';
|
||||||
import { useAlert } from 'dashboard/composables';
|
import { useAlert } from 'dashboard/composables';
|
||||||
import { useI18n } from 'vue-i18n';
|
import { useI18n } from 'vue-i18n';
|
||||||
import wootConstants from 'dashboard/constants/globals';
|
|
||||||
|
|
||||||
import Banner from 'dashboard/components/ui/Banner.vue';
|
import Banner from 'dashboard/components/ui/Banner.vue';
|
||||||
|
|
||||||
@@ -34,6 +33,7 @@ const assignedAgent = computed({
|
|||||||
store.dispatch('setCurrentChatAssignee', {
|
store.dispatch('setCurrentChatAssignee', {
|
||||||
conversationId: currentChat.value?.id,
|
conversationId: currentChat.value?.id,
|
||||||
assignee: agent,
|
assignee: agent,
|
||||||
|
assigneeType: agent ? 'User' : null,
|
||||||
});
|
});
|
||||||
store.dispatch('assignAgent', {
|
store.dispatch('assignAgent', {
|
||||||
conversationId: currentChat.value?.id,
|
conversationId: currentChat.value?.id,
|
||||||
@@ -42,9 +42,8 @@ const assignedAgent = computed({
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const isUserTyping = computed(
|
const hasMessage = computed(() => props.message !== '');
|
||||||
() => props.message !== '' && !props.isOnPrivateNote
|
const isUserTyping = computed(() => hasMessage.value && !props.isOnPrivateNote);
|
||||||
);
|
|
||||||
const isUnassigned = computed(() => !assignedAgent.value);
|
const isUnassigned = computed(() => !assignedAgent.value);
|
||||||
const isAssignedToOtherAgent = computed(
|
const isAssignedToOtherAgent = computed(
|
||||||
() => assignedAgent.value?.id !== currentUser.value?.id
|
() => assignedAgent.value?.id !== currentUser.value?.id
|
||||||
@@ -56,11 +55,11 @@ const showSelfAssignBanner = computed(() => {
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
const showBotHandoffBanner = computed(
|
const showBotHandoffBanner = computed(() => {
|
||||||
() =>
|
return (
|
||||||
isUserTyping.value &&
|
hasMessage.value && currentChat.value?.meta?.assignee_type === 'AgentBot'
|
||||||
currentChat.value?.status === wootConstants.STATUS_TYPE.PENDING
|
);
|
||||||
);
|
});
|
||||||
|
|
||||||
const botHandoffActionLabel = computed(() => {
|
const botHandoffActionLabel = computed(() => {
|
||||||
return assignedAgent.value?.id === currentUser.value?.id
|
return assignedAgent.value?.id === currentUser.value?.id
|
||||||
@@ -89,7 +88,7 @@ const onClickSelfAssign = async () => {
|
|||||||
const reopenConversation = async () => {
|
const reopenConversation = async () => {
|
||||||
await store.dispatch('toggleStatus', {
|
await store.dispatch('toggleStatus', {
|
||||||
conversationId: currentChat.value?.id,
|
conversationId: currentChat.value?.id,
|
||||||
status: wootConstants.STATUS_TYPE.OPEN,
|
status: 'open',
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -223,8 +223,11 @@ const actions = {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
setCurrentChatAssignee({ commit }, { conversationId, assignee }) {
|
setCurrentChatAssignee(
|
||||||
commit(types.ASSIGN_AGENT, { conversationId, assignee });
|
{ commit },
|
||||||
|
{ conversationId, assignee, assigneeType }
|
||||||
|
) {
|
||||||
|
commit(types.ASSIGN_AGENT, { conversationId, assignee, assigneeType });
|
||||||
},
|
},
|
||||||
|
|
||||||
assignTeam: async ({ dispatch }, { conversationId, teamId }) => {
|
assignTeam: async ({ dispatch }, { conversationId, teamId }) => {
|
||||||
|
|||||||
@@ -108,10 +108,13 @@ export const mutations = {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
[types.ASSIGN_AGENT](_state, { conversationId, assignee }) {
|
[types.ASSIGN_AGENT](_state, { conversationId, assignee, assigneeType }) {
|
||||||
const chat = getConversationById(_state)(conversationId);
|
const chat = getConversationById(_state)(conversationId);
|
||||||
if (chat) {
|
if (chat) {
|
||||||
chat.meta.assignee = assignee;
|
chat.meta.assignee = assignee;
|
||||||
|
if (assigneeType !== undefined) {
|
||||||
|
chat.meta.assignee_type = assigneeType;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|||||||
@@ -716,6 +716,21 @@ describe('#mutations', () => {
|
|||||||
expect(state.allConversations[0].meta.assignee).toEqual(assignee);
|
expect(state.allConversations[0].meta.assignee).toEqual(assignee);
|
||||||
expect(state.allConversations[1].meta.assignee).toBeUndefined();
|
expect(state.allConversations[1].meta.assignee).toBeUndefined();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('should update assignee type when provided', () => {
|
||||||
|
const assignee = { id: 1, name: 'Agent' };
|
||||||
|
const state = {
|
||||||
|
allConversations: [{ id: 1, meta: { assignee_type: 'AgentBot' } }],
|
||||||
|
};
|
||||||
|
|
||||||
|
mutations[types.ASSIGN_AGENT](state, {
|
||||||
|
conversationId: 1,
|
||||||
|
assignee,
|
||||||
|
assigneeType: 'User',
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(state.allConversations[0].meta.assignee_type).toEqual('User');
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('#ASSIGN_PRIORITY', () => {
|
describe('#ASSIGN_PRIORITY', () => {
|
||||||
|
|||||||
@@ -36,6 +36,33 @@ RSpec.describe 'Conversation Messages API', type: :request do
|
|||||||
expect(conversation.messages.first.content).to eq(params[:content])
|
expect(conversation.messages.first.content).to eq(params[:content])
|
||||||
end
|
end
|
||||||
|
|
||||||
|
it 'blocks public replies when an agent bot owns the conversation' do
|
||||||
|
conversation.update!(assignee_agent_bot: create(:agent_bot, account: account))
|
||||||
|
params = { content: 'test-message', private: false }
|
||||||
|
|
||||||
|
post api_v1_account_conversation_messages_url(account_id: account.id, conversation_id: conversation.display_id),
|
||||||
|
params: params,
|
||||||
|
headers: agent.create_new_auth_token,
|
||||||
|
as: :json
|
||||||
|
|
||||||
|
expect(response).to have_http_status(:unprocessable_entity)
|
||||||
|
expect(response.parsed_body['error']).to eq('Conversation is assigned to an Agent Bot. Take over the conversation before replying.')
|
||||||
|
expect(conversation.messages.count).to eq(0)
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'allows private notes when an agent bot owns the conversation' do
|
||||||
|
conversation.update!(assignee_agent_bot: create(:agent_bot, account: account))
|
||||||
|
params = { content: 'test-note', private: true }
|
||||||
|
|
||||||
|
post api_v1_account_conversation_messages_url(account_id: account.id, conversation_id: conversation.display_id),
|
||||||
|
params: params,
|
||||||
|
headers: agent.create_new_auth_token,
|
||||||
|
as: :json
|
||||||
|
|
||||||
|
expect(response).to have_http_status(:success)
|
||||||
|
expect(conversation.messages.last).to be_private
|
||||||
|
end
|
||||||
|
|
||||||
it 'does not create the message' do
|
it 'does not create the message' do
|
||||||
params = { content: "#{'h' * 150 * 1000}a", private: true }
|
params = { content: "#{'h' * 150 * 1000}a", private: true }
|
||||||
|
|
||||||
@@ -109,7 +136,7 @@ RSpec.describe 'Conversation Messages API', type: :request do
|
|||||||
end
|
end
|
||||||
|
|
||||||
context 'when it is an authenticated agent bot' do
|
context 'when it is an authenticated agent bot' do
|
||||||
let!(:agent_bot) { create(:agent_bot) }
|
let!(:agent_bot) { create(:agent_bot, account: account) }
|
||||||
|
|
||||||
it 'creates a new outgoing message' do
|
it 'creates a new outgoing message' do
|
||||||
create(:agent_bot_inbox, inbox: inbox, agent_bot: agent_bot)
|
create(:agent_bot_inbox, inbox: inbox, agent_bot: agent_bot)
|
||||||
@@ -125,6 +152,21 @@ RSpec.describe 'Conversation Messages API', type: :request do
|
|||||||
expect(conversation.messages.first.content).to eq(params[:content])
|
expect(conversation.messages.first.content).to eq(params[:content])
|
||||||
end
|
end
|
||||||
|
|
||||||
|
it 'creates a new outgoing message when the agent bot owns the conversation' do
|
||||||
|
create(:agent_bot_inbox, inbox: inbox, agent_bot: agent_bot)
|
||||||
|
conversation.update!(assignee_agent_bot: agent_bot)
|
||||||
|
params = { content: 'test-message' }
|
||||||
|
|
||||||
|
post api_v1_account_conversation_messages_url(account_id: account.id, conversation_id: conversation.display_id),
|
||||||
|
params: params,
|
||||||
|
headers: { api_access_token: agent_bot.access_token.token },
|
||||||
|
as: :json
|
||||||
|
|
||||||
|
expect(response).to have_http_status(:success)
|
||||||
|
expect(conversation.messages.count).to eq(1)
|
||||||
|
expect(conversation.messages.first.content).to eq(params[:content])
|
||||||
|
end
|
||||||
|
|
||||||
it 'creates a new outgoing input select message' do
|
it 'creates a new outgoing input select message' do
|
||||||
create(:agent_bot_inbox, inbox: inbox, agent_bot: agent_bot)
|
create(:agent_bot_inbox, inbox: inbox, agent_bot: agent_bot)
|
||||||
select_item1 = build(:bot_message_select)
|
select_item1 = build(:bot_message_select)
|
||||||
|
|||||||
Reference in New Issue
Block a user