Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions src/api/controllers/chat.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import {
ProfileStatusDto,
ReadMessageDto,
SendPresenceDto,
UpdateContactDto,
UpdateMessageDto,
WhatsAppNumberDto,
} from '@api/dto/chat.dto';
Expand Down Expand Up @@ -104,6 +105,10 @@ export class ChatController {
return await this.waMonitor.waInstances[instanceName].updateProfileName(data.name);
}

public async updateContact({ instanceName }: InstanceDto, data: UpdateContactDto) {
return await this.waMonitor.waInstances[instanceName].updateContact(data);
}

public async updateProfileStatus({ instanceName }: InstanceDto, data: ProfileStatusDto) {
return await this.waMonitor.waInstances[instanceName].updateProfileStatus(data.status);
}
Expand Down
7 changes: 7 additions & 0 deletions src/api/dto/chat.dto.ts
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,13 @@ export class ProfileNameDto {
name: string;
}

export class UpdateContactDto {
number: string;
fullName: string;
firstName?: string;
saveOnPrimaryAddressbook?: boolean;
}

export class ProfileStatusDto {
status: string;
}
Expand Down
71 changes: 71 additions & 0 deletions src/api/integrations/channel/whatsapp/whatsapp.baileys.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import {
PrivacySettingDto,
ReadMessageDto,
SendPresenceDto,
UpdateContactDto,
UpdateMessageDto,
WhatsAppNumberDto,
} from '@api/dto/chat.dto';
Expand Down Expand Up @@ -118,6 +119,7 @@ import makeWASocket, {
isJidBroadcast,
isJidGroup,
isJidNewsletter,
isLidUser,
isPnUser,
jidNormalizedUser,
makeCacheableSignalKeyStore,
Expand Down Expand Up @@ -4541,6 +4543,75 @@ export class BaileysStartupService extends ChannelStartupService {
}
}

public async updateContact(data: UpdateContactDto) {
const jid = createJid(data.number);

if (isJidGroup(jid) || isJidNewsletter(jid) || isJidBroadcast(jid)) {
throw new BadRequestException('Only individual contacts can be updated');
}

let pnJid: string | null = null;
let lidJid: string | null = null;

if (isLidUser(jid)) {
lidJid = jid;
} else {
const onWhatsapp = (await this.whatsappNumber({ numbers: [jid] }))?.shift();

if (!onWhatsapp?.exists) {
throw new BadRequestException(onWhatsapp);
}

if (isLidUser(onWhatsapp.jid)) {
lidJid = onWhatsapp.jid;
} else {
pnJid = onWhatsapp.jid;
}
}

const lidMapping = this.client.signalRepository.lidMapping;
if (!pnJid) {
const pn = await lidMapping.getPNForLID(lidJid);
pnJid = pn ? jidNormalizedUser(pn) : isPnUser(jid) ? jid : null;
}
if (!lidJid) {
const lid = await lidMapping.getLIDForPN(pnJid);
lidJid = lid ? jidNormalizedUser(lid) : null;
}

if (!pnJid) {
throw new BadRequestException(`No phone number is known for ${lidJid}`);
}

// Same shape the phone uses when it syncs a contact: the phone number JID
// goes in the patch index and the LID travels inside the action.
const contact: proto.SyncActionValue.IContactAction = { fullName: data.fullName };
if (data.firstName) contact.firstName = data.firstName;
if (lidJid) contact.lidJid = lidJid;
if (data.saveOnPrimaryAddressbook !== undefined) {
contact.saveOnPrimaryAddressbook = data.saveOnPrimaryAddressbook;
}

try {
await this.client.addOrEditContact(pnJid, contact);
} catch (error) {
throw new InternalServerErrorException('Error updating contact', error.toString());
}

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

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.


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

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.


return { update: 'success', remoteJid: pnJid, lid: lidJid, fullName: data.fullName };
}

public async updateProfileStatus(status: string) {
try {
await this.client.updateProfileStatus(status);
Expand Down
12 changes: 12 additions & 0 deletions src/api/routes/chat.router.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import {
ProfileStatusDto,
ReadMessageDto,
SendPresenceDto,
UpdateContactDto,
UpdateMessageDto,
WhatsAppNumberDto,
} from '@api/dto/chat.dto';
Expand All @@ -39,6 +40,7 @@ import {
profileSchema,
profileStatusSchema,
readMessageSchema,
updateContactSchema,
updateMessageSchema,
whatsappNumberSchema,
} from '@validate/validate.schema';
Expand Down Expand Up @@ -257,6 +259,16 @@ export class ChatRouter extends RouterBroker {

return res.status(HttpStatus.OK).json(response);
})
.post(this.routerPath('updateContact'), ...guards, async (req, res) => {
const response = await this.dataValidate<UpdateContactDto>({
request: req,
schema: updateContactSchema,
ClassRef: UpdateContactDto,
execute: (instance, data) => chatController.updateContact(instance, data),
});

return res.status(HttpStatus.OK).json(response);
})
.post(this.routerPath('updateProfileStatus'), ...guards, async (req, res) => {
const response = await this.dataValidate<ProfileStatusDto>({
request: req,
Expand Down
13 changes: 13 additions & 0 deletions src/validate/chat.schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -319,6 +319,19 @@ export const profileNameSchema: JSONSchema7 = {
...isNotEmpty('name'),
};

export const updateContactSchema: JSONSchema7 = {
$id: v4(),
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`.

firstName: { type: 'string' },
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.

};

export const profileStatusSchema: JSONSchema7 = {
$id: v4(),
type: 'object',
Expand Down