Repository navigation
feat(chat): add updateContact endpoint to set a contact's name - #2758
luantaraschi wants to merge 1 commit into
Conversation
POST /chat/updateContact/{instance} sends Baileys' addOrEditContact
(app-state contactAction) so a name set by the API is written to the
account's WhatsApp contact list instead of living only in our database.
The patch is indexed by the phone number JID with the LID inside the
action, the same layout Baileys receives when the phone syncs a
contact. Both JIDs are resolved through the LID mapping, so either one
can be passed as `number`. The local Contact rows (PN and LID) and the
CONTACTS_UPDATE webhook are refreshed through the existing
contacts.update handler, since emitOwnEvents is off.
Refs evolution-foundation#2740
Reviewer's GuideAdds POST /chat/updateContact/{instance} with schema validation, PN/LID-aware identity resolution, and Baileys addOrEditContact integration; successful patches update the PN contact event flow and any persisted LID contact row, while unsupported or unresolved identifiers are rejected. Sequence diagram for the update contact endpointsequenceDiagram
actor Client
participant Router
participant Service as BaileysStartupService
participant Mapping as lidMapping
participant Baileys
participant Contacts as ContactPersistence
Client->>Router: POST updateContact/{instance}
Router->>Router: dataValidate
Router->>Service: updateContact(data)
Service->>Service: createJid(number)
alt LID input
Service->>Mapping: getPNForLID(lidJid)
else Phone number input
Service->>Baileys: whatsappNumber(numbers)
Baileys-->>Service: resolved JID
Service->>Mapping: getLIDForPN(pnJid)
end
Service->>Baileys: addOrEditContact(pnJid, contact)
Baileys-->>Service: patch accepted
Service->>Contacts: contacts.update(pnJid, fullName)
opt LID contact exists and persistence enabled
Service->>Contacts: contact.updateMany(lidJid, fullName)
end
Service-->>Router: success response
Router-->>Client: 200 update success
Flow diagram for contact identity validationflowchart TD
A["number and fullName"] --> B["createJid(number)"]
B --> C{"Individual contact?"}
C -- No --> D["400 Bad Request"]
C -- Yes --> E{"Input is a LID?"}
E -- Yes --> F["getPNForLID"]
E -- No --> G["whatsappNumber"]
G --> H{"Number exists on WhatsApp?"}
H -- No --> D
H -- Yes --> I["Resolve PN/LID mapping"]
F --> I
I --> J{"Phone number known?"}
J -- No --> D
J -- Yes --> K["addOrEditContact"]
K --> L["Update PN and LID contact rows"]
File-Level Changes
Possibly linked issues
Tips and commandsInteracting with Sourcery
Customizing Your ExperienceAccess your dashboard to:
Getting Help
|
There was a problem hiding this comment.
Hey - I've found 5 issues
Prompt for AI Agents
Please address the comments from this code review:
## Individual Comments
### Comment 1
<location path="src/api/integrations/channel/whatsapp/whatsapp.baileys.service.ts" line_range="4602" />
<code_context>
+ }
+
+ // emitOwnEvents is off, so Baileys will not echo this change back to us.
+ await this.contactHandle['contacts.update']([{ id: pnJid, name: data.fullName }]);
+
+ // Chats that arrived addressed to the LID keep their own contact row.
</code_context>
<issue_to_address>
**Accepted updates leave contacts stale**
When baileys accepts the patch, then the profile-picture lookup, event adapter, or PN contact write in `contacts.update` fails, `updateContact` propagates the `contacts.update` failure after `addOrEditContact` has accepted the WhatsApp change, so the API returns an error while the local contact remains stale; a profile-picture lookup failure also prevents the webhook from being sent.
Make local synchronization failure-safe after the remote patch, retrying or reconciling failed contact updates instead of leaving the accepted change stale.
</issue_to_address>
### Comment 2
<location path="src/validate/chat.schema.ts" line_range="327" />
<code_context>
+ type: 'object',
+ properties: {
+ number: { type: 'string' },
+ fullName: { type: 'string' },
+ firstName: { type: 'string' },
+ saveOnPrimaryAddressbook: { type: 'boolean' },
</code_context>
<issue_to_address>
**Long names fail local persistence**
When `fullName` exceeds 100 characters and contact persistence is enabled, the schema accepts the name, but the Contact `pushName` column is limited to 100 characters. Baileys accepts the patch before Prisma rejects the local write, so the request fails and the local contact remains stale.
Set a 100-character maximum for `fullName` in `updateContactSchema`.
</issue_to_address>
### Comment 3
<location path="src/api/integrations/channel/whatsapp/whatsapp.baileys.service.ts" line_range="4602" />
<code_context>
+ }
+
+ // emitOwnEvents is off, so Baileys will not echo this change back to us.
+ await this.contactHandle['contacts.update']([{ id: pnJid, name: data.fullName }]);
+
+ // Chats that arrived addressed to the LID keep their own contact row.
</code_context>
<issue_to_address>
**Saved avatars disappear**
When a contact has a saved avatar and the profile-picture lookup during a name update fails or returns null, `contacts.update` writes the null `profilePicture` result into `profilePicUrl`, replacing the saved avatar, so contact listings lose it.
Preserve the existing `profilePicUrl` when the lookup fails or returns null.
</issue_to_address>
### Comment 4
<location path="src/validate/chat.schema.ts" line_range="332" />
<code_context>
+ saveOnPrimaryAddressbook: { type: 'boolean' },
+ },
+ required: ['number', 'fullName'],
+ ...isNotEmpty('number', 'fullName'),
+};
+
</code_context>
<issue_to_address>
**Empty names are accepted**
When the request includes an unrecognized property or an optional field and `fullName` is empty, `isNotEmpty` skips the `minLength` check when a property name is outside its enum, so the empty `fullName` reaches `addOrEditContact` and leaves the contact with a blank name.
Validate that `fullName` is nonempty independently of the other request properties.
</issue_to_address>
### Comment 5
<location path="src/api/integrations/channel/whatsapp/whatsapp.baileys.service.ts" line_range="4602-4610" />
<code_context>
+ }
+
+ // emitOwnEvents is off, so Baileys will not echo this change back to us.
+ await this.contactHandle['contacts.update']([{ id: pnJid, name: data.fullName }]);
+
+ // Chats that arrived addressed to the LID keep their own contact row.
+ if (lidJid && this.configService.get<Database>('DATABASE').SAVE_DATA.CONTACTS) {
+ await this.prismaRepository.contact.updateMany({
+ where: { remoteJid: lidJid, instanceId: this.instanceId },
+ data: { pushName: data.fullName },
+ });
+ }
+
+ return { update: 'success', remoteJid: pnJid, lid: lidJid, fullName: data.fullName };
</code_context>
<issue_to_address>
**PN and LID names diverge**
When the PN update succeeds but the separate LID contact update fails, the PN row is committed while the LID row retains the old name, so LID-based contact reads remain stale and the caller receives an error despite the accepted WhatsApp patch.
Make the PN and LID contact updates atomic, or retry/reconcile a failed LID update.
</issue_to_address>Sourcery assessment
Needs a human reviewer. 5 findings to address first, and the endpoint writes the supplied name to WhatsApp’s contact state and updates matching local contact rows, so reverting the code would not restore the previous name. The affected contacts are bounded, and their names can be corrected with another update.
Blocking findings: src/api/integrations/channel/whatsapp/whatsapp.baileys.service.ts:4602, src/validate/chat.schema.ts:327, src/api/integrations/channel/whatsapp/whatsapp.baileys.service.ts:4602, src/validate/chat.schema.ts:332, src/api/integrations/channel/whatsapp/whatsapp.baileys.service.ts:4610
| } | ||
|
|
||
| // emitOwnEvents is off, so Baileys will not echo this change back to us. | ||
| await this.contactHandle['contacts.update']([{ id: pnJid, name: data.fullName }]); |
There was a problem hiding this comment.
🟡 Medium · Accepted updates leave contacts stale
When baileys accepts the patch, then the profile-picture lookup, event adapter, or PN contact write in contacts.update fails, updateContact propagates the contacts.update failure after addOrEditContact has accepted the WhatsApp change, so the API returns an error while the local contact remains stale; a profile-picture lookup failure also prevents the webhook from being sent.
Make local synchronization failure-safe after the remote patch, retrying or reconciling failed contact updates instead of leaving the accepted change stale.
Prompt for AI agents
In `src/api/integrations/channel/whatsapp/whatsapp.baileys.service.ts` at line 4602:
**Accepted updates leave contacts stale**
When baileys accepts the patch, then the profile-picture lookup, event adapter, or PN contact write in `contacts.update` fails, `updateContact` propagates the `contacts.update` failure after `addOrEditContact` has accepted the WhatsApp change, so the API returns an error while the local contact remains stale; a profile-picture lookup failure also prevents the webhook from being sent.
Make local synchronization failure-safe after the remote patch, retrying or reconciling failed contact updates instead of leaving the accepted change stale.| type: 'object', | ||
| properties: { | ||
| number: { type: 'string' }, | ||
| fullName: { type: 'string' }, |
There was a problem hiding this comment.
🟡 Medium · Long names fail local persistence
When fullName exceeds 100 characters and contact persistence is enabled, the schema accepts the name, but the Contact pushName column is limited to 100 characters. Baileys accepts the patch before Prisma rejects the local write, so the request fails and the local contact remains stale.
Set a 100-character maximum for fullName in updateContactSchema.
Prompt for AI agents
In `src/validate/chat.schema.ts` at line 327:
**Long names fail local persistence**
When `fullName` exceeds 100 characters and contact persistence is enabled, the schema accepts the name, but the Contact `pushName` column is limited to 100 characters. Baileys accepts the patch before Prisma rejects the local write, so the request fails and the local contact remains stale.
Set a 100-character maximum for `fullName` in `updateContactSchema`.| } | ||
|
|
||
| // emitOwnEvents is off, so Baileys will not echo this change back to us. | ||
| await this.contactHandle['contacts.update']([{ id: pnJid, name: data.fullName }]); |
There was a problem hiding this comment.
🟡 Medium · Saved avatars disappear
When a contact has a saved avatar and the profile-picture lookup during a name update fails or returns null, contacts.update writes the null profilePicture result into profilePicUrl, replacing the saved avatar, so contact listings lose it.
Preserve the existing profilePicUrl when the lookup fails or returns null.
Prompt for AI agents
In `src/api/integrations/channel/whatsapp/whatsapp.baileys.service.ts` at line 4602:
**Saved avatars disappear**
When a contact has a saved avatar and the profile-picture lookup during a name update fails or returns null, `contacts.update` writes the null `profilePicture` result into `profilePicUrl`, replacing the saved avatar, so contact listings lose it.
Preserve the existing `profilePicUrl` when the lookup fails or returns null.| saveOnPrimaryAddressbook: { type: 'boolean' }, | ||
| }, | ||
| required: ['number', 'fullName'], | ||
| ...isNotEmpty('number', 'fullName'), |
There was a problem hiding this comment.
🟡 Medium · Empty names are accepted
When the request includes an unrecognized property or an optional field and fullName is empty, isNotEmpty skips the minLength check when a property name is outside its enum, so the empty fullName reaches addOrEditContact and leaves the contact with a blank name.
Validate that fullName is nonempty independently of the other request properties.
Prompt for AI agents
In `src/validate/chat.schema.ts` at line 332:
**Empty names are accepted**
When the request includes an unrecognized property or an optional field and `fullName` is empty, `isNotEmpty` skips the `minLength` check when a property name is outside its enum, so the empty `fullName` reaches `addOrEditContact` and leaves the contact with a blank name.
Validate that `fullName` is nonempty independently of the other request properties.| await this.contactHandle['contacts.update']([{ id: pnJid, name: data.fullName }]); | ||
|
|
||
| // Chats that arrived addressed to the LID keep their own contact row. | ||
| if (lidJid && this.configService.get<Database>('DATABASE').SAVE_DATA.CONTACTS) { | ||
| await this.prismaRepository.contact.updateMany({ | ||
| where: { remoteJid: lidJid, instanceId: this.instanceId }, | ||
| data: { pushName: data.fullName }, | ||
| }); | ||
| } |
There was a problem hiding this comment.
🟡 Medium · PN and LID names diverge
When the PN update succeeds but the separate LID contact update fails, the PN row is committed while the LID row retains the old name, so LID-based contact reads remain stale and the caller receives an error despite the accepted WhatsApp patch.
Make the PN and LID contact updates atomic, or retry/reconcile a failed LID update.
Prompt for AI agents
In `src/api/integrations/channel/whatsapp/whatsapp.baileys.service.ts` at lines 4602-4610:
**PN and LID names diverge**
When the PN update succeeds but the separate LID contact update fails, the PN row is committed while the LID row retains the old name, so LID-based contact reads remain stale and the caller receives an error despite the accepted WhatsApp patch.
Make the PN and LID contact updates atomic, or retry/reconcile a failed LID update.
📋 Description
Adds
POST /chat/updateContact/{instance}for Baileys instances. It calls Baileys'addOrEditContact, which sends acontactActionapp-state patch, so a name set through the API is written to the account's WhatsApp contact list instead of staying only in the application's database. The request follows the shape proposed in #2740:{ "number": "5546999103009", "fullName": "Ricardo Cavalcante | Ortopedista", "firstName": "Ricardo", "saveOnPrimaryAddressbook": false }numbertakes a phone number or a JID (@s.whatsapp.netor@lid).firstNameandsaveOnPrimaryAddressbookare optional and are only put in the action when given. Groups, newsletters and broadcasts get a 400.The phone number JID goes in the patch index and the LID goes in
contactAction.lidJid. That's the layout of contact actions that come from the phone: Baileys'processContactActionreads the PN fromindex[1]and notes thatcontactAction.pnJidis usually null. Whichever JID is missing is resolved throughsignalRepository.lidMappingand normalized, sincegetPNForLIDreturns a device JID (...:0@s.whatsapp.net). If a LID is passed and no phone number is known for it, the endpoint returns 400 rather than sending a patch indexed by the LID.Evolution runs Baileys with
emitOwnEvents: false, so the change is not echoed back. Once the patch is accepted, the method runs the existingcontacts.updatehandler for the PN JID (Contact row andCONTACTS_UPDATEwebhook) and also renames the LID Contact row if one exists, sofindContactsstops returning the old name.I left
removeContactout. I wasn't sure what the local Contact row should show after a removal, and it can be its own change.🔗 Related Issue
Closes #2740
🧪 Type of Change
🧪 Testing
I have not tried this against a paired phone. #2711/#2712 attempted the same feature in August, and its author closed it after reporting that the server acknowledged the patch but the name did not show up on the phone. This PR differs from #2712 in a few places: the action leaves
pnJidunset, does not forcesaveOnPrimaryAddressbook: true, and only one patch is sent (no second one indexed by the LID). I don't know whether any of that changes what the device does. One data point: WAHA had the same report (devlikeapro/waha#2285, the API returns true and the phone doesn't change), and the fix they shipped in 2026.9.2 (devlikeapro/waha@235ec01) adds the normalized LID aslidJidwith the PN in the index, which is the action this PR sends. The reporter there hasn't confirmed it yet. @biancastronco, you described this exact setup in #2740; if you can try the branch on your instance, that would settle it.What I ran, on Node 20 as in the CI workflow:
npm run lint:check,npm run db:generateandtsc --noEmitare clean.tsupruns out of memory with the default heap in my 8 GB container, on a cleandevelopas well. With--max-old-space-size=6144it builds.There is no test suite, so I called
updateContactwith a stubbed client and repository (tsx on Node 24, which the runtime needs). 28 checks: PN with and without a known LID, LID with and without a known PN, a PN that the onWhatsApp cache resolves to a LID, group JID, number not on WhatsApp, Baileys throwing (500, nothing written locally), contact persistence turned off, the patch Baileys builds from the action (index: ['contact', <pn>],critical_unblock_low), and the request schema. I didn't commit the script because there is no runner to hook it into. I can add it undertest/if you want it.Manual testing completed
Functionality verified in development environment
No breaking changes introduced
Tested with different connection types (if applicable)
✅ Checklist