Files
Baileys/src/Connection/chats.ts
2021-09-02 14:28:28 +05:30

478 lines
14 KiB
TypeScript

import BinaryNode from "../BinaryNode";
import { Chat, Contact, Presence, PresenceData, SocketConfig, WAFlag, WAMetric, WABusinessProfile, ChatModification, WAMessageKey, WAMessage, WAMessageUpdate, BaileysEventMap } from "../Types";
import { debouncedTimeout, unixTimestampSeconds, whatsappID } from "../Utils/generics";
import makeAuthSocket from "./auth";
import { Attributes, BinaryNode as BinaryNodeBase } from "../BinaryNode/types";
const makeChatsSocket = (config: SocketConfig) => {
const { logger } = config
const sock = makeAuthSocket(config)
const {
ev,
ws: socketEvents,
currentEpoch,
setQuery,
query,
sendMessage,
getState
} = sock
const chatsDebounceTimeout = debouncedTimeout(10_000, () => sendChatsQuery(1))
const sendChatsQuery = (epoch: number) => (
sendMessage({
json: new BinaryNode('query', {type: 'chat', epoch: epoch.toString()}),
binaryTag: [ WAMetric.queryChat, WAFlag.ignore ]
})
)
const fetchImageUrl = async(jid: string) => {
const response = await query({
json: ['query', 'ProfilePicThumb', jid],
expect200: false,
requiresPhoneConnection: false
})
return response.eurl as string | undefined
}
const executeChatModification = (node: BinaryNodeBase) => {
const { attributes } = node
const updateType = attributes.type
const jid = whatsappID(attributes?.jid)
switch(updateType) {
case 'delete':
ev.emit('chats.delete', [jid])
break
case 'clear':
if(node.data) {
const ids = (node.data as BinaryNode[]).map(
({ attributes }) => attributes.index
)
ev.emit('messages.delete', { jid, ids })
} else {
ev.emit('messages.delete', { jid, all: true })
}
break
case 'archive':
ev.emit('chats.update', [ { jid, archive: 'true' } ])
break
case 'unarchive':
ev.emit('chats.update', [ { jid, archive: 'false' } ])
break
case 'pin':
ev.emit('chats.update', [ { jid, pin: attributes.pin } ])
break
case 'star':
case 'unstar':
const starred = updateType === 'star'
const updates: WAMessageUpdate[] = (node.data as BinaryNode[]).map(
({ attributes }) => ({
key: {
remoteJid: jid,
id: attributes.index,
fromMe: attributes.owner === 'true'
},
update: { starred }
})
)
ev.emit('messages.update', updates)
break
case 'mute':
if(attributes.mute === '0') {
ev.emit('chats.update', [{ jid, mute: null }])
} else {
ev.emit('chats.update', [{ jid, mute: attributes.mute }])
}
break
default:
logger.warn({ node }, `received unrecognized chat update`)
break
}
}
const applyingPresenceUpdate = (update: Attributes): BaileysEventMap['presence.update'] => {
const jid = whatsappID(update.id)
const participant = whatsappID(update.participant || update.id)
const presence: PresenceData = {
lastSeen: update.t ? +update.t : undefined,
lastKnownPresence: update.type as Presence
}
return { jid, presences: { [participant]: presence } }
}
ev.on('connection.update', async({ connection }) => {
if(connection !== 'open') return
try {
await Promise.all([
sendMessage({
json: new BinaryNode('query', {type: 'contacts', epoch: '1'}),
binaryTag: [ WAMetric.queryContact, WAFlag.ignore ]
}),
sendMessage({
json: new BinaryNode('query', {type: 'status', epoch: '1'}),
binaryTag: [ WAMetric.queryStatus, WAFlag.ignore ]
}),
sendMessage({
json: new BinaryNode('query', {type: 'quick_reply', epoch: '1'}),
binaryTag: [ WAMetric.queryQuickReply, WAFlag.ignore ]
}),
sendMessage({
json: new BinaryNode('query', {type: 'label', epoch: '1'}),
binaryTag: [ WAMetric.queryLabel, WAFlag.ignore ]
}),
sendMessage({
json: new BinaryNode('query', {type: 'emoji', epoch: '1'}),
binaryTag: [ WAMetric.queryEmoji, WAFlag.ignore ]
}),
sendMessage({
json: new BinaryNode(
'action',
{ type: 'set', epoch: '1' },
[
new BinaryNode('presence', {type: 'available'})
]
),
binaryTag: [ WAMetric.presence, WAFlag.available ]
})
])
chatsDebounceTimeout.start()
logger.debug('sent init queries')
} catch(error) {
logger.error(`error in sending init queries: ${error}`)
}
})
socketEvents.on('CB:response,type:chat', async ({ data }: BinaryNode) => {
chatsDebounceTimeout.cancel()
if(Array.isArray(data)) {
const chats = data.map(({ attributes }) => {
return {
...attributes,
jid: whatsappID(attributes.jid),
t: +attributes.t,
count: +attributes.count
} as Chat
})
logger.info(`got ${chats.length} chats`)
ev.emit('chats.set', { chats })
}
})
// got all contacts from phone
socketEvents.on('CB:response,type:contacts', async ({ data }: BinaryNode) => {
if(Array.isArray(data)) {
const contacts = data.map(({ attributes }) => {
const contact = attributes as any as Contact
contact.jid = whatsappID(contact.jid)
return contact
})
logger.info(`got ${contacts.length} contacts`)
ev.emit('contacts.set', { contacts })
}
})
// status updates
socketEvents.on('CB:Status,status', json => {
const jid = whatsappID(json[1].id)
ev.emit('contacts.update', [ { jid, status: json[1].status } ])
})
// User Profile Name Updates
socketEvents.on('CB:Conn,pushname', json => {
const { user, connection } = getState()
if(connection === 'open' && json[1].pushname !== user.name) {
user.name = json[1].pushname
ev.emit('connection.update', { user })
}
})
// read updates
socketEvents.on ('CB:action,,read', async ({ data }: BinaryNode) => {
if(Array.isArray(data)) {
const { attributes } = data[0]
const update: Partial<Chat> = {
jid: whatsappID(attributes.jid)
}
if (attributes.type === 'false') update.count = -1
else update.count = 0
ev.emit('chats.update', [update])
}
})
socketEvents.on('CB:Cmd,type:picture', async json => {
json = json[1]
const jid = whatsappID(json.jid)
const imgUrl = await fetchImageUrl(jid).catch(() => '')
ev.emit('contacts.update', [ { jid, imgUrl } ])
})
// chat archive, pin etc.
socketEvents.on('CB:action,,chat', ({ data }: BinaryNode) => {
if(Array.isArray(data)) {
const [node] = data
executeChatModification(node)
}
})
socketEvents.on('CB:action,,user', (json: BinaryNode) => {
if(Array.isArray(json.data)) {
const user = json.data[0].attributes as any as Contact
user.jid = whatsappID(user.jid)
ev.emit('contacts.upsert', [user])
}
})
// presence updates
socketEvents.on('CB:Presence', json => {
const update = applyingPresenceUpdate(json[1])
ev.emit('presence.update', update)
})
// blocklist updates
socketEvents.on('CB:Blocklist', json => {
json = json[1]
const blocklist = json.blocklist
ev.emit('blocklist.set', { blocklist })
})
return {
...sock,
sendChatsQuery,
fetchImageUrl,
chatRead: async(fromMessage: WAMessageKey, count: number) => {
await setQuery (
[
new BinaryNode(
'read',
{
jid: fromMessage.remoteJid,
count: count.toString(),
index: fromMessage.id,
owner: fromMessage.fromMe ? 'true' : 'false'
}
)
],
[ WAMetric.read, WAFlag.ignore ]
)
ev.emit ('chats.update', [{ jid: fromMessage.remoteJid, count: count < 0 ? -1 : 0 }])
},
/**
* Modify a given chat (archive, pin etc.)
* @param jid the ID of the person/group you are modifiying
*/
modifyChat: async(jid: string, modification: ChatModification, index?: WAMessageKey) => {
let chatAttrs: Attributes = { jid: jid }
let data: BinaryNode[] | undefined = undefined
const stamp = unixTimestampSeconds()
if('archive' in modification) {
chatAttrs.type = modification.archive ? 'archive' : 'unarchive'
} else if('pin' in modification) {
chatAttrs.type = 'pin'
if(typeof modification.pin === 'object') {
chatAttrs.previous = modification.pin.remove.toString()
} else {
chatAttrs.pin = stamp.toString()
}
} else if('mute' in modification) {
chatAttrs.type = 'mute'
if(typeof modification.mute === 'object') {
chatAttrs.previous = modification.mute.remove.toString()
} else {
chatAttrs.mute = (stamp + modification.mute).toString()
}
} else if('clear' in modification) {
chatAttrs.type = 'clear'
chatAttrs.modify_tag = Math.round(Math.random ()*1000000).toString()
if(modification.clear !== 'all') {
data = modification.clear.messages.map(({ id, fromMe }) => (
new BinaryNode(
'item',
{ owner: (!!fromMe).toString(), index: id }
)
))
}
} else if('star' in modification) {
chatAttrs.type = modification.star.star ? 'star' : 'unstar'
data = modification.star.messages.map(({ id, fromMe }) => (
new BinaryNode(
'item',
{ owner: (!!fromMe).toString(), index: id }
)
))
}
if(index) {
chatAttrs.index = index.id
chatAttrs.owner = index.fromMe ? 'true' : 'false'
}
const node = new BinaryNode('chat', chatAttrs, data)
const response = await setQuery([node], [ WAMetric.chat, WAFlag.ignore ])
// apply it and emit events
executeChatModification(node)
return response
},
/**
* Query whether a given number is registered on WhatsApp
* @param str phone number/jid you want to check for
* @returns undefined if the number doesn't exists, otherwise the correctly formatted jid
*/
isOnWhatsApp: async (str: string) => {
const { status, jid, biz } = await query({
json: ['query', 'exist', str],
requiresPhoneConnection: false
})
if (status === 200) {
return {
exists: true,
jid: whatsappID(jid),
isBusiness: biz as boolean
}
}
},
/**
* Tell someone about your presence -- online, typing, offline etc.
* @param jid the ID of the person/group who you are updating
* @param type your presence
*/
updatePresence: (jid: string | undefined, type: Presence) => (
sendMessage({
binaryTag: [WAMetric.presence, WAFlag[type]], // weird stuff WA does
json: new BinaryNode(
'action',
{ epoch: currentEpoch().toString(), type: 'set' },
[
new BinaryNode(
'presence',
{ type: type, to: jid }
)
]
)
})
),
/**
* Request updates on the presence of a user
* this returns nothing, you'll receive updates in chats.update event
* */
requestPresenceUpdate: async (jid: string) => (
sendMessage({ json: ['action', 'presence', 'subscribe', jid] })
),
/** Query the status of the person (see groupMetadata() for groups) */
getStatus: async(jid: string) => {
const status: { status: string } = await query({ json: ['query', 'Status', jid], requiresPhoneConnection: false })
return status
},
setStatus: async(status: string) => {
const response = await setQuery(
[
new BinaryNode(
'status',
{},
Buffer.from (status, 'utf-8')
)
]
)
ev.emit('contacts.update', [{ jid: getState().user!.jid, status }])
return response
},
/** Updates business profile. */
updateBusinessProfile: async(profile: WABusinessProfile) => {
if (profile.business_hours?.config) {
profile.business_hours.business_config = profile.business_hours.config
delete profile.business_hours.config
}
const json = ['action', "editBusinessProfile", {...profile, v: 2}]
await query({ json, expect200: true, requiresPhoneConnection: true })
},
updateProfileName: async(name: string) => {
const response = (await setQuery(
[
new BinaryNode(
'profile',
{ name }
)
]
)) as any as {status: number, pushname: string}
if (response.status === 200) {
const user = { ...getState().user!, name }
ev.emit('connection.update', { user })
ev.emit('contacts.update', [{ jid: user.jid, name }])
}
return response
},
/**
* Update the profile picture
* @param jid
* @param img
*/
async updateProfilePicture (jid: string, img: Buffer) {
jid = whatsappID (jid)
const data = { img: Buffer.from([]), preview: Buffer.from([]) } //await generateProfilePicture(img) TODO
const tag = this.generateMessageTag ()
const query = new BinaryNode(
'picture',
{ jid: jid, id: tag, type: 'set' },
[
new BinaryNode('image', {}, data.img),
new BinaryNode('preview', {}, data.preview)
]
)
const user = getState().user
const { eurl } = await this.setQuery ([query], [WAMetric.picture, 136], tag) as { eurl: string, status: number }
if (jid === user.jid) {
user.imgUrl = eurl
ev.emit('connection.update', { user })
}
ev.emit('contacts.update', [ { jid, imgUrl: eurl } ])
},
/**
* Add or remove user from blocklist
* @param jid the ID of the person who you are blocking/unblocking
* @param type type of operation
*/
blockUser: async(jid: string, type: 'add' | 'remove' = 'add') => {
const json = new BinaryNode(
'block',
{ type },
[ new BinaryNode('user', { jid }) ]
)
await setQuery ([json], [WAMetric.block, WAFlag.ignore])
ev.emit('blocklist.update', { blocklist: [jid], type })
},
/**
* Query Business Profile (Useful for VCards)
* @param jid Business Jid
* @returns profile object or undefined if not business account
*/
getBusinessProfile: async(jid: string) => {
jid = whatsappID(jid)
const {
profiles: [{
profile,
wid
}]
} = await query({
json: [
"query", "businessProfile",
[ { "wid": jid.replace('@s.whatsapp.net', '@c.us') } ],
84
],
expect200: true,
requiresPhoneConnection: false,
})
return {
...profile,
wid: whatsappID(wid)
} as WABusinessProfile
}
}
}
export default makeChatsSocket