From 5351dce3a573d441ca9327b97bdc6ee277355b5d Mon Sep 17 00:00:00 2001 From: Renato Alcara Date: Sat, 25 Apr 2026 12:15:01 -0300 Subject: [PATCH] feat: add inbound username support and USync username protocol MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Aligns with Baileys upstream PR #2480. WhatsApp is rolling out an inbound username field that maps to the user's LID. This change is purely additive — it captures and propagates the new optional field through types, decoders, USync queries, and group/contact events. Skipped intentionally to preserve InfiniteAPI's LID/PN customization: - handleGroupNotification in messages-recv.ts (custom LID->PN flow on groups.upsert / participants.map). Username for these specific events still flows via process-message's emitParticipantsUpdate (reads message.key.participantUsername added in decode-wa-message). Carousel/buttons code (messages.ts, messages-send.ts) untouched. Co-Authored-By: Claude Opus 4.7 (1M context) --- src/Socket/groups.ts | 6 ++++ src/Socket/messages-recv.ts | 1 + src/Types/Contact.ts | 2 ++ src/Types/Events.ts | 2 ++ src/Types/GroupMetadata.ts | 4 +++ src/Types/Message.ts | 2 ++ src/Utils/chat-utils.ts | 1 + src/Utils/decode-wa-message.ts | 4 +++ src/Utils/history.ts | 1 + src/Utils/process-message.ts | 10 +++++- src/Utils/sync-action-utils.ts | 1 + src/WAUSync/Protocols/USyncContactProtocol.ts | 32 +++++++++++++++++-- .../Protocols/USyncUsernameProtocol.ts | 28 ++++++++++++++++ src/WAUSync/Protocols/index.ts | 1 + src/WAUSync/USyncQuery.ts | 8 ++++- src/WAUSync/USyncUser.ts | 12 +++++++ 16 files changed, 110 insertions(+), 5 deletions(-) create mode 100644 src/WAUSync/Protocols/USyncUsernameProtocol.ts diff --git a/src/Socket/groups.ts b/src/Socket/groups.ts index 0c4716da..18cab5c6 100644 --- a/src/Socket/groups.ts +++ b/src/Socket/groups.ts @@ -343,11 +343,13 @@ export const extractGroupMetadata = (result: BinaryNode) => { let descId: string | undefined let descOwner: string | undefined let descOwnerPn: string | undefined + let descOwnerUsername: string | undefined let descTime: number | undefined if (descChild) { desc = getBinaryNodeChildString(descChild, 'body') descOwner = descChild.attrs.participant ? jidNormalizedUser(descChild.attrs.participant) : undefined descOwnerPn = descChild.attrs.participant_pn ? jidNormalizedUser(descChild.attrs.participant_pn) : undefined + descOwnerUsername = descChild.attrs.participant_username || undefined descTime = +descChild.attrs.t! descId = descChild.attrs.id } @@ -362,16 +364,19 @@ export const extractGroupMetadata = (result: BinaryNode) => { subject: group.attrs.subject!, subjectOwner: group.attrs.s_o, subjectOwnerPn: group.attrs.s_o_pn, + subjectOwnerUsername: group.attrs.s_o_username, subjectTime: +(group.attrs.s_t ?? '0'), size: group.attrs.size ? +group.attrs.size : getBinaryNodeChildren(group, 'participant').length, creation: +(group.attrs.creation ?? '0'), owner: group.attrs.creator ? jidNormalizedUser(group.attrs.creator) : undefined, ownerPn: group.attrs.creator_pn ? jidNormalizedUser(group.attrs.creator_pn) : undefined, + ownerUsername: group.attrs.creator_username || undefined, owner_country_code: group.attrs.creator_country_code, desc, descId, descOwner, descOwnerPn, + descOwnerUsername, descTime, linkedParent: getBinaryNodeChild(group, 'linked_parent')?.attrs.jid || undefined, restrict: !!getBinaryNodeChild(group, 'locked'), @@ -386,6 +391,7 @@ export const extractGroupMetadata = (result: BinaryNode) => { id: attrs.jid!, phoneNumber: isLidUser(attrs.jid) && isPnUser(attrs.phone_number) ? attrs.phone_number : undefined, lid: isPnUser(attrs.jid) && isLidUser(attrs.lid) ? attrs.lid : undefined, + username: attrs.participant_username || attrs.username || undefined, admin: (attrs.type || null) as GroupParticipant['admin'] } }), diff --git a/src/Socket/messages-recv.ts b/src/Socket/messages-recv.ts index 51f9c5e1..1f6f909c 100644 --- a/src/Socket/messages-recv.ts +++ b/src/Socket/messages-recv.ts @@ -2223,6 +2223,7 @@ export const makeMessagesRecvSocket = (config: SocketConfig) => { fromMe, participant: node.attrs.participant, participantAlt, + participantUsername: node.attrs.participant_username, addressingMode, id: node.attrs.id, ...(msg.key || {}) diff --git a/src/Types/Contact.ts b/src/Types/Contact.ts index 315f7e15..836185b8 100644 --- a/src/Types/Contact.ts +++ b/src/Types/Contact.ts @@ -9,6 +9,8 @@ export interface Contact { name?: string /** name of the contact, the contact has set on their own on WA */ notify?: string + /** username associated with this contact, when provided by WA */ + username?: string /** I have no idea */ verifiedName?: string // Baileys Added diff --git a/src/Types/Events.ts b/src/Types/Events.ts index 96a14096..5bea4e29 100644 --- a/src/Types/Events.ts +++ b/src/Types/Events.ts @@ -81,6 +81,7 @@ export type BaileysEventMap = { id: string author: string authorPn?: string + authorUsername?: string participants: GroupParticipant[] action: ParticipantAction } @@ -88,6 +89,7 @@ export type BaileysEventMap = { id: string author: string authorPn?: string + authorUsername?: string participant: string participantPn?: string action: RequestJoinAction diff --git a/src/Types/GroupMetadata.ts b/src/Types/GroupMetadata.ts index 97bbe1a8..4465e47e 100644 --- a/src/Types/GroupMetadata.ts +++ b/src/Types/GroupMetadata.ts @@ -20,17 +20,20 @@ export interface GroupMetadata { addressingMode?: WAMessageAddressingMode owner: string | undefined ownerPn?: string | undefined + ownerUsername?: string | undefined owner_country_code?: string | undefined subject: string /** group subject owner */ subjectOwner?: string subjectOwnerPn?: string + subjectOwnerUsername?: string /** group subject modification date */ subjectTime?: number creation?: number desc?: string descOwner?: string descOwnerPn?: string + descOwnerUsername?: string descId?: string descTime?: number /** if this group is part of a community, it returns the jid of the community to which it belongs */ @@ -56,6 +59,7 @@ export interface GroupMetadata { /** the person who added you to group or changed some setting in group */ author?: string authorPn?: string + authorUsername?: string } export interface WAGroupCreateResponse { diff --git a/src/Types/Message.ts b/src/Types/Message.ts index ce8273c1..a6717e49 100644 --- a/src/Types/Message.ts +++ b/src/Types/Message.ts @@ -19,7 +19,9 @@ export type WAContactMessage = proto.Message.IContactMessage export type WAContactsArrayMessage = proto.Message.IContactsArrayMessage export type WAMessageKey = proto.IMessageKey & { remoteJidAlt?: string + remoteJidUsername?: string participantAlt?: string + participantUsername?: string server_id?: string addressingMode?: string isViewOnce?: boolean // TODO: remove out of the message key, place in WebMessageInfo diff --git a/src/Utils/chat-utils.ts b/src/Utils/chat-utils.ts index 99e40725..6c8e8a1b 100644 --- a/src/Utils/chat-utils.ts +++ b/src/Utils/chat-utils.ts @@ -1083,6 +1083,7 @@ export const processSyncAction = ( action.lidContactAction.firstName || action.lidContactAction.username || undefined, + username: action.lidContactAction.username || undefined, lid: id!, phoneNumber: undefined } diff --git a/src/Utils/decode-wa-message.ts b/src/Utils/decode-wa-message.ts index 4f00b1b9..a88e06be 100644 --- a/src/Utils/decode-wa-message.ts +++ b/src/Utils/decode-wa-message.ts @@ -242,10 +242,14 @@ export function decodeMessageNode(stanza: BinaryNode, meId: string, meLid: strin const key: WAMessageKey = { remoteJid: chatId, remoteJidAlt: !isJidGroup(chatId) ? addressingContext.senderAlt : undefined, + remoteJidUsername: !isJidGroup(chatId) + ? stanza.attrs.peer_recipient_username || stanza.attrs.recipient_username + : undefined, fromMe, id: msgId, participant, participantAlt: isJidGroup(chatId) ? addressingContext.senderAlt : undefined, + participantUsername: stanza.attrs.participant ? stanza.attrs.participant_username : undefined, addressingMode: addressingContext.addressingMode, ...(msgType === 'newsletter' && stanza.attrs.server_id ? { server_id: stanza.attrs.server_id } : {}) } diff --git a/src/Utils/history.ts b/src/Utils/history.ts index e58aa8b9..5021ae04 100644 --- a/src/Utils/history.ts +++ b/src/Utils/history.ts @@ -315,6 +315,7 @@ export const processHistoryMessage = (item: proto.IHistorySync, logger?: ILogger contacts.push({ id: chatId, name: chat.displayName || chat.name || chat.username || undefined, + username: chat.username || undefined, lid: chat.lidJid || chat.accountLid || undefined, phoneNumber: chat.pnJid || undefined }) diff --git a/src/Utils/process-message.ts b/src/Utils/process-message.ts index 19b95a2b..04ec7f5e 100644 --- a/src/Utils/process-message.ts +++ b/src/Utils/process-message.ts @@ -763,12 +763,19 @@ const processMessage = async ( id: jid, author: message.key.participant!, authorPn: message.key.participantAlt!, + authorUsername: message.key.participantUsername!, participants, action }) const emitGroupUpdate = (update: Partial) => { ev.emit('groups.update', [ - { id: jid, ...update, author: message.key.participant ?? undefined, authorPn: message.key.participantAlt } + { + id: jid, + ...update, + author: message.key.participant ?? undefined, + authorPn: message.key.participantAlt, + authorUsername: message.key.participantUsername + } ]) } @@ -777,6 +784,7 @@ const processMessage = async ( id: jid, author: message.key.participant!, authorPn: message.key.participantAlt!, + authorUsername: message.key.participantUsername!, participant: participant.lid, participantPn: participant.pn, action, diff --git a/src/Utils/sync-action-utils.ts b/src/Utils/sync-action-utils.ts index 2c4ac1a3..c1f03174 100644 --- a/src/Utils/sync-action-utils.ts +++ b/src/Utils/sync-action-utils.ts @@ -46,6 +46,7 @@ export const processContactAction = ( { id, name: action.fullName || action.firstName || action.username || undefined, + username: action.username || undefined, lid: lidJid || undefined, phoneNumber } diff --git a/src/WAUSync/Protocols/USyncContactProtocol.ts b/src/WAUSync/Protocols/USyncContactProtocol.ts index 13472e11..539b01ae 100644 --- a/src/WAUSync/Protocols/USyncContactProtocol.ts +++ b/src/WAUSync/Protocols/USyncContactProtocol.ts @@ -13,11 +13,37 @@ export class USyncContactProtocol implements USyncQueryProtocol { } getUserElement(user: USyncUser): BinaryNode { - //TODO: Implement type / username fields (not yet supported) + if (user.phone) { + return { + tag: 'contact', + attrs: {}, + content: user.phone + } + } + + if (user.username) { + return { + tag: 'contact', + attrs: { + username: user.username, + ...(user.usernameKey ? { pin: user.usernameKey } : {}), + ...(user.lid ? { lid: user.lid } : {}) + } + } + } + + if (user.type) { + return { + tag: 'contact', + attrs: { + type: user.type + } + } + } + return { tag: 'contact', - attrs: {}, - content: user.phone + attrs: {} } } diff --git a/src/WAUSync/Protocols/USyncUsernameProtocol.ts b/src/WAUSync/Protocols/USyncUsernameProtocol.ts new file mode 100644 index 00000000..dff58c9a --- /dev/null +++ b/src/WAUSync/Protocols/USyncUsernameProtocol.ts @@ -0,0 +1,28 @@ +import type { USyncQueryProtocol } from '../../Types/USync' +import { assertNodeErrorFree, type BinaryNode } from '../../WABinary' +import { USyncUser } from '../USyncUser' + +export class USyncUsernameProtocol implements USyncQueryProtocol { + name = 'username' + + getQueryElement(): BinaryNode { + return { + tag: 'username', + attrs: {} + } + } + + getUserElement(user: USyncUser): BinaryNode | null { + void user + return null + } + + parser(node: BinaryNode): string | null { + if (node.tag === 'username') { + assertNodeErrorFree(node) + return typeof node.content === 'string' ? node.content : null + } + + return null + } +} diff --git a/src/WAUSync/Protocols/index.ts b/src/WAUSync/Protocols/index.ts index c6e6442a..2e5afaaa 100644 --- a/src/WAUSync/Protocols/index.ts +++ b/src/WAUSync/Protocols/index.ts @@ -2,3 +2,4 @@ export * from './USyncDeviceProtocol' export * from './USyncContactProtocol' export * from './USyncStatusProtocol' export * from './USyncDisappearingModeProtocol' +export * from './USyncUsernameProtocol' diff --git a/src/WAUSync/USyncQuery.ts b/src/WAUSync/USyncQuery.ts index 39b8b9e2..a25951e7 100644 --- a/src/WAUSync/USyncQuery.ts +++ b/src/WAUSync/USyncQuery.ts @@ -6,7 +6,8 @@ import { USyncContactProtocol, USyncDeviceProtocol, USyncDisappearingModeProtocol, - USyncStatusProtocol + USyncStatusProtocol, + USyncUsernameProtocol } from './Protocols' import { USyncUser } from './USyncUser' @@ -137,4 +138,9 @@ export class USyncQuery { this.protocols.push(new USyncLIDProtocol()) return this } + + withUsernameProtocol() { + this.protocols.push(new USyncUsernameProtocol()) + return this + } } diff --git a/src/WAUSync/USyncUser.ts b/src/WAUSync/USyncUser.ts index 8cfc364b..2ae1e3a3 100644 --- a/src/WAUSync/USyncUser.ts +++ b/src/WAUSync/USyncUser.ts @@ -2,6 +2,8 @@ export class USyncUser { id?: string lid?: string phone?: string + username?: string + usernameKey?: string type?: string personaId?: string @@ -20,6 +22,16 @@ export class USyncUser { return this } + withUsername(username: string) { + this.username = username + return this + } + + withUsernameKey(usernameKey: string) { + this.usernameKey = usernameKey + return this + } + withType(type: string) { this.type = type return this