From c6a38e2fc61a05af85cc966fd35fcbcd12883295 Mon Sep 17 00:00:00 2001 From: Muhsin Keloth Date: Fri, 12 Jun 2026 09:39:26 +0400 Subject: [PATCH 1/2] fix: Keep Instagram scopes out of new Messenger OAuth flows (#14695) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Removes Instagram permissions from the Facebook Messenger inbox setup flow to avoid blocking Meta App Review for Messenger-only apps. Meta rejects or stalls Messenger-only app reviews when the OAuth flow requests unrelated Instagram permissions such as `instagram_basic` and `instagram_manage_messages`. This blocks approval for Messenger permissions like `human_agent`, even when the user only wants to configure a Facebook Messenger inbox. New Facebook Messenger inbox setup now requests only the Page and Messenger permissions required for Messenger. Existing Facebook Messenger inboxes that already have Instagram details continue to request Instagram permissions during reauthorization, preserving support for the legacy combined Facebook/Instagram inbox flow. Going forward, new Instagram setups should use the dedicated Instagram channel inbox instead of being bundled into the Messenger setup flow. Closes #13860 ## How to test 1. Go to Settings → Inboxes → Add Inbox → Facebook Messenger. 2. Click Login with Facebook. 3. Verify the OAuth scope does not include `instagram_basic` or `instagram_manage_messages`. 4. Reauthorize an existing Facebook Messenger inbox with `instagram_id` present. 5. Verify the reauth scope still includes `instagram_basic` and `instagram_manage_messages`. --------- Co-authored-by: Muhsin <12408980+muhsin-k@users.noreply.github.com> --- .../spec/useFacebookPageConnect.spec.js | 3 ++- .../composables/useFacebookPageConnect.js | 8 ++----- .../dashboard/helper/facebookScopes.js | 22 +++++++++++++++++++ .../settings/inbox/facebook/Reauthorize.vue | 9 ++++++-- 4 files changed, 33 insertions(+), 9 deletions(-) create mode 100644 app/javascript/dashboard/helper/facebookScopes.js 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', } ); From c041fde3a2d745b9b69d73df8726c687cf351164 Mon Sep 17 00:00:00 2001 From: Rorrick <34399621+Rorrick@users.noreply.github.com> Date: Fri, 12 Jun 2026 08:21:42 +0200 Subject: [PATCH 2/2] fix: Preserve original filenames for WhatsApp cloud attachments (#14168) ## Summary Preserves the original filename provided in the WhatsApp Cloud attachment payload when downloading media files. ## Problem Files containing accented or special characters could be saved with malformed names or incorrect extensions due to relying on remote download metadata. ## Changes Made - Uses attachment_payload[:filename] when present - Creates a tempfile using the original filename and extension - Preserves content type metadata - Falls back to existing behavior when no filename is provided ## Notes This should improve attachment downloads for filenames containing accented characters. Related to #10973 --------- Co-authored-by: Rorrick Smith Co-authored-by: Muhsin Keloth Co-authored-by: Muhsin <12408980+muhsin-k@users.noreply.github.com> Co-authored-by: Claude Opus 4.8 (1M context) Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- ...incoming_message_whatsapp_cloud_service.rb | 11 +++++- ...ing_message_whatsapp_cloud_service_spec.rb | 35 +++++++++++++++++++ 2 files changed, 45 insertions(+), 1 deletion(-) 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 {