From 7dc790a7e0bf05d37f6d42675af442e408bb2715 Mon Sep 17 00:00:00 2001 From: Tejaswini Chile Date: Thu, 8 Dec 2022 18:25:24 +0530 Subject: [PATCH] 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 --- .../messages/messenger/message_builder.rb | 3 ++ .../widgets/conversation/bubble/Actions.vue | 13 -------- app/models/attachment.rb | 5 +++- app/models/channel/facebook_page.rb | 17 +++++++++++ app/models/concerns/message_filter_helpers.rb | 4 +++ app/models/inbox.rb | 4 +++ app/models/message.rb | 10 +++++++ .../instagram/message_builder_spec.rb | 2 +- ...atgram_channel.rb => instagram_channel.rb} | 4 +++ spec/factories/messages.rb | 12 ++++++++ .../webhooks/instagram_events_job_spec.rb | 3 ++ spec/models/attachment_spec.rb | 19 ++++++++++++ spec/models/message_spec.rb | 30 +++++++++++++++++++ 13 files changed, 111 insertions(+), 15 deletions(-) rename spec/factories/channel/{insatgram_channel.rb => instagram_channel.rb} (66%) diff --git a/app/builders/messages/messenger/message_builder.rb b/app/builders/messages/messenger/message_builder.rb index d3b5bf6b9..0739829aa 100644 --- a/app/builders/messages/messenger/message_builder.rb +++ b/app/builders/messages/messenger/message_builder.rb @@ -46,6 +46,7 @@ class Messages::Messenger::MessageBuilder end def update_attachment_file_type(attachment) + return if @message.reload.attachments.blank? return unless attachment.file_type == 'share' || attachment.file_type == 'story_mention' attachment.file_type = file_type(attachment.file&.content_type) @@ -62,6 +63,7 @@ class Messages::Messenger::MessageBuilder story_sender = result['from']['username'] message.content_attributes[:story_sender] = story_sender 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.save! end @@ -74,6 +76,7 @@ class Messages::Messenger::MessageBuilder raise rescue Koala::Facebook::ClientError => e # 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')) Rails.logger.error e {} diff --git a/app/javascript/dashboard/components/widgets/conversation/bubble/Actions.vue b/app/javascript/dashboard/components/widgets/conversation/bubble/Actions.vue index 67b7e6a05..e2bc6d0dc 100644 --- a/app/javascript/dashboard/components/widgets/conversation/bubble/Actions.vue +++ b/app/javascript/dashboard/components/widgets/conversation/bubble/Actions.vue @@ -54,19 +54,6 @@ size="16" /> - - - 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 diff --git a/app/models/concerns/message_filter_helpers.rb b/app/models/concerns/message_filter_helpers.rb index 08b0f5660..55d04a5dc 100644 --- a/app/models/concerns/message_filter_helpers.rb +++ b/app/models/concerns/message_filter_helpers.rb @@ -16,4 +16,8 @@ module MessageFilterHelpers def email_reply_summarizable? incoming? || outgoing? || input_csat? end + + def instagram_story_mention? + inbox.instagram? && try(:content_attributes)[:image_type] == 'story_mention' + end end diff --git a/app/models/inbox.rb b/app/models/inbox.rb index 98419e9ab..f13fcf5f6 100644 --- a/app/models/inbox.rb +++ b/app/models/inbox.rb @@ -78,6 +78,10 @@ class Inbox < ApplicationRecord channel_type == 'Channel::FacebookPage' end + def instagram? + facebook? && channel.instagram_id.present? + end + def web_widget? channel_type == 'Channel::WebWidget' end diff --git a/app/models/message.rb b/app/models/message.rb index fbb28bb45..2c8827b12 100644 --- a/app/models/message.rb +++ b/app/models/message.rb @@ -107,10 +107,20 @@ class Message < ApplicationRecord conversation: { assignee_id: conversation.assignee_id } ) 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? merge_sender_attributes(data) 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) 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) diff --git a/spec/builders/messages/instagram/message_builder_spec.rb b/spec/builders/messages/instagram/message_builder_spec.rb index b2e073fbd..2220c3295 100644 --- a/spec/builders/messages/instagram/message_builder_spec.rb +++ b/spec/builders/messages/instagram/message_builder_spec.rb @@ -68,7 +68,7 @@ describe ::Messages::Instagram::MessageBuilder do expect(contact.name).to eq('Jane Dae') 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 it 'does not create message for unsupported file type' do diff --git a/spec/factories/channel/insatgram_channel.rb b/spec/factories/channel/instagram_channel.rb similarity index 66% rename from spec/factories/channel/insatgram_channel.rb rename to spec/factories/channel/instagram_channel.rb index e2192d683..ab1e0aa60 100644 --- a/spec/factories/channel/insatgram_channel.rb +++ b/spec/factories/channel/instagram_channel.rb @@ -6,5 +6,9 @@ FactoryBot.define do user_access_token { SecureRandom.uuid } page_id { SecureRandom.uuid } account + + before :create do |_channel| + WebMock::API.stub_request(:post, 'https://graph.facebook.com/v3.2/me/subscribed_apps') + end end end diff --git a/spec/factories/messages.rb b/spec/factories/messages.rb index 9a0d3be47..e14a8000d 100644 --- a/spec/factories/messages.rb +++ b/spec/factories/messages.rb @@ -8,6 +8,18 @@ FactoryBot.define do content_type { 'text' } 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| message.sender ||= message.outgoing? ? create(:user, account: message.account) : create(:contact, account: message.account) message.inbox ||= message.conversation&.inbox || create(:inbox, account: message.account) diff --git a/spec/jobs/webhooks/instagram_events_job_spec.rb b/spec/jobs/webhooks/instagram_events_job_spec.rb index 89951e33a..5cdeeb33d 100644 --- a/spec/jobs/webhooks/instagram_events_job_spec.rb +++ b/spec/jobs/webhooks/instagram_events_job_spec.rb @@ -113,6 +113,9 @@ describe Webhooks::InstagramEventsJob do expect(instagram_inbox.messages.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 it 'creates does not create contact or messages' do diff --git a/spec/models/attachment_spec.rb b/spec/models/attachment_spec.rb index 14e35fea9..9201a2540 100644 --- a/spec/models/attachment_spec.rb +++ b/spec/models/attachment_spec.rb @@ -9,4 +9,23 @@ RSpec.describe Attachment, type: :model do expect(attachment.download_url).not_to be_nil 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 diff --git a/spec/models/message_spec.rb b/spec/models/message_spec.rb index 23c74a558..de237415f 100644 --- a/spec/models/message_spec.rb +++ b/spec/models/message_spec.rb @@ -180,4 +180,34 @@ RSpec.describe Message, type: :model do expect(message.email_notifiable_message?).to be true 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