## Linear ticket
https://linear.app/chatwoot/issue/CW-6839/blocked-contact-can-still-send-messages-to-whatsapp-inbox
## Description
Drop WhatsApp incoming messages from blocked contacts
## Type of change
- [ ] Bug fix (non-breaking change which fixes an issue)
## How Has This Been Tested?
- Incoming messages for blocked contacts
## Checklist:
- [ ] My code follows the style guidelines of this project
- [ ] I have performed a self-review of my code
- [ ] I have commented on my code, particularly in hard-to-understand
areas
- [ ] I have made corresponding changes to the documentation
- [ ] My changes generate no new warnings
- [ ] I have added tests that prove my fix is effective or that my
feature works
- [ ] New and existing unit tests pass locally with my changes
- [ ] Any dependent changes have been merged and published in downstream
modules
---------
Co-authored-by: Muhsin Keloth <muhsinkeramam@gmail.com>
* fix(whatsapp): preserve green color on chat list typing indicator
The messagePreviewClass computed includes text-n-slate-11/12, which
overrode text-green-500 in the compiled Tailwind order. Split padding
into a dedicated computed and apply only it on the typing preview.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix(whatsapp): clear contact typing indicator when message is received
Dispatch CONVERSATION_TYPING_OFF after a new incoming message is
persisted from baileys messages.upsert, so the dashboard clears the
typing/recording indicator without waiting for a paused/unavailable
presence event.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix(conversations): dispatch messages.read event when unread messages exist
The throttling introduced in upstream #13355 returned early for the
"has unread" branches, skipping dispatch_messages_read_event. That
meant the MESSAGES_READ event only fired when there were no unread
messages, so ChannelListener never called channel.read_messages on
the baileys provider when an agent actually read a conversation.
Consolidate the unread/throttle guard so the dispatch runs in all
paths where update_last_seen_on_conversation runs.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Rename consolidate_contact to consolidate_presence_contact in
PresenceUpdate to avoid overriding the 3-arg version from
GroupContactMessageHandler when both modules are mixed into
IncomingMessageBaileysService.
Fix CSAT spec side effects where conversation callbacks triggered
ActivityMessageJob unexpectedly during test setup.
* feat(whatsapp): show contact typing and recording indicators via baileys presence
Subscribe to WhatsApp presence updates via the baileys-api provider to
display real-time typing and recording indicators in the dashboard.
- Handle presence.update webhook events (composing, recording, paused,
available) and broadcast via ActionCable
- Add conversation.recording event to ActionCable, webhook, and channel
listeners for parity with typing_on/typing_off
- Show "typing..." / "recording..." in green text on the chat list,
replacing the message preview
- Show "X is typing" / "X is recording audio" in the conversation view
- Add presence_subscribe provider config option (default off) to gate
all subscription calls to the baileys-api
- Subscribe to presence on conversation open and periodically (1 min)
for the top 10 chat list conversations
- Consolidate contact LID from presence.update jidAlt payload
- Prevent echo-back of contact typing events to the channel
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix: address review feedback
- Filter chat list typing indicator to contact-only events
- Add dedupe to presence subscribe bulk calls
- Use strong parameters for conversation_ids
- Remove redundant YAML quotes in swagger webhook enum
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix: address review feedback
- Extract phone from data[:id] when JID is @s.whatsapp.net (fallback
when jidAlt is absent)
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix: address review feedback
- Filter recording users in getTypingUsersText to show correct names
- Add 10s timeout to presence_subscribe HTTP request
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix: scope typing timer per user instead of per conversation
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix: make presence subscribe best-effort with rescue per channel
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix: address review feedback
- Add messagePreviewClass to typing preview for consistent padding
- Fix specs to use WebMock assertions instead of instance spying
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix(whatsapp): add idempotent message sending to prevent duplicates on timeout retry
When sending media messages via Baileys, Net::ReadTimeout causes Sidekiq to
retry the job, potentially sending the same message multiple times. This adds
a chatwootMessageId parameter to the Baileys API request, enabling server-side
deduplication via Redis. Also increases HTTP timeout to 120s and channel lock
to 130s to reduce false timeouts.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix: address review feedback
- Use error.class.name assertions for parallel/reloading safety
- Assert reconnect endpoint was not called on 409 (stronger assertion)
* fix: address review feedback (round 2)
- Only release channel lock in ensure if it was actually acquired (prevents
clearing another worker's lock on timeout)
- Assert chatwootMessageId in reproduction spec body matcher
---------
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix(whatsapp): use payload filename for documents to avoid Content-Disposition parsing issues
When receiving documents via the WhatsApp Cloud API, filenames with
spaces or special characters were mangled due to the Down gem's
case-sensitive Content-Disposition header parsing. Now uses the
filename from the WhatsApp message payload when available, falling
back to Content-Disposition for other attachment types.
* fix: scope payload filename preference to document messages only
* fix: revert document-only scope, apply payload filename for all attachment types
* fix(whatsapp): resolve phone_number conflict when converting inbox between providers
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix: move transfer_identifier_to inside transaction for atomic consolidation
* fix: reassign message sender before destroying merged contact
* fix: resolve identifier conflicts account-wide in adopt and consolidate paths
* fix: scope sender reassignment to moved conversations only
---------
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
After a successful WhatsApp OAuth reauthorization, the health check runs
immediately and finds the phone number in a pending provisioning state
(`platform_type: NOT_APPLICABLE`). This incorrectly triggers
`prompt_reauthorization!`, re-setting the Redis disconnect flag and
sending a disconnect email — even though the reauth just succeeded.
The fix skips the health check during reauthorization flows. It still
runs for new channel creation.
Closes https://github.com/chatwoot/chatwoot/pull/12556
## Type of change
- [x] Bug fix (non-breaking change which fixes an issue)
## How to reproduce
1. Have a WhatsApp channel with a phone number in pending provisioning
state (display name not yet approved by Meta)
2. Complete the OAuth reauthorization flow
3. Observe that the user receives a "success" response but immediately
gets a disconnect email
## What changed
- `Whatsapp::EmbeddedSignupService#perform` now skips
`check_channel_health_and_prompt_reauth` when `inbox_id` is present
(reauthorization flow)
🤖 Generated with [Claude Code](https://claude.com/claude-code)
The Baileys `/on-whatsapp` endpoint can return a bare JSON array instead
of a hash with a `data` key. Calling `Array#dig` with a string key
raises `TypeError (no implicit conversion of String into Integer)`,
causing a 500 error. Handle both response shapes.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
GROUPS_ENABLED was a Ruby constant evaluated once at class load time,
so changes to the env var were not picked up on boot.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* feat: add group and conversation types to contacts and conversations, and implement conversation group membership model
* chore: add factory and specs for conversation group member model
* chore: add group type checks and associations for contacts and conversations
* refactor: remove scopes from ConversationGroupMember model
* refactor: remove scopes from ConversationGroupMember model specs
* refactor: enhance conversation type migration with concurrent indexing
* feat: add is_active index and scopes to ConversationGroupMember model
* feat: implement GroupConversationHandler for managing group conversations
* feat: add group_type attribute to contact creation
* fix: update WHATSAPP_CHANNEL_REGEX to allow up to 20 digits to handle group jid
* feat: handle group JID format in remote_jid method
* chore: update group contact info when finding or creating group contact
* chore: refactor and implement contact message handling and message creation logic for baileys single contact conversation
* feat: implement group message handling and metadata fetching in WhatsApp service
* chore: add spec for group type handling in contact creation for individual and group contacts
* chore: add specs for test scopes in conversation group members
* chore: update documentation for sender phone extraction in group conversation handler
* chore: move GroupConversationHandler concern to correct dir
* chore: implement specs for recipient_id handling to individual and group contacts
* chore: add group message handling specs for incoming messages
* chore: enhance group message handling to prevent race conditions
* chore: add group_metadata method to with error handling
* chore: add test for sending messages to group recipients in WhatsappBaileysService
* chore: raise error for unsuccessful response in group_metadata method
* chore: adds tests for group metadata retrieval and error handling
* chore: refactor build_sender_contact_attributes to avoid double call methods
* chore: update error handling for attachment download failure in message creation
* chore: optimize update_contact_info method to use compact hash for updates
* chore: simplify find_or_create_sender_contact method return values
* chore: rename group and individual contact message handlers
* chore: remove pointless comments from group contact message handler methods
* chore: refine sender JID extraction logic to remove unnecessary checks
* chore: remove phone number in spec for group contact attributes
* chore: implement sync_group route
* chore: implement get group_members route
* fix: sync_group participants creation handling
* chore: update contact avatar handling in group message processing
* chore: move sync_group functionality for conversation model
* feat: add sync_group action to ConversationsController and route
* fix: set contact name to phone in group message processing
* chore: refine group member retrieval logic in sync_group service and view
* feat: implement group participants update handling
* feat: implement group updates handling and localization for group activities
* chore: add handling for group membership requests and icon changes
* chore: add authorization for sync_group action in ContactsController
* chore: add sync_group endpoint specs for contact management
* chore: add authorization for sync_group action in ConversationsController
* chore: add specs for sync_group endpoint in ConversationsController
* chore: refactor index action in GroupMembersController for improved conversation filtering
* chore: add request specs for group_members endpoint in ContactsController
* chore: add specs for sync_group method in Conversation model
* chore: add specs for sync_group method in Channel::Whatsapp model
* chore: remove comments in find_or_create_group_conversation method
* chore: add specs for Contacts::SyncGroupService to validate group contact behavior
* chore: add specs for Whatsapp::BaileysHandlers::GroupsUpdate to validate group updates
* chore: add specs for Whatsapp::BaileysHandlers::GroupParticipantsUpdate to handle group participant actions
* chore: add fallback for identifier when contact has no phone_number in SendOnWhatsappService
* chore: add specs for group membership request and icon change handling in MessagesUpsert
* chore: add specs for sync_group method to handle group metadata and participant updates
* chore: update sync_group method to retrieve group members and adjust JSON response
* chore: update conversation query to filter by group type in GroupMembersController
* chore: update conversation creation in group_members_controller_spec to specify conversation_type as group
* chore: update find_or_create_group_conversation to include pending conversations
* chore: refactor sync_group method and enhance specs for group conversation handling
* feat: add GroupEventHelper module for managing group activities and contacts
* chore: refactor group contact inbox and conversation creation methods in group handlers
* chore: remove unnecessary check for blank participant contacts in sync_group_members method
* feat: implement message receipt update handling for WhatsApp integration
* chore: resolve rubocop rule for update_last_seen_at method
* chore: update swagger with endpoints for syncing group information and listing group members
* chore: integrate Contacts::SyncGroupService in group members controller, enhance error handling and update swagger
* chore: include participant information in reaction and quoted message keys for send message in group conversations
* chore: enhance whatsapp_baileys_service with participant handling for message keys
* feat: add skill for writing RSpec tests in the project
* fix: update recipient_id logic to directly use contact identifier for group contacts
* chore: implement group stub message handling for membership requests and icon changes
* fix: update whatsapp inbox source_id validation regex spec
* chore: fix spec for contact syncing group
* chore: remove readTimestamp handling and related tests for message read updates in group
* Cayo oliveira/cu 86af01932/4 backend gerenciamento dos grupos (#221)
* feat: add is_active index and scopes to ConversationGroupMember model
* feat: implement GroupConversationHandler for managing group conversations
* feat: add group_type attribute to contact creation
* fix: update WHATSAPP_CHANNEL_REGEX to allow up to 20 digits to handle group jid
* feat: handle group JID format in remote_jid method
* chore: update group contact info when finding or creating group contact
* chore: refactor and implement contact message handling and message creation logic for baileys single contact conversation
* feat: implement group message handling and metadata fetching in WhatsApp service
* chore: add spec for group type handling in contact creation for individual and group contacts
* chore: add specs for test scopes in conversation group members
* chore: update documentation for sender phone extraction in group conversation handler
* chore: move GroupConversationHandler concern to correct dir
* chore: implement specs for recipient_id handling to individual and group contacts
* chore: add group message handling specs for incoming messages
* chore: enhance group message handling to prevent race conditions
* chore: add group_metadata method to with error handling
* chore: add test for sending messages to group recipients in WhatsappBaileysService
* chore: raise error for unsuccessful response in group_metadata method
* chore: adds tests for group metadata retrieval and error handling
* chore: refactor build_sender_contact_attributes to avoid double call methods
* chore: update error handling for attachment download failure in message creation
* chore: optimize update_contact_info method to use compact hash for updates
* chore: simplify find_or_create_sender_contact method return values
* chore: rename group and individual contact message handlers
* chore: remove pointless comments from group contact message handler methods
* chore: refine sender JID extraction logic to remove unnecessary checks
* chore: remove phone number in spec for group contact attributes
* chore: implement sync_group route
* chore: implement get group_members route
* fix: sync_group participants creation handling
* chore: update contact avatar handling in group message processing
* chore: move sync_group functionality for conversation model
* feat: add sync_group action to ConversationsController and route
* fix: set contact name to phone in group message processing
* chore: refine group member retrieval logic in sync_group service and view
* feat: implement group participants update handling
* feat: implement group updates handling and localization for group activities
* chore: add handling for group membership requests and icon changes
* chore: add authorization for sync_group action in ContactsController
* chore: add sync_group endpoint specs for contact management
* chore: add authorization for sync_group action in ConversationsController
* chore: add specs for sync_group endpoint in ConversationsController
* chore: refactor index action in GroupMembersController for improved conversation filtering
* chore: add request specs for group_members endpoint in ContactsController
* chore: add specs for sync_group method in Conversation model
* chore: add specs for sync_group method in Channel::Whatsapp model
* chore: remove comments in find_or_create_group_conversation method
* chore: add specs for Contacts::SyncGroupService to validate group contact behavior
* chore: add specs for Whatsapp::BaileysHandlers::GroupsUpdate to validate group updates
* chore: add specs for Whatsapp::BaileysHandlers::GroupParticipantsUpdate to handle group participant actions
* chore: add fallback for identifier when contact has no phone_number in SendOnWhatsappService
* chore: add specs for group membership request and icon change handling in MessagesUpsert
* chore: add specs for sync_group method to handle group metadata and participant updates
* chore: update sync_group method to retrieve group members and adjust JSON response
* chore: update conversation query to filter by group type in GroupMembersController
* chore: update conversation creation in group_members_controller_spec to specify conversation_type as group
* chore: update find_or_create_group_conversation to include pending conversations
* chore: refactor sync_group method and enhance specs for group conversation handling
* feat: add GroupEventHelper module for managing group activities and contacts
* chore: refactor group contact inbox and conversation creation methods in group handlers
* chore: remove unnecessary check for blank participant contacts in sync_group_members method
* chore: update swagger with endpoints for syncing group information and listing group members
* chore: integrate Contacts::SyncGroupService in group members controller, enhance error handling and update swagger
* fix: update recipient_id logic to directly use contact identifier for group contacts
* chore: implement group stub message handling for membership requests and icon changes
* fix: update whatsapp inbox source_id validation regex spec
* chore: fix spec for contact syncing group
* fix: optimize update_last_seen_at method to use update_columns
* feat: Implement full frontend and backend support for group conversations
- Added PRD for group conversations detailing frontend and backend requirements.
- Created new Baileys TypeScript definitions for group-related functions.
- Renamed `conversation_type` to `group_type` in the database and updated all references.
- Implemented API serialization for `group_type` in conversation and contact responses.
- Developed Vuex store module for managing group members.
- Created UI components for group management, including group creation, member management, and metadata editing.
- Integrated @mention functionality for group conversations and real-time updates via ActionCable.
* feat: [US-001] - Rename conversation_type to group_type on conversations
- Add migration to rename column and indexes
- Update Conversation model enum to group_type
- Update GroupConversationHandler concern
- Update controllers (contacts, group_members)
- Update all backend specs
* chore: mark US-001 complete, update progress log, fix rubocop annotation
* feat: [US-002] - Serialize group_type fields in API responses
* feat: [US-003] - Add group_type filter to conversations index
* feat: [US-004] - Add group_type to filter_keys.yml and FilterService
* feat: US-005 - Backend group creation endpoint
- Add POST /api/v1/accounts/:account_id/groups endpoint
- Add Groups::CreateService to orchestrate Baileys group creation
- Extend WhatsappBaileysService and BaseService with group management methods
- Add routes for group members, metadata, invite, and join requests
- Returns 403 when agent lacks inbox access, 422 when provider is unavailable
* feat: US-006 - Backend add/remove members and role management endpoints
- Add create/destroy/update actions to GroupMembersController
- Delegate group management methods from Channel::Whatsapp to provider_service
- create adds members via Baileys and creates ConversationGroupMember records
- destroy removes a member by ID and sets is_active false
- update promotes/demotes a member and updates their role
* feat: US-007 - Backend group metadata update endpoint
- Add PATCH /contacts/:id/group_metadata endpoint
- Updates group subject via Baileys and syncs contact name
- Updates group description via Baileys and syncs additional_attributes.description
- Returns 422 when provider is unavailable
* feat: US-008 - Backend invite link management endpoints
- Add GET /contacts/:id/group_invite to retrieve current invite code/url
- Add POST /contacts/:id/group_invite/revoke to revoke and get new invite code/url
- Returns 422 when provider is unavailable
* feat: US-009 - Backend join request management endpoints
- Add GET /contacts/:id/group_join_requests to list pending join requests
- Add POST /contacts/:id/group_join_requests/handle to approve/reject requests
- Uses request_action param to avoid conflict with Rails reserved params[:action]
- Returns 422 when provider is unavailable
* feat: US-010 - Extend MentionService for contact mentions
- Extract mention://contact/ID/Name URIs from message content
- Store mentioned contact IDs in message.content_attributes[mentioned_contacts]
- Existing user/team mention handling unchanged
* feat: US-011 - Frontend API clients for all group endpoints
- Add app/javascript/dashboard/api/groupMembers.js
- Exports 11 methods: getGroupMembers, syncGroup, createGroup, updateGroupMetadata,
addMembers, removeMembers, updateMemberRole, getInviteLink, revokeInviteLink,
getPendingRequests, handleJoinRequest
* feat: US-012 - Frontend Vuex store module groupMembers
- Add groupMembers store module with fetch, sync, addMembers, removeMembers, updateMemberRole actions
- Add SET_GROUP_MEMBERS and SET_GROUP_MEMBERS_UI_FLAG mutation types
- Register module in store index
* feat: US-013 - Frontend i18n keys for group features
- Add groups.json with keys for group info, filter, creation modal, metadata editing, invite link, member management, join requests, and mention dropdown
- Register groups.json in i18n locale en/index.js
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* feat: US-014 - Frontend group_type filter in ConversationBasicFilter
- Add chatGroupTypeFilter state, getter, mutation, and action to conversations store
- Add getChatGroupTypeFilter getter
- Add group_type param to ConversationApi.get()
- Add Type filter section to ConversationBasicFilter with All/Individual/Group options
- Persist group_type to UI settings under conversations_filter_by.group_type
- Restore group_type from UI settings on page load
- Include groupType in conversationFilters and pass as group_type param to API
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* feat: US-013 - Frontend — i18n keys for group features (en + pt-BR)
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* feat: [US-014] - Frontend — add group_type filter to ConversationBasicFilter
All implementation was already in place from prior work:
- ConversationBasicFilter.vue has Type section with All/Individual/Group options
- ChatList.vue handles group_type in conversationFilters and restores from UI settings
- Store has setChatGroupTypeFilter action, getChatGroupTypeFilter getter
- API maps groupType → group_type query param
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* feat: US-015 - Frontend — add group_type to advanced filter system
- Add GROUP_TYPE to CONVERSATION_ATTRIBUTES in filterHelper.js
- Add group_type filter definition in provider.js (components-next)
- Add group_type to legacy advancedFilterItems/index.js and filterAttributeGroups
- Add group_type to automationHelper conditionFilterMaps
- Add group_type to customViewsHelper getValuesForFilter
- Add group_type options to ChatList setParamsForEditFolderModal
- Add GROUP_TYPE i18n key in en and pt_BR advancedFilters.json
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* feat: US-016 - Frontend — GroupContactInfo basic display
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* feat: US-017 - Frontend — GroupContactInfo sync button
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* feat: US-018 - integrate GroupContactInfo in ContactPanel
- Import GroupContactInfo component
- Conditionally render GroupContactInfo when group_type === 'group'
- Keep ContactInfo for individual conversations (no regression)
- Dynamic sidebar title: 'Group' for groups, 'Contact' for individual
- contact_notes and contact_attributes accordion sections unchanged
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* feat: US-019 - Frontend — group creation UI modal
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* feat: US-020 - Frontend — member management UI in GroupContactInfo
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* feat: US-021 - Frontend — group metadata editing UI
Add inline editing for group name, description, and avatar in GroupContactInfo:
- Click group name to edit inline, save on Enter/blur
- Click description to edit inline with textarea, save on blur
- Click avatar to open file picker for upload via contacts/update
- Loading states on all fields during save
- Success/error alerts for all operations
- updateGroupMetadata action added to groupMembers store
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* feat: US-022 - Frontend — invite link management UI
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* feat: US-023 - Frontend — join request management UI
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* feat: US-024 - Frontend — group message bubbles: sender name with color
- Add sender name display above incoming message bubbles in group conversations
- Deterministic color per sender using AVATAR_COLORS palette (name.length % 6)
- Sender name hidden for consecutive messages from the same sender
- Individual conversation bubbles unchanged
- Pass groupWithPrevious and isGroupConversation props through MessageList → Message
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* feat: US-025 - Frontend — group message bubbles: sender avatar
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* feat: Add Ralph Wiggum AI agent script for managing tool execution and progress tracking
* feat: US-026 - Frontend — @mention dropdown for group conversations
- Create TagGroupMembers.vue component for group member mention suggestions
- Modify Editor.vue: add isGroupConversation/groupContactId props, render
TagGroupMembers for group non-private context
- Modify ReplyBox.vue: compute isGroupConversation and groupContactId from
currentChat, pass to WootMessageEditor
- @ mention plugin isAllowed now triggers for group conversations
- In individual conversations or private notes, existing behavior unchanged
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* feat: US-027 - Frontend — mention rendering in group message bubbles
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* feat: US-028 - Frontend ActionCable handler for contact.group_synced event
- Backend: Include group_members data in contact.group_synced ActionCable payload
- Frontend: Register contact.group_synced handler in ActionCableConnector
- Frontend: Add setGroupMembers action to groupMembers store for direct commits
- Tests: ActionCable handler spec + groupMembers store spec for new action
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* feat: Update progress tracking for group conversations feature - mark tests as passing
* fix: sender click case mismatch and filter dropdown spacing
- Message.vue: use case-insensitive comparison for sender type check
(Contact.push_event_data returns 'contact' but SENDER_TYPES.CONTACT is 'Contact')
- ConversationBasicFilter.vue: replace last:mt-4 with flex-col gap-4
for consistent spacing between all three filter sections
* fix: four bugs found during manual testing review
- ContactPanel.vue: fix i18n key GROUP.INFO.SIDEBAR_TITLE → GROUP.SIDEBAR_TITLE
- groupMembers.js API: fix syncGroup HTTP method GET → POST to match backend route
- group_members_controller.rb: remove SyncGroupService from index action
- filterHelpers.js: add missing group_type case to getValueFromConversation
* docs: update progress with bug fix learnings
* chore: implement group creation functionality in UI components
* chore: add copy invite link functionality and update UI components
* feat: US-041 - Backend — ensure group_type is set on existing contacts and conversations
GroupConversationHandler#update_group_contact_info now sets group_type: :group
on contacts that are incorrectly typed as individual.
GroupConversationHandler#find_or_create_group_conversation updates existing
conversation's group_type to :group if it is currently :individual.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* chore: mark US-041 as complete
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* feat: US-029 - i18n keys for You badge and group settings (en + pt-BR)
All i18n keys already existed from prior iterations. Verified presence
and updated PRD status.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* feat: US-030 - fix Baileys API route/method mismatches
Fix on_whatsapp to dig('data') before accessing first element.
Update spec stubs to match { data: [...] } response envelope.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* feat: US-031 - group_leave, group_setting_update, group_join_approval_mode methods
All methods, delegates, and error handling already implemented.
Verified specs pass.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* feat: US-032 - persist group settings, invite code, and profile picture during sync
Add try_update_group_avatar to fetch and attach group profile picture
during sync_group. Update spec stubs for profile-picture-url endpoint.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* feat: US-033 - GroupSettingsController with leave, update, toggle
Controller and routes already implemented. Verified rubocop passes.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* feat: US-034 - remove inbox_contact_id from provider_config and jbuilder
Already removed in prior iterations. Verified no references remain.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* feat: US-035 - refactor TagGroupMembers to phone_number matching
Already implemented. Verified excludePhoneNumber prop and filtering.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* feat: US-036 - remove InboxContact.vue and settings tab
Already removed in prior iterations. Verified no references remain.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* feat: US-037 - add You badge in GroupContactInfo member list
Already implemented with isOwnMember check and blue badge styling.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* feat: US-038 - fix inline edit for group name and description
Already implemented with phone number normalization. Verified code.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* feat: US-039 - group settings section UI with toggles
Already implemented. Settings toggles, API calls, and i18n verified.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* feat: US-040 - leave group UI with confirmation and auto-resolve
Already implemented. Leave button, confirmation, and API call verified.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* feat: US-050 - Create GroupMember model and migration
New group_members table with group_contact_id, contact_id, role, is_active.
Unique index on (group_contact_id, contact_id). Associations added to Contact.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* feat: US-064 - Helper method to find channel from group contact
Add Contact#group_channel to decouple channel lookup from conversations.
Update GroupMembersController and GroupSettingsController to use it.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* feat: US-052 - Update GroupConversationHandler to use GroupMember
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* feat: US-057 - Update GroupMembersController to query GroupMember
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* feat: US-058 - Update GroupSettingsController to not depend on conversations
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* feat: US-060 - Update group_members jbuilder views
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* feat: US-059 - Remove group_members association from Conversation model
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* feat: US-051 - Remove ConversationGroupMember model and table
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* chore: mark all stories complete, update progress
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* feat(groups): real-time group panel, avatar refresh on icon change, editable name/description
- Add group_type to Conversations::EventDataPresenter#push_data and
Contact#push_event_data so WebSocket events carry the field, enabling
the frontend to switch to GroupContactInfo in real-time
- Update handle_icon_change_stub to call try_update_group_avatar with
force: true, purging the cached avatar and fetching the new one
- Add force parameter to try_update_group_avatar to support re-fetching
- Remove isInboxAdmin gate from name/description editing in
GroupContactInfo so any user can click to edit (server validates)
* fix(groups): rewrite SyncGroupService and simplify group metadata channel lookup
- Rewrite SyncGroupService to use contact.group_channel directly instead
of iterating conversations; find or create a conversation for sync
- Simplify GroupMetadataController to use @contact.group_channel instead
of querying conversations; remove local contact/attribute updates since
the Baileys API handles persistence via webhook events
* feat(groups): resolve conversations when inbox phone leaves or is removed
- Add resolve_conversations_if_inbox_left to GroupParticipantsUpdate
- Resolves all open/pending conversations when the inbox phone number
is removed from or leaves a group
* feat(groups): add paginated member list with infinite scroll
- Backend: add pagination to GroupMembersController (page/per_page,
default 10, ordered admins first); add meta with pagination info
to the jbuilder response
- Frontend: update groupMembers API to accept page param; add
APPEND_GROUP_MEMBERS and SET_GROUP_MEMBERS_META mutations; implement
paginated fetch with append and isFetchingMore flag in store
* feat(groups): support Ctrl+Click on group message sender to open in new tab
- navigateToGroupSender now accepts the event and checks for
Ctrl/Cmd+Click to open the sender contact in a new tab
* chore(i18n): update leave group confirmation text in en and pt_BR
* fix(groups): handle phone format differences in You badge and admin detection
- Extract phonesMatch helper that compares last 8 digits as fallback,
handling Brazilian 9th digit discrepancy (e.g. +5587988465072 vs
+558788465072)
- Apply to both isOwnMember and isInboxAdmin computed properties
* feat(groups): auto-sync members on mount, show existing members immediately
- On mount, fetch existing DB members first so they display instantly
- Then silently attempt a background sync to refresh from WhatsApp
- If sync fails (e.g. WhatsApp disconnected), existing members remain
displayed without any user-facing error
* fix(groups): pin own member on first page and return inbox phone in meta
The "You" badge was not appearing because the inbox's own member could be
missing from the first paginated page in large groups (admins sorted first).
Backend:
- Pin the inbox's own member at the top of page 1 regardless of sort order
- Return inbox_phone_number in the group members meta response
- Use last-8-digit SQL fallback for Brazilian 9th-digit phone mismatches
Frontend:
- Use meta.inbox_phone_number for the inboxPhone computed
- Fix declaration order to satisfy no-use-before-define lint rule
* fix(groups): fix member action dropdown clipped by overflow container
The promote/demote/remove dropdown menu was invisible because the member
list had `overflow-y-auto max-h-80`, clipping any absolutely-positioned
dropdown rendered inside it.
- Remove overflow container from member list; let the sidebar scroll
- Replace scroll-based infinite loading with IntersectionObserver on a
sentinel element for cleaner pagination trigger
- All member action logic (promote, demote, remove) was already wired;
the dropdown is now visible on hover
* fix(groups): keep member action dropdown visible when menu is open
The opacity-0/group-hover classes on the action menu wrapper caused the
DropdownMenu to become invisible as soon as the mouse left the row.
Now the wrapper stays fully opaque while the menu is active.
* fix(groups): move clickaway to member list wrapper to prevent instant close
v-on-clickaway was bound to every member's action div individually.
Clicking the three-dot button on one member fired closeMemberMenu from
all other members' clickaway handlers, closing the menu instantly.
Moved the directive to the single member list container instead.
* feat: add WhatsApp mention conversion (incoming + outgoing)
- New MentionConverterService for bidirectional mention handling
- Incoming: converts @phone/mentionedJid to mention://contact/ URIs
- Outgoing: extracts mention://contact/ URIs into WhatsApp mentions array
- Supports @everyone/todos group mentions
- WhatsApp renderer preserves mention display text instead of raw URI
* fix: preserve mention display text in WhatsApp renderer
mention:// URIs now render as display name text instead of the raw URL
when converting markdown to WhatsApp format
* feat: add @everyone mention option in group conversations
- Everyone item shown at top of mention dropdown
- Searchable by 'all', 'todos', 'everyone' keywords
- i18n keys added for en and pt-BR
* refactor: use Switch component for group settings toggles
- Add disabled prop to Switch component
- Replace custom toggle buttons in GroupContactInfo with Switch
- Loading spinner shown alongside toggle while toggling
* feat(whatsapp): add group sync status tracking (group_left, group_last_synced_at)
* feat(whatsapp): hide group management UI when group_left is true
* fix(groupMembers): include inbox phone number in group members state and sync event
* feat(whatsapp): wrap group settings and leave in Accordion component
* feat(groupMembers): handle group creator modification errors and update error messages
* feat(groupMembers): enhance invite link functionality and clean up UI state after copying
* refactor: remove sync_group functionality from conversations and related specs
* feat(GroupContactInfo): implement scroll-based loading for group members
* docs(swagger): add group API endpoints and remove conversation sync_group
- Remove dead conversation/{id}/sync_group swagger entry and file
- Update group_members.yml with pagination params, POST operation, and $ref schema
- Add swagger for: group_members_member (PATCH/DELETE), group_metadata,
group_invite, group_invite_revoke, group_join_requests,
group_join_requests_handle, group_settings, group_settings_leave,
group_settings_toggle_join_approval, groups/create
- Add group_member schema definition
- Add Groups tag to application tag_groups
- Register all 12 group endpoints in paths/index.yml
* feat(WhatsappBaileysService): enhance mention handling by replacing @DisplayName with @lid/@phone in outgoing text
* feat(groups): move group sync to background job with 15-min cooldown
- Create Contacts::SyncGroupJob that checks group_last_synced_at
before calling SyncGroupService (skips if < 15 min)
- Controller sync_group now enqueues the job and returns 202 Accepted
- Delete sync_group.json.jbuilder (no longer needed)
- Frontend sync action is fire-and-forget; results via ActionCable
- Auto-trigger sync on conversation select and panel mount
- Remove manual sync button from GroupContactInfo
* fix: show group members list even after leaving group\n\nKeep the members section visible in read-only mode when\ngroup_left is true. Admin actions (add member, promote,\ndemote, remove) remain hidden. Pending Join Requests and\nAdvanced Options also stay hidden.
* fix: disable group name/description/avatar editing when group_left is true
* fix: remove @all mention and fix Enter key in group mention dropdown\n\n- Remove the @all/everyone special mention from TagGroupMembers since\n no channel provider currently supports mentioning all participants\n- Fix Enter key sending message instead of inserting selected mention\n in group conversations. The root cause was Editor.vue only emitting\n toggleUserMention=true for private notes (isPrivate), leaving\n ReplyBox unaware the group mention dropdown was open. Now also\n emits for isGroupConversation.\n- Add TagGroupMembers spec covering filtering, exclusion, and emission"
* fix: address PR review feedback for group conversations
- Fix nil safety in group_invites and group_join_requests controllers
by replacing group_conversation.inbox.channel with @contact.group_channel
- Add before_action guard in group_members_controller to validate
contact is a group with identifier before create/update/destroy
- Persist metadata locally in group_metadata_controller after
provider calls (subject -> name, description -> additional_attributes)
- Add server-side allow_group_creation? check in groups_controller
- Add word boundary to mention regex to prevent matching inside words
- Remove useless catch clauses in groupMembers store (try/finally only)
- Default groupType to [] in customViewsHelper to prevent crash
- Fix swagger parameter name mismatch (contact_id -> id) across
all group endpoint YML files for consistency
* fix: address PR #228 review feedback - strong params, guards, and safety fixes
* fix: dispatch real-time events for Baileys group participant and metadata updates
Both group-participants.update and groups.update handlers were updating
backend data (GroupMember records, Contact attributes) but never
dispatching ActionCable events, leaving the frontend member list and
group metadata stale until manual sync.
Changes:
- Add dispatch_group_synced_event helper to GroupEventHelper concern
- Dispatch CONTACT_GROUP_SYNCED after participant add/remove/promote/demote
- Dispatch CONTACT_GROUP_SYNCED after group subject/description/settings changes
- Frontend: onContactGroupSynced also dispatches contacts/updateContact
to refresh group name, description and settings in the sidebar
* fix: enhance member menu positioning and close behavior on sidebar scroll
* feat: implement group property updates and enhance toast notifications
* fix: update WhatsApp channel regex to allow optional hyphenated numbers
* feat: implement group admin functionalities including leave, update properties, and toggle join approval
* refactor: simplify group message handling by removing metadata fetching and syncing methods
* chore: remove raph files
* feat: update Portuguese translations for 'Read More' and 'Insert Read More' phrases
* feat: enhance group admin functionalities with join approval and member add modes
* feat: enhance group join request handling by adding removal of handled requests and updating pending join requests
* feat: restrict message sending in announcement mode groups
When a Baileys WhatsApp group has announcement mode enabled (announce=true),
only admin members can send messages. This adds:
- Frontend: disabled editor + banner for non-admin inbox in announcement groups
- Backend: validation in SendOnWhatsappService to reject messages
- Shared phone helper utility extracted from GroupContactInfo
- i18n keys for en and pt_BR
* feat: add group sync job enqueueing and improve avatar update handling
* feat: add functionality to reset invite link and confirm member addition restrictions
* feat: update group name extraction logic to handle nil values
* feat: add inbox admin status handling and update related components
* feat: remove group conversation resolution on leave action
* feat: enhance group sender avatar interaction with tooltip and cursor pointer
* feat: add force option to SyncGroupJob and update related specs
* feat: enhance invite link handling and avatar update logic in group conversations
* chore: remove prd.json
* fix: change group sender name display from block to inline-block for better layout
* feat: add group members loading check and fetch logic in MessagesView and ReplyBox components
* feat: allow id and firstUnreadId props to accept both Number and String types
feat: add vOnClickOutside import to Editor component
feat: enhance Portuguese translations for integrations and settings
fix: change button color in GroupContactInfo component from green to teal
* feat: soft-disabled group conversations with activity tracking
Groups start in a soft-disabled state by default when using Baileys.
Chatwoot still creates group conversations but does not process every
incoming message. Instead, Baileys accumulates group messages and sends
periodic groups.activity webhook events to update last_activity_at.
Backend:
- Add WHATSAPP_GROUPS_ENABLED env var and groups_enabled? class method
- Send groupsEnabled in Baileys connection setup
- Create groups.activity handler to update conversation last_activity_at
- Gate group message processing behind groups_enabled? check
- Expose groups_enabled via inbox API
Frontend:
- Add warning banner with CTA to app.fazer.ai on disabled group conversations
- Disable reply editor for non-private-note mode when groups disabled
- Add i18n strings for en and pt_BR
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix: use method for groups disabled banner action to avoid window scope issue
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix: broadcast conversation update after groups.activity event
update_columns bypasses ActiveRecord callbacks, so the ActionCable
broadcast was never triggered when last_activity_at changed. Dispatch
a CONVERSATION_UPDATED event explicitly so the sidebar updates in
real-time.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* feat: show unread dot for soft-disabled group conversations with activity
Since soft-disabled groups don't create messages, unread_count is
always 0 and the standard badge won't show. Detect unread state by
comparing last_activity_at > agent_last_seen_at for these groups
and display a teal dot indicator instead of a count badge.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix: clear unread dot when agent opens soft-disabled group conversation
The update_last_seen endpoint skipped updating agent_last_seen_at when
there were no unread messages (the throttle path). For soft-disabled
groups that never create messages, this meant the dot indicator could
never be cleared. Add an unseen_activity? check that bypasses the
throttle when last_activity_at > agent_last_seen_at.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* feat: group avatar upload to provider and fix icon change sync
- Route avatar upload through GroupMetadataController to push to WhatsApp
provider before saving locally
- Add update_group_picture to baileys service and base service
- Fix buildContactFormData crash when social_profiles is undefined
- Make try_update_group_avatar public so GROUP_CHANGE_ICON stub handler
can call it from outside the service class
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix: update specs for group conversations feature changes
- Add groupsEnabled param to setup_channel_provider and handle_channel_error WebMock stubs
- Add group-request-participants-list stub for sync_group tests
- Add group_type to push_event_data expected hash
- Set last_activity_at in throttle tests to prevent unseen_activity? bypass
- Update sync_group delegation expectation to include soft: false
- Stub groups_enabled? in group message handling tests
- Update WhatsApp source_id regex expectation for group contact IDs
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* chore: add settings file for additional directories configuration
* chore: undo unrelated changes
* chore: remove planning doc, fix migration version, fix swagger param consistency
- Remove planejamento-chat-interno.md (unrelated planning document)
- Fix CreateGroupMembers migration API version from 7.0 to 7.1
- Fix swagger.json: normalize group endpoint paths from {contact_id} to {id}
to match YAML sources and existing contact sub-resource conventions
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: CayoPOliveira <cayoproliveira@gmail.com>
Co-authored-by: Cayo P. R. Oliveira <cayo@fazer.ai>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
* fix(whatsapp): consolidate fragmented phone/LID contacts to prevent lost messages
When a WhatsApp user had two separate contacts (one by phone, one by LID)
with different contact_ids, the consolidation service did nothing. This caused
update_contact_info to crash with a phone_number uniqueness violation, silently
dropping incoming messages.
Now properly merges the two contacts by treating the phone contact as canonical,
moving conversations from the LID contact, and cleaning up duplicates.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix(whatsapp): address CodeRabbit review feedback
- Destroy orphaned LID contact when it has no remaining contact_inboxes
- Clarify spec comment to past-tense (no longer an active bug)
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix(whatsapp): route legacy-source contact_inbox into merge path
When a phone contact_inbox has a non-standard source_id (legacy format)
and a separate LID contact_inbox exists for a different contact,
the consolidation now merges them instead of early-returning.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* test: assert orphaned LID contact is destroyed in legacy-source spec
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
## Description
Adds webhook configuration management for WhatsApp Cloud API channels,
allowing administrators to check webhook status and register webhooks
directly from Chatwoot without accessing Meta Business Manager.
## Type of change
- [ ] New feature (non-breaking change which adds functionality)
## Screenshots
<img width="1130" height="676" alt="Screenshot 2026-03-05 at 7 04 18 PM"
src="https://github.com/user-attachments/assets/f5dcd9dd-8827-42c5-a52b-1024012703c2"
/>
<img width="1101" height="651" alt="Screenshot 2026-03-05 at 7 04 29 PM"
src="https://github.com/user-attachments/assets/e0bd59f9-2a90-4f24-87c0-b79f21e721ee"
/>
## Checklist:
- [ ] My code follows the style guidelines of this project
- [ ] I have performed a self-review of my code
- [ ] I have commented on my code, particularly in hard-to-understand
areas
- [ ] I have made corresponding changes to the documentation
- [ ] My changes generate no new warnings
- [ ] I have added tests that prove my fix is effective or that my
feature works
- [ ] New and existing unit tests pass locally with my changes
- [ ] Any dependent changes have been merged and published in downstream
modules
---------
Co-authored-by: Muhsin Keloth <muhsinkeramam@gmail.com>
CSAT templates for WhatsApp are submitted as Utility, but Meta may
reclassify them as Marketing based on content, which can significantly
increase messaging costs.
This PR introduces a Captain-powered CSAT template analyzer for
WhatsApp/Twilio WhatsApp that predicts utility fit, explains likely
risks, and suggests safer rewrites before submission. The flow is manual
(button-triggered), Captain-gated, and applies rewrites only on explicit
user action. It also updates UX copy to clearly set expectations: the
system submits as Utility, Meta makes the final categorization decision.
Fixes
https://linear.app/chatwoot/issue/CW-6424/ai-powered-whatsapp-template-classifier-for-csat-submissionshttps://github.com/user-attachments/assets/8fd1d6db-2f91-447c-9771-3de271b16fd9
* feat: add audio transcoding support for WhatsApp Cloud API
- Introduced `Audio::TranscodeService` to handle audio transcoding to OGG/Opus format.
- Updated `Messages::MessageBuilder` to transcode audio attachments based on `transcode_audio` parameter.
- Enhanced `WhatsappCloudService` to normalize audio content types and send voice flag for recorded audio in OGG format.
- Added utility functions for audio conversion in JavaScript.
- Updated Dockerfile to include FFmpeg for audio processing.
- Added tests for audio transcoding and WhatsApp Cloud service interactions.
* feat: enhance audio handling with transcoding support and error management
* feat: improve audio transcoding error handling and enhance audio recording features
* feat: enhance audio transcoding process and error handling for better reliability
* feat: update recorded audio handling to support boolean and array formats
* feat: Implement existing template linking for CSAT surveys
- Added functionality to link existing CSAT templates for WhatsApp channels.
- Introduced a new component for selecting existing templates.
- Updated the dashboard settings page to support template mode switching between creating new and using existing templates.
- Enhanced the CSAT template management service to handle linking existing templates and fetching available templates.
- Updated API routes to include linking and fetching available templates.
- Added tests for the new linking functionality and template availability checks.
* feat: Enhance CSAT template handling and validation across services and components
* feat: Refactor body variable extraction for CSAT templates and update related validations
* feat: Add linked_at field to CSAT template responses and update related handling
* feat: Add tests for ConversationDrop date formatting and CSAT template body variable handling
Some customers using WhatsApp inboxes with account-level webhooks were
reporting receiving duplicate `message_created` webhook deliveries for
every incoming message. Upon inspection, here's what we found
- Both payloads are identical.
- No errors appear in the application logs
- Webhook URL is only configured in one place.
This meant, the system was sending the webhooks twice. For some context,
there's a know related issue... Meta's WhatsApp Business API can deliver
the same webhook notification multiple times for a single message. The
codebase already acknowledges this — there's a comment in
`IncomingMessageBaseService#process_messages` noting that "multiple
webhook events can be received against the same message due to
misconfigurations in the Meta business manager account." A deduplication
guard exists, but it doesn't actually work under concurrency.
### Rationale
The existing dedup was a three-step sequence: check Redis (`GET`), check
the database, then set a Redis flag (`SETEX`). Two Sidekiq workers
processing duplicate Meta webhooks simultaneously would both complete
the `GET` before either executed the `SETEX`, so both would proceed to
create a message. The `source_id` column has a non-unique index, so the
database wouldn't catch the duplicate either. Each message then
independently fires `after_create_commit`, dispatching two
`message_created` webhook events to the customer.
```
Worker A Worker B
│ │
▼ ▼
Redis GET key ──► nil Redis GET key ──► nil
│ │
│ ◄── both pass guard ──► │
│ │
▼ ▼
Redis SETEX key Redis SETEX key
│ │
▼ ▼
BEGIN transaction BEGIN transaction
INSERT message INSERT message
DELETE Redis key ◄─┐ │
COMMIT │ DELETE Redis key
│ COMMIT
│ │
└── key gone before ───┘
B's commit lands
▼ ▼
after_create_commit after_create_commit
dispatch MESSAGE_CREATED dispatch MESSAGE_CREATED
│ │
▼ ▼
WebhookJob ──► n8n WebhookJob ──► n8n
(duplicate!)
```
There was a second, subtler problem visible in the diagram: the Redis
key was cleared *inside* the database transaction, before the
transaction committed. This opened a window where neither the Redis
check nor the database check would see the in-flight message.
The fix collapses the check-and-set into a single `SET NX EX` call,
which is atomic in Redis. The key is no longer eagerly cleared — it
expires naturally after 24 hours. The database lookup
(`find_message_by_source_id`) remains as a fallback for messages that
were created before the lock expired.
```
Worker A Worker B
│ │
▼ ▼
Redis SET NX ──► OK Redis SET NX ──► nil
│ │
▼ ▼
proceeds to create returns early
message normally (lock already held)
```
### Implementation Notes
The lock logic is extracted into `Whatsapp::MessageDedupLock`, a small
class that wraps a single `Redis SET NX EX` call. This makes the
concurrency guarantee testable in isolation — the spec uses a
`CyclicBarrier` to race two threads against the same key and asserts
exactly one wins, without needing database writes,
`use_transactional_tests = false`, or monkey-patching.
Because the Redis lock now persists (instead of being cleared
mid-transaction), existing WhatsApp specs needed an `after` hook to
clean up `MESSAGE_SOURCE_KEY::*` keys between examples. Transactional
fixtures only roll back the database, not Redis.
This change https://github.com/chatwoot/chatwoot/pull/13371 broke the
functionality. When a user replies to a WhatsApp message, the reply
context wasn't being properly stored in Chatwoot due to #13371
WhatsApp sends reply messages with a `context` field containing the
original message ID:
```json
{
"messages": [{
"context": {
"from": "phone_number",
"id": "wamid.ORIGINAL_MESSAGE_ID"
},
"from": "phone_number",
"id": "wamid.REPLY_MESSAGE_ID",
"text": { "body": "This is a reply" }
}]
}
```
However, the in_reply_to_external_id was being overridden when building
the message because content_attributes was explicitly set to either {
external_echo: true } or {}, which discarded the reply-to information.
#### Problem
Meta requires the app to be subscribed to the WABA before
`override_callback_uri` can be used. The current implementation tries to
use `override_callback_uri` directly, which fails with:
> Error 100: "Before override the current callback uri, your app must be
subscribed to receive messages for WhatsApp Business Account"
This causes embedded signup to fail silently, the inbox appears
connected but never receives messages.
#### Solution
Split `subscribe_waba_webhook` into two sequential API calls:
```ruby
def subscribe_waba_webhook(waba_id, callback_url, verify_token)
# Step 1: Subscribe app to WABA first (required before override)
subscribe_app_to_waba(waba_id)
# Step 2: Override callback URL for this specific WABA
override_waba_callback(waba_id, callback_url, verify_token)
end
```
#### References
- Subscribe app to WABA's webhooks: https://www.postman.com/meta/whatsapp-business-platform/request/ju40fld/subscribe-app-to-waba-s-webhooks
- Override Callback URL (Embedded Signup): https://www.postman.com/meta/whatsapp-business-platform/request/l6a09ow/override-callback-url
Co-authored-by: Sojan Jose <sojan@pepalo.com>
When businesses use WhatsApp Business App (co-existence mode) or
Instagram App or TikTok alongside Chatwoot, messages sent from the
native apps were not synced properly back to Chatwoot. This left agents
with an incomplete conversation history and no visibility into responses
sent outside the dashboard. Additionally, if these echo messages did
arrive, they appeared as "Sent by: Bot" in the UI since they had no
sender, making it confusing for agents.
This PR subscribes to WhatsApp `smb_message_echoes` webhook events and
routes them through the existing service with an `outgoing_echo` flag,
mirroring how Instagram already handles echoes. On the Instagram side,
echo messages now also carry the `external_echo` content attribute and
`delivered` status.
On the frontend, messages with `externalEcho` are distinguished from bot
messages showing a "Native app" avatar and an advisory note encouraging
agents to reply from Chatwoot to maintain the service window.
<img width="1518" height="524" alt="CleanShot 2026-01-29 at 13 37 57@2x"
src="https://github.com/user-attachments/assets/5aa0b552-6382-441f-96aa-9a62ca716e4a"
/>
Fixes
https://linear.app/chatwoot/issue/CW-4204/display-messages-not-sent-from-chatwoot-in-case-of-outgoing-echo
Fixes
https://linear.app/chatwoot/issue/PLA-33/incoming-from-me-messages-from-whatsapp-business-app-are-not-falling
* chore: update scheduled messages author association to nullable and adjust related specs
* chore: update sender handling for WhatsApp messages and add external sender name
* fix(whatsapp): update message source ID handling and improve Redis key formatting
* fix(whatsapp): prevent updating contact_inbox on identifier conflict
* fix(whatsapp): refactor conversation routing tests to use shared examples for better clarity and maintainability
Fixes https://github.com/chatwoot/chatwoot/issues/13317
Fixes an issue where WhatsApp attachment messages (images, audio, video,
documents) were failing to download. Messages were being created but
without attachments.
The `phone_number_id` parameter was being passed to the `GET
/<MEDIA_ID>` endpoint when downloading incoming media. According to
Meta's documentation:
> "Note that `phone_number_id` is optional. If included, the request
will only be processed if the business phone number ID included in the
query matches the ID of the business
phone number **that the media was uploaded on**."
For incoming messages, media is uploaded by the customer, not by the
business phone number. Passing the business's `phone_number_id` causes
validation to fail with error: `Param phone_number_id is not a valid
whatsapp business phone number id ID`
This PR removes the `phone_number_id` parameter from the media URL
request for incoming messages.
Fixes https://github.com/chatwoot/chatwoot/issues/13317
Fixes an issue where WhatsApp attachment messages (images, audio, video,
documents) were failing to download. Messages were being created but
without attachments.
The `phone_number_id` parameter was being passed to the `GET
/<MEDIA_ID>` endpoint when downloading incoming media. According to
Meta's documentation:
> "Note that `phone_number_id` is optional. If included, the request
will only be processed if the business phone number ID included in the
query matches the ID of the business
phone number **that the media was uploaded on**."
For incoming messages, media is uploaded by the customer, not by the
business phone number. Passing the business's `phone_number_id` causes
validation to fail with error: `Param phone_number_id is not a valid
whatsapp business phone number id ID`
This PR removes the `phone_number_id` parameter from the media URL
request for incoming messages.
Fixes https://github.com/chatwoot/chatwoot/issues/13097
### Problem
The PR #12176 removed the `before_save :setup_webhooks` callback to fix
a race condition where Meta's webhook verification request arrived
before the channel was saved to the database. This change broke manual
WhatsApp Cloud channel setup. While embedded signup explicitly calls
`channel.setup_webhooks` in `EmbeddedSignupService`, manual setup had no
equivalent call - meaning the `subscribed_apps` endpoint was never
invoked and Meta never sent webhook events to Chatwoot.
### Solution
Added an `after_commit` callback that triggers webhook setup for manual
WhatsApp Cloud channels
Fixes https://github.com/chatwoot/chatwoot/issues/13257
When sending WhatsApp template messages via API with `processed_params`,
users receiving error `(#132000) Number of parameters does not match the
expected number of params` from WhatsApp. The find template method
performed case-sensitive string comparison on language codes. If a user
sent `language: "ES"` but the template was stored as `language: "es"`,
the template wouldn't be found, resulting in empty `components: []`
being sent to WhatsApp.
* fix: z-api read messages
* fix: use threads for parallel requests
* feat: use jobs instead
* fix: refactor ZapiReadMessageJob to use service method for sending read messages
This PR add the backend changes for the feature [sending CSAT surveys
via WhatsApp message templates
](https://github.com/chatwoot/chatwoot/pull/12787)
---------
Co-authored-by: Vinay Keerthi <11478411+stonecharioteer@users.noreply.github.com>