fix: Automatically remove expired story mention (#5300)
When a user mentions the connected Instagram page in a story, the story's content is downloaded in Chatwoot, then if the user deletes the story, the content persists in the platform. fixes: #5258
This commit is contained in:
parent
65de030244
commit
92ea452680
13 changed files with 111 additions and 15 deletions
|
@ -46,6 +46,7 @@ class Messages::Messenger::MessageBuilder
|
||||||
end
|
end
|
||||||
|
|
||||||
def update_attachment_file_type(attachment)
|
def update_attachment_file_type(attachment)
|
||||||
|
return if @message.reload.attachments.blank?
|
||||||
return unless attachment.file_type == 'share' || attachment.file_type == 'story_mention'
|
return unless attachment.file_type == 'share' || attachment.file_type == 'story_mention'
|
||||||
|
|
||||||
attachment.file_type = file_type(attachment.file&.content_type)
|
attachment.file_type = file_type(attachment.file&.content_type)
|
||||||
|
@ -62,6 +63,7 @@ class Messages::Messenger::MessageBuilder
|
||||||
story_sender = result['from']['username']
|
story_sender = result['from']['username']
|
||||||
message.content_attributes[:story_sender] = story_sender
|
message.content_attributes[:story_sender] = story_sender
|
||||||
message.content_attributes[:story_id] = story_id
|
message.content_attributes[:story_id] = story_id
|
||||||
|
message.content_attributes[:image_type] = 'story_mention'
|
||||||
message.content = I18n.t('conversations.messages.instagram_story_content', story_sender: story_sender)
|
message.content = I18n.t('conversations.messages.instagram_story_content', story_sender: story_sender)
|
||||||
message.save!
|
message.save!
|
||||||
end
|
end
|
||||||
|
@ -74,6 +76,7 @@ class Messages::Messenger::MessageBuilder
|
||||||
raise
|
raise
|
||||||
rescue Koala::Facebook::ClientError => e
|
rescue Koala::Facebook::ClientError => e
|
||||||
# The exception occurs when we are trying fetch the deleted story or blocked story.
|
# The exception occurs when we are trying fetch the deleted story or blocked story.
|
||||||
|
@message.attachments.destroy_all
|
||||||
@message.update(content: I18n.t('conversations.messages.instagram_deleted_story_content'))
|
@message.update(content: I18n.t('conversations.messages.instagram_deleted_story_content'))
|
||||||
Rails.logger.error e
|
Rails.logger.error e
|
||||||
{}
|
{}
|
||||||
|
|
|
@ -54,19 +54,6 @@
|
||||||
size="16"
|
size="16"
|
||||||
/>
|
/>
|
||||||
</button>
|
</button>
|
||||||
<a
|
|
||||||
v-if="hasInstagramStory && (isIncoming || isOutgoing) && linkToStory"
|
|
||||||
:href="linkToStory"
|
|
||||||
target="_blank"
|
|
||||||
rel="noopener noreferrer nofollow"
|
|
||||||
>
|
|
||||||
<fluent-icon
|
|
||||||
v-tooltip.top-start="$t('CHAT_LIST.LINK_TO_STORY')"
|
|
||||||
icon="open"
|
|
||||||
class="action--icon cursor-pointer"
|
|
||||||
size="16"
|
|
||||||
/>
|
|
||||||
</a>
|
|
||||||
<a
|
<a
|
||||||
v-if="isATweet && (isOutgoing || isIncoming) && linkToTweet"
|
v-if="isATweet && (isOutgoing || isIncoming) && linkToTweet"
|
||||||
:href="linkToTweet"
|
:href="linkToTweet"
|
||||||
|
|
|
@ -70,12 +70,15 @@ class Attachment < ApplicationRecord
|
||||||
private
|
private
|
||||||
|
|
||||||
def file_metadata
|
def file_metadata
|
||||||
{
|
metadata = {
|
||||||
extension: extension,
|
extension: extension,
|
||||||
data_url: file_url,
|
data_url: file_url,
|
||||||
thumb_url: thumb_url,
|
thumb_url: thumb_url,
|
||||||
file_size: file.byte_size
|
file_size: file.byte_size
|
||||||
}
|
}
|
||||||
|
|
||||||
|
metadata[:data_url] = metadata[:thumb_url] = external_url if message.instagram_story_mention?
|
||||||
|
metadata
|
||||||
end
|
end
|
||||||
|
|
||||||
def location_metadata
|
def location_metadata
|
||||||
|
|
|
@ -63,4 +63,21 @@ class Channel::FacebookPage < ApplicationRecord
|
||||||
Rails.logger.debug { "Rescued: #{e.inspect}" }
|
Rails.logger.debug { "Rescued: #{e.inspect}" }
|
||||||
true
|
true
|
||||||
end
|
end
|
||||||
|
|
||||||
|
# TODO: We will be removing this code after instagram_manage_insights is implemented
|
||||||
|
def fetch_instagram_story_link(message)
|
||||||
|
k = Koala::Facebook::API.new(page_access_token)
|
||||||
|
result = k.get_object(message.source_id, fields: %w[story]) || {}
|
||||||
|
story_link = result['story']['mention']['link']
|
||||||
|
# If the story is expired then it raises the ClientError and if the story is deleted with valid story-id it responses with nil
|
||||||
|
delete_instagram_story(message) if story_link.blank?
|
||||||
|
story_link
|
||||||
|
rescue Koala::Facebook::ClientError => e
|
||||||
|
delete_instagram_story(message)
|
||||||
|
end
|
||||||
|
|
||||||
|
def delete_instagram_story(message)
|
||||||
|
message.update(content: I18n.t('conversations.messages.instagram_deleted_story_content'), content_attributes: {})
|
||||||
|
message.attachments.destroy_all
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -16,4 +16,8 @@ module MessageFilterHelpers
|
||||||
def email_reply_summarizable?
|
def email_reply_summarizable?
|
||||||
incoming? || outgoing? || input_csat?
|
incoming? || outgoing? || input_csat?
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def instagram_story_mention?
|
||||||
|
inbox.instagram? && try(:content_attributes)[:image_type] == 'story_mention'
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -78,6 +78,10 @@ class Inbox < ApplicationRecord
|
||||||
channel_type == 'Channel::FacebookPage'
|
channel_type == 'Channel::FacebookPage'
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def instagram?
|
||||||
|
facebook? && channel.instagram_id.present?
|
||||||
|
end
|
||||||
|
|
||||||
def web_widget?
|
def web_widget?
|
||||||
channel_type == 'Channel::WebWidget'
|
channel_type == 'Channel::WebWidget'
|
||||||
end
|
end
|
||||||
|
|
|
@ -107,10 +107,20 @@ class Message < ApplicationRecord
|
||||||
conversation: { assignee_id: conversation.assignee_id }
|
conversation: { assignee_id: conversation.assignee_id }
|
||||||
)
|
)
|
||||||
data.merge!(echo_id: echo_id) if echo_id.present?
|
data.merge!(echo_id: echo_id) if echo_id.present?
|
||||||
|
validate_instagram_story if instagram_story_mention?
|
||||||
data.merge!(attachments: attachments.map(&:push_event_data)) if attachments.present?
|
data.merge!(attachments: attachments.map(&:push_event_data)) if attachments.present?
|
||||||
merge_sender_attributes(data)
|
merge_sender_attributes(data)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
# TODO: We will be removing this code after instagram_manage_insights is implemented
|
||||||
|
# Better logic is to listen to webhook and remove stories proactively rather than trying
|
||||||
|
# a fetch every time a message is returned
|
||||||
|
def validate_instagram_story
|
||||||
|
inbox.channel.fetch_instagram_story_link(self)
|
||||||
|
# we want to reload the message in case the story has expired and data got removed
|
||||||
|
reload
|
||||||
|
end
|
||||||
|
|
||||||
def merge_sender_attributes(data)
|
def merge_sender_attributes(data)
|
||||||
data.merge!(sender: sender.push_event_data) if sender && !sender.is_a?(AgentBot)
|
data.merge!(sender: sender.push_event_data) if sender && !sender.is_a?(AgentBot)
|
||||||
data.merge!(sender: sender.push_event_data(inbox)) if sender.is_a?(AgentBot)
|
data.merge!(sender: sender.push_event_data(inbox)) if sender.is_a?(AgentBot)
|
||||||
|
|
|
@ -68,7 +68,7 @@ describe ::Messages::Instagram::MessageBuilder do
|
||||||
|
|
||||||
expect(contact.name).to eq('Jane Dae')
|
expect(contact.name).to eq('Jane Dae')
|
||||||
expect(message.content).to eq('This story is no longer available.')
|
expect(message.content).to eq('This story is no longer available.')
|
||||||
expect(message.attachments.count).to eq(1)
|
expect(message.attachments.count).to eq(0)
|
||||||
end
|
end
|
||||||
|
|
||||||
it 'does not create message for unsupported file type' do
|
it 'does not create message for unsupported file type' do
|
||||||
|
|
|
@ -6,5 +6,9 @@ FactoryBot.define do
|
||||||
user_access_token { SecureRandom.uuid }
|
user_access_token { SecureRandom.uuid }
|
||||||
page_id { SecureRandom.uuid }
|
page_id { SecureRandom.uuid }
|
||||||
account
|
account
|
||||||
|
|
||||||
|
before :create do |_channel|
|
||||||
|
WebMock::API.stub_request(:post, 'https://graph.facebook.com/v3.2/me/subscribed_apps')
|
||||||
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
|
@ -8,6 +8,18 @@ FactoryBot.define do
|
||||||
content_type { 'text' }
|
content_type { 'text' }
|
||||||
account { create(:account) }
|
account { create(:account) }
|
||||||
|
|
||||||
|
trait :instagram_story_mention do
|
||||||
|
content_attributes { { image_type: 'story_mention' } }
|
||||||
|
after(:build) do |message|
|
||||||
|
unless message.inbox.instagram?
|
||||||
|
message.inbox = create(:inbox, account: message.account,
|
||||||
|
channel: create(:channel_instagram_fb_page, account: message.account, instagram_id: 'instagram-123'))
|
||||||
|
end
|
||||||
|
attachment = message.attachments.new(account_id: message.account_id, file_type: :image, external_url: 'https://www.example.com/test.jpeg')
|
||||||
|
attachment.file.attach(io: File.open(Rails.root.join('spec/assets/avatar.png')), filename: 'avatar.png', content_type: 'image/png')
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
after(:build) do |message|
|
after(:build) do |message|
|
||||||
message.sender ||= message.outgoing? ? create(:user, account: message.account) : create(:contact, account: message.account)
|
message.sender ||= message.outgoing? ? create(:user, account: message.account) : create(:contact, account: message.account)
|
||||||
message.inbox ||= message.conversation&.inbox || create(:inbox, account: message.account)
|
message.inbox ||= message.conversation&.inbox || create(:inbox, account: message.account)
|
||||||
|
|
|
@ -113,6 +113,9 @@ describe Webhooks::InstagramEventsJob do
|
||||||
|
|
||||||
expect(instagram_inbox.messages.count).to be 1
|
expect(instagram_inbox.messages.count).to be 1
|
||||||
expect(instagram_inbox.messages.last.attachments.count).to be 1
|
expect(instagram_inbox.messages.last.attachments.count).to be 1
|
||||||
|
|
||||||
|
attachment = instagram_inbox.messages.last.attachments.last
|
||||||
|
expect(attachment.push_event_data[:data_url]).to eq(attachment.external_url)
|
||||||
end
|
end
|
||||||
|
|
||||||
it 'creates does not create contact or messages' do
|
it 'creates does not create contact or messages' do
|
||||||
|
|
|
@ -9,4 +9,23 @@ RSpec.describe Attachment, type: :model do
|
||||||
expect(attachment.download_url).not_to be_nil
|
expect(attachment.download_url).not_to be_nil
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
describe 'push_event_data for instagram story mentions' do
|
||||||
|
let(:instagram_message) { create(:message, :instagram_story_mention) }
|
||||||
|
|
||||||
|
before do
|
||||||
|
# stubbing the request to facebook api during the message creation
|
||||||
|
stub_request(:get, %r{https://graph.facebook.com/.*}).to_return(status: 200, body: {
|
||||||
|
story: { mention: { link: 'http://graph.facebook.com/test-story-mention', id: '17920786367196703' } },
|
||||||
|
from: { username: 'Sender-id-1', id: 'Sender-id-1' },
|
||||||
|
id: 'instagram-message-id-1234'
|
||||||
|
}.to_json, headers: {})
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'returns external url as data and thumb urls' do
|
||||||
|
external_url = instagram_message.attachments.first.external_url
|
||||||
|
expect(instagram_message.attachments.first.push_event_data[:data_url]).to eq external_url
|
||||||
|
expect(instagram_message.attachments.first.push_event_data[:thumb_url]).to eq external_url
|
||||||
|
end
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -180,4 +180,34 @@ RSpec.describe Message, type: :model do
|
||||||
expect(message.email_notifiable_message?).to be true
|
expect(message.email_notifiable_message?).to be true
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
context 'when facebook channel with unavailable story link' do
|
||||||
|
let(:instagram_message) { create(:message, :instagram_story_mention) }
|
||||||
|
|
||||||
|
before do
|
||||||
|
# stubbing the request to facebook api during the message creation
|
||||||
|
stub_request(:get, %r{https://graph.facebook.com/.*}).to_return(status: 200, body: {
|
||||||
|
story: { mention: { link: 'http://graph.facebook.com/test-story-mention', id: '17920786367196703' } },
|
||||||
|
from: { username: 'Sender-id-1', id: 'Sender-id-1' },
|
||||||
|
id: 'instagram-message-id-1234'
|
||||||
|
}.to_json, headers: {})
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'deletes the attachment for deleted stories' do
|
||||||
|
expect(instagram_message.attachments.count).to eq 1
|
||||||
|
stub_request(:get, %r{https://graph.facebook.com/.*}).to_return(status: 404)
|
||||||
|
instagram_message.push_event_data
|
||||||
|
expect(instagram_message.reload.attachments.count).to eq 0
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'deletes the attachment for expired stories' do
|
||||||
|
expect(instagram_message.attachments.count).to eq 1
|
||||||
|
# for expired stories, the link will be empty
|
||||||
|
stub_request(:get, %r{https://graph.facebook.com/.*}).to_return(status: 200, body: {
|
||||||
|
story: { mention: { link: '', id: '17920786367196703' } }
|
||||||
|
}.to_json, headers: {})
|
||||||
|
instagram_message.push_event_data
|
||||||
|
expect(instagram_message.reload.attachments.count).to eq 0
|
||||||
|
end
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
Loading…
Reference in a new issue