From 11693c81b3ec21a23f5d81d98482ba20847d2cef Mon Sep 17 00:00:00 2001 From: kkzaadev Date: Sun, 7 Jun 2026 13:18:23 +0700 Subject: [PATCH 1/5] Wire bridge label events, ReceiptType "sent", and wantedPreKeyCount option Mirrors the bridge surface added in WhiskeySockets/whatsapp-rust-bridge#19; depends on that PR's new events/types being merged and published. - events: adapt `label_edit_update` / `label_association_update` and dispatch to upstream `labels.edit` / `labels.association` (Label / ChatLabelAssociation) - receipts: map the new `Sent` ReceiptType ("sent") in RECEIPT_TYPE_MAP and the CanonicalReceipt union - api: thread `wantedPreKeyCount` from SocketConfig through to createWhatsAppClient --- src/Bridge/schema.ts | 25 +++++++++++++++++++++++++ src/Bridge/types.ts | 31 +++++++++++++++++++++++++++++++ src/Socket/events.ts | 18 ++++++++++++++++++ src/Socket/index.ts | 3 ++- src/Types/Socket.ts | 8 ++++++++ 5 files changed, 84 insertions(+), 1 deletion(-) diff --git a/src/Bridge/schema.ts b/src/Bridge/schema.ts index c568a0b..a68dbc6 100644 --- a/src/Bridge/schema.ts +++ b/src/Bridge/schema.ts @@ -291,6 +291,29 @@ const ADAPTERS = { if (!jid) return null return { type: 'markChatAsReadUpdate', jid, read: asBoolOr(extractAction(data)?.read, true) } }, + label_edit_update: data => { + const labelId = asString(data.label_id) + if (!labelId) return { type: 'noop', bridgeType: 'label_edit_update' } + const action = extractAction(data) + // `predefinedId` is proto `predefined_id` (a number); upstream `Label` + // wants it as a string. Dual-read the spelling, then stringify. + const predefined = asNumber(action?.predefinedId ?? action?.predefined_id) + return { + type: 'labelEdit', + labelId, + name: asString(action?.name) ?? '', + color: asNumber(action?.color) ?? 0, + deleted: asBoolOr(action?.deleted, false), + predefinedId: predefined != null ? String(predefined) : undefined + } + }, + label_association_update: data => { + const labelId = asString(data.label_id) + const chatJid = asJidString(data.chat_jid) + if (!labelId || !chatJid) return { type: 'noop', bridgeType: 'label_association_update' } + // `action.labeled === true` → label added to the chat, else removed. + return { type: 'labelAssociation', labelId, chatJid, labeled: asBoolOr(extractAction(data)?.labeled, true) } + }, // ── Calls ── incoming_call: (data, logger) => adaptIncomingCall(data, logger), @@ -579,6 +602,7 @@ const adaptContactUpdate = (data: unknown): CanonicalEvent | null => { */ const RECEIPT_TYPE_MAP: Record> = { Delivered: 'delivered', + Sent: 'sent', Sender: 'sender', Retry: 'retry', EncRekeyRetry: 'enc-rekey-retry', @@ -591,6 +615,7 @@ const RECEIPT_TYPE_MAP: Record + // Upstream `labels.edit` carries a `Label`. A delete arrives with + // `deleted: true` (consumers remove it from their store). + ctx.ev.emit('labels.edit', { + id: evt.labelId, + name: evt.name, + color: evt.color, + deleted: evt.deleted, + predefinedId: evt.predefinedId + }), + labelAssociation: (evt, { ctx }) => + // Inbound sync only ever carries chat associations (the bridge event + // has a `chat_jid`); message-label associations are a separate path. + ctx.ev.emit('labels.association', { + association: { type: LabelAssociationType.Chat, chatId: evt.chatJid, labelId: evt.labelId }, + type: evt.labeled ? 'add' : 'remove' + }), // ── Calls ── incomingCall: (evt, { ctx }) => { diff --git a/src/Socket/index.ts b/src/Socket/index.ts index f2955e6..9e48c69 100644 --- a/src/Socket/index.ts +++ b/src/Socket/index.ts @@ -389,7 +389,8 @@ const makeWASocket = (config: UserFacingSocketConfig) => { handleEvent, bridgeStore, fullConfig.cache ?? null, - fullConfig.version + fullConfig.version, + fullConfig.wantedPreKeyCount ?? null ) // Make this client the fallback for standalone helpers like // downloadContentFromMessage that have no socket reference. diff --git a/src/Types/Socket.ts b/src/Types/Socket.ts index a01c4a7..7de377d 100644 --- a/src/Types/Socket.ts +++ b/src/Types/Socket.ts @@ -74,6 +74,14 @@ export type SocketConfig = { */ deviceProps?: DevicePropsInput + /** + * Number of one-time pre-keys generated and uploaded per batch. Defaults to + * WhatsApp Web's 812; clamped to the protocol-safe range at upload time. + * Lower it to cut memory/CPU of the first pre-key upload (the whole batch is + * generated and encoded in one shot). Must be set before connecting. + */ + wantedPreKeyCount?: number + // ───────────────────────────────────────────────────────────────────── // Upstream-Baileys options accepted for type-level compatibility. // From b68e14248e859e2a037aa338e904ad2fd896ce22 Mon Sep 17 00:00:00 2001 From: kkzaadev Date: Wed, 10 Jun 2026 01:22:29 +0700 Subject: [PATCH 2/5] Wire played receipts, saveContact, and verified name from bridge 3b2bfa3 Baileyrs-side companion to the whatsapp-rust-bridge 3b2bfa3 sync (sync-log 045/047/051). - receipts: route sendReceipt/sendReceipts type 'played' -> client.markPlayed for voice/video-note played receipts (#737); was previously a no-op warning. Doc comment moves 'played' from "Not supported" to "Supported via bridge". - chat-actions: route chatModify({ contact }) -> client.saveContact for contact save/rename (#742); drop 'contact' from the unsupported list. contact: null (removal) has no bridge/core path yet, so it is ignored. - contacts: surface verifiedName on OnWhatsAppResult, mapped in onWhatsApp from the bridge's verified_name (#741). package.json intentionally not included. --- src/Socket/chat-actions.ts | 14 +++++++++++++- src/Socket/contacts.ts | 3 +++ src/Socket/messages.ts | 24 +++++++++++++++++++++--- 3 files changed, 37 insertions(+), 4 deletions(-) diff --git a/src/Socket/chat-actions.ts b/src/Socket/chat-actions.ts index 25f4ef4..74465e5 100644 --- a/src/Socket/chat-actions.ts +++ b/src/Socket/chat-actions.ts @@ -45,9 +45,21 @@ export const makeChatActionMethods = (ctx: SocketContext) => ({ await client.deleteMessageForMe(jid, mod.deleteForMe.key.id!, !!mod.deleteForMe.key.fromMe) } else if ('pushNameSetting' in mod) { await client.setPushName(mod.pushNameSetting) + } else if ('contact' in mod) { + // Save/rename a contact (syncs the name to linked devices). `jid` is the + // contact's bare PN jid. `contact: null` (removal) has no bridge/core path + // yet, so it is ignored. + if (mod.contact) { + await client.saveContact( + jid, + mod.contact.fullName ?? undefined, + mod.contact.firstName ?? undefined, + mod.contact.saveOnPrimaryAddressbook ?? true + ) + } } else { // App-state-patch variants not yet exposed by bridge: - // clear, contact, disableLinkPreviews, addLabel, addChatLabel, + // clear, disableLinkPreviews, addLabel, addChatLabel, // removeChatLabel, addMessageLabel, removeMessageLabel, quickReply const variant = Object.keys(mod)[0] ctx.logger.warn( diff --git a/src/Socket/contacts.ts b/src/Socket/contacts.ts index 9d57174..3e72598 100644 --- a/src/Socket/contacts.ts +++ b/src/Socket/contacts.ts @@ -7,6 +7,8 @@ export type OnWhatsAppResult = { /** PN counterpart, present when the server returned a LID as primary JID. */ pnJid?: string isBusiness?: boolean + /** Verified business name (from usync ``), present for verified businesses. */ + verifiedName?: string } export const makeContactMethods = (ctx: SocketContext) => ({ @@ -19,6 +21,7 @@ export const makeContactMethods = (ctx: SocketContext) => ({ const out: OnWhatsAppResult = { exists: r.isRegistered, jid: r.jid, isBusiness: r.isBusiness } if (r.lid) out.lid = r.lid if (r.pnJid) out.pnJid = r.pnJid + if (r.verifiedName) out.verifiedName = r.verifiedName return out }) }, diff --git a/src/Socket/messages.ts b/src/Socket/messages.ts index 8e27f25..a3fad40 100644 --- a/src/Socket/messages.ts +++ b/src/Socket/messages.ts @@ -211,9 +211,9 @@ export const makeMessageMethods = (ctx: SocketContext) => ({ * Send a receipt for messages. The bridge handles most receipt types automatically * (delivered, sender). Use `readMessages` for the common case of sending read receipts. * - * Supported types via bridge: 'read', 'read-self' + * Supported types via bridge: 'read', 'read-self', 'played' * Auto-handled by bridge: 'sender', 'inactive', undefined (delivered) - * Not supported: 'played', 'hist_sync', 'peer_msg' (logged as warning) + * Not supported: 'hist_sync', 'peer_msg' (logged as warning) */ sendReceipt: async (jid: string, participant: string | undefined, messageIds: string[], type: MessageReceiptType) => { if (!messageIds.length) return @@ -225,9 +225,20 @@ export const makeMessageMethods = (ctx: SocketContext) => ({ ...(participant ? { participant } : {}) })) await (await ctx.getClient()).readMessages(keys) + } else if (type === 'played') { + // Voice/video-note played receipts. The bridge (and core) pick the wire + // type (`played` vs `played-self` for newsletters) and the `participant` + // attr from the chat jid, so we just hand over the keys — same shape as + // readMessages. + const keys = messageIds.map(id => ({ + remoteJid: jid, + id, + ...(participant ? { participant } : {}) + })) + await (await ctx.getClient()).markPlayed(keys) } else { // delivered/sender/inactive receipts are sent automatically by the Rust bridge - // played/hist_sync/peer_msg require bridge-side support + // hist_sync/peer_msg require bridge-side support ctx.logger.debug( { type, jid, count: messageIds.length }, 'sendReceipt: type handled automatically by bridge or not yet supported' @@ -248,6 +259,13 @@ export const makeMessageMethods = (ctx: SocketContext) => ({ if (readKeys.length) { await client.readMessages(readKeys) } + } else if (type === 'played') { + const playedKeys = keys + .filter(k => !k.fromMe && k.remoteJid && k.id) + .map(k => ({ remoteJid: k.remoteJid!, id: k.id!, ...(k.participant ? { participant: k.participant } : {}) })) + if (playedKeys.length) { + await client.markPlayed(playedKeys) + } } else { ctx.logger.debug( { type, count: keys.length }, From 7a88c3fe5a763b57d85666a545c70b4e37181f50 Mon Sep 17 00:00:00 2001 From: kkzaadev Date: Wed, 10 Jun 2026 16:46:04 +0700 Subject: [PATCH 3/5] Wire clearChat, newsletterMute, and userStatusMute from bridge 20189cb0 Baileyrs-side companion to the whatsapp-rust-bridge 20189cb0 sync (sync-log 055-107). - chat: route chatModify({ clear }) -> client.clearChat (#755); adapt incoming clear_chat_update -> messages.delete { jid, all: true } - newsletter: add newsletterMute(jid, mute) -> client.newsletterMute (#757) - events: noop-adapt user_status_mute_update -- forwarded for surface completeness, but Baileys has no status-mute event to map it onto (#760) package.json intentionally not included. --- src/Bridge/schema.ts | 10 ++++++++++ src/Bridge/types.ts | 7 +++++++ src/Socket/chat-actions.ts | 14 +++++++++++--- src/Socket/events.ts | 1 + src/Socket/newsletter.ts | 8 ++++++++ 5 files changed, 37 insertions(+), 3 deletions(-) diff --git a/src/Bridge/schema.ts b/src/Bridge/schema.ts index a68dbc6..e51a1a0 100644 --- a/src/Bridge/schema.ts +++ b/src/Bridge/schema.ts @@ -421,6 +421,16 @@ const ADAPTERS = { const jid = asJidString(data.jid) return jid ? { type: 'chatDelete', jid } : { type: 'noop', bridgeType: 'delete_chat_update' } }, + clear_chat_update: data => { + // Clear = drop all messages but keep the chat. Maps to upstream + // `messages.delete` `{ jid, all: true }` (the chat-clear surface noted in + // the messageDelete dispatcher), distinct from chatDelete (whole chat gone). + const jid = asJidString(data.jid) + return jid ? { type: 'chatClear', jid } : { type: 'noop', bridgeType: 'clear_chat_update' } + }, + // Muting a contact's status (stories) updates. Forwarded for surface completeness, + // but noop'd: upstream Baileys has no status-mute event/chatModify to map it onto. + user_status_mute_update: () => ({ type: 'noop', bridgeType: 'user_status_mute_update' }), delete_message_for_me_update: data => { const chatJid = asJidString(data.chat_jid) const messageId = asString(data.message_id) diff --git a/src/Bridge/types.ts b/src/Bridge/types.ts index 69b5d78..3f9f45b 100644 --- a/src/Bridge/types.ts +++ b/src/Bridge/types.ts @@ -475,6 +475,12 @@ export interface CanonicalChatDelete { jid: string } +/** A chat's messages were cleared (the chat itself is kept) via app-state sync. */ +export interface CanonicalChatClear { + type: 'chatClear' + jid: string +} + /** "Delete for me" of a single message via app-state sync. */ export interface CanonicalMessageDelete { type: 'messageDelete' @@ -626,6 +632,7 @@ export type CanonicalEvent = | CanonicalLidMappingUpdate | CanonicalNewsletterLiveUpdate | CanonicalChatDelete + | CanonicalChatClear | CanonicalMessageDelete | CanonicalDisappearingModeChanged | CanonicalHistorySync diff --git a/src/Socket/chat-actions.ts b/src/Socket/chat-actions.ts index 74465e5..6aba813 100644 --- a/src/Socket/chat-actions.ts +++ b/src/Socket/chat-actions.ts @@ -22,8 +22,8 @@ export const makeChatActionMethods = (ctx: SocketContext) => ({ * Compatibility wrapper for original Baileys chatModify API. * Routes to the appropriate bridge method based on the modification type. * - * Fully supported: archive, pin, mute, star, markRead, delete, deleteForMe, pushNameSetting - * Not yet in bridge (app-state patches): clear, contact, disableLinkPreviews, labels, quickReply + * Fully supported: archive, pin, mute, star, markRead, delete, deleteForMe, pushNameSetting, contact, clear + * Not yet in bridge (app-state patches): disableLinkPreviews, labels, quickReply */ chatModify: async (mod: ChatModification, jid: string) => { const client = await ctx.getClient() @@ -57,9 +57,17 @@ export const makeChatActionMethods = (ctx: SocketContext) => ({ mod.contact.saveOnPrimaryAddressbook ?? true ) } + } else if ('clear' in mod) { + // Clear a chat's messages while keeping the chat. `lastMessages` (the + // message range) is ignored, same as the `delete` branch — the bridge + // clears the whole chat. deleteStarred/deleteMedia aren't part of the + // Baileys `clear` shape, so default both to false (keep starred + media). + if (mod.clear) { + await client.clearChat(jid, false, false) + } } else { // App-state-patch variants not yet exposed by bridge: - // clear, disableLinkPreviews, addLabel, addChatLabel, + // disableLinkPreviews, addLabel, addChatLabel, // removeChatLabel, addMessageLabel, removeMessageLabel, quickReply const variant = Object.keys(mod)[0] ctx.logger.warn( diff --git a/src/Socket/events.ts b/src/Socket/events.ts index edc1a06..f55e16c 100644 --- a/src/Socket/events.ts +++ b/src/Socket/events.ts @@ -589,6 +589,7 @@ const DISPATCHERS: DispatcherMap = { } }, chatDelete: (evt, { ctx }) => ctx.ev.emit('chats.delete', [evt.jid]), + chatClear: (evt, { ctx }) => ctx.ev.emit('messages.delete', { jid: evt.jid, all: true }), messageDelete: (evt, { ctx }) => // Upstream `chat-utils.ts:857` shape: `{ keys: WAMessageKey[] }` for // per-message delete (not the `{ jid, all: true }` chat-clear case diff --git a/src/Socket/newsletter.ts b/src/Socket/newsletter.ts index 70148b7..77b3438 100644 --- a/src/Socket/newsletter.ts +++ b/src/Socket/newsletter.ts @@ -19,5 +19,13 @@ export const makeNewsletterMethods = (ctx: SocketContext) => ({ newsletterReactMessage: async (jid: string, serverId: string, reaction?: string) => { await (await ctx.getClient()).newsletterReactMessage(jid, serverId, reaction ?? null) + }, + + /** + * Mute or unmute a newsletter (channel) — silences its follower-activity + * notifications, the mute a subscriber toggles. `mute = true` silences. + */ + newsletterMute: async (jid: string, mute: boolean) => { + await (await ctx.getClient()).newsletterMute(jid, mute) } }) From f0831949f1d434635b9a8d2efe724ae1e42eef4d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20Lucas?= Date: Wed, 10 Jun 2026 18:12:49 -0300 Subject: [PATCH 4/5] test: cover the new bridge-event adapters; polish predefinedId narrowing - tests: label_edit_update full mapping (predefinedId int -> string) and sparse-action defaults; label_association_update add/remove; clear_chat_update -> chatClear plus the missing-jid noop; user_status_mute_update noop. - schema: narrow predefinedId per spelling (asNumber(camel) ?? asNumber(snake), the mute adapter's pattern) so a non-numeric camel value falls through to the snake one instead of discarding it. - types: move the CanonicalLabel* block out of the union section into the chat-state region where the other sync-action canonicals live. --- src/Bridge/__tests__/adapt.test.ts | 85 ++++++++++++++++++++++++++++++ src/Bridge/schema.ts | 2 +- src/Bridge/types.ts | 56 ++++++++++---------- 3 files changed, 114 insertions(+), 29 deletions(-) diff --git a/src/Bridge/__tests__/adapt.test.ts b/src/Bridge/__tests__/adapt.test.ts index c080413..cf2bb16 100644 --- a/src/Bridge/__tests__/adapt.test.ts +++ b/src/Bridge/__tests__/adapt.test.ts @@ -385,6 +385,91 @@ describe('adaptBridgeEvent — anti-corruption layer', () => { expect(result).toEqual({ type: 'archiveUpdate', jid: '5511@s.whatsapp.net', archived: false }) }) + it('label_edit_update maps to canonical labelEdit (predefinedId stringified)', () => { + const result = adaptBridgeEvent({ + type: 'label_edit_update', + data: { + label_id: '7', + action: { name: 'Clientes', color: 3, deleted: false, predefinedId: 2 } + } + } as never) + expect(result).toEqual({ + type: 'labelEdit', + labelId: '7', + name: 'Clientes', + color: 3, + deleted: false, + predefinedId: '2' + }) + }) + + it('label_edit_update tolerates a sparse action (typed defaults, no undefined leaks)', () => { + const result = adaptBridgeEvent({ + type: 'label_edit_update', + data: { label_id: '7', action: { deleted: true } } + } as never) + expect(result).toEqual({ + type: 'labelEdit', + labelId: '7', + name: '', + color: 0, + deleted: true, + predefinedId: undefined + }) + }) + + it('label_association_update maps add/remove from action.labeled', () => { + const add = adaptBridgeEvent({ + type: 'label_association_update', + data: { + label_id: '7', + chat_jid: { user: '5511', server: 's.whatsapp.net' }, + action: { labeled: true } + } + } as never) + expect(add).toEqual({ + type: 'labelAssociation', + labelId: '7', + chatJid: '5511@s.whatsapp.net', + labeled: true + }) + + const remove = adaptBridgeEvent({ + type: 'label_association_update', + data: { + label_id: '7', + chat_jid: { user: '5511', server: 's.whatsapp.net' }, + action: { labeled: false } + } + } as never) + expect(remove).toEqual({ + type: 'labelAssociation', + labelId: '7', + chatJid: '5511@s.whatsapp.net', + labeled: false + }) + }) + + it('clear_chat_update maps to chatClear (messages.delete all) and noops without a jid', () => { + expect( + adaptBridgeEvent({ + type: 'clear_chat_update', + data: { jid: { user: '5511', server: 's.whatsapp.net' } } + } as never) + ).toEqual({ type: 'chatClear', jid: '5511@s.whatsapp.net' }) + expect(adaptBridgeEvent({ type: 'clear_chat_update', data: {} } as never)).toEqual({ + type: 'noop', + bridgeType: 'clear_chat_update' + }) + }) + + it('user_status_mute_update is a documented noop (no Baileys counterpart)', () => { + expect(adaptBridgeEvent({ type: 'user_status_mute_update', data: {} } as never)).toEqual({ + type: 'noop', + bridgeType: 'user_status_mute_update' + }) + }) + it('star_update reads action.starred even when wrapped', () => { const result = adaptBridgeEvent({ type: 'star_update', diff --git a/src/Bridge/schema.ts b/src/Bridge/schema.ts index e51a1a0..2f6b4c0 100644 --- a/src/Bridge/schema.ts +++ b/src/Bridge/schema.ts @@ -297,7 +297,7 @@ const ADAPTERS = { const action = extractAction(data) // `predefinedId` is proto `predefined_id` (a number); upstream `Label` // wants it as a string. Dual-read the spelling, then stringify. - const predefined = asNumber(action?.predefinedId ?? action?.predefined_id) + const predefined = asNumber(action?.predefinedId) ?? asNumber(action?.predefined_id) return { type: 'labelEdit', labelId, diff --git a/src/Bridge/types.ts b/src/Bridge/types.ts index 3f9f45b..1335df9 100644 --- a/src/Bridge/types.ts +++ b/src/Bridge/types.ts @@ -381,6 +381,34 @@ export interface CanonicalMarkChatAsReadUpdate { read: boolean } +// ── Labels ── + +/** + * A label was created, renamed/recolored, or deleted on a linked device + * (bridge `label_edit_update`). Maps to upstream `labels.edit` (`Label`). + */ +export interface CanonicalLabelEdit { + type: 'labelEdit' + labelId: string + name: string + color: number + deleted: boolean + /** Predefined-label id, stringified to match upstream `Label.predefinedId`. */ + predefinedId?: string +} + +/** + * A label was associated with / removed from a chat on a linked device + * (bridge `label_association_update`). Maps to upstream `labels.association`. + */ +export interface CanonicalLabelAssociation { + type: 'labelAssociation' + labelId: string + chatJid: string + /** `true` = label added to the chat, `false` = removed. */ + labeled: boolean +} + // ── Calls ── export type CanonicalCallActionType = 'offer' | 'preAccept' | 'accept' | 'reject' | 'terminate' @@ -571,34 +599,6 @@ export interface CanonicalNoop { // ── Union ── -// ── Labels ── - -/** - * A label was created, renamed/recolored, or deleted on a linked device - * (bridge `label_edit_update`). Maps to upstream `labels.edit` (`Label`). - */ -export interface CanonicalLabelEdit { - type: 'labelEdit' - labelId: string - name: string - color: number - deleted: boolean - /** Predefined-label id, stringified to match upstream `Label.predefinedId`. */ - predefinedId?: string -} - -/** - * A label was associated with / removed from a chat on a linked device - * (bridge `label_association_update`). Maps to upstream `labels.association`. - */ -export interface CanonicalLabelAssociation { - type: 'labelAssociation' - labelId: string - chatJid: string - /** `true` = label added to the chat, `false` = removed. */ - labeled: boolean -} - export type CanonicalEvent = | CanonicalConnected | CanonicalDisconnected From b53cd611186ccdc3a7cf0901a9cb9d39263d1b33 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20Lucas?= Date: Wed, 10 Jun 2026 18:26:29 -0300 Subject: [PATCH 5/5] fix(chat): warn on chatModify contact removal instead of silently dropping it contact: null is upstream Baileys' remove-contact variant; the bridge/core has no removal path yet, so surface the gap through the same logger.warn convention the unsupported-variant fallback uses (review: cubic P2). --- src/Socket/chat-actions.ts | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/Socket/chat-actions.ts b/src/Socket/chat-actions.ts index 6aba813..46ac6f3 100644 --- a/src/Socket/chat-actions.ts +++ b/src/Socket/chat-actions.ts @@ -47,8 +47,7 @@ export const makeChatActionMethods = (ctx: SocketContext) => ({ await client.setPushName(mod.pushNameSetting) } else if ('contact' in mod) { // Save/rename a contact (syncs the name to linked devices). `jid` is the - // contact's bare PN jid. `contact: null` (removal) has no bridge/core path - // yet, so it is ignored. + // contact's bare PN jid. if (mod.contact) { await client.saveContact( jid, @@ -56,6 +55,10 @@ export const makeChatActionMethods = (ctx: SocketContext) => ({ mod.contact.firstName ?? undefined, mod.contact.saveOnPrimaryAddressbook ?? true ) + } else { + // `contact: null` = remove-contact in upstream Baileys; no + // bridge/core path yet — warn instead of silently dropping. + ctx.logger.warn({ jid }, 'chatModify: contact removal (contact: null) not yet supported by bridge') } } else if ('clear' in mod) { // Clear a chat's messages while keeping the chat. `lastMessages` (the