diff --git a/app/javascript/dashboard/composables/spec/useFacebookPageConnect.spec.js b/app/javascript/dashboard/composables/spec/useFacebookPageConnect.spec.js index 5d9edb993..4ec17e753 100644 --- a/app/javascript/dashboard/composables/spec/useFacebookPageConnect.spec.js +++ b/app/javascript/dashboard/composables/spec/useFacebookPageConnect.spec.js @@ -70,7 +70,8 @@ describe('useFacebookPageConnect', () => { ACCOUNT_ID ); expect(window.FB.login).toHaveBeenCalledWith(expect.any(Function), { - scope: expect.stringContaining('pages_show_list'), + scope: + 'pages_manage_metadata,business_management,pages_messaging,pages_show_list,pages_read_engagement', }); }); diff --git a/app/javascript/dashboard/composables/useFacebookPageConnect.js b/app/javascript/dashboard/composables/useFacebookPageConnect.js index 46a177d58..dbe06dd0d 100644 --- a/app/javascript/dashboard/composables/useFacebookPageConnect.js +++ b/app/javascript/dashboard/composables/useFacebookPageConnect.js @@ -1,13 +1,9 @@ import { ref } from 'vue'; import { useMapGetter } from 'dashboard/composables/store'; import ChannelApi from 'dashboard/api/channels'; +import { buildFacebookLoginScopes } from 'dashboard/helper/facebookScopes'; import { setupFacebookSdk } from 'dashboard/routes/dashboard/settings/inbox/channels/whatsapp/utils'; -// Page-management + messaging scopes required to list pages and create a -// Channel::FacebookPage inbox (mirrors the standalone settings flow). -const FB_PAGE_SCOPES = - 'pages_manage_metadata,business_management,pages_messaging,instagram_basic,pages_show_list,pages_read_engagement,instagram_manage_messages'; - // Headless half of the Facebook Page connect flow: load the Meta SDK, run // FB.login for page scopes, and fetch the user's pages. The caller owns the // page-picker UI and the channel creation, because choosing a page is an @@ -51,7 +47,7 @@ export function useFacebookPageConnect() { : null ); }, - { scope: FB_PAGE_SCOPES } + { scope: buildFacebookLoginScopes() } ); }); diff --git a/app/javascript/dashboard/helper/facebookScopes.js b/app/javascript/dashboard/helper/facebookScopes.js new file mode 100644 index 000000000..755b3f465 --- /dev/null +++ b/app/javascript/dashboard/helper/facebookScopes.js @@ -0,0 +1,22 @@ +export const FACEBOOK_PAGE_SCOPES = [ + 'pages_manage_metadata', + 'business_management', + 'pages_messaging', + 'pages_show_list', + 'pages_read_engagement', +]; + +export const INSTAGRAM_SCOPES = [ + 'instagram_basic', + 'instagram_manage_messages', +]; + +export const buildFacebookLoginScopes = ({ + includeInstagramScopes = false, +} = {}) => { + const scopes = [...FACEBOOK_PAGE_SCOPES]; + if (includeInstagramScopes) { + scopes.push(...INSTAGRAM_SCOPES); + } + return scopes.join(','); +}; diff --git a/app/javascript/dashboard/routes/dashboard/settings/inbox/facebook/Reauthorize.vue b/app/javascript/dashboard/routes/dashboard/settings/inbox/facebook/Reauthorize.vue index dd566f131..cf25177ea 100644 --- a/app/javascript/dashboard/routes/dashboard/settings/inbox/facebook/Reauthorize.vue +++ b/app/javascript/dashboard/routes/dashboard/settings/inbox/facebook/Reauthorize.vue @@ -4,6 +4,7 @@ import InboxReconnectionRequired from '../components/InboxReconnectionRequired.v import { useAlert } from 'dashboard/composables'; import { loadScript } from 'dashboard/helper/DOMHelpers'; +import { buildFacebookLoginScopes } from 'dashboard/helper/facebookScopes'; import * as Sentry from '@sentry/vue'; export default { @@ -20,6 +21,11 @@ export default { inboxId() { return this.inbox.id; }, + facebookLoginScopes() { + return buildFacebookLoginScopes({ + includeInstagramScopes: !!this.inbox.instagram_id, + }); + }, }, mounted() { window.fbAsyncInit = this.runFBInit; @@ -77,8 +83,7 @@ export default { } }, { - scope: - 'pages_manage_metadata,business_management,pages_messaging,instagram_basic,pages_show_list,pages_read_engagement,instagram_manage_messages', + scope: this.facebookLoginScopes, auth_type: 'reauthorize', } ); diff --git a/app/services/whatsapp/incoming_message_whatsapp_cloud_service.rb b/app/services/whatsapp/incoming_message_whatsapp_cloud_service.rb index 164c3ac12..f836a63c3 100644 --- a/app/services/whatsapp/incoming_message_whatsapp_cloud_service.rb +++ b/app/services/whatsapp/incoming_message_whatsapp_cloud_service.rb @@ -13,8 +13,17 @@ class Whatsapp::IncomingMessageWhatsappCloudService < Whatsapp::IncomingMessageB inbox.channel.media_url(attachment_payload[:id]), headers: inbox.channel.api_headers ) + # This url response will be failure if the access token has expired. inbox.channel.authorization_error! if url_response.unauthorized? - Down.download(url_response.parsed_response['url'], headers: inbox.channel.api_headers) if url_response.success? + + return unless url_response.success? + + downloaded_file = Down.download(url_response.parsed_response['url'], headers: inbox.channel.api_headers) + # WhatsApp Cloud sends the original filename in the payload; preserve it so accented + # names keep their correct extension instead of relying on the mangled remote metadata. + filename = attachment_payload[:filename] + downloaded_file.define_singleton_method(:original_filename) { filename } if filename.present? + downloaded_file end end diff --git a/spec/services/whatsapp/incoming_message_whatsapp_cloud_service_spec.rb b/spec/services/whatsapp/incoming_message_whatsapp_cloud_service_spec.rb index 70c29c092..1bbd6d8f4 100644 --- a/spec/services/whatsapp/incoming_message_whatsapp_cloud_service_spec.rb +++ b/spec/services/whatsapp/incoming_message_whatsapp_cloud_service_spec.rb @@ -59,6 +59,41 @@ describe Whatsapp::IncomingMessageWhatsappCloudService do end end + context 'when document attachment includes an accented filename' do + let(:document_params) do + { + phone_number: whatsapp_channel.phone_number, + object: 'whatsapp_business_account', + entry: [{ + changes: [{ + value: { + contacts: [{ profile: { name: 'Sojan Jose' }, wa_id: '2423423243' }], + messages: [{ + from: '2423423243', + document: { + id: 'b1c68f38-8734-4ad3-b4a1-ef0c10d683', + mime_type: 'application/pdf', + filename: 'Currículum café.pdf', + caption: 'My résumé' + }, + timestamp: '1664799904', type: 'document' + }] + } + }] + }] + }.with_indifferent_access + end + + it 'preserves the original filename from the payload' do + stub_media_url_request + stub_sample_png_request + described_class.new(inbox: whatsapp_channel.inbox, params: document_params).perform + + attachment = whatsapp_channel.inbox.messages.first.attachments.first + expect(attachment.file.filename.to_s).to eq('Currículum café.pdf') + end + end + context 'when invalid attachment message params' do let(:error_params) do {