48b63565b6
PRs 124-129 replaced TypeScript non-null assertions (!) with optional chaining (?.) and empty string defaults (?? ''). While defensive programming is usually good, in Signal protocol and WhatsApp binary node handling code these changes caused: - Malformed JIDs with empty user components (e.g. @s.whatsapp.net) - Silent device enumeration failures (messages skip recipients) - Wrong own-device detection (message routing errors) - Broken prekey count handling (prekey uploads silently skipped) - Wrong retry count tracking in Signal protocol These empty-string defaults are dangerous because Signal session lookups fail for malformed JIDs, causing "SessionError: No sessions". Reverted to original Baileys pattern using non-null assertions (!) which match the upstream reference (infinitezap/Teste_InfiniteAPI). Files fixed: messages-send.ts, messages-recv.ts, socket.ts, chats.ts, groups.ts, communities.ts, business.ts https://claude.ai/code/session_01XaA7GwNaB6azTHFYQ8WEpB
362 lines
11 KiB
TypeScript
362 lines
11 KiB
TypeScript
import { proto } from '../../WAProto/index.js'
|
|
import type { GroupMetadata, GroupParticipant, ParticipantAction, SocketConfig, WAMessageKey } from '../Types'
|
|
import { WAMessageAddressingMode, WAMessageStubType } from '../Types'
|
|
import { generateMessageIDV2, unixTimestampSeconds } from '../Utils'
|
|
import {
|
|
type BinaryNode,
|
|
getBinaryNodeChild,
|
|
getBinaryNodeChildren,
|
|
getBinaryNodeChildString,
|
|
isLidUser,
|
|
isPnUser,
|
|
jidEncode,
|
|
jidNormalizedUser
|
|
} from '../WABinary'
|
|
import { makeChatsSocket } from './chats'
|
|
|
|
export const makeGroupsSocket = (config: SocketConfig) => {
|
|
const sock = makeChatsSocket(config)
|
|
const { authState, ev, query, upsertMessage } = sock
|
|
|
|
const groupQuery = async (jid: string, type: 'get' | 'set', content: BinaryNode[]) =>
|
|
query({
|
|
tag: 'iq',
|
|
attrs: {
|
|
type,
|
|
xmlns: 'w:g2',
|
|
to: jid
|
|
},
|
|
content
|
|
})
|
|
|
|
const groupMetadata = async (jid: string) => {
|
|
const result = await groupQuery(jid, 'get', [{ tag: 'query', attrs: { request: 'interactive' } }])
|
|
return extractGroupMetadata(result)
|
|
}
|
|
|
|
const groupFetchAllParticipating = async () => {
|
|
const result = await query({
|
|
tag: 'iq',
|
|
attrs: {
|
|
to: '@g.us',
|
|
xmlns: 'w:g2',
|
|
type: 'get'
|
|
},
|
|
content: [
|
|
{
|
|
tag: 'participating',
|
|
attrs: {},
|
|
content: [
|
|
{ tag: 'participants', attrs: {} },
|
|
{ tag: 'description', attrs: {} }
|
|
]
|
|
}
|
|
]
|
|
})
|
|
const data: { [_: string]: GroupMetadata } = {}
|
|
const groupsChild = getBinaryNodeChild(result, 'groups')
|
|
if (groupsChild) {
|
|
const groups = getBinaryNodeChildren(groupsChild, 'group')
|
|
for (const groupNode of groups) {
|
|
const meta = extractGroupMetadata({
|
|
tag: 'result',
|
|
attrs: {},
|
|
content: [groupNode]
|
|
})
|
|
data[meta.id] = meta
|
|
}
|
|
}
|
|
|
|
// TODO: properly parse LID / PN DATA
|
|
sock.ev.emit('groups.update', Object.values(data))
|
|
|
|
return data
|
|
}
|
|
|
|
sock.ws.on('CB:ib,,dirty', async (node: BinaryNode) => {
|
|
const { attrs } = getBinaryNodeChild(node, 'dirty')!
|
|
if (attrs.type !== 'groups') {
|
|
return
|
|
}
|
|
|
|
await groupFetchAllParticipating()
|
|
await sock.cleanDirtyBits('groups')
|
|
})
|
|
|
|
return {
|
|
...sock,
|
|
groupMetadata,
|
|
groupCreate: async (subject: string, participants: string[]) => {
|
|
const key = generateMessageIDV2()
|
|
const result = await groupQuery('@g.us', 'set', [
|
|
{
|
|
tag: 'create',
|
|
attrs: {
|
|
subject,
|
|
key
|
|
},
|
|
content: participants.map(jid => ({
|
|
tag: 'participant',
|
|
attrs: { jid }
|
|
}))
|
|
}
|
|
])
|
|
return extractGroupMetadata(result)
|
|
},
|
|
groupLeave: async (id: string) => {
|
|
await groupQuery('@g.us', 'set', [
|
|
{
|
|
tag: 'leave',
|
|
attrs: {},
|
|
content: [{ tag: 'group', attrs: { id } }]
|
|
}
|
|
])
|
|
},
|
|
groupUpdateSubject: async (jid: string, subject: string) => {
|
|
await groupQuery(jid, 'set', [
|
|
{
|
|
tag: 'subject',
|
|
attrs: {},
|
|
content: Buffer.from(subject, 'utf-8')
|
|
}
|
|
])
|
|
},
|
|
groupRequestParticipantsList: async (jid: string) => {
|
|
const result = await groupQuery(jid, 'get', [
|
|
{
|
|
tag: 'membership_approval_requests',
|
|
attrs: {}
|
|
}
|
|
])
|
|
const node = getBinaryNodeChild(result, 'membership_approval_requests')
|
|
const participants = getBinaryNodeChildren(node, 'membership_approval_request')
|
|
return participants.map(v => v.attrs)
|
|
},
|
|
groupRequestParticipantsUpdate: async (jid: string, participants: string[], action: 'approve' | 'reject') => {
|
|
const result = await groupQuery(jid, 'set', [
|
|
{
|
|
tag: 'membership_requests_action',
|
|
attrs: {},
|
|
content: [
|
|
{
|
|
tag: action,
|
|
attrs: {},
|
|
content: participants.map(jid => ({
|
|
tag: 'participant',
|
|
attrs: { jid }
|
|
}))
|
|
}
|
|
]
|
|
}
|
|
])
|
|
const node = getBinaryNodeChild(result, 'membership_requests_action')
|
|
const nodeAction = getBinaryNodeChild(node, action)
|
|
const participantsAffected = getBinaryNodeChildren(nodeAction, 'participant')
|
|
return participantsAffected.map(p => {
|
|
return { status: p.attrs.error || '200', jid: p.attrs.jid }
|
|
})
|
|
},
|
|
groupParticipantsUpdate: async (jid: string, participants: string[], action: ParticipantAction) => {
|
|
const result = await groupQuery(jid, 'set', [
|
|
{
|
|
tag: action,
|
|
attrs: {},
|
|
content: participants.map(jid => ({
|
|
tag: 'participant',
|
|
attrs: { jid }
|
|
}))
|
|
}
|
|
])
|
|
const node = getBinaryNodeChild(result, action)
|
|
const participantsAffected = getBinaryNodeChildren(node, 'participant')
|
|
return participantsAffected.map(p => {
|
|
return { status: p.attrs.error || '200', jid: p.attrs.jid, content: p }
|
|
})
|
|
},
|
|
groupUpdateDescription: async (jid: string, description?: string) => {
|
|
const metadata = await groupMetadata(jid)
|
|
const prev = metadata.descId ?? null
|
|
|
|
await groupQuery(jid, 'set', [
|
|
{
|
|
tag: 'description',
|
|
attrs: {
|
|
...(description ? { id: generateMessageIDV2() } : { delete: 'true' }),
|
|
...(prev ? { prev } : {})
|
|
},
|
|
content: description ? [{ tag: 'body', attrs: {}, content: Buffer.from(description, 'utf-8') }] : undefined
|
|
}
|
|
])
|
|
},
|
|
groupInviteCode: async (jid: string) => {
|
|
const result = await groupQuery(jid, 'get', [{ tag: 'invite', attrs: {} }])
|
|
const inviteNode = getBinaryNodeChild(result, 'invite')
|
|
return inviteNode?.attrs.code
|
|
},
|
|
groupRevokeInvite: async (jid: string) => {
|
|
const result = await groupQuery(jid, 'set', [{ tag: 'invite', attrs: {} }])
|
|
const inviteNode = getBinaryNodeChild(result, 'invite')
|
|
return inviteNode?.attrs.code
|
|
},
|
|
groupAcceptInvite: async (code: string) => {
|
|
const results = await groupQuery('@g.us', 'set', [{ tag: 'invite', attrs: { code } }])
|
|
const result = getBinaryNodeChild(results, 'group')
|
|
return result?.attrs.jid
|
|
},
|
|
|
|
/**
|
|
* revoke a v4 invite for someone
|
|
* @param groupJid group jid
|
|
* @param invitedJid jid of person you invited
|
|
* @returns true if successful
|
|
*/
|
|
groupRevokeInviteV4: async (groupJid: string, invitedJid: string) => {
|
|
const result = await groupQuery(groupJid, 'set', [
|
|
{ tag: 'revoke', attrs: {}, content: [{ tag: 'participant', attrs: { jid: invitedJid } }] }
|
|
])
|
|
return !!result
|
|
},
|
|
|
|
/**
|
|
* accept a GroupInviteMessage
|
|
* @param key the key of the invite message, or optionally only provide the jid of the person who sent the invite
|
|
* @param inviteMessage the message to accept
|
|
*/
|
|
groupAcceptInviteV4: ev.createBufferedFunction(
|
|
async (key: string | WAMessageKey, inviteMessage: proto.Message.IGroupInviteMessage) => {
|
|
key = typeof key === 'string' ? { remoteJid: key } : key
|
|
const results = await groupQuery(inviteMessage.groupJid!, 'set', [
|
|
{
|
|
tag: 'accept',
|
|
attrs: {
|
|
code: inviteMessage.inviteCode!,
|
|
expiration: inviteMessage.inviteExpiration!.toString(),
|
|
admin: key.remoteJid!
|
|
}
|
|
}
|
|
])
|
|
|
|
// if we have the full message key
|
|
// update the invite message to be expired
|
|
if (key.id) {
|
|
// create new invite message that is expired
|
|
inviteMessage = proto.Message.GroupInviteMessage.fromObject(inviteMessage)
|
|
inviteMessage.inviteExpiration = 0
|
|
inviteMessage.inviteCode = ''
|
|
ev.emit('messages.update', [
|
|
{
|
|
key,
|
|
update: {
|
|
message: {
|
|
groupInviteMessage: inviteMessage
|
|
}
|
|
}
|
|
}
|
|
])
|
|
}
|
|
|
|
// generate the group add message
|
|
await upsertMessage(
|
|
{
|
|
key: {
|
|
remoteJid: inviteMessage.groupJid,
|
|
id: generateMessageIDV2(sock.user?.id),
|
|
fromMe: false,
|
|
participant: key.remoteJid
|
|
},
|
|
messageStubType: WAMessageStubType.GROUP_PARTICIPANT_ADD,
|
|
messageStubParameters: [JSON.stringify(authState.creds.me)],
|
|
participant: key.remoteJid,
|
|
messageTimestamp: unixTimestampSeconds()
|
|
},
|
|
'notify'
|
|
)
|
|
|
|
return results.attrs.from
|
|
}
|
|
),
|
|
groupGetInviteInfo: async (code: string) => {
|
|
const results = await groupQuery('@g.us', 'get', [{ tag: 'invite', attrs: { code } }])
|
|
return extractGroupMetadata(results)
|
|
},
|
|
groupToggleEphemeral: async (jid: string, ephemeralExpiration: number) => {
|
|
const content: BinaryNode = ephemeralExpiration
|
|
? { tag: 'ephemeral', attrs: { expiration: ephemeralExpiration.toString() } }
|
|
: { tag: 'not_ephemeral', attrs: {} }
|
|
await groupQuery(jid, 'set', [content])
|
|
},
|
|
groupSettingUpdate: async (jid: string, setting: 'announcement' | 'not_announcement' | 'locked' | 'unlocked') => {
|
|
await groupQuery(jid, 'set', [{ tag: setting, attrs: {} }])
|
|
},
|
|
groupMemberAddMode: async (jid: string, mode: 'admin_add' | 'all_member_add') => {
|
|
await groupQuery(jid, 'set', [{ tag: 'member_add_mode', attrs: {}, content: mode }])
|
|
},
|
|
groupJoinApprovalMode: async (jid: string, mode: 'on' | 'off') => {
|
|
await groupQuery(jid, 'set', [
|
|
{ tag: 'membership_approval_mode', attrs: {}, content: [{ tag: 'group_join', attrs: { state: mode } }] }
|
|
])
|
|
},
|
|
groupFetchAllParticipating
|
|
}
|
|
}
|
|
|
|
export const extractGroupMetadata = (result: BinaryNode) => {
|
|
const group = getBinaryNodeChild(result, 'group')!
|
|
const descChild = getBinaryNodeChild(group, 'description')
|
|
let desc: string | undefined
|
|
let descId: string | undefined
|
|
let descOwner: string | undefined
|
|
let descOwnerPn: 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
|
|
descTime = +descChild.attrs.t!
|
|
descId = descChild.attrs.id
|
|
}
|
|
|
|
const groupId = group.attrs.id!.includes('@') ? group.attrs.id! : jidEncode(group.attrs.id!, 'g.us')
|
|
const eph = getBinaryNodeChild(group, 'ephemeral')?.attrs.expiration
|
|
const memberAddMode = getBinaryNodeChildString(group, 'member_add_mode') === 'all_member_add'
|
|
const metadata: GroupMetadata = {
|
|
id: groupId,
|
|
notify: group.attrs.notify,
|
|
addressingMode: group.attrs.addressing_mode === 'lid' ? WAMessageAddressingMode.LID : WAMessageAddressingMode.PN,
|
|
subject: group.attrs.subject!,
|
|
subjectOwner: group.attrs.s_o,
|
|
subjectOwnerPn: group.attrs.s_o_pn,
|
|
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,
|
|
owner_country_code: group.attrs.creator_country_code,
|
|
desc,
|
|
descId,
|
|
descOwner,
|
|
descOwnerPn,
|
|
descTime,
|
|
linkedParent: getBinaryNodeChild(group, 'linked_parent')?.attrs.jid || undefined,
|
|
restrict: !!getBinaryNodeChild(group, 'locked'),
|
|
announce: !!getBinaryNodeChild(group, 'announcement'),
|
|
isCommunity: !!getBinaryNodeChild(group, 'parent'),
|
|
isCommunityAnnounce: !!getBinaryNodeChild(group, 'default_sub_group'),
|
|
joinApprovalMode: !!getBinaryNodeChild(group, 'membership_approval_mode'),
|
|
memberAddMode,
|
|
participants: getBinaryNodeChildren(group, 'participant').map(({ attrs }) => {
|
|
// TODO: Store LID MAPPINGS
|
|
return {
|
|
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,
|
|
admin: (attrs.type || null) as GroupParticipant['admin']
|
|
}
|
|
}),
|
|
ephemeralDuration: eph ? +eph : undefined
|
|
}
|
|
return metadata
|
|
}
|
|
|
|
export type GroupsSocket = ReturnType<typeof makeGroupsSocket>
|