mirror of
https://github.com/FranP-code/Baileys.git
synced 2025-10-13 00:32:22 +00:00
194 lines
8.3 KiB
TypeScript
194 lines
8.3 KiB
TypeScript
import { MessageType, HKDFInfoKeys, MessageOptions, WAMessageType } from './Constants'
|
|
import Jimp from 'jimp'
|
|
import * as fs from 'fs'
|
|
import fetch from 'node-fetch'
|
|
import { WAMessage, WAMessageContent, BaileysError } from '../WAConnection/Constants'
|
|
import { hmacSign, aesDecryptWithIV, hkdf } from '../WAConnection/Utils'
|
|
import { proto } from '../../WAMessage/WAMessage'
|
|
import { randomBytes } from 'crypto'
|
|
import { exec } from 'child_process'
|
|
|
|
export function validateJIDForSending (jid: string) {
|
|
const regexp = /^[0-9]{1,20}(-[0-9]{1,20}@g.us|@s.whatsapp.net)$/
|
|
if (!regexp.test (jid)) {
|
|
throw new Error (
|
|
`Invalid WhatsApp id: ${jid}
|
|
1. Please ensure you suffix '@s.whatsapp.net' for individual numbers & '@g.us' for groups
|
|
2. Please do not put any alphabets or special characters like a '+' in the number. A '-' symbol in groups is fine`
|
|
)
|
|
}
|
|
}
|
|
/**
|
|
* Type of notification
|
|
* @deprecated use WA_MESSAGE_STUB_TYPE instead
|
|
* */
|
|
export function getNotificationType(message: WAMessage): [string, MessageType?] {
|
|
if (message.message) {
|
|
return ['message', Object.keys(message.message)[0] as MessageType]
|
|
} else if (message.messageStubType) {
|
|
return [WAMessageType[message.messageStubType], null]
|
|
} else {
|
|
return ['unknown', null]
|
|
}
|
|
}
|
|
/** generates all the keys required to encrypt/decrypt & sign a media message */
|
|
export function getMediaKeys(buffer, mediaType: MessageType) {
|
|
if (typeof buffer === 'string') {
|
|
buffer = Buffer.from (buffer.replace('data:;base64,', ''), 'base64')
|
|
}
|
|
// expand using HKDF to 112 bytes, also pass in the relevant app info
|
|
const expandedMediaKey = hkdf(buffer, 112, HKDFInfoKeys[mediaType])
|
|
return {
|
|
iv: expandedMediaKey.slice(0, 16),
|
|
cipherKey: expandedMediaKey.slice(16, 48),
|
|
macKey: expandedMediaKey.slice(48, 80),
|
|
}
|
|
}
|
|
/** Extracts video thumb using FFMPEG */
|
|
const extractVideoThumb = async (
|
|
path: string,
|
|
destPath: string,
|
|
time: string,
|
|
size: { width: number; height: number },
|
|
) =>
|
|
new Promise((resolve, reject) => {
|
|
const cmd = `ffmpeg -ss ${time} -i ${path} -y -s ${size.width}x${size.height} -vframes 1 -f image2 ${destPath}`
|
|
exec(cmd, (err) => {
|
|
if (err) reject(err)
|
|
else resolve()
|
|
})
|
|
}) as Promise<void>
|
|
|
|
export const compressImage = async (buffer: Buffer) => {
|
|
const jimp = await Jimp.read (buffer)
|
|
return jimp.resize(48, 48).getBufferAsync (Jimp.MIME_JPEG)
|
|
}
|
|
export const generateProfilePicture = async (buffer: Buffer) => {
|
|
const jimp = await Jimp.read (buffer)
|
|
const min = Math.min(jimp.getWidth (), jimp.getHeight ())
|
|
const cropped = jimp.crop (0, 0, min, min)
|
|
return {
|
|
img: await cropped.resize(640, 640).getBufferAsync (Jimp.MIME_JPEG),
|
|
preview: await cropped.resize(96, 96).getBufferAsync (Jimp.MIME_JPEG)
|
|
}
|
|
}
|
|
/** generates a thumbnail for a given media, if required */
|
|
export async function generateThumbnail(buffer: Buffer, mediaType: MessageType, info: MessageOptions) {
|
|
if (info.thumbnail === null || info.thumbnail) {
|
|
// don't do anything if the thumbnail is already provided, or is null
|
|
if (mediaType === MessageType.audio) {
|
|
throw new Error('audio messages cannot have thumbnails')
|
|
}
|
|
} else if (mediaType === MessageType.image || mediaType === MessageType.sticker) {
|
|
const buff = await compressImage (buffer)
|
|
info.thumbnail = buff.toString('base64')
|
|
} else if (mediaType === MessageType.video) {
|
|
const filename = './' + randomBytes(5).toString('hex') + '.mp4'
|
|
const imgFilename = filename + '.jpg'
|
|
fs.writeFileSync(filename, buffer)
|
|
try {
|
|
await extractVideoThumb(filename, imgFilename, '00:00:00', { width: 48, height: 48 })
|
|
const buff = fs.readFileSync(imgFilename)
|
|
info.thumbnail = buff.toString('base64')
|
|
fs.unlinkSync(imgFilename)
|
|
} catch (err) {
|
|
console.log('could not generate video thumb: ' + err)
|
|
}
|
|
fs.unlinkSync(filename)
|
|
}
|
|
}
|
|
/**
|
|
* Decode a media message (video, image, document, audio) & return decrypted buffer
|
|
* @param message the media message you want to decode
|
|
*/
|
|
export async function decodeMediaMessageBuffer(message: WAMessageContent, fetchHeaders: {[k: string]: string} = {}) {
|
|
/*
|
|
One can infer media type from the key in the message
|
|
it is usually written as [mediaType]Message. Eg. imageMessage, audioMessage etc.
|
|
*/
|
|
const type = Object.keys(message)[0] as MessageType
|
|
if (!type) {
|
|
throw new BaileysError('unknown message type', message)
|
|
}
|
|
if (type === MessageType.text || type === MessageType.extendedText) {
|
|
throw new BaileysError('cannot decode text message', message)
|
|
}
|
|
if (type === MessageType.location || type === MessageType.liveLocation) {
|
|
return new Buffer(message[type].jpegThumbnail)
|
|
}
|
|
let messageContent: proto.IVideoMessage | proto.IImageMessage | proto.IAudioMessage | proto.IDocumentMessage
|
|
if (message.productMessage) {
|
|
const product = message.productMessage.product?.productImage
|
|
if (!product) throw new BaileysError ('product has no image', message)
|
|
messageContent = product
|
|
} else {
|
|
messageContent = message[type]
|
|
}
|
|
|
|
// download the message
|
|
const headers = { Origin: 'https://web.whatsapp.com' }
|
|
const fetched = await fetch(messageContent.url, { headers })
|
|
const buffer = await fetched.buffer()
|
|
|
|
if (buffer.length <= 10) {
|
|
throw new BaileysError ('Empty buffer returned. File has possibly been deleted from WA servers. Run `client.updateMediaMessage()` to refresh the url', {status: 404})
|
|
}
|
|
|
|
const decryptedMedia = (type: MessageType) => {
|
|
// get the keys to decrypt the message
|
|
const mediaKeys = getMediaKeys(messageContent.mediaKey, type) //getMediaKeys(Buffer.from(messageContent.mediaKey, 'base64'), type)
|
|
// first part is actual file
|
|
const file = buffer.slice(0, buffer.length - 10)
|
|
// last 10 bytes is HMAC sign of file
|
|
const mac = buffer.slice(buffer.length - 10, buffer.length)
|
|
// sign IV+file & check for match with mac
|
|
const testBuff = Buffer.concat([mediaKeys.iv, file])
|
|
const sign = hmacSign(testBuff, mediaKeys.macKey).slice(0, 10)
|
|
// our sign should equal the mac
|
|
if (!sign.equals(mac)) throw new Error()
|
|
|
|
return aesDecryptWithIV(file, mediaKeys.cipherKey, mediaKeys.iv) // decrypt media
|
|
}
|
|
const allTypes = [type, ...Object.keys(HKDFInfoKeys)]
|
|
for (let i = 0; i < allTypes.length;i++) {
|
|
try {
|
|
const decrypted = decryptedMedia (allTypes[i] as MessageType)
|
|
|
|
if (i > 0) { console.log (`decryption of ${type} media with HKDF key of ${allTypes[i]}`) }
|
|
return decrypted
|
|
} catch {
|
|
if (i === 0) { console.log (`decryption of ${type} media with original HKDF key failed`) }
|
|
}
|
|
}
|
|
throw new BaileysError('Decryption failed, HMAC sign does not match', {status: 400})
|
|
}
|
|
export function extensionForMediaMessage(message: WAMessageContent) {
|
|
const getExtension = (mimetype: string) => mimetype.split(';')[0].split('/')[1]
|
|
const type = Object.keys(message)[0] as MessageType
|
|
let extension: string
|
|
if (type === MessageType.location || type === MessageType.liveLocation || type === MessageType.product) {
|
|
extension = '.jpeg'
|
|
} else {
|
|
const messageContent = message[type] as
|
|
| proto.VideoMessage
|
|
| proto.ImageMessage
|
|
| proto.AudioMessage
|
|
| proto.DocumentMessage
|
|
extension = getExtension (messageContent.mimetype)
|
|
}
|
|
return extension
|
|
}
|
|
|
|
/**
|
|
* Decode a media message (video, image, document, audio) & save it to the given file
|
|
* @deprecated use `client.downloadAndSaveMediaMessage`
|
|
*/
|
|
export async function decodeMediaMessage(message: WAMessageContent, filename: string, attachExtension: boolean=true) {
|
|
const buffer = await decodeMediaMessageBuffer (message, {})
|
|
const extension = extensionForMediaMessage (message)
|
|
const trueFileName = attachExtension ? (filename + '.' + extension) : filename
|
|
fs.writeFileSync(trueFileName, buffer)
|
|
return trueFileName
|
|
}
|
|
|