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:
Tejaswini Chile 2022-12-08 18:25:24 +05:30 committed by GitHub
parent 431e2931c4
commit 7dc790a7e0
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
13 changed files with 111 additions and 15 deletions

View file

@ -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
{} {}

View file

@ -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"

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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)

View file

@ -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

View file

@ -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

View file

@ -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)

View file

@ -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

View file

@ -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

View file

@ -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