Skip to content

feat(chat): add updateContact endpoint to set a contact's name - #2758

Open
luantaraschi wants to merge 1 commit into
evolution-foundation:developfrom
luantaraschi:feat/update-contact
Open

luantaraschi wants to merge 1 commit into
evolution-foundation:developfrom
luantaraschi:feat/update-contact

Conversation

@luantaraschi

@luantaraschi luantaraschi commented Oct 15, 2026 •

Copy link
Copy Markdown

📋 Description

Adds POST /chat/updateContact/{instance} for Baileys instances. It calls Baileys' addOrEditContact, which sends a contactAction app-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
}

number takes a phone number or a JID (@s.whatsapp.net or @lid). firstName and saveOnPrimaryAddressbook are 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' processContactAction reads the PN from index[1] and notes that contactAction.pnJid is usually null. Whichever JID is missing is resolved through signalRepository.lidMapping and normalized, since getPNForLID returns 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 existing contacts.update handler for the PN JID (Contact row and CONTACTS_UPDATE webhook) and also renames the LID Contact row if one exists, so findContacts stops returning the old name.

I left removeContact out. 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

  • 🐛 Bug fix (non-breaking change which fixes an issue)
  • ✨ New feature (non-breaking change which adds functionality)
  • 💥 Breaking change (fix or feature that would cause existing functionality to not work as expected)
  • 📚 Documentation update
  • 🔧 Refactoring (no functional changes)
  • ⚡ Performance improvement
  • 🧹 Code cleanup
  • 🔒 Security fix

🧪 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 pnJid unset, does not force saveOnPrimaryAddressbook: 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 as lidJid with 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:generate and tsc --noEmit are clean.

  • tsup runs out of memory with the default heap in my 8 GB container, on a clean develop as well. With --max-old-space-size=6144 it builds.

  • There is no test suite, so I called updateContact with 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 under test/ if you want it.

  • Manual testing completed

  • Functionality verified in development environment

  • No breaking changes introduced

  • Tested with different connection types (if applicable)

✅ Checklist

  • My code follows the project's style guidelines
  • I have performed a self-review of my code
  • I have commented my code, particularly in hard-to-understand areas
  • I have made corresponding changes to the documentation
  • My changes generate no new warnings
  • I have manually tested my changes thoroughly
  • I have verified the changes work with different scenarios
  • Any dependent changes have been merged and published

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
@sourcery-ai

sourcery-ai Bot commented Oct 15, 2026 •

Copy link
Copy Markdown
Contributor

Reviewer's Guide

Adds 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 endpoint

sequenceDiagram
    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
Loading

Flow diagram for contact identity validation

flowchart 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"]
Loading

File-Level Changes

Change Details Files
Exposes a validated POST endpoint for updating an individual contact's name.
  • Adds the updateContact controller delegation and router registration under the instance-scoped chat route.
  • Defines DTO and JSON schema validation for required number/fullName fields and optional firstName/saveOnPrimaryAddressbook fields.
  • Rejects group, newsletter, broadcast, and unresolved contact identifiers through request validation and BadRequestException paths.
src/api/controllers/chat.controller.ts
src/api/dto/chat.dto.ts
src/api/routes/chat.router.ts
src/validate/chat.schema.ts
Resolves phone-number and LID identities and writes the contact name through Baileys' app-state contact action.
  • Resolves PN/LID mappings in both directions and normalizes device JIDs before constructing the action.
  • Builds a single contact action with the phone JID in the patch identity and optional LID, first name, and address-book fields in the payload.
  • Delegates to addOrEditContact and converts Baileys failures into a 500 response without performing local persistence.
src/api/integrations/channel/whatsapp/whatsapp.baileys.service.ts
Synchronizes local contact records after a successful Baileys update.
  • Invokes the existing contacts.update handler for the PN contact and webhook flow.
  • Updates any corresponding LID contact row when contact persistence is enabled.
  • Returns the PN JID, LID, and updated full name in the success response.
src/api/integrations/channel/whatsapp/whatsapp.baileys.service.ts

Possibly linked issues


Tips and commands

Interacting with Sourcery

  • Trigger a new review: Comment @sourcery-ai review on the pull request.
  • Continue discussions: Reply directly to Sourcery's review comments.
  • Generate a GitHub issue from a review comment: Ask Sourcery to create an
    issue from a review comment by replying to it. You can also reply to a
    review comment with @sourcery-ai issue to create an issue from it.
  • Generate a pull request title: Write @sourcery-ai anywhere in the pull
    request title to generate a title at any time. You can also comment
    @sourcery-ai title on the pull request to (re-)generate the title at any time.
  • Generate a pull request summary: Write @sourcery-ai summary anywhere in
    the pull request body to generate a PR summary at any time exactly where you
    want it. You can also comment @sourcery-ai summary on the pull request to
    (re-)generate the summary at any time.
  • Generate reviewer's guide: Comment @sourcery-ai guide on the pull
    request to (re-)generate the reviewer's guide at any time.
  • Resolve all Sourcery comments: Comment @sourcery-ai resolve on the
    pull request to resolve all Sourcery comments. Useful if you've already
    addressed all the comments and don't want to see them anymore.
  • Dismiss all Sourcery reviews: Comment @sourcery-ai dismiss on the pull
    request to dismiss all existing Sourcery reviews. Especially useful if you
    want to start fresh with a new review - don't forget to comment
    @sourcery-ai review to trigger a new review!

Customizing Your Experience

Access your dashboard to:

  • Enable or disable review features such as the Sourcery-generated pull request
    summary, the reviewer's guide, and others.
  • Change the review language.
  • Add, remove or edit custom review instructions.
  • Adjust other review settings.

Getting Help

@sourcery-ai sourcery-ai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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


Sourcery is free for open source - if you like our reviews please consider sharing them ✨

}

// emitOwnEvents is off, so Baileys will not echo this change back to us.
await this.contactHandle['contacts.update']([{ id: pnJid, name: data.fullName }]);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 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' },

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 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 }]);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 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'),

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 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.

Comment on lines +4602 to +4610
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 },
});
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 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.

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant