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.

![CleanShot 2025-03-10 at 21 32
28@2x](https://github.com/user-attachments/assets/1b019001-8d16-4e59-aca2-ced81e98f538)


---

## 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:
Muhsin Keloth
2025-04-08 10:47:41 +05:30
committed by GitHub
parent ae0b68147e
commit d827e66453
40 changed files with 1868 additions and 831 deletions

View File

@@ -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"
# }
# }
# ]
# }
# ]