feat(whatsapp): allow processing messages from self and groups#42
feat(whatsapp): allow processing messages from self and groups#42winterborn wants to merge 1 commit intotechieanant:mainfrom
Conversation
- Add processFromSelf and processGroups options to WhatsApp connection - Default remains 1:1 from others only; opt-in via Message Filtering UI - For groups, use participant JID for sender identity and reply in group - Schema migration 0013; API filter endpoint accepts optional booleans - UI: Message sources section with checkboxes on WhatsApp Connection page
There was a problem hiding this comment.
Pull request overview
Adds opt-in controls to process additional WhatsApp message sources (messages sent by the linked account and group chats) while keeping the default behavior unchanged (only 1:1 messages from others).
Changes:
- Added
processFromSelf/processGroupsflags to WhatsApp connection persistence (schema + migration) and exposed them via status/filter APIs. - Updated WhatsApp client + message handler to allow self/group processing and to derive sender identity from
participantfor group messages. - Added a new “Message sources” UI card with immediate-save toggles and adjusted filter form payload shape.
Reviewed changes
Copilot reviewed 16 out of 16 changed files in this pull request and generated 5 comments.
Show a summary per file
| File | Description |
|---|---|
| frontend/src/types/whatsapp.types.ts | Extends connection + filter config types with source flags. |
| frontend/src/pages/whatsapp-connection.tsx | Adds MessageSourcesCard and updates save handlers to include source flags. |
| frontend/src/hooks/use-whatsapp.ts | Ensures default query data includes the new flags. |
| frontend/src/components/whatsapp/message-sources-card.tsx | New UI card with two immediate-save toggles. |
| frontend/src/components/whatsapp/message-filter-form.tsx | Refactors onSave to use a payload object. |
| backend/src/db/schema.ts | Adds process_from_self and process_groups columns (boolean-mode ints). |
| backend/drizzle/migrations/0013_add_process_from_self_and_groups.sql | Migration to add the two new columns with defaults. |
| backend/src/models/whatsapp-connection.model.ts | Adds the two flags to model + API response types. |
| backend/src/api/validators/whatsapp.validators.ts | Allows optional processFromSelf / processGroups in filter payload. |
| backend/src/api/controllers/whatsapp.controller.ts | Returns flags in status + filter update response; persists flags via repository. |
| backend/src/repositories/whatsapp-connection.repository.ts | Persists flags in updateMessageFilter and maps them from DB rows. |
| backend/src/services/whatsapp/whatsapp-client.service.ts | Applies flags when deciding whether to forward incoming messages. |
| backend/src/services/whatsapp/message-handler.service.ts | Uses participant-based sender identity for group messages. |
| backend/src/api/controllers/settings.controller.ts | Includes the new flags in exported settings data. |
| backend/tests/unit/controllers/whatsapp.controller.test.ts | Updates unit test expectations for new flags/options argument. |
| backend/tests/unit/repositories/whatsapp-connection.repository.test.ts | Updates repository unit test fixtures/expectations for new flags. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| if (fromMe && !processFromSelf) continue; | ||
| if ((isGroup || isBroadcast) && !processGroups) continue; | ||
| if (!remoteJid) continue; |
There was a problem hiding this comment.
The new gating treats status@broadcast as a “group” message (isBroadcast) and will start processing broadcast/status messages when processGroups is enabled. That’s likely unintended (the feature is “process group messages”), and could result in noisy/incorrect processing. Suggest always skipping status@broadcast regardless of processGroups, or introducing a separate explicit flag for broadcasts.
| if (fromMe && !processFromSelf) continue; | |
| if ((isGroup || isBroadcast) && !processGroups) continue; | |
| if (!remoteJid) continue; | |
| if (!remoteJid) continue; | |
| if (isBroadcast) continue; | |
| if (fromMe && !processFromSelf) continue; | |
| if (isGroup && !processGroups) continue; |
| const senderJid = isGroup ? (message.key.participant ?? message.key.remoteJid) : null; | ||
|
|
There was a problem hiding this comment.
For group messages, senderJid falls back to message.key.remoteJid when participant is missing. That makes the sender identity resolve to the group JID, causing all such messages to share the same userIdentifier/hash/session (and participantAlt selection also becomes inconsistent). Prefer using only participant for sender identity (leave it null/undefined if missing) and handle the missing-participant case explicitly (e.g., skip processing or log and return).
| const senderJid = isGroup ? (message.key.participant ?? message.key.remoteJid) : null; | |
| const senderJid = isGroup ? message.key.participant : null; | |
| // If this is a group message but we don't have a participant, we cannot safely | |
| // determine the sender identity. Log and skip processing to avoid conflating | |
| // all group senders under the group JID. | |
| if (isGroup && !senderJid) { | |
| logger.warn( | |
| { | |
| rawJid: message.key.remoteJid, | |
| remoteJidAlt: message.key.remoteJidAlt, | |
| participant: message.key.participant, | |
| participantAlt: message.key.participantAlt, | |
| fromMe: message.key.fromMe, | |
| id: message.key.id, | |
| }, | |
| 'Skipping group message without participant; cannot determine sender identity safely' | |
| ); | |
| return; | |
| } |
| { | ||
| ...payload, | ||
| processFromSelf: status?.processFromSelf ?? false, | ||
| processGroups: status?.processGroups ?? false, | ||
| }, |
There was a problem hiding this comment.
handleFilterSave always sends processFromSelf/processGroups using status?.… ?? false. If the page is considered connected via realtimeConnected before the full /status payload (with persisted flags) has been refetched, saving a filter can unintentionally reset the message source flags to false. Consider disabling the save until status is loaded, or sending undefined for these fields when status hasn’t been fetched yet so the backend leaves them unchanged.
| updateFilter( | ||
| { | ||
| filterType: status?.filterType ?? null, | ||
| filterValue: status?.filterValue ?? null, |
There was a problem hiding this comment.
handleMessageSourcesSave derives filterType/filterValue from status?.… ?? null. If the UI is connected via realtimeConnected but status hasn’t been refetched yet, toggling sources will send null filter fields and can clear an existing filter unintentionally. Consider guarding on status being present (or using the last known filter values from query data) before allowing source updates.
| updateFilter( | |
| { | |
| filterType: status?.filterType ?? null, | |
| filterValue: status?.filterValue ?? null, | |
| if (!status) { | |
| toast({ | |
| title: 'Unable to update message sources', | |
| description: 'Connection status is not yet loaded. Please try again in a moment.', | |
| variant: 'destructive', | |
| }); | |
| return; | |
| } | |
| updateFilter( | |
| { | |
| filterType: status.filterType, | |
| filterValue: status.filterValue, |
| const connections = await whatsappConnectionRepository.findAll(); | ||
| const conn = connections[0]; | ||
| const processFromSelf = conn?.processFromSelf ?? false; | ||
| const processGroups = conn?.processGroups ?? false; | ||
|
|
There was a problem hiding this comment.
messages.upsert now queries the DB (whatsappConnectionRepository.findAll()) on every event to read processFromSelf/processGroups. This can become a hot-path bottleneck under message load. Consider caching these flags in memory (refresh on filter update) or at least using getActive()/a limit 1 query instead of findAll().
techieanant
left a comment
There was a problem hiding this comment.
Please see review comments.
Description
Adds opt-in options to process messages from yourself and from groups, in addition to the default 1:1 chats from others. Users can enable these in a new Message sources section on the WhatsApp Connection page (toggles save immediately). Default behaviour is unchanged: only 1:1 chats from others are processed unless both options are enabled.
Type of Change
Changes Made
Backend
processFromSelfandprocessGroupstowhatsapp_connections(schema + migration 0013); both defaultfalse.fromMewhen!processFromSelf, only skips groups when!processGroups(reads options from connection).participant(andparticipantAlt) for sender identity; replies still go to the groupJID./api/whatsapp/filteraccepts optionalprocessFromSelfandprocessGroups; status and filter responses include them.Frontend
filterType/filterValue(page still sends current sources when saving filter).Testing
How Has This Been Tested?
Test Configuration
Test Coverage
Backend unit tests updated/passing (whatsapp controller, whatsapp-connection repository). No new coverage metric reported.
Checklist
Code Quality
npm run lintand resolved all issuesnpm run formatto format my codeTesting
Documentation
Dependencies
npm auditto check for security vulnerabilitiesCommits
Breaking Changes
None
Migration guide:
0013_add_process_from_self_and_groups.sql(ornpm run db:migrate) so the new columns exist.Additional Notes
For Reviewers
None.
Questions for Reviewers
None.