feat: Read/Delivery status for Whatsapp Cloud API (#5157)
Process field statuses received in webhook WhatsApp cloud API ref: #1021 Co-authored-by: Sojan <sojan@pepalo.com> Co-authored-by: Nithin David <1277421+nithindavid@users.noreply.github.com>
This commit is contained in:
parent
a397f01692
commit
edcbd53425
9 changed files with 148 additions and 17 deletions
|
@ -59,10 +59,12 @@
|
||||||
:story-sender="storySender"
|
:story-sender="storySender"
|
||||||
:story-id="storyId"
|
:story-id="storyId"
|
||||||
:is-a-tweet="isATweet"
|
:is-a-tweet="isATweet"
|
||||||
|
:is-a-whatsapp-channel="isAWhatsAppChannel"
|
||||||
:has-instagram-story="hasInstagramStory"
|
:has-instagram-story="hasInstagramStory"
|
||||||
:is-email="isEmailContentType"
|
:is-email="isEmailContentType"
|
||||||
:is-private="data.private"
|
:is-private="data.private"
|
||||||
:message-type="data.message_type"
|
:message-type="data.message_type"
|
||||||
|
:message-status="status"
|
||||||
:readable-time="readableTime"
|
:readable-time="readableTime"
|
||||||
:source-id="data.source_id"
|
:source-id="data.source_id"
|
||||||
:inbox-id="data.inbox_id"
|
:inbox-id="data.inbox_id"
|
||||||
|
@ -157,6 +159,10 @@ export default {
|
||||||
type: Boolean,
|
type: Boolean,
|
||||||
default: false,
|
default: false,
|
||||||
},
|
},
|
||||||
|
isAWhatsAppChannel: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
hasInstagramStory: {
|
hasInstagramStory: {
|
||||||
type: Boolean,
|
type: Boolean,
|
||||||
default: false,
|
default: false,
|
||||||
|
@ -231,6 +237,9 @@ export default {
|
||||||
sender() {
|
sender() {
|
||||||
return this.data.sender || {};
|
return this.data.sender || {};
|
||||||
},
|
},
|
||||||
|
status() {
|
||||||
|
return this.data.status;
|
||||||
|
},
|
||||||
storySender() {
|
storySender() {
|
||||||
return this.contentAttributes.story_sender || null;
|
return this.contentAttributes.story_sender || null;
|
||||||
},
|
},
|
||||||
|
|
|
@ -38,6 +38,7 @@
|
||||||
class="message--read ph-no-capture"
|
class="message--read ph-no-capture"
|
||||||
:data="message"
|
:data="message"
|
||||||
:is-a-tweet="isATweet"
|
:is-a-tweet="isATweet"
|
||||||
|
:is-a-whatsapp-channel="isAWhatsAppChannel"
|
||||||
:has-instagram-story="hasInstagramStory"
|
:has-instagram-story="hasInstagramStory"
|
||||||
:has-user-read-message="
|
:has-user-read-message="
|
||||||
hasUserReadMessage(message.created_at, getLastSeenAt)
|
hasUserReadMessage(message.created_at, getLastSeenAt)
|
||||||
|
@ -60,6 +61,7 @@
|
||||||
class="message--unread ph-no-capture"
|
class="message--unread ph-no-capture"
|
||||||
:data="message"
|
:data="message"
|
||||||
:is-a-tweet="isATweet"
|
:is-a-tweet="isATweet"
|
||||||
|
:is-a-whatsapp-channel="isAWhatsAppChannel"
|
||||||
:has-instagram-story="hasInstagramStory"
|
:has-instagram-story="hasInstagramStory"
|
||||||
:has-user-read-message="
|
:has-user-read-message="
|
||||||
hasUserReadMessage(message.created_at, getLastSeenAt)
|
hasUserReadMessage(message.created_at, getLastSeenAt)
|
||||||
|
|
|
@ -3,20 +3,30 @@
|
||||||
<span class="time" :class="{ delivered: messageRead }">{{
|
<span class="time" :class="{ delivered: messageRead }">{{
|
||||||
readableTime
|
readableTime
|
||||||
}}</span>
|
}}</span>
|
||||||
<span v-if="showSentIndicator" class="time">
|
<span v-if="showReadIndicator" class="read-indicator-wrap">
|
||||||
<fluent-icon
|
<fluent-icon
|
||||||
v-tooltip.top-start="$t('CHAT_LIST.SENT')"
|
v-tooltip.top-start="$t('CHAT_LIST.MESSAGE_READ')"
|
||||||
icon="checkmark"
|
icon="checkmark-double"
|
||||||
|
class="action--icon read-tick read-indicator"
|
||||||
size="14"
|
size="14"
|
||||||
/>
|
/>
|
||||||
</span>
|
</span>
|
||||||
|
<span v-if="showDeliveredIndicator" class="read-indicator-wrap">
|
||||||
<fluent-icon
|
<fluent-icon
|
||||||
v-if="messageRead"
|
v-tooltip.top-start="$t('CHAT_LIST.DELIVERED')"
|
||||||
v-tooltip.top-start="$t('CHAT_LIST.MESSAGE_READ')"
|
|
||||||
icon="checkmark-double"
|
icon="checkmark-double"
|
||||||
class="action--icon read-tick"
|
class="action--icon read-tick"
|
||||||
size="12"
|
size="14"
|
||||||
/>
|
/>
|
||||||
|
</span>
|
||||||
|
<span v-if="showSentIndicator" class="read-indicator-wrap">
|
||||||
|
<fluent-icon
|
||||||
|
v-tooltip.top-start="$t('CHAT_LIST.SENT')"
|
||||||
|
icon="checkmark"
|
||||||
|
class="action--icon read-tick"
|
||||||
|
size="14"
|
||||||
|
/>
|
||||||
|
</span>
|
||||||
<fluent-icon
|
<fluent-icon
|
||||||
v-if="isEmail"
|
v-if="isEmail"
|
||||||
v-tooltip.top-start="$t('CHAT_LIST.RECEIVED_VIA_EMAIL')"
|
v-tooltip.top-start="$t('CHAT_LIST.RECEIVED_VIA_EMAIL')"
|
||||||
|
@ -74,7 +84,7 @@
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import { MESSAGE_TYPE } from 'shared/constants/messages';
|
import { MESSAGE_TYPE, MESSAGE_STATUS } from 'shared/constants/messages';
|
||||||
import { BUS_EVENTS } from 'shared/constants/busEvents';
|
import { BUS_EVENTS } from 'shared/constants/busEvents';
|
||||||
import inboxMixin from 'shared/mixins/inboxMixin';
|
import inboxMixin from 'shared/mixins/inboxMixin';
|
||||||
|
|
||||||
|
@ -117,6 +127,10 @@ export default {
|
||||||
type: Number,
|
type: Number,
|
||||||
default: 1,
|
default: 1,
|
||||||
},
|
},
|
||||||
|
messageStatus: {
|
||||||
|
type: String,
|
||||||
|
default: '',
|
||||||
|
},
|
||||||
sourceId: {
|
sourceId: {
|
||||||
type: String,
|
type: String,
|
||||||
default: '',
|
default: '',
|
||||||
|
@ -144,6 +158,15 @@ export default {
|
||||||
isOutgoing() {
|
isOutgoing() {
|
||||||
return MESSAGE_TYPE.OUTGOING === this.messageType;
|
return MESSAGE_TYPE.OUTGOING === this.messageType;
|
||||||
},
|
},
|
||||||
|
isDelivered() {
|
||||||
|
return MESSAGE_STATUS.DELIVERED === this.messageStatus;
|
||||||
|
},
|
||||||
|
isRead() {
|
||||||
|
return MESSAGE_STATUS.READ === this.messageStatus;
|
||||||
|
},
|
||||||
|
isSent() {
|
||||||
|
return MESSAGE_STATUS.SENT === this.messageStatus;
|
||||||
|
},
|
||||||
screenName() {
|
screenName() {
|
||||||
const { additional_attributes: additionalAttributes = {} } =
|
const { additional_attributes: additionalAttributes = {} } =
|
||||||
this.sender || {};
|
this.sender || {};
|
||||||
|
@ -168,7 +191,23 @@ export default {
|
||||||
return (
|
return (
|
||||||
this.isOutgoing &&
|
this.isOutgoing &&
|
||||||
this.sourceId &&
|
this.sourceId &&
|
||||||
(this.isAnEmailChannel || this.isAWhatsAppChannel)
|
(this.isAnEmailChannel || (this.isAWhatsAppChannel && this.isSent))
|
||||||
|
);
|
||||||
|
},
|
||||||
|
showDeliveredIndicator() {
|
||||||
|
return (
|
||||||
|
this.isOutgoing &&
|
||||||
|
this.sourceId &&
|
||||||
|
this.isAWhatsAppChannel &&
|
||||||
|
this.isDelivered
|
||||||
|
);
|
||||||
|
},
|
||||||
|
showReadIndicator() {
|
||||||
|
return (
|
||||||
|
this.isOutgoing &&
|
||||||
|
this.sourceId &&
|
||||||
|
this.isAWhatsAppChannel &&
|
||||||
|
this.isRead
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
@ -185,16 +224,20 @@ export default {
|
||||||
|
|
||||||
.right {
|
.right {
|
||||||
.message-text--metadata {
|
.message-text--metadata {
|
||||||
|
align-items: center;
|
||||||
.time {
|
.time {
|
||||||
color: var(--w-100);
|
color: var(--w-100);
|
||||||
}
|
}
|
||||||
|
|
||||||
.action--icon {
|
.action--icon {
|
||||||
|
color: var(--white);
|
||||||
&.read-tick {
|
&.read-tick {
|
||||||
color: var(--v-100);
|
color: var(--v-100);
|
||||||
margin-top: calc(var(--space-micro) + var(--space-micro) / 2);
|
|
||||||
}
|
}
|
||||||
color: var(--white);
|
|
||||||
|
&.read-indicator {
|
||||||
|
color: var(--g-300);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.lock--icon--private {
|
.lock--icon--private {
|
||||||
|
@ -296,4 +339,10 @@ export default {
|
||||||
.delivered-icon {
|
.delivered-icon {
|
||||||
margin-left: -var(--space-normal);
|
margin-left: -var(--space-normal);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.read-indicator-wrap {
|
||||||
|
line-height: 1;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
|
@ -56,6 +56,8 @@
|
||||||
"REPLY_TO_TWEET": "Reply to this tweet",
|
"REPLY_TO_TWEET": "Reply to this tweet",
|
||||||
"LINK_TO_STORY": "Go to instagram story",
|
"LINK_TO_STORY": "Go to instagram story",
|
||||||
"SENT": "Sent successfully",
|
"SENT": "Sent successfully",
|
||||||
|
"READ": "Read successfully",
|
||||||
|
"DELIVERED": "Delivered successfully",
|
||||||
"NO_MESSAGES": "No Messages",
|
"NO_MESSAGES": "No Messages",
|
||||||
"NO_CONTENT": "No content available",
|
"NO_CONTENT": "No content available",
|
||||||
"HIDE_QUOTED_TEXT": "Hide Quoted Text",
|
"HIDE_QUOTED_TEXT": "Hide Quoted Text",
|
||||||
|
|
|
@ -1,6 +1,8 @@
|
||||||
export const MESSAGE_STATUS = {
|
export const MESSAGE_STATUS = {
|
||||||
FAILED: 'failed',
|
FAILED: 'failed',
|
||||||
SENT: 'sent',
|
SENT: 'sent',
|
||||||
|
DELIVERED: 'delivered',
|
||||||
|
READ: 'read',
|
||||||
PROGRESS: 'progress',
|
PROGRESS: 'progress',
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -65,8 +65,9 @@ class Message < ApplicationRecord
|
||||||
# [:in_reply_to] : Used to reply to a particular tweet in threads
|
# [:in_reply_to] : Used to reply to a particular tweet in threads
|
||||||
# [:deleted] : Used to denote whether the message was deleted by the agent
|
# [:deleted] : Used to denote whether the message was deleted by the agent
|
||||||
# [:external_created_at] : Can specify if the message was created at a different timestamp externally
|
# [:external_created_at] : Can specify if the message was created at a different timestamp externally
|
||||||
|
# [:external_error : Can specify if the message creation failed due to an error at external API
|
||||||
store :content_attributes, accessors: [:submitted_email, :items, :submitted_values, :email, :in_reply_to, :deleted,
|
store :content_attributes, accessors: [:submitted_email, :items, :submitted_values, :email, :in_reply_to, :deleted,
|
||||||
:external_created_at, :story_sender, :story_id], coder: JSON
|
:external_created_at, :story_sender, :story_id, :external_error], coder: JSON
|
||||||
|
|
||||||
store :external_source_ids, accessors: [:slack], coder: JSON, prefix: :external_source_id
|
store :external_source_ids, accessors: [:slack], coder: JSON, prefix: :external_source_id
|
||||||
|
|
||||||
|
|
|
@ -7,11 +7,38 @@ class Whatsapp::IncomingMessageBaseService
|
||||||
def perform
|
def perform
|
||||||
processed_params
|
processed_params
|
||||||
|
|
||||||
|
perform_statuses
|
||||||
|
|
||||||
set_contact
|
set_contact
|
||||||
return unless @contact
|
return unless @contact
|
||||||
|
|
||||||
set_conversation
|
set_conversation
|
||||||
|
|
||||||
|
perform_messages
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def perform_statuses
|
||||||
|
return if @processed_params[:statuses].blank?
|
||||||
|
|
||||||
|
status = @processed_params[:statuses].first
|
||||||
|
@message = Message.find_by(source_id: status[:id])
|
||||||
|
return unless @message
|
||||||
|
|
||||||
|
update_message_with_status(@message, status)
|
||||||
|
end
|
||||||
|
|
||||||
|
def update_message_with_status(message, status)
|
||||||
|
message.status = status[:status]
|
||||||
|
if status[:status] == 'failed' && status[:errors].present?
|
||||||
|
error = status[:errors]&.first
|
||||||
|
message.external_error = "#{error[:code]}: #{error[:title]}"
|
||||||
|
end
|
||||||
|
message.save!
|
||||||
|
end
|
||||||
|
|
||||||
|
def perform_messages
|
||||||
return if @processed_params[:messages].blank? || unprocessable_message_type?
|
return if @processed_params[:messages].blank? || unprocessable_message_type?
|
||||||
|
|
||||||
@message = @conversation.messages.build(
|
@message = @conversation.messages.build(
|
||||||
|
@ -27,8 +54,6 @@ class Whatsapp::IncomingMessageBaseService
|
||||||
@message.save!
|
@message.save!
|
||||||
end
|
end
|
||||||
|
|
||||||
private
|
|
||||||
|
|
||||||
def processed_params
|
def processed_params
|
||||||
@processed_params ||= params
|
@processed_params ||= params
|
||||||
end
|
end
|
||||||
|
|
|
@ -5,6 +5,7 @@ json.echo_id message.echo_id if message.echo_id
|
||||||
json.conversation_id message.conversation.display_id
|
json.conversation_id message.conversation.display_id
|
||||||
json.message_type message.message_type_before_type_cast
|
json.message_type message.message_type_before_type_cast
|
||||||
json.content_type message.content_type
|
json.content_type message.content_type
|
||||||
|
json.status message.status
|
||||||
json.content_attributes message.content_attributes
|
json.content_attributes message.content_attributes
|
||||||
json.created_at message.created_at.to_i
|
json.created_at message.created_at.to_i
|
||||||
json.private message.private
|
json.private message.private
|
||||||
|
|
|
@ -70,6 +70,46 @@ describe Whatsapp::IncomingMessageService do
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
context 'when valid status params' do
|
||||||
|
let(:from) { '2423423243' }
|
||||||
|
let(:contact_inbox) { create(:contact_inbox, inbox: whatsapp_channel.inbox, source_id: from) }
|
||||||
|
let(:params) do
|
||||||
|
{
|
||||||
|
'contacts' => [{ 'profile' => { 'name' => 'Sojan Jose' }, 'wa_id' => from }],
|
||||||
|
'messages' => [{ 'from' => from, 'id' => from, 'text' => { 'body' => 'Test' },
|
||||||
|
'timestamp' => '1633034394', 'type' => 'text' }]
|
||||||
|
}.with_indifferent_access
|
||||||
|
end
|
||||||
|
|
||||||
|
before do
|
||||||
|
create(:conversation, inbox: whatsapp_channel.inbox, contact_inbox: contact_inbox)
|
||||||
|
described_class.new(inbox: whatsapp_channel.inbox, params: params).perform
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'update status message to read' do
|
||||||
|
status_params = {
|
||||||
|
'statuses' => [{ 'recipient_id' => from, 'id' => from, 'status' => 'read' }]
|
||||||
|
}.with_indifferent_access
|
||||||
|
message = Message.find_by!(source_id: from)
|
||||||
|
expect(message.status).to eq('sent')
|
||||||
|
described_class.new(inbox: whatsapp_channel.inbox, params: status_params).perform
|
||||||
|
expect(message.reload.status).to eq('read')
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'update status message to failed' do
|
||||||
|
status_params = {
|
||||||
|
'statuses' => [{ 'recipient_id' => from, 'id' => from, 'status' => 'failed',
|
||||||
|
'errors' => [{ 'code': 123, 'title': 'abc' }] }]
|
||||||
|
}.with_indifferent_access
|
||||||
|
|
||||||
|
message = Message.find_by!(source_id: from)
|
||||||
|
expect(message.status).to eq('sent')
|
||||||
|
described_class.new(inbox: whatsapp_channel.inbox, params: status_params).perform
|
||||||
|
expect(message.reload.status).to eq('failed')
|
||||||
|
expect(message.external_error).to eq('123: abc')
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
context 'when valid interactive message params' do
|
context 'when valid interactive message params' do
|
||||||
it 'creates appropriate conversations, message and contacts' do
|
it 'creates appropriate conversations, message and contacts' do
|
||||||
params = {
|
params = {
|
||||||
|
|
Loading…
Reference in a new issue