feat: Instagram Inbox using Instagram Business Login (#11054)
This PR introduces basic minimum version of **Instagram Business Login**, making Instagram inbox setup more straightforward by removing the Facebook Page dependency. This update enhances user experience and aligns with Meta’s recommended best practices. Fixes https://linear.app/chatwoot/issue/CW-3728/instagram-login-how-to-implement-the-changes ## Why Introduce Instagram as a Separate Inbox? Currently, our Instagram integration requires linking an Instagram account to a Facebook Page, making setup complex. To simplify this process, Instagram now offers **Instagram Business Login**, which allows users to authenticate directly with their Instagram credentials. The **Instagram API with Instagram Login** enables businesses and creators to send and receive messages without needing a Facebook Page connection. While an Instagram Business or Creator account is still required, this approach provides a more straightforward integration process. | **Existing Approach (Facebook Login for Business)** | **New Approach (Instagram Business Login)** | | --- | --- | | Requires linking Instagram to a Facebook Page | No Facebook Page required | | Users log in via Facebook credentials | Users log in via Instagram credentials | | Configuration is more complex | Simpler setup | Meta recommends using **Instagram Business Login** as the preferred authentication method due to its easier configuration and improved developer experience. --- ## Implementation Plan The core messaging functionality is already in place, but the transition to **Instagram Business Login** requires adjustments. ### Changes & Considerations - **API Adjustments**: The Instagram API uses `graph.instagram`, whereas Koala (our existing library) interacts with `graph.facebook`. We may need to modify API calls accordingly. - **Three Main Modules**: 1. **Instagram Business Login** – Handle authentication flow. 2. **Permissions & Features** – Ensure necessary API scopes are granted. 3. **Webhooks** – Enable real-time message retrieval.  --- ## Instagram Login Flow 1. User clicks **"Create Inbox"** for Instagram. 2. App redirects to the [Instagram Authorization URL](https://developers.facebook.com/docs/instagram-platform/instagram-api-with-instagram-login/business-login#embed-the-business-login-url). 3. After authentication, Instagram returns an authorization code. 5. The app exchanges the code for a **long-lived token** (valid for 60 days). 6. Tokens are refreshed periodically to maintain access. 7. Once completed, the app creates an inbox and redirects to the Chatwoot dashboard. --- ## How to Test the Instagram Inbox 1. Create a new app on [Meta's Developer Portal](https://developers.facebook.com/apps/). 2. Select **Business** as the app type and configure it. 3. Add the Instagram product and connect a business account. 4. Copy Instagram app ID and Instagram app secret 5. Add the Instagram app ID and Instagram app secret to your app config via `{Chatwoot installation url}/super_admin/app_config?config=instagram` 6. Configure Webhooks: - Callback URL: `{your_chatwoot_url}/webhooks/instagram` - Verify Token: `INSTAGRAM_VERIFY_TOKEN` - Subscribe to `messages`, `messaging_seen`, and `message_reactions` events. 7. Set up **Instagram Business Login**: - Redirect URL: `{your_chatwoot_url}/instagram/callback` 8. Test inbox creation via the Chatwoot dashboard. ## Troubleshooting & Common Errors ### Insufficient Developer Role Error - Ensure the Instagram user is added as a developer: - **Meta Dashboard → App Roles → Roles → Add People → Enter Instagram ID** ### API Access Deactivated - Ensure the **Privacy Policy URL** is valid and correctly set. ### Invalid request: Request parameters are invalid: Invalid redirect_uri - Please configure the Frontend URL. The Frontend URL does not match the authorization URL. --- ## To-Do List - [x] Basic integration setup completed. - [x] Enable sending messages via [Messaging API](https://developers.facebook.com/docs/instagram-platform/instagram-api-with-instagram-login/messaging-api). - [x] Implement automatic webhook subscriptions on inbox creation. - [x] Handle **canceled authorization errors**. - [x] Handle all the errors https://developers.facebook.com/docs/instagram-platform/instagram-graph-api/reference/error-codes - [x] Dynamically fetch **account IDs** instead of hardcoding them. - [x] Prevent duplicate Instagram channel creation for the same account. - [x] Use **Global Config** instead of environment variables. - [x] Explore **Human Agent feature** for message handling. - [x] Write and refine **test cases** for all scenarios. - [x] Implement **token refresh mechanism** (tokens expire after 60 days). Fixes https://github.com/chatwoot/chatwoot/issues/10440 --------- Co-authored-by: Sivin Varghese <64252451+iamsivin@users.noreply.github.com> Co-authored-by: Copilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com> Co-authored-by: Shivam Mishra <scm.mymail@gmail.com>
This commit is contained in:
@@ -2,10 +2,6 @@ class Webhooks::InstagramEventsJob < MutexApplicationJob
|
||||
queue_as :default
|
||||
retry_on LockAcquisitionError, wait: 1.second, attempts: 8
|
||||
|
||||
include HTTParty
|
||||
|
||||
base_uri 'https://graph.facebook.com/v11.0/me'
|
||||
|
||||
# @return [Array] We will support further events like reaction or seen in future
|
||||
SUPPORTED_EVENTS = [:message, :read].freeze
|
||||
|
||||
@@ -18,18 +14,46 @@ class Webhooks::InstagramEventsJob < MutexApplicationJob
|
||||
end
|
||||
end
|
||||
|
||||
# @see https://developers.facebook.com/docs/messenger-platform/instagram/features/webhook
|
||||
# https://developers.facebook.com/docs/messenger-platform/instagram/features/webhook
|
||||
def process_entries(entries)
|
||||
entries.each do |entry|
|
||||
entry = entry.with_indifferent_access
|
||||
messages(entry).each do |messaging|
|
||||
send(@event_name, messaging) if event_name(messaging)
|
||||
end
|
||||
process_single_entry(entry.with_indifferent_access)
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def process_single_entry(entry)
|
||||
process_messages(entry)
|
||||
end
|
||||
|
||||
def process_messages(entry)
|
||||
messages(entry).each do |messaging|
|
||||
Rails.logger.info("Instagram Events Job Messaging: #{messaging}")
|
||||
|
||||
instagram_id = instagram_id(messaging)
|
||||
channel = find_channel(instagram_id)
|
||||
|
||||
next if channel.blank?
|
||||
|
||||
if (event_name = event_name(messaging))
|
||||
send(event_name, messaging, channel)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def agent_message_via_echo?(messaging)
|
||||
messaging[:message].present? && messaging[:message][:is_echo].present?
|
||||
end
|
||||
|
||||
def instagram_id(messaging)
|
||||
if agent_message_via_echo?(messaging)
|
||||
messaging[:sender][:id]
|
||||
else
|
||||
messaging[:recipient][:id]
|
||||
end
|
||||
end
|
||||
|
||||
def ig_account_id
|
||||
@entries&.first&.dig(:id)
|
||||
end
|
||||
@@ -38,19 +62,116 @@ class Webhooks::InstagramEventsJob < MutexApplicationJob
|
||||
@entries&.dig(0, :messaging, 0, :sender, :id)
|
||||
end
|
||||
|
||||
def find_channel(instagram_id)
|
||||
# There will be chances for the instagram account to be connected to a facebook page,
|
||||
# so we need to check for both instagram and facebook page channels
|
||||
# priority is for instagram channel which created via instagram login
|
||||
channel = Channel::Instagram.find_by(instagram_id: instagram_id)
|
||||
# If not found, fallback to the facebook page channel
|
||||
channel ||= Channel::FacebookPage.find_by(instagram_id: instagram_id)
|
||||
|
||||
channel
|
||||
end
|
||||
|
||||
def event_name(messaging)
|
||||
@event_name ||= SUPPORTED_EVENTS.find { |key| messaging.key?(key) }
|
||||
end
|
||||
|
||||
def message(messaging)
|
||||
::Instagram::MessageText.new(messaging).perform
|
||||
def message(messaging, channel)
|
||||
if channel.is_a?(Channel::Instagram)
|
||||
::Instagram::MessageText.new(messaging, channel).perform
|
||||
else
|
||||
::Instagram::Messenger::MessageText.new(messaging, channel).perform
|
||||
end
|
||||
end
|
||||
|
||||
def read(messaging)
|
||||
::Instagram::ReadStatusService.new(params: messaging).perform
|
||||
def read(messaging, channel)
|
||||
# Use a single service to handle read status for both channel types since the params are same
|
||||
::Instagram::ReadStatusService.new(params: messaging, channel: channel).perform
|
||||
end
|
||||
|
||||
def messages(entry)
|
||||
(entry[:messaging].presence || entry[:standby] || [])
|
||||
end
|
||||
end
|
||||
|
||||
# Actual response from Instagram webhook (both via Facebook page and Instagram direct)
|
||||
# [
|
||||
# {
|
||||
# "time": <timestamp>,
|
||||
# "id": <INSTAGRAM_USER_ID>,
|
||||
# "messaging": [
|
||||
# {
|
||||
# "sender": {
|
||||
# "id": <INSTAGRAM_USER_ID>
|
||||
# },
|
||||
# "recipient": {
|
||||
# "id": <INSTAGRAM_USER_ID>
|
||||
# },
|
||||
# "timestamp": <timestamp>,
|
||||
# "message": {
|
||||
# "mid": <MESSAGE_ID>,
|
||||
# "text": <MESSAGE_TEXT>
|
||||
# }
|
||||
# }
|
||||
# ]
|
||||
# }
|
||||
# ]
|
||||
|
||||
# Instagram's webhook via Instagram direct testing quirk: Test payloads vs Actual payloads
|
||||
# When testing in Facebook's developer dashboard, you'll get a Page-style
|
||||
# payload with a "changes" object. But don't be fooled! Real Instagram DMs
|
||||
# arrive in the familiar Messenger format with a "messaging" array.
|
||||
# This apparent inconsistency is actually by design - Instagram's webhooks
|
||||
# use different formats for testing vs production to maintain compatibility
|
||||
# with both Instagram Direct and Facebook Page integrations.
|
||||
# See: https://developers.facebook.com/docs/instagram-platform/webhooks#event-notifications
|
||||
|
||||
# Test response from via Instagram direct
|
||||
# [
|
||||
# {
|
||||
# "id": "0",
|
||||
# "time": <timestamp>,
|
||||
# "changes": [
|
||||
# {
|
||||
# "field": "messages",
|
||||
# "value": {
|
||||
# "sender": {
|
||||
# "id": "12334"
|
||||
# },
|
||||
# "recipient": {
|
||||
# "id": "23245"
|
||||
# },
|
||||
# "timestamp": "1527459824",
|
||||
# "message": {
|
||||
# "mid": "random_mid",
|
||||
# "text": "random_text"
|
||||
# }
|
||||
# }
|
||||
# }
|
||||
# ]
|
||||
# }
|
||||
# ]
|
||||
|
||||
# Test response via Facebook page
|
||||
# [
|
||||
# {
|
||||
# "time": <timestamp>,,
|
||||
# "id": "0",
|
||||
# "messaging": [
|
||||
# {
|
||||
# "sender": {
|
||||
# "id": "12334"
|
||||
# },
|
||||
# "recipient": {
|
||||
# "id": "23245"
|
||||
# },
|
||||
# "timestamp": <timestamp>,
|
||||
# "message": {
|
||||
# "mid": "random_mid",
|
||||
# "text": "random_text"
|
||||
# }
|
||||
# }
|
||||
# ]
|
||||
# }
|
||||
# ]
|
||||
|
||||
Reference in New Issue
Block a user