mirror of
https://github.com/FranP-code/Baileys.git
synced 2025-10-13 00:32:22 +00:00
finalize multi-device
This commit is contained in:
430
src/Socket/chats.ts
Normal file
430
src/Socket/chats.ts
Normal file
@@ -0,0 +1,430 @@
|
||||
import { decodeSyncdPatch, encodeSyncdPatch } from "../Utils/chat-utils";
|
||||
import { SocketConfig, WAPresence, PresenceData, Chat, ChatModification, WAMediaUpload } from "../Types";
|
||||
import { BinaryNode, getBinaryNodeChild, getBinaryNodeChildren, jidNormalizedUser, S_WHATSAPP_NET } from "../WABinary";
|
||||
import { makeSocket } from "./socket";
|
||||
import { proto } from '../../WAProto'
|
||||
import { toNumber } from "../Utils/generics";
|
||||
import { compressImage, generateProfilePicture } from "..";
|
||||
|
||||
export const makeChatsSocket = (config: SocketConfig) => {
|
||||
const { logger } = config
|
||||
const sock = makeSocket(config)
|
||||
const {
|
||||
ev,
|
||||
ws,
|
||||
authState,
|
||||
generateMessageTag,
|
||||
sendNode,
|
||||
query
|
||||
} = sock
|
||||
|
||||
const interactiveQuery = async(userNodes: BinaryNode[], queryNode: BinaryNode) => {
|
||||
const result = await query({
|
||||
tag: 'iq',
|
||||
attrs: {
|
||||
to: S_WHATSAPP_NET,
|
||||
type: 'get',
|
||||
xmlns: 'usync',
|
||||
|
||||
},
|
||||
content: [
|
||||
{
|
||||
tag: 'usync',
|
||||
attrs: {
|
||||
sid: generateMessageTag(),
|
||||
mode: 'query',
|
||||
last: 'true',
|
||||
index: '0',
|
||||
context: 'interactive',
|
||||
},
|
||||
content: [
|
||||
{
|
||||
tag: 'query',
|
||||
attrs: { },
|
||||
content: [ queryNode ]
|
||||
},
|
||||
{
|
||||
tag: 'list',
|
||||
attrs: { },
|
||||
content: userNodes
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
})
|
||||
|
||||
const usyncNode = getBinaryNodeChild(result, 'usync')
|
||||
const listNode = getBinaryNodeChild(usyncNode, 'list')
|
||||
const users = getBinaryNodeChildren(listNode, 'user')
|
||||
|
||||
return users
|
||||
}
|
||||
|
||||
const onWhatsApp = async(...jids: string[]) => {
|
||||
const results = await interactiveQuery(
|
||||
[
|
||||
{
|
||||
tag: 'user',
|
||||
attrs: { },
|
||||
content: jids.map(
|
||||
jid => ({
|
||||
tag: 'contact',
|
||||
attrs: { },
|
||||
content: `+${jid}`
|
||||
})
|
||||
)
|
||||
}
|
||||
],
|
||||
{ tag: 'contact', attrs: { } }
|
||||
)
|
||||
|
||||
return results.map(user => {
|
||||
const contact = getBinaryNodeChild(user, 'contact')
|
||||
return { exists: contact.attrs.type === 'in', jid: user.attrs.jid }
|
||||
}).filter(item => item.exists)
|
||||
}
|
||||
|
||||
const fetchStatus = async(jid: string) => {
|
||||
const [result] = await interactiveQuery(
|
||||
[{ tag: 'user', attrs: { jid } }],
|
||||
{ tag: 'status', attrs: { } }
|
||||
)
|
||||
if(result) {
|
||||
const status = getBinaryNodeChild(result, 'status')
|
||||
return {
|
||||
status: status.content!.toString(),
|
||||
setAt: new Date(+status.attrs.t * 1000)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const updateProfilePicture = async(jid: string, content: WAMediaUpload) => {
|
||||
const { img } = await generateProfilePicture('url' in content ? content.url.toString() : content)
|
||||
await query({
|
||||
tag: 'iq',
|
||||
attrs: {
|
||||
to: jidNormalizedUser(jid),
|
||||
type: 'set',
|
||||
xmlns: 'w:profile:picture'
|
||||
},
|
||||
content: [
|
||||
{
|
||||
tag: 'picture',
|
||||
attrs: { type: 'image' },
|
||||
content: img
|
||||
}
|
||||
]
|
||||
})
|
||||
}
|
||||
|
||||
const fetchBlocklist = async() => {
|
||||
const result = await query({
|
||||
tag: 'iq',
|
||||
attrs: {
|
||||
xmlns: 'blocklist',
|
||||
to: S_WHATSAPP_NET,
|
||||
type: 'get'
|
||||
}
|
||||
})
|
||||
console.log('blocklist', result)
|
||||
}
|
||||
|
||||
const updateBlockStatus = async(jid: string, action: 'block' | 'unblock') => {
|
||||
await query({
|
||||
tag: 'iq',
|
||||
attrs: {
|
||||
to: S_WHATSAPP_NET,
|
||||
type: 'set'
|
||||
},
|
||||
content: [
|
||||
{
|
||||
tag: 'item',
|
||||
attrs: {
|
||||
action,
|
||||
jid
|
||||
}
|
||||
}
|
||||
]
|
||||
})
|
||||
}
|
||||
|
||||
const fetchPrivacySettings = async() => {
|
||||
const result = await query({
|
||||
tag: 'iq',
|
||||
attrs: {
|
||||
xmlns: 'privacy',
|
||||
to: S_WHATSAPP_NET,
|
||||
type: 'get'
|
||||
}
|
||||
})
|
||||
console.log('privacy', result)
|
||||
}
|
||||
|
||||
const updateAccountSyncTimestamp = async() => {
|
||||
await sendNode({
|
||||
tag: 'iq',
|
||||
attrs: {
|
||||
to: S_WHATSAPP_NET,
|
||||
type: 'set',
|
||||
xmlns: 'urn:xmpp:whatsapp:dirty',
|
||||
id: generateMessageTag(),
|
||||
},
|
||||
content: [
|
||||
{
|
||||
tag: 'clean',
|
||||
attrs: { }
|
||||
}
|
||||
]
|
||||
})
|
||||
}
|
||||
|
||||
const collectionSync = async() => {
|
||||
const COLLECTIONS = ['critical_block', 'critical_unblock_low', 'regular_low', 'regular_high']
|
||||
await sendNode({
|
||||
tag: 'iq',
|
||||
attrs: {
|
||||
to: S_WHATSAPP_NET,
|
||||
xmlns: 'w:sync:app:state',
|
||||
type: 'set',
|
||||
id: generateMessageTag(),
|
||||
},
|
||||
content: [
|
||||
{
|
||||
tag: 'sync',
|
||||
attrs: { },
|
||||
content: COLLECTIONS.map(
|
||||
name => ({
|
||||
tag: 'collection',
|
||||
attrs: { name, version: '0', return_snapshot: 'true' }
|
||||
})
|
||||
)
|
||||
}
|
||||
]
|
||||
})
|
||||
logger.info('synced collection')
|
||||
}
|
||||
|
||||
const profilePictureUrl = async(jid: string) => {
|
||||
const result = await query({
|
||||
tag: 'iq',
|
||||
attrs: {
|
||||
to: jid,
|
||||
type: 'get',
|
||||
xmlns: 'w:profile:picture'
|
||||
},
|
||||
content: [
|
||||
{ tag: 'picture', attrs: { type: 'preview', query: 'url' } }
|
||||
]
|
||||
})
|
||||
const child = getBinaryNodeChild(result, 'picture')
|
||||
return child?.attrs?.url
|
||||
}
|
||||
|
||||
const sendPresenceUpdate = async(type: WAPresence, toJid?: string) => {
|
||||
if(type === 'available' || type === 'unavailable') {
|
||||
await sendNode({
|
||||
tag: 'presence',
|
||||
attrs: {
|
||||
name: authState.creds.me!.name,
|
||||
type
|
||||
}
|
||||
})
|
||||
} else {
|
||||
await sendNode({
|
||||
tag: 'chatstate',
|
||||
attrs: {
|
||||
from: authState.creds.me!.id!,
|
||||
to: toJid,
|
||||
},
|
||||
content: [
|
||||
{ tag: type, attrs: { } }
|
||||
]
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
const presenceSubscribe = (toJid: string) => (
|
||||
sendNode({
|
||||
tag: 'presence',
|
||||
attrs: {
|
||||
to: toJid,
|
||||
id: generateMessageTag(),
|
||||
type: 'subscribe'
|
||||
}
|
||||
})
|
||||
)
|
||||
|
||||
const handlePresenceUpdate = ({ tag, attrs, content }: BinaryNode) => {
|
||||
let presence: PresenceData
|
||||
const jid = attrs.from
|
||||
const participant = attrs.participant || attrs.from
|
||||
if(tag === 'presence') {
|
||||
presence = {
|
||||
lastKnownPresence: attrs.type === 'unavailable' ? 'unavailable' : 'available',
|
||||
lastSeen: attrs.t ? +attrs.t : undefined
|
||||
}
|
||||
} else if(Array.isArray(content)) {
|
||||
const [firstChild] = content
|
||||
let type = firstChild.tag as WAPresence
|
||||
if(type === 'paused') {
|
||||
type = 'available'
|
||||
}
|
||||
presence = { lastKnownPresence: type }
|
||||
} else {
|
||||
logger.error({ tag, attrs, content }, 'recv invalid presence node')
|
||||
}
|
||||
if(presence) {
|
||||
ev.emit('presence.update', { id: jid, presences: { [participant]: presence } })
|
||||
}
|
||||
}
|
||||
|
||||
const processSyncActions = (actions: { action: proto.ISyncActionValue, index: [string, string] }[]) => {
|
||||
const updates: Partial<Chat>[] = []
|
||||
for(const { action, index: [_, id] } of actions) {
|
||||
const update: Partial<Chat> = { id }
|
||||
if(action?.muteAction) {
|
||||
update.mute = action.muteAction?.muted ?
|
||||
toNumber(action.muteAction!.muteEndTimestamp!) :
|
||||
undefined
|
||||
} else if(action?.archiveChatAction) {
|
||||
update.archive = !!action.archiveChatAction?.archived
|
||||
} else if(action?.markChatAsReadAction) {
|
||||
update.unreadCount = !!action.markChatAsReadAction?.read ? 0 : -1
|
||||
} else if(action?.clearChatAction) {
|
||||
console.log(action.clearChatAction)
|
||||
} else if(action?.contactAction) {
|
||||
ev.emit('contacts.update', [{ id, name: action.contactAction!.fullName }])
|
||||
} else if(action?.pushNameSetting) {
|
||||
authState.creds.me!.name = action?.pushNameSetting?.name!
|
||||
ev.emit('auth-state.update', authState)
|
||||
} else {
|
||||
logger.warn({ action, id }, 'unprocessable update')
|
||||
}
|
||||
updates.push(update)
|
||||
}
|
||||
ev.emit('chats.update', updates)
|
||||
}
|
||||
|
||||
const patchChat = async(
|
||||
jid: string,
|
||||
modification: ChatModification
|
||||
) => {
|
||||
const patch = encodeSyncdPatch(modification, { remoteJid: jid }, authState)
|
||||
const type = 'regular_high'
|
||||
const ver = authState.creds.appStateVersion![type] || 0
|
||||
const node: BinaryNode = {
|
||||
tag: 'iq',
|
||||
attrs: {
|
||||
to: S_WHATSAPP_NET,
|
||||
type: 'set',
|
||||
xmlns: 'w:sync:app:state'
|
||||
},
|
||||
content: [
|
||||
{
|
||||
tag: 'patch',
|
||||
attrs: {
|
||||
name: type,
|
||||
version: (ver+1).toString(),
|
||||
return_snapshot: 'false'
|
||||
},
|
||||
content: proto.SyncdPatch.encode(patch).finish()
|
||||
}
|
||||
]
|
||||
}
|
||||
await query(node)
|
||||
|
||||
authState.creds.appStateVersion![type] += 1
|
||||
ev.emit('auth-state.update', authState)
|
||||
}
|
||||
|
||||
const resyncState = async(name: 'regular_high' | 'regular_low' = 'regular_high') => {
|
||||
authState.creds.appStateVersion = authState.creds.appStateVersion || {
|
||||
regular_high: 0,
|
||||
regular_low: 0,
|
||||
critical_unblock_low: 0,
|
||||
critical_block: 0
|
||||
}
|
||||
const result = await query({
|
||||
tag: 'iq',
|
||||
attrs: {
|
||||
type: 'set',
|
||||
xmlns: 'w:sync:app:state',
|
||||
to: S_WHATSAPP_NET
|
||||
},
|
||||
content: [
|
||||
{
|
||||
tag: 'sync',
|
||||
attrs: { },
|
||||
content: [
|
||||
{
|
||||
tag: 'collection',
|
||||
attrs: {
|
||||
name,
|
||||
version: authState.creds.appStateVersion[name].toString(),
|
||||
return_snapshot: 'false'
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
})
|
||||
const syncNode = getBinaryNodeChild(result, 'sync')
|
||||
const collectionNode = getBinaryNodeChild(syncNode, 'collection')
|
||||
const patchesNode = getBinaryNodeChild(collectionNode, 'patches')
|
||||
|
||||
const patches = getBinaryNodeChildren(patchesNode, 'patch')
|
||||
const successfulMutations = patches.flatMap(({ content }) => {
|
||||
if(content) {
|
||||
const syncd = proto.SyncdPatch.decode(content! as Uint8Array)
|
||||
const version = toNumber(syncd.version!.version!)
|
||||
if(version) {
|
||||
authState.creds.appStateVersion[name] = Math.max(version, authState.creds.appStateVersion[name])
|
||||
}
|
||||
const { mutations, failures } = decodeSyncdPatch(syncd, authState)
|
||||
if(failures.length) {
|
||||
logger.info(
|
||||
{ failures: failures.map(f => ({ trace: f.stack, data: f.data })) },
|
||||
'failed to decode'
|
||||
)
|
||||
}
|
||||
return mutations
|
||||
}
|
||||
return []
|
||||
})
|
||||
processSyncActions(successfulMutations)
|
||||
ev.emit('auth-state.update', authState)
|
||||
}
|
||||
|
||||
ws.on('CB:presence', handlePresenceUpdate)
|
||||
ws.on('CB:chatstate', handlePresenceUpdate)
|
||||
|
||||
ws.on('CB:notification,type:server_sync', (node: BinaryNode) => {
|
||||
const update = getBinaryNodeChild(node, 'collection')
|
||||
if(update) {
|
||||
resyncState(update.attrs.name as any)
|
||||
}
|
||||
})
|
||||
|
||||
ev.on('connection.update', ({ connection }) => {
|
||||
if(connection === 'open') {
|
||||
sendPresenceUpdate('available')
|
||||
fetchBlocklist()
|
||||
fetchPrivacySettings()
|
||||
//collectionSync()
|
||||
}
|
||||
})
|
||||
|
||||
return {
|
||||
...sock,
|
||||
patchChat,
|
||||
sendPresenceUpdate,
|
||||
presenceSubscribe,
|
||||
profilePictureUrl,
|
||||
onWhatsApp,
|
||||
fetchBlocklist,
|
||||
fetchPrivacySettings,
|
||||
fetchStatus,
|
||||
updateProfilePicture,
|
||||
updateBlockStatus
|
||||
}
|
||||
}
|
||||
149
src/Socket/groups.ts
Normal file
149
src/Socket/groups.ts
Normal file
@@ -0,0 +1,149 @@
|
||||
import { generateMessageID } from "../Utils";
|
||||
import { SocketConfig, GroupMetadata, ParticipantAction } from "../Types";
|
||||
import { BinaryNode, getBinaryNodeChild, getBinaryNodeChildren, jidDecode, jidEncode } from "../WABinary";
|
||||
import { makeChatsSocket } from "./chats";
|
||||
|
||||
const extractGroupMetadata = (result: BinaryNode) => {
|
||||
const group = getBinaryNodeChild(result, 'group')
|
||||
const descChild = getBinaryNodeChild(group, 'description')
|
||||
let desc: string | undefined
|
||||
let descId: string | undefined
|
||||
if(descChild) {
|
||||
desc = getBinaryNodeChild(descChild, 'body')?.content as string
|
||||
descId = descChild.attrs.id
|
||||
}
|
||||
|
||||
const metadata: GroupMetadata = {
|
||||
id: jidEncode(jidDecode(group.attrs.id).user, 'g.us'),
|
||||
subject: group.attrs.subject,
|
||||
creation: +group.attrs.creation,
|
||||
owner: group.attrs.creator,
|
||||
desc,
|
||||
descId,
|
||||
restrict: !!getBinaryNodeChild(result, 'locked') ? 'true' : 'false',
|
||||
announce: !!getBinaryNodeChild(result, 'announcement') ? 'true' : 'false',
|
||||
participants: getBinaryNodeChildren(group, 'participant').map(
|
||||
({ attrs }) => {
|
||||
return {
|
||||
id: attrs.jid,
|
||||
admin: attrs.type || null as any,
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
return metadata
|
||||
}
|
||||
|
||||
export const makeGroupsSocket = (config: SocketConfig) => {
|
||||
const sock = makeChatsSocket(config)
|
||||
const { query } = 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)
|
||||
}
|
||||
|
||||
return {
|
||||
...sock,
|
||||
groupMetadata,
|
||||
groupCreate: async(subject: string, participants: string[]) => {
|
||||
const key = generateMessageID()
|
||||
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(jid: string) => {
|
||||
await groupQuery(
|
||||
'@g.us',
|
||||
'set',
|
||||
[
|
||||
{
|
||||
tag: 'leave',
|
||||
attrs: { },
|
||||
content: [
|
||||
{ tag: 'group', attrs: { jid } }
|
||||
]
|
||||
}
|
||||
]
|
||||
)
|
||||
},
|
||||
groupUpdateSubject: async(jid: string, subject: string) => {
|
||||
await groupQuery(
|
||||
jid,
|
||||
'set',
|
||||
[
|
||||
{
|
||||
tag: 'subject',
|
||||
attrs: { },
|
||||
content: Buffer.from(subject, 'utf-8')
|
||||
}
|
||||
]
|
||||
)
|
||||
},
|
||||
groupParticipantsUpdate: async(
|
||||
jid: string,
|
||||
participants: string[],
|
||||
action: ParticipantAction
|
||||
) => {
|
||||
const result = await groupQuery(
|
||||
jid,
|
||||
'set',
|
||||
participants.map(
|
||||
jid => ({
|
||||
tag: action,
|
||||
attrs: { },
|
||||
content: [{ tag: 'participant', attrs: { jid } }]
|
||||
})
|
||||
)
|
||||
)
|
||||
const node = getBinaryNodeChild(result, action)
|
||||
const participantsAffected = getBinaryNodeChildren(node!, 'participant')
|
||||
return participantsAffected.map(p => p.attrs.jid)
|
||||
},
|
||||
groupInviteCode: async(jid: string) => {
|
||||
const result = await groupQuery(jid, 'get', [{ tag: 'invite', attrs: {} }])
|
||||
const inviteNode = getBinaryNodeChild(result, 'invite')
|
||||
return inviteNode.attrs.code
|
||||
},
|
||||
groupToggleEphemeral: async(jid: string, ephemeralExpiration: number) => {
|
||||
const content: BinaryNode = ephemeralExpiration ?
|
||||
{ tag: 'ephemeral', attrs: { ephemeral: 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: { } } ])
|
||||
}
|
||||
}
|
||||
}
|
||||
13
src/Socket/index.ts
Normal file
13
src/Socket/index.ts
Normal file
@@ -0,0 +1,13 @@
|
||||
import { SocketConfig } from '../Types'
|
||||
import { DEFAULT_CONNECTION_CONFIG } from '../Defaults'
|
||||
import { makeMessagesSocket as _makeSocket } from './messages-send'
|
||||
|
||||
// export the last socket layer
|
||||
const makeWASocket = (config: Partial<SocketConfig>) => (
|
||||
_makeSocket({
|
||||
...DEFAULT_CONNECTION_CONFIG,
|
||||
...config
|
||||
})
|
||||
)
|
||||
|
||||
export default makeWASocket
|
||||
437
src/Socket/messages-recv.ts
Normal file
437
src/Socket/messages-recv.ts
Normal file
@@ -0,0 +1,437 @@
|
||||
|
||||
import { makeGroupsSocket } from "./groups"
|
||||
import { SocketConfig, WAMessageStubType, ParticipantAction, Chat, GroupMetadata } from "../Types"
|
||||
import { decodeMessageStanza, encodeBigEndian, toNumber, whatsappID } from "../Utils"
|
||||
import { BinaryNode, jidDecode, jidEncode, isJidStatusBroadcast, S_WHATSAPP_NET, areJidsSameUser, getBinaryNodeChildren, getBinaryNodeChild } from '../WABinary'
|
||||
import { downloadIfHistory } from '../Utils/history'
|
||||
import { proto } from "../../WAProto"
|
||||
import { generateSignalPubKey, xmppPreKey, xmppSignedPreKey } from "../Utils/signal"
|
||||
import { KEY_BUNDLE_TYPE } from "../Defaults"
|
||||
|
||||
export const makeMessagesRecvSocket = (config: SocketConfig) => {
|
||||
const { logger } = config
|
||||
const sock = makeGroupsSocket(config)
|
||||
const {
|
||||
ev,
|
||||
authState,
|
||||
ws,
|
||||
assertingPreKeys,
|
||||
sendNode,
|
||||
} = sock
|
||||
|
||||
const sendMessageAck = async({ attrs }: BinaryNode) => {
|
||||
const isGroup = !!attrs.participant
|
||||
const { user: meUser } = jidDecode(authState.creds.me!.id!)
|
||||
const stanza: BinaryNode = {
|
||||
tag: 'ack',
|
||||
attrs: {
|
||||
class: 'receipt',
|
||||
id: attrs.id,
|
||||
to: isGroup ? attrs.from : authState.creds.me!.id,
|
||||
}
|
||||
}
|
||||
if(isGroup) {
|
||||
stanza.attrs.participant = jidEncode(meUser, 's.whatsapp.net')
|
||||
}
|
||||
await sendNode(stanza)
|
||||
}
|
||||
|
||||
const sendRetryRequest = async(node: BinaryNode) => {
|
||||
const retryCount = +(node.attrs.retryCount || 0) + 1
|
||||
const isGroup = !!node.attrs.participant
|
||||
const { account, signedPreKey, signedIdentityKey: identityKey } = authState.creds
|
||||
|
||||
const deviceIdentity = proto.ADVSignedDeviceIdentity.encode(account).finish()
|
||||
await assertingPreKeys(1, async preKeys => {
|
||||
const [keyId] = Object.keys(preKeys)
|
||||
const key = preKeys[+keyId]
|
||||
|
||||
const decFrom = node.attrs.from ? jidDecode(node.attrs.from) : undefined
|
||||
const receipt: BinaryNode = {
|
||||
tag: 'receipt',
|
||||
attrs: {
|
||||
id: node.attrs.id,
|
||||
type: 'retry',
|
||||
to: isGroup ? node.attrs.from : jidEncode(decFrom!.user, 's.whatsapp.net', decFrom!.device, 0)
|
||||
},
|
||||
content: [
|
||||
{
|
||||
tag: 'retry',
|
||||
attrs: {
|
||||
count: retryCount.toString(), id: node.attrs.id,
|
||||
t: node.attrs.t,
|
||||
v: '1'
|
||||
}
|
||||
},
|
||||
{
|
||||
tag: 'registration',
|
||||
attrs: { },
|
||||
content: encodeBigEndian(authState.creds.registrationId)
|
||||
}
|
||||
]
|
||||
}
|
||||
if(node.attrs.recipient) {
|
||||
receipt.attrs.recipient = node.attrs.recipient
|
||||
}
|
||||
if(node.attrs.participant) {
|
||||
receipt.attrs.participant = node.attrs.participant
|
||||
}
|
||||
if(retryCount > 1) {
|
||||
const exec = generateSignalPubKey(Buffer.from(KEY_BUNDLE_TYPE)).slice(0, 1);
|
||||
|
||||
(node.content! as BinaryNode[]).push({
|
||||
tag: 'keys',
|
||||
attrs: { },
|
||||
content: [
|
||||
{ tag: 'type', attrs: { }, content: exec },
|
||||
{ tag: 'identity', attrs: { }, content: identityKey.public },
|
||||
xmppPreKey(key, +keyId),
|
||||
xmppSignedPreKey(signedPreKey),
|
||||
{ tag: 'device-identity', attrs: { }, content: deviceIdentity }
|
||||
]
|
||||
})
|
||||
}
|
||||
await sendNode(node)
|
||||
|
||||
logger.info({ msgId: node.attrs.id, retryCount }, 'sent retry receipt')
|
||||
|
||||
ev.emit('auth-state.update', authState)
|
||||
})
|
||||
}
|
||||
|
||||
const processMessage = (message: proto.IWebMessageInfo, chatUpdate: Partial<Chat>) => {
|
||||
const protocolMsg = message.message?.protocolMessage
|
||||
if(protocolMsg) {
|
||||
switch(protocolMsg.type) {
|
||||
case proto.ProtocolMessage.ProtocolMessageType.APP_STATE_SYNC_KEY_SHARE:
|
||||
const newKeys = JSON.parse(JSON.stringify(protocolMsg.appStateSyncKeyShare!.keys))
|
||||
authState.creds.appStateSyncKeys = [
|
||||
...(authState.creds.appStateSyncKeys || []),
|
||||
...newKeys
|
||||
]
|
||||
ev.emit('auth-state.update', authState)
|
||||
break
|
||||
case proto.ProtocolMessage.ProtocolMessageType.REVOKE:
|
||||
ev.emit('messages.update', [
|
||||
{
|
||||
key: protocolMsg.key,
|
||||
update: { message: null, messageStubType: 1, key: message.key }
|
||||
}
|
||||
])
|
||||
break
|
||||
case proto.ProtocolMessage.ProtocolMessageType.EPHEMERAL_SETTING:
|
||||
chatUpdate.ephemeralSettingTimestamp = toNumber(message.messageTimestamp)
|
||||
chatUpdate.ephemeralExpiration = protocolMsg.ephemeralExpiration
|
||||
break
|
||||
}
|
||||
} else if(message.messageStubType) {
|
||||
const meJid = authState.creds.me!.id
|
||||
const jid = message.key!.remoteJid!
|
||||
//let actor = whatsappID (message.participant)
|
||||
let participants: string[]
|
||||
const emitParticipantsUpdate = (action: ParticipantAction) => (
|
||||
ev.emit('group-participants.update', { id: jid, participants, action })
|
||||
)
|
||||
const emitGroupUpdate = (update: Partial<GroupMetadata>) => {
|
||||
ev.emit('groups.update', [ { id: jid, ...update } ])
|
||||
}
|
||||
|
||||
switch (message.messageStubType) {
|
||||
case WAMessageStubType.GROUP_PARTICIPANT_LEAVE:
|
||||
case WAMessageStubType.GROUP_PARTICIPANT_REMOVE:
|
||||
participants = message.messageStubParameters.map(whatsappID)
|
||||
emitParticipantsUpdate('remove')
|
||||
// mark the chat read only if you left the group
|
||||
if (participants.includes(meJid)) {
|
||||
chatUpdate.readOnly = true
|
||||
}
|
||||
break
|
||||
case WAMessageStubType.GROUP_PARTICIPANT_ADD:
|
||||
case WAMessageStubType.GROUP_PARTICIPANT_INVITE:
|
||||
case WAMessageStubType.GROUP_PARTICIPANT_ADD_REQUEST_JOIN:
|
||||
participants = message.messageStubParameters.map(whatsappID)
|
||||
if (participants.includes(meJid)) {
|
||||
chatUpdate.readOnly = false
|
||||
}
|
||||
emitParticipantsUpdate('add')
|
||||
break
|
||||
case WAMessageStubType.GROUP_CHANGE_ANNOUNCE:
|
||||
const announce = message.messageStubParameters[0] === 'on' ? 'true' : 'false'
|
||||
emitGroupUpdate({ announce })
|
||||
break
|
||||
case WAMessageStubType.GROUP_CHANGE_RESTRICT:
|
||||
const restrict = message.messageStubParameters[0] === 'on' ? 'true' : 'false'
|
||||
emitGroupUpdate({ restrict })
|
||||
break
|
||||
case WAMessageStubType.GROUP_CHANGE_SUBJECT:
|
||||
case WAMessageStubType.GROUP_CREATE:
|
||||
chatUpdate.name = message.messageStubParameters[0]
|
||||
emitGroupUpdate({ subject: chatUpdate.name })
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const processHistoryMessage = (item: proto.HistorySync) => {
|
||||
switch(item.syncType) {
|
||||
case proto.HistorySync.HistorySyncHistorySyncType.INITIAL_BOOTSTRAP:
|
||||
const messages: proto.IWebMessageInfo[] = []
|
||||
const chats = item.conversations!.map(
|
||||
c => {
|
||||
const chat: Chat = { ...c }
|
||||
//@ts-expect-error
|
||||
delete chat.messages
|
||||
for(const item of c.messages || []) {
|
||||
messages.push(item.message)
|
||||
}
|
||||
return chat
|
||||
}
|
||||
)
|
||||
ev.emit('chats.set', { chats, messages })
|
||||
break
|
||||
case proto.HistorySync.HistorySyncHistorySyncType.PUSH_NAME:
|
||||
const contacts = item.pushnames.map(
|
||||
p => ({ notify: p.pushname, id: p.id })
|
||||
)
|
||||
ev.emit('contacts.upsert', contacts)
|
||||
break
|
||||
case proto.HistorySync.HistorySyncHistorySyncType.INITIAL_STATUS_V3:
|
||||
// TODO
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
const processNotification = (node: BinaryNode): Partial<proto.IWebMessageInfo> => {
|
||||
const result: Partial<proto.IWebMessageInfo> = { }
|
||||
const child = (node.content as BinaryNode[])?.[0]
|
||||
|
||||
if(node.attrs.type === 'w:gp2') {
|
||||
switch(child?.tag) {
|
||||
case 'ephemeral':
|
||||
case 'not_ephemeral':
|
||||
result.message = {
|
||||
protocolMessage: {
|
||||
type: proto.ProtocolMessage.ProtocolMessageType.EPHEMERAL_SETTING,
|
||||
ephemeralExpiration: +(child.attrs.expiration || 0)
|
||||
}
|
||||
}
|
||||
break
|
||||
case 'promote':
|
||||
case 'demote':
|
||||
case 'remove':
|
||||
case 'add':
|
||||
case 'leave':
|
||||
const stubType = `GROUP_PARTICIPANT_${child.tag!.toUpperCase()}`
|
||||
result.messageStubType = WAMessageStubType[stubType]
|
||||
result.messageStubParameters = getBinaryNodeChildren(child, 'participant').map(p => p.attrs.jid)
|
||||
break
|
||||
case 'subject':
|
||||
result.messageStubType = WAMessageStubType.GROUP_CHANGE_SUBJECT
|
||||
result.messageStubParameters = [ child.attrs.subject ]
|
||||
break
|
||||
case 'announcement':
|
||||
case 'not_announcement':
|
||||
result.messageStubType = WAMessageStubType.GROUP_CHANGE_ANNOUNCE
|
||||
result.messageStubParameters = [ (child.tag === 'announcement').toString() ]
|
||||
break
|
||||
case 'locked':
|
||||
case 'unlocked':
|
||||
result.messageStubType = WAMessageStubType.GROUP_CHANGE_RESTRICT
|
||||
result.messageStubParameters = [ (child.tag === 'locked').toString() ]
|
||||
break
|
||||
|
||||
}
|
||||
} else {
|
||||
switch(child.tag) {
|
||||
case 'count':
|
||||
if(child.attrs.value === '0') {
|
||||
logger.info('recv all pending notifications')
|
||||
ev.emit('connection.update', { receivedPendingNotifications: true })
|
||||
}
|
||||
break
|
||||
case 'devices':
|
||||
const devices = getBinaryNodeChildren(child, 'device')
|
||||
if(areJidsSameUser(child.attrs.jid, authState.creds!.me!.id)) {
|
||||
const deviceJids = devices.map(d => d.attrs.jid)
|
||||
logger.info({ deviceJids }, 'got my own devices')
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
if(Object.keys(result).length) {
|
||||
return result
|
||||
}
|
||||
}
|
||||
// recv a message
|
||||
ws.on('CB:message', async(stanza: BinaryNode) => {
|
||||
const dec = await decodeMessageStanza(stanza, authState)
|
||||
const fullMessages: proto.IWebMessageInfo[] = []
|
||||
for(const msg of dec.successes) {
|
||||
const { attrs } = stanza
|
||||
const isGroup = !!stanza.attrs.participant
|
||||
const sender = (attrs.participant || attrs.from)?.toString()
|
||||
const isMe = areJidsSameUser(sender, authState.creds.me!.id)
|
||||
|
||||
await sendMessageAck(stanza)
|
||||
|
||||
logger.debug({ msgId: dec.msgId, sender }, 'send message ack')
|
||||
|
||||
// send delivery receipt
|
||||
let recpAttrs: { [_: string]: any }
|
||||
if(isMe) {
|
||||
recpAttrs = {
|
||||
type: 'sender',
|
||||
id: stanza.attrs.id,
|
||||
to: stanza.attrs.from,
|
||||
}
|
||||
if(isGroup) {
|
||||
recpAttrs.participant = stanza.attrs.participant
|
||||
} else {
|
||||
recpAttrs.recipient = stanza.attrs.recipient
|
||||
}
|
||||
} else {
|
||||
const isStatus = isJidStatusBroadcast(stanza.attrs.from)
|
||||
recpAttrs = {
|
||||
//type: 'inactive',
|
||||
id: stanza.attrs.id,
|
||||
to: dec.chatId,
|
||||
}
|
||||
if(isGroup || isStatus) {
|
||||
recpAttrs.participant = stanza.attrs.participant
|
||||
}
|
||||
}
|
||||
await sendNode({ tag: 'receipt', attrs: recpAttrs })
|
||||
|
||||
logger.debug({ msgId: dec.msgId }, 'send message receipt')
|
||||
|
||||
const possibleHistory = downloadIfHistory(msg)
|
||||
if(possibleHistory) {
|
||||
const history = await possibleHistory
|
||||
logger.info({ msgId: dec.msgId, type: history.syncType }, 'recv history')
|
||||
|
||||
processHistoryMessage(history)
|
||||
} else {
|
||||
const message = msg.deviceSentMessage?.message || msg
|
||||
fullMessages.push({
|
||||
key: {
|
||||
remoteJid: dec.chatId,
|
||||
fromMe: isMe,
|
||||
id: dec.msgId,
|
||||
participant: dec.participant
|
||||
},
|
||||
message,
|
||||
status: isMe ? proto.WebMessageInfo.WebMessageInfoStatus.SERVER_ACK : null,
|
||||
messageTimestamp: dec.timestamp,
|
||||
pushName: dec.pushname
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
if(dec.successes.length) {
|
||||
ev.emit('auth-state.update', authState)
|
||||
if(fullMessages.length) {
|
||||
ev.emit(
|
||||
'messages.upsert',
|
||||
{
|
||||
messages: fullMessages.map(m => proto.WebMessageInfo.fromObject(m)),
|
||||
type: stanza.attrs.offline ? 'append' : 'notify'
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
for(const { error } of dec.failures) {
|
||||
logger.error(
|
||||
{ msgId: dec.msgId, trace: error.stack, data: error.data },
|
||||
'failure in decrypting message'
|
||||
)
|
||||
await sendRetryRequest(stanza)
|
||||
}
|
||||
})
|
||||
|
||||
ws.on('CB:ack,class:message', async(node: BinaryNode) => {
|
||||
await sendNode({
|
||||
tag: 'ack',
|
||||
attrs: {
|
||||
class: 'receipt',
|
||||
id: node.attrs.id,
|
||||
from: node.attrs.from
|
||||
}
|
||||
})
|
||||
logger.debug({ attrs: node.attrs }, 'sending receipt for ack')
|
||||
})
|
||||
|
||||
const handleReceipt = ({ attrs, content }: BinaryNode) => {
|
||||
const sender = attrs.participant || attrs.from
|
||||
const status = attrs.type === 'read' ? proto.WebMessageInfo.WebMessageInfoStatus.READ : proto.WebMessageInfo.WebMessageInfoStatus.DELIVERY_ACK
|
||||
const ids = [attrs.id]
|
||||
if(Array.isArray(content)) {
|
||||
const items = getBinaryNodeChildren(content[0], 'item')
|
||||
ids.push(...items.map(i => i.attrs.id))
|
||||
}
|
||||
|
||||
ev.emit('messages.update', ids.map(id => ({
|
||||
key: {
|
||||
remoteJid: attrs.from,
|
||||
id: id,
|
||||
fromMe: areJidsSameUser(sender, authState.creds.me!.id!),
|
||||
participant: attrs.participant
|
||||
},
|
||||
update: { status }
|
||||
})))
|
||||
}
|
||||
|
||||
ws.on('CB:receipt,type:read', handleReceipt)
|
||||
ws.on('CB:ack,class:receipt', handleReceipt)
|
||||
|
||||
ws.on('CB:notification', async(node: BinaryNode) => {
|
||||
const sendAck = async() => {
|
||||
await sendNode({
|
||||
tag: 'ack',
|
||||
attrs: {
|
||||
class: 'notification',
|
||||
id: node.attrs.id,
|
||||
type: node.attrs.type,
|
||||
to: node.attrs.from
|
||||
}
|
||||
})
|
||||
|
||||
logger.debug({ msgId: node.attrs.id }, 'ack notification')
|
||||
}
|
||||
|
||||
await sendAck()
|
||||
|
||||
const msg = processNotification(node)
|
||||
if(msg) {
|
||||
const fromMe = areJidsSameUser(node.attrs.participant || node.attrs.from, authState.creds.me!.id)
|
||||
msg.key = {
|
||||
remoteJid: node.attrs.from,
|
||||
fromMe,
|
||||
participant: node.attrs.participant,
|
||||
id: node.attrs.id
|
||||
}
|
||||
msg.messageTimestamp = +node.attrs.t
|
||||
|
||||
const fullMsg = proto.WebMessageInfo.fromObject(msg)
|
||||
ev.emit('messages.upsert', { messages: [fullMsg], type: 'append' })
|
||||
}
|
||||
})
|
||||
|
||||
ev.on('messages.upsert', ({ messages }) => {
|
||||
const chat: Partial<Chat> = { id: messages[0].key.remoteJid }
|
||||
for(const msg of messages) {
|
||||
processMessage(msg, chat)
|
||||
if(!!msg.message && !msg.message!.protocolMessage) {
|
||||
chat.conversationTimestamp = toNumber(msg.messageTimestamp)
|
||||
if(!msg.key.fromMe) {
|
||||
chat.unreadCount = (chat.unreadCount || 0) + 1
|
||||
}
|
||||
}
|
||||
}
|
||||
if(Object.keys(chat).length > 1) {
|
||||
ev.emit('chats.update', [ chat ])
|
||||
}
|
||||
})
|
||||
|
||||
return sock
|
||||
}
|
||||
392
src/Socket/messages-send.ts
Normal file
392
src/Socket/messages-send.ts
Normal file
@@ -0,0 +1,392 @@
|
||||
|
||||
import { makeMessagesRecvSocket } from "./messages-recv"
|
||||
import { SocketConfig, MediaConnInfo, AnyMessageContent, MiscMessageGenerationOptions, WAMediaUploadFunction } from "../Types"
|
||||
import { encodeWAMessage, generateMessageID, generateWAMessage } from "../Utils"
|
||||
import { BinaryNode, getBinaryNodeChild, getBinaryNodeChildren, isJidGroup, jidDecode, jidEncode, S_WHATSAPP_NET } from '../WABinary'
|
||||
import { proto } from "../../WAProto"
|
||||
import { encryptSenderKeyMsgSignalProto, encryptSignalProto, extractDeviceJids, jidToSignalProtocolAddress, parseAndInjectE2ESession } from "../Utils/signal"
|
||||
import { WA_DEFAULT_EPHEMERAL, DEFAULT_ORIGIN, MEDIA_PATH_MAP } from "../Defaults"
|
||||
import got from "got"
|
||||
import { Boom } from "@hapi/boom"
|
||||
|
||||
export const makeMessagesSocket = (config: SocketConfig) => {
|
||||
const { logger } = config
|
||||
const sock = makeMessagesRecvSocket(config)
|
||||
const {
|
||||
ev,
|
||||
authState,
|
||||
query,
|
||||
generateMessageTag,
|
||||
sendNode,
|
||||
groupMetadata,
|
||||
groupToggleEphemeral
|
||||
} = sock
|
||||
|
||||
let mediaConn: Promise<MediaConnInfo>
|
||||
const refreshMediaConn = async(forceGet = false) => {
|
||||
let media = await mediaConn
|
||||
if (!media || forceGet || (new Date().getTime()-media.fetchDate.getTime()) > media.ttl*1000) {
|
||||
mediaConn = (async() => {
|
||||
const result = await query({
|
||||
tag: 'iq',
|
||||
attrs: {
|
||||
type: 'set',
|
||||
xmlns: 'w:m',
|
||||
to: S_WHATSAPP_NET,
|
||||
},
|
||||
content: [ { tag: 'media_conn', attrs: { } } ]
|
||||
})
|
||||
const mediaConnNode = getBinaryNodeChild(result, 'media_conn')
|
||||
const node: MediaConnInfo = {
|
||||
hosts: getBinaryNodeChildren(mediaConnNode, 'host').map(
|
||||
item => item.attrs as any
|
||||
),
|
||||
auth: mediaConnNode.attrs.auth,
|
||||
ttl: +mediaConnNode.attrs.ttl,
|
||||
fetchDate: new Date()
|
||||
}
|
||||
logger.debug('fetched media conn')
|
||||
return node
|
||||
})()
|
||||
}
|
||||
return mediaConn
|
||||
}
|
||||
|
||||
const sendReadReceipt = async(jid: string, participant: string | undefined, messageIds: string[]) => {
|
||||
const node: BinaryNode = {
|
||||
tag: 'receipt',
|
||||
attrs: {
|
||||
id: messageIds[0],
|
||||
t: Date.now().toString(),
|
||||
to: jid,
|
||||
type: 'read'
|
||||
},
|
||||
}
|
||||
if(participant) {
|
||||
node.attrs.participant = participant
|
||||
}
|
||||
messageIds = messageIds.slice(1)
|
||||
if(messageIds.length) {
|
||||
node.content = [
|
||||
{
|
||||
tag: 'list',
|
||||
attrs: { },
|
||||
content: messageIds.map(id => ({
|
||||
tag: 'item',
|
||||
attrs: { id }
|
||||
}))
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
logger.debug({ jid, messageIds }, 'reading messages')
|
||||
await sendNode(node)
|
||||
}
|
||||
|
||||
const getUSyncDevices = async(jids: string[], ignoreZeroDevices: boolean) => {
|
||||
const users = jids.map<BinaryNode>(jid => ({ tag: 'user', attrs: { jid } }))
|
||||
const iq: BinaryNode = {
|
||||
tag: 'iq',
|
||||
attrs: {
|
||||
to: S_WHATSAPP_NET,
|
||||
type: 'get',
|
||||
xmlns: 'usync',
|
||||
},
|
||||
content: [
|
||||
{
|
||||
tag: 'usync',
|
||||
attrs: {
|
||||
sid: generateMessageTag(),
|
||||
mode: 'query',
|
||||
last: 'true',
|
||||
index: '0',
|
||||
context: 'message',
|
||||
},
|
||||
content: [
|
||||
{
|
||||
tag: 'query',
|
||||
attrs: { },
|
||||
content: [
|
||||
{
|
||||
tag: 'devices',
|
||||
attrs: { version: '2' }
|
||||
}
|
||||
]
|
||||
},
|
||||
{ tag: 'list', attrs: { }, content: users }
|
||||
]
|
||||
},
|
||||
],
|
||||
}
|
||||
const result = await query(iq)
|
||||
let resultJids = extractDeviceJids(result)
|
||||
if(ignoreZeroDevices) {
|
||||
resultJids = resultJids.filter(item => item.device !== 0)
|
||||
}
|
||||
|
||||
return resultJids
|
||||
}
|
||||
|
||||
const assertSession = async(jid: string, force: boolean) => {
|
||||
const addr = jidToSignalProtocolAddress(jid).toString()
|
||||
const session = await authState.keys.getSession(addr)
|
||||
if(!session || force) {
|
||||
logger.debug({ jid }, `fetching session`)
|
||||
const identity: BinaryNode = {
|
||||
tag: 'user',
|
||||
attrs: { jid, reason: 'identity' },
|
||||
}
|
||||
const result = await query({
|
||||
tag: 'iq',
|
||||
attrs: {
|
||||
xmlns: 'encrypt',
|
||||
type: 'get',
|
||||
to: S_WHATSAPP_NET,
|
||||
},
|
||||
content: [
|
||||
{
|
||||
tag: 'key',
|
||||
attrs: { },
|
||||
content: [ identity ]
|
||||
}
|
||||
]
|
||||
})
|
||||
await parseAndInjectE2ESession(result, authState)
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
const createParticipantNode = async(jid: string, bytes: Buffer) => {
|
||||
await assertSession(jid, false)
|
||||
|
||||
const { type, ciphertext } = await encryptSignalProto(jid, bytes, authState)
|
||||
const node: BinaryNode = {
|
||||
tag: 'to',
|
||||
attrs: { jid },
|
||||
content: [{
|
||||
tag: 'enc',
|
||||
attrs: { v: '2', type },
|
||||
content: ciphertext
|
||||
}]
|
||||
}
|
||||
return node
|
||||
}
|
||||
|
||||
const relayMessage = async(jid: string, message: proto.IMessage, msgId?: string) => {
|
||||
const { user, server } = jidDecode(jid)
|
||||
const isGroup = server === 'g.us'
|
||||
msgId = msgId || generateMessageID()
|
||||
const encodedMsg = encodeWAMessage(message)
|
||||
const participants: BinaryNode[] = []
|
||||
let stanza: BinaryNode
|
||||
|
||||
const destinationJid = jidEncode(user, isGroup ? 'g.us' : 's.whatsapp.net')
|
||||
|
||||
if(isGroup) {
|
||||
const { ciphertext, senderKeyDistributionMessageKey } = await encryptSenderKeyMsgSignalProto(destinationJid, encodedMsg, authState)
|
||||
const groupData = await groupMetadata(jid)
|
||||
const participantsList = groupData.participants.map(p => p.id)
|
||||
const devices = await getUSyncDevices(participantsList, false)
|
||||
|
||||
logger.debug(`got ${devices.length} additional devices`)
|
||||
|
||||
const encSenderKeyMsg = encodeWAMessage({
|
||||
senderKeyDistributionMessage: {
|
||||
axolotlSenderKeyDistributionMessage: senderKeyDistributionMessageKey,
|
||||
groupId: destinationJid
|
||||
}
|
||||
})
|
||||
|
||||
for(const {user, device, agent} of devices) {
|
||||
const jid = jidEncode(user, 's.whatsapp.net', device, agent)
|
||||
const participant = await createParticipantNode(jid, encSenderKeyMsg)
|
||||
participants.push(participant)
|
||||
}
|
||||
|
||||
const binaryNodeContent: BinaryNode[] = []
|
||||
if( // if there are some participants with whom the session has not been established
|
||||
// if there are, we overwrite the senderkey
|
||||
!!participants.find((p) => (
|
||||
!!(p.content as BinaryNode[]).find(({ attrs }) => attrs.type == 'pkmsg')
|
||||
))
|
||||
) {
|
||||
binaryNodeContent.push({
|
||||
tag: 'participants',
|
||||
attrs: { },
|
||||
content: participants
|
||||
})
|
||||
}
|
||||
|
||||
binaryNodeContent.push({
|
||||
tag: 'enc',
|
||||
attrs: { v: '2', type: 'skmsg' },
|
||||
content: ciphertext
|
||||
})
|
||||
|
||||
stanza = {
|
||||
tag: 'message',
|
||||
attrs: {
|
||||
id: msgId,
|
||||
type: 'text',
|
||||
to: destinationJid
|
||||
},
|
||||
content: binaryNodeContent
|
||||
}
|
||||
} else {
|
||||
const { user: meUser } = jidDecode(authState.creds.me!.id!)
|
||||
|
||||
const messageToMyself: proto.IMessage = {
|
||||
deviceSentMessage: {
|
||||
destinationJid,
|
||||
message
|
||||
}
|
||||
}
|
||||
const encodedMeMsg = encodeWAMessage(messageToMyself)
|
||||
|
||||
participants.push(
|
||||
await createParticipantNode(jidEncode(user, 's.whatsapp.net'), encodedMsg)
|
||||
)
|
||||
participants.push(
|
||||
await createParticipantNode(jidEncode(meUser, 's.whatsapp.net'), encodedMeMsg)
|
||||
)
|
||||
const devices = await getUSyncDevices([ authState.creds.me!.id!, jid ], true)
|
||||
|
||||
logger.debug(`got ${devices.length} additional devices`)
|
||||
|
||||
for(const { user, device, agent } of devices) {
|
||||
const isMe = user === meUser
|
||||
participants.push(
|
||||
await createParticipantNode(
|
||||
jidEncode(user, 's.whatsapp.net', device, agent),
|
||||
isMe ? encodedMeMsg : encodedMsg
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
stanza = {
|
||||
tag: 'message',
|
||||
attrs: {
|
||||
id: msgId,
|
||||
type: 'text',
|
||||
to: destinationJid
|
||||
},
|
||||
content: [
|
||||
{
|
||||
tag: 'participants',
|
||||
attrs: { },
|
||||
content: participants
|
||||
},
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
const shouldHaveIdentity = !!participants.find((p) => (
|
||||
!!(p.content as BinaryNode[]).find(({ attrs }) => attrs.type == 'pkmsg')
|
||||
))
|
||||
|
||||
if(shouldHaveIdentity) {
|
||||
(stanza.content as BinaryNode[]).push({
|
||||
tag: 'device-identity',
|
||||
attrs: { },
|
||||
content: proto.ADVSignedDeviceIdentity.encode(authState.creds.account).finish()
|
||||
})
|
||||
}
|
||||
logger.debug({ msgId }, 'sending message')
|
||||
|
||||
await sendNode(stanza)
|
||||
|
||||
ev.emit('auth-state.update', authState)
|
||||
return msgId
|
||||
}
|
||||
|
||||
const waUploadToServer: WAMediaUploadFunction = async(stream, { mediaType, fileEncSha256B64 }) => {
|
||||
// send a query JSON to obtain the url & auth token to upload our media
|
||||
let uploadInfo = await refreshMediaConn(false)
|
||||
|
||||
let mediaUrl: string
|
||||
for (let host of uploadInfo.hosts) {
|
||||
const auth = encodeURIComponent(uploadInfo.auth) // the auth token
|
||||
const url = `https://${host.hostname}${MEDIA_PATH_MAP[mediaType]}/${fileEncSha256B64}?auth=${auth}&token=${fileEncSha256B64}`
|
||||
|
||||
try {
|
||||
const {body: responseText} = await got.post(
|
||||
url,
|
||||
{
|
||||
headers: {
|
||||
'Content-Type': 'application/octet-stream',
|
||||
'Origin': DEFAULT_ORIGIN
|
||||
},
|
||||
agent: {
|
||||
https: config.agent
|
||||
},
|
||||
body: stream
|
||||
}
|
||||
)
|
||||
const result = JSON.parse(responseText)
|
||||
mediaUrl = result?.url
|
||||
|
||||
if (mediaUrl) break
|
||||
else {
|
||||
uploadInfo = await refreshMediaConn(true)
|
||||
throw new Error(`upload failed, reason: ${JSON.stringify(result)}`)
|
||||
}
|
||||
} catch (error) {
|
||||
const isLast = host.hostname === uploadInfo.hosts[uploadInfo.hosts.length-1].hostname
|
||||
logger.debug(`Error in uploading to ${host.hostname} (${error}) ${isLast ? '' : ', retrying...'}`)
|
||||
}
|
||||
}
|
||||
if (!mediaUrl) {
|
||||
throw new Boom(
|
||||
'Media upload failed on all hosts',
|
||||
{ statusCode: 500 }
|
||||
)
|
||||
}
|
||||
return { mediaUrl }
|
||||
}
|
||||
|
||||
return {
|
||||
...sock,
|
||||
assertSession,
|
||||
relayMessage,
|
||||
sendReadReceipt,
|
||||
refreshMediaConn,
|
||||
sendMessage: async(
|
||||
jid: string,
|
||||
content: AnyMessageContent,
|
||||
options: MiscMessageGenerationOptions = { }
|
||||
) => {
|
||||
const userJid = authState.creds.me!.id
|
||||
if(
|
||||
typeof content === 'object' &&
|
||||
'disappearingMessagesInChat' in content &&
|
||||
typeof content['disappearingMessagesInChat'] !== 'undefined' &&
|
||||
isJidGroup(jid)
|
||||
) {
|
||||
const { disappearingMessagesInChat } = content
|
||||
const value = typeof disappearingMessagesInChat === 'boolean' ?
|
||||
(disappearingMessagesInChat ? WA_DEFAULT_EPHEMERAL : 0) :
|
||||
disappearingMessagesInChat
|
||||
await groupToggleEphemeral(jid, value)
|
||||
} else {
|
||||
const fullMsg = await generateWAMessage(
|
||||
jid,
|
||||
content,
|
||||
{
|
||||
...options,
|
||||
logger,
|
||||
userJid: userJid,
|
||||
// multi-device does not have this yet
|
||||
//getUrlInfo: generateUrlInfo,
|
||||
upload: waUploadToServer
|
||||
}
|
||||
)
|
||||
await relayMessage(jid, fullMsg.message)
|
||||
process.nextTick(() => {
|
||||
ev.emit('messages.upsert', { messages: [fullMsg], type: 'append' })
|
||||
})
|
||||
return fullMsg
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
469
src/Socket/socket.ts
Normal file
469
src/Socket/socket.ts
Normal file
@@ -0,0 +1,469 @@
|
||||
import { Boom } from '@hapi/boom'
|
||||
import EventEmitter from 'events'
|
||||
import { promisify } from "util"
|
||||
import WebSocket from "ws"
|
||||
import { randomBytes } from 'crypto'
|
||||
import { proto } from '../../WAProto'
|
||||
import { DisconnectReason, SocketConfig, BaileysEventEmitter } from "../Types"
|
||||
import { generateCurveKeyPair, initAuthState, generateRegistrationNode, configureSuccessfulPairing, generateLoginNode, encodeBigEndian, promiseTimeout } from "../Utils"
|
||||
import { DEFAULT_ORIGIN, DEF_TAG_PREFIX, DEF_CALLBACK_PREFIX, KEY_BUNDLE_TYPE } from "../Defaults"
|
||||
import { assertNodeErrorFree, BinaryNode, encodeBinaryNode, S_WHATSAPP_NET } from '../WABinary'
|
||||
import noiseHandler from '../Utils/noise-handler'
|
||||
import { generateOrGetPreKeys, xmppSignedPreKey, xmppPreKey, getPreKeys } from '../Utils/signal'
|
||||
|
||||
/**
|
||||
* Connects to WA servers and performs:
|
||||
* - simple queries (no retry mechanism, wait for connection establishment)
|
||||
* - listen to messages and emit events
|
||||
* - query phone connection
|
||||
*/
|
||||
export const makeSocket = ({
|
||||
waWebSocketUrl,
|
||||
connectTimeoutMs,
|
||||
logger,
|
||||
agent,
|
||||
keepAliveIntervalMs,
|
||||
version,
|
||||
browser,
|
||||
auth: initialAuthState
|
||||
}: SocketConfig) => {
|
||||
const ws = new WebSocket(waWebSocketUrl, undefined, {
|
||||
origin: DEFAULT_ORIGIN,
|
||||
timeout: connectTimeoutMs,
|
||||
agent,
|
||||
headers: {
|
||||
'Accept-Encoding': 'gzip, deflate, br',
|
||||
'Accept-Language': 'en-US,en;q=0.9',
|
||||
'Cache-Control': 'no-cache',
|
||||
'Host': 'web.whatsapp.com',
|
||||
'Pragma': 'no-cache',
|
||||
'Sec-WebSocket-Extensions': 'permessage-deflate; client_max_window_bits'
|
||||
}
|
||||
})
|
||||
ws.setMaxListeners(0)
|
||||
/** ephemeral key pair used to encrypt/decrypt communication. Unique for each connection */
|
||||
const ephemeralKeyPair = generateCurveKeyPair()
|
||||
/** WA noise protocol wrapper */
|
||||
const noise = noiseHandler(ephemeralKeyPair)
|
||||
const authState = initialAuthState || initAuthState()
|
||||
const { creds } = authState
|
||||
const ev = new EventEmitter() as BaileysEventEmitter
|
||||
|
||||
let lastDateRecv: Date
|
||||
let epoch = 0
|
||||
let keepAliveReq: NodeJS.Timeout
|
||||
|
||||
const uqTagId = `${randomBytes(1).toString('hex')[0]}.${randomBytes(1).toString('hex')[0]}-`
|
||||
const generateMessageTag = () => `${uqTagId}${epoch++}`
|
||||
|
||||
const sendPromise = promisify<void>(ws.send)
|
||||
/** send a raw buffer */
|
||||
const sendRawMessage = (data: Buffer | Uint8Array) => {
|
||||
const bytes = noise.encodeFrame(data)
|
||||
return sendPromise.call(ws, bytes) as Promise<void>
|
||||
}
|
||||
/** send a binary node */
|
||||
const sendNode = (node: BinaryNode) => {
|
||||
let buff = encodeBinaryNode(node)
|
||||
return sendRawMessage(buff)
|
||||
}
|
||||
/** await the next incoming message */
|
||||
const awaitNextMessage = async(sendMsg?: Uint8Array) => {
|
||||
if(ws.readyState !== ws.OPEN) {
|
||||
throw new Boom('Connection Closed', { statusCode: DisconnectReason.connectionClosed })
|
||||
}
|
||||
let onOpen: (data: any) => void
|
||||
let onClose: (err: Error) => void
|
||||
|
||||
const result = new Promise<any>((resolve, reject) => {
|
||||
onOpen = (data: any) => resolve(data)
|
||||
onClose = reject
|
||||
ws.on('frame', onOpen)
|
||||
ws.on('close', onClose)
|
||||
ws.on('error', onClose)
|
||||
})
|
||||
.finally(() => {
|
||||
ws.off('frame', onOpen)
|
||||
ws.off('close', onClose)
|
||||
ws.off('error', onClose)
|
||||
})
|
||||
|
||||
if(sendMsg) {
|
||||
sendRawMessage(sendMsg).catch(onClose)
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
/**
|
||||
* Wait for a message with a certain tag to be received
|
||||
* @param tag the message tag to await
|
||||
* @param json query that was sent
|
||||
* @param timeoutMs timeout after which the promise will reject
|
||||
*/
|
||||
const waitForMessage = async(msgId: string, timeoutMs?: number) => {
|
||||
let onRecv: (json) => void
|
||||
let onErr: (err) => void
|
||||
try {
|
||||
const result = await promiseTimeout(timeoutMs,
|
||||
(resolve, reject) => {
|
||||
onRecv = resolve
|
||||
onErr = err => {
|
||||
reject(err || new Boom('Connection Closed', { statusCode: DisconnectReason.connectionClosed }))
|
||||
}
|
||||
|
||||
ws.on(`TAG:${msgId}`, onRecv)
|
||||
ws.on('close', onErr) // if the socket closes, you'll never receive the message
|
||||
},
|
||||
)
|
||||
return result as any
|
||||
} finally {
|
||||
ws.off(`TAG:${msgId}`, onRecv)
|
||||
ws.off('close', onErr) // if the socket closes, you'll never receive the message
|
||||
}
|
||||
}
|
||||
/** send a query, and wait for its response. auto-generates message ID if not provided */
|
||||
const query = async(node: BinaryNode, timeoutMs?: number) => {
|
||||
if(!node.attrs.id) node.attrs.id = generateMessageTag()
|
||||
|
||||
const msgId = node.attrs.id
|
||||
const wait = waitForMessage(msgId, timeoutMs)
|
||||
|
||||
await sendNode(node)
|
||||
|
||||
const result = await (wait as Promise<BinaryNode>)
|
||||
if('tag' in result) {
|
||||
assertNodeErrorFree(result)
|
||||
}
|
||||
return result
|
||||
}
|
||||
/** connection handshake */
|
||||
const validateConnection = async () => {
|
||||
logger.info('connected to WA Web')
|
||||
|
||||
const init = proto.HandshakeMessage.encode({
|
||||
clientHello: { ephemeral: ephemeralKeyPair.public }
|
||||
}).finish()
|
||||
|
||||
const result = await awaitNextMessage(init)
|
||||
const handshake = proto.HandshakeMessage.decode(result)
|
||||
|
||||
logger.debug('handshake recv from WA Web')
|
||||
|
||||
const keyEnc = noise.processHandshake(handshake, creds.noiseKey)
|
||||
logger.info('handshake complete')
|
||||
|
||||
let node: Uint8Array
|
||||
if(!creds.me) {
|
||||
logger.info('not logged in, attempting registration...')
|
||||
node = generateRegistrationNode(creds, { version, browser })
|
||||
} else {
|
||||
logger.info('logging in...')
|
||||
node = generateLoginNode(creds.me!.id, { version, browser })
|
||||
}
|
||||
const payloadEnc = noise.encrypt(node)
|
||||
await sendRawMessage(
|
||||
proto.HandshakeMessage.encode({
|
||||
clientFinish: {
|
||||
static: new Uint8Array(keyEnc),
|
||||
payload: new Uint8Array(payloadEnc),
|
||||
},
|
||||
}).finish()
|
||||
)
|
||||
noise.finishInit()
|
||||
startKeepAliveRequest()
|
||||
}
|
||||
/** get some pre-keys and do something with them */
|
||||
const assertingPreKeys = async(range: number, execute: (keys: { [_: number]: any }) => Promise<void>) => {
|
||||
const { newPreKeys, lastPreKeyId, preKeysRange } = generateOrGetPreKeys(authState, range)
|
||||
const preKeys = await getPreKeys(authState.keys, preKeysRange[0], preKeysRange[1])
|
||||
|
||||
await execute(preKeys)
|
||||
|
||||
creds.serverHasPreKeys = true
|
||||
creds.nextPreKeyId = Math.max(lastPreKeyId+1, creds.nextPreKeyId)
|
||||
creds.firstUnuploadedPreKeyId = Math.max(creds.firstUnuploadedPreKeyId, lastPreKeyId+1)
|
||||
await Promise.all(
|
||||
Object.keys(newPreKeys).map(k => authState.keys.setPreKey(+k, newPreKeys[+k]))
|
||||
)
|
||||
|
||||
ev.emit('auth-state.update', authState)
|
||||
}
|
||||
/** generates and uploads a set of pre-keys */
|
||||
const uploadPreKeys = async() => {
|
||||
await assertingPreKeys(50, async preKeys => {
|
||||
const node: BinaryNode = {
|
||||
tag: 'iq',
|
||||
attrs: {
|
||||
id: generateMessageTag(),
|
||||
xmlns: 'encrypt',
|
||||
type: 'set',
|
||||
to: S_WHATSAPP_NET,
|
||||
},
|
||||
content: [
|
||||
{ tag: 'registration', attrs: { }, content: encodeBigEndian(creds.registrationId) },
|
||||
{ tag: 'type', attrs: { }, content: KEY_BUNDLE_TYPE },
|
||||
{ tag: 'identity', attrs: { }, content: creds.signedIdentityKey.public },
|
||||
{ tag: 'list', attrs: { }, content: Object.keys(preKeys).map(k => xmppPreKey(preKeys[+k], +k)) },
|
||||
xmppSignedPreKey(creds.signedPreKey)
|
||||
]
|
||||
}
|
||||
await sendNode(node)
|
||||
|
||||
logger.info('uploaded pre-keys')
|
||||
})
|
||||
}
|
||||
|
||||
const onMessageRecieved = (data: Buffer) => {
|
||||
noise.decodeFrame(data, frame => {
|
||||
ws.emit('frame', frame)
|
||||
// if it's a binary node
|
||||
if(!(frame instanceof Uint8Array)) {
|
||||
const msgId = frame.attrs.id
|
||||
|
||||
if(logger.level === 'trace') {
|
||||
logger.trace({ msgId, fromMe: false, frame }, 'communication')
|
||||
}
|
||||
|
||||
let anyTriggered = false
|
||||
/* Check if this is a response to a message we sent */
|
||||
anyTriggered = ws.emit(`${DEF_TAG_PREFIX}${msgId}`, frame)
|
||||
/* Check if this is a response to a message we are expecting */
|
||||
const l0 = frame.tag
|
||||
const l1 = frame.attrs || { }
|
||||
const l2 = Array.isArray(frame.content) ? frame.content[0]?.tag : ''
|
||||
|
||||
Object.keys(l1).forEach(key => {
|
||||
anyTriggered = ws.emit(`${DEF_CALLBACK_PREFIX}${l0},${key}:${l1[key]},${l2}`, frame) || anyTriggered
|
||||
anyTriggered = ws.emit(`${DEF_CALLBACK_PREFIX}${l0},${key}:${l1[key]}`, frame) || anyTriggered
|
||||
anyTriggered = ws.emit(`${DEF_CALLBACK_PREFIX}${l0},${key}`, frame) || anyTriggered
|
||||
})
|
||||
anyTriggered = ws.emit(`${DEF_CALLBACK_PREFIX}${l0},,${l2}`, frame) || anyTriggered
|
||||
anyTriggered = ws.emit(`${DEF_CALLBACK_PREFIX}${l0}`, frame) || anyTriggered
|
||||
anyTriggered = ws.emit('frame', frame) || anyTriggered
|
||||
|
||||
if (!anyTriggered && logger.level === 'debug') {
|
||||
logger.debug({ unhandled: true, msgId, fromMe: false, frame }, 'communication recv')
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const end = (error: Error | undefined) => {
|
||||
logger.info({ error }, 'connection closed')
|
||||
|
||||
clearInterval(keepAliveReq)
|
||||
|
||||
ws.removeAllListeners('close')
|
||||
ws.removeAllListeners('error')
|
||||
ws.removeAllListeners('open')
|
||||
ws.removeAllListeners('message')
|
||||
|
||||
if(ws.readyState !== ws.CLOSED && ws.readyState !== ws.CLOSING) {
|
||||
try { ws.close() } catch { }
|
||||
}
|
||||
|
||||
ev.emit('connection.update', {
|
||||
connection: 'close',
|
||||
lastDisconnect: {
|
||||
error,
|
||||
date: new Date()
|
||||
}
|
||||
})
|
||||
ws.removeAllListeners('connection.update')
|
||||
}
|
||||
|
||||
const waitForSocketOpen = async() => {
|
||||
if(ws.readyState === ws.OPEN) return
|
||||
if(ws.readyState === ws.CLOSED || ws.readyState === ws.CLOSING) {
|
||||
throw new Boom('Connection Closed', { statusCode: DisconnectReason.connectionClosed })
|
||||
}
|
||||
let onOpen: () => void
|
||||
let onClose: (err: Error) => void
|
||||
await new Promise((resolve, reject) => {
|
||||
onOpen = () => resolve(undefined)
|
||||
onClose = reject
|
||||
ws.on('open', onOpen)
|
||||
ws.on('close', onClose)
|
||||
ws.on('error', onClose)
|
||||
})
|
||||
.finally(() => {
|
||||
ws.off('open', onOpen)
|
||||
ws.off('close', onClose)
|
||||
ws.off('error', onClose)
|
||||
})
|
||||
}
|
||||
|
||||
const startKeepAliveRequest = () => (
|
||||
keepAliveReq = setInterval(() => {
|
||||
if (!lastDateRecv) lastDateRecv = new Date()
|
||||
const diff = Date.now() - lastDateRecv.getTime()
|
||||
/*
|
||||
check if it's been a suspicious amount of time since the server responded with our last seen
|
||||
it could be that the network is down
|
||||
*/
|
||||
if (diff > keepAliveIntervalMs+5000) {
|
||||
end(new Boom('Connection was lost', { statusCode: DisconnectReason.connectionLost }))
|
||||
} else if(ws.readyState === ws.OPEN) {
|
||||
// if its all good, send a keep alive request
|
||||
query(
|
||||
{
|
||||
tag: 'iq',
|
||||
attrs: {
|
||||
id: generateMessageTag(),
|
||||
to: S_WHATSAPP_NET,
|
||||
type: 'get',
|
||||
xmlns: 'w:p',
|
||||
},
|
||||
content: [{ tag: 'ping', attrs: { } }]
|
||||
},
|
||||
keepAliveIntervalMs
|
||||
)
|
||||
.then(() => {
|
||||
lastDateRecv = new Date()
|
||||
logger.trace('recv keep alive')
|
||||
})
|
||||
.catch(err => end(err))
|
||||
} else {
|
||||
logger.warn('keep alive called when WS not open')
|
||||
}
|
||||
}, keepAliveIntervalMs)
|
||||
)
|
||||
/** i have no idea why this exists. pls enlighten me */
|
||||
const sendPassiveIq = (tag: 'passive' | 'active') => (
|
||||
sendNode({
|
||||
tag: 'iq',
|
||||
attrs: {
|
||||
to: S_WHATSAPP_NET,
|
||||
xmlns: 'passive',
|
||||
type: 'set',
|
||||
id: generateMessageTag(),
|
||||
},
|
||||
content: [
|
||||
{ tag, attrs: { } }
|
||||
]
|
||||
})
|
||||
)
|
||||
/** logout & invalidate connection */
|
||||
const logout = async() => {
|
||||
await sendNode({
|
||||
tag: 'iq',
|
||||
attrs: {
|
||||
to: S_WHATSAPP_NET,
|
||||
type: 'set',
|
||||
id: generateMessageTag(),
|
||||
xmlns: 'md'
|
||||
},
|
||||
content: [
|
||||
{
|
||||
tag: 'remove-companion-device',
|
||||
attrs: {
|
||||
jid: authState.creds.me!.id,
|
||||
reason: 'user_initiated'
|
||||
}
|
||||
}
|
||||
]
|
||||
})
|
||||
end(new Boom('Intentional Logout', { statusCode: DisconnectReason.loggedOut }))
|
||||
}
|
||||
|
||||
ws.on('message', onMessageRecieved)
|
||||
ws.on('open', validateConnection)
|
||||
ws.on('error', end)
|
||||
ws.on('close', () => end(new Boom('Connection Terminated', { statusCode: DisconnectReason.connectionClosed })))
|
||||
// the server terminated the connection
|
||||
ws.on('CB:xmlstreamend', () => {
|
||||
end(new Boom('Connection Terminated by Server', { statusCode: DisconnectReason.connectionClosed }))
|
||||
})
|
||||
// QR gen
|
||||
ws.on('CB:iq,type:set,pair-device', async (stanza: BinaryNode) => {
|
||||
const postQR = async() => {
|
||||
const QR = await import('qrcode-terminal').catch(err => {
|
||||
logger.error('add `qrcode-terminal` as a dependency to auto-print QR')
|
||||
})
|
||||
QR?.generate(qr, { small: true })
|
||||
}
|
||||
|
||||
const refs = ((stanza.content[0] as BinaryNode).content as BinaryNode[]).map(n => n.content as string)
|
||||
const iq: BinaryNode = {
|
||||
tag: 'iq',
|
||||
attrs: {
|
||||
to: S_WHATSAPP_NET,
|
||||
type: 'result',
|
||||
id: stanza.attrs.id,
|
||||
}
|
||||
}
|
||||
const noiseKeyB64 = Buffer.from(creds.noiseKey.public).toString('base64');
|
||||
const identityKeyB64 = Buffer.from(creds.signedIdentityKey.public).toString('base64')
|
||||
const advB64 = creds.advSecretKey
|
||||
const qr = [refs[0], noiseKeyB64, identityKeyB64, advB64].join(',');
|
||||
|
||||
ev.emit('connection.update', { qr })
|
||||
await postQR()
|
||||
await sendNode(iq)
|
||||
})
|
||||
// device paired for the first time
|
||||
// if device pairs successfully, the server asks to restart the connection
|
||||
ws.on('CB:iq,,pair-success', async(stanza: BinaryNode) => {
|
||||
logger.debug('pair success recv')
|
||||
try {
|
||||
const { reply, creds: updatedCreds } = configureSuccessfulPairing(stanza, creds)
|
||||
|
||||
logger.debug('pairing configured successfully')
|
||||
|
||||
const waiting = awaitNextMessage()
|
||||
await sendNode(reply)
|
||||
|
||||
const value = (await waiting) as BinaryNode
|
||||
if(value.tag === 'stream:error') {
|
||||
if(value.attrs?.code !== '515') {
|
||||
throw new Boom('Authentication failed', { statusCode: +(value.attrs.code || 500) })
|
||||
}
|
||||
}
|
||||
Object.assign(creds, updatedCreds)
|
||||
logger.info({ jid: creds.me!.id }, 'registered connection, restart server')
|
||||
|
||||
ev.emit('auth-state.update', authState)
|
||||
ev.emit('connection.update', { isNewLogin: true, qr: undefined })
|
||||
|
||||
end(new Boom('Restart Required', { statusCode: DisconnectReason.restartRequired }))
|
||||
} catch(error) {
|
||||
logger.info({ trace: error.stack }, 'error in pairing')
|
||||
end(error)
|
||||
}
|
||||
})
|
||||
// login complete
|
||||
ws.on('CB:success', async() => {
|
||||
if(!creds.serverHasPreKeys) {
|
||||
await uploadPreKeys()
|
||||
}
|
||||
await sendPassiveIq('active')
|
||||
ev.emit('connection.update', { connection: 'open' })
|
||||
})
|
||||
// logged out
|
||||
ws.on('CB:failure,reason:401', () => {
|
||||
end(new Boom('Logged Out', { statusCode: DisconnectReason.loggedOut }))
|
||||
})
|
||||
process.nextTick(() => {
|
||||
ev.emit('connection.update', { connection: 'connecting', receivedPendingNotifications: false })
|
||||
})
|
||||
|
||||
return {
|
||||
ws,
|
||||
ev,
|
||||
authState,
|
||||
get user () {
|
||||
return authState.creds.me
|
||||
},
|
||||
assertingPreKeys,
|
||||
generateMessageTag,
|
||||
query,
|
||||
waitForMessage,
|
||||
waitForSocketOpen,
|
||||
sendRawMessage,
|
||||
sendNode,
|
||||
logout,
|
||||
end
|
||||
}
|
||||
}
|
||||
export type Socket = ReturnType<typeof makeSocket>
|
||||
Reference in New Issue
Block a user