mirror of
https://github.com/FranP-code/Baileys.git
synced 2025-10-13 00:32:22 +00:00
Wrap up connection + in memory store
This commit is contained in:
@@ -1,4 +1,4 @@
|
||||
import Boom from "boom"
|
||||
import { Boom } from '@hapi/boom'
|
||||
import BinaryNode from "../BinaryNode"
|
||||
import { aesDecrypt, hmacSign } from "./generics"
|
||||
import { DisconnectReason, WATag } from "../Types"
|
||||
@@ -1,4 +1,4 @@
|
||||
import Boom from 'boom'
|
||||
import { Boom } from '@hapi/boom'
|
||||
import { createCipheriv, createDecipheriv, createHash, createHmac, randomBytes } from 'crypto'
|
||||
import HKDF from 'futoin-hkdf'
|
||||
import { platform, release } from 'os'
|
||||
|
||||
286
src/Utils/messages-media.ts
Normal file
286
src/Utils/messages-media.ts
Normal file
@@ -0,0 +1,286 @@
|
||||
import type { Agent } from 'https'
|
||||
import type { Logger } from 'pino'
|
||||
import type { IAudioMetadata } from 'music-metadata'
|
||||
import * as Crypto from 'crypto'
|
||||
import { Readable, Transform } from 'stream'
|
||||
import Jimp from 'jimp'
|
||||
import { createReadStream, createWriteStream, promises as fs, WriteStream } from 'fs'
|
||||
import { exec } from 'child_process'
|
||||
import { tmpdir } from 'os'
|
||||
import HttpsProxyAgent from 'https-proxy-agent'
|
||||
import { URL } from 'url'
|
||||
import { MessageType, WAMessageContent, WAMessageProto, WAGenericMediaMessage, WAMediaUpload } from '../Types'
|
||||
import got, { Options, Response } from 'got'
|
||||
import { join } from 'path'
|
||||
import { generateMessageID, hkdf } from './generics'
|
||||
import { Boom } from '@hapi/boom'
|
||||
import { MediaType } from '../Types'
|
||||
import { DEFAULT_ORIGIN } from '../Defaults'
|
||||
|
||||
export const hkdfInfoKey = (type: MediaType) => {
|
||||
let hkdfInfo = type[0].toUpperCase() + type.slice(1)
|
||||
return `WhatsApp ${hkdfInfo} Keys`
|
||||
}
|
||||
/** generates all the keys required to encrypt/decrypt & sign a media message */
|
||||
export function getMediaKeys(buffer, mediaType: MediaType) {
|
||||
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, hkdfInfoKey(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 (bufferOrFilePath: Buffer | string) => {
|
||||
const jimp = await Jimp.read(bufferOrFilePath as any)
|
||||
const result = await jimp.resize(48, 48).getBufferAsync(Jimp.MIME_JPEG)
|
||||
return result
|
||||
}
|
||||
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)
|
||||
}
|
||||
}
|
||||
export const ProxyAgent = (host: string | URL) => HttpsProxyAgent(host) as any as Agent
|
||||
/** gets the SHA256 of the given media message */
|
||||
export const mediaMessageSHA256B64 = (message: WAMessageContent) => {
|
||||
const media = Object.values(message)[0] as WAGenericMediaMessage
|
||||
return media?.fileSha256 && Buffer.from(media.fileSha256).toString ('base64')
|
||||
}
|
||||
export async function getAudioDuration (buffer: Buffer | string) {
|
||||
const musicMetadata = await import ('music-metadata')
|
||||
let metadata: IAudioMetadata
|
||||
if(Buffer.isBuffer(buffer)) {
|
||||
metadata = await musicMetadata.parseBuffer(buffer, null, { duration: true })
|
||||
} else {
|
||||
const rStream = createReadStream(buffer)
|
||||
metadata = await musicMetadata.parseStream(rStream, null, { duration: true })
|
||||
rStream.close()
|
||||
}
|
||||
return metadata.format.duration;
|
||||
}
|
||||
export const toReadable = (buffer: Buffer) => {
|
||||
const readable = new Readable({ read: () => {} })
|
||||
readable.push(buffer)
|
||||
readable.push(null)
|
||||
return readable
|
||||
}
|
||||
export const getStream = async (item: WAMediaUpload) => {
|
||||
if(Buffer.isBuffer(item)) return { stream: toReadable(item), type: 'buffer' }
|
||||
if(item.url.toString().startsWith('http://') || item.url.toString().startsWith('https://')) {
|
||||
return { stream: await getGotStream(item.url), type: 'remote' }
|
||||
}
|
||||
return { stream: createReadStream(item.url), type: 'file' }
|
||||
}
|
||||
/** generates a thumbnail for a given media, if required */
|
||||
export async function generateThumbnail(
|
||||
file: string,
|
||||
mediaType: 'video' | 'image',
|
||||
options: {
|
||||
logger?: Logger
|
||||
}
|
||||
) {
|
||||
let thumbnail: string
|
||||
if(mediaType === 'image') {
|
||||
const buff = await compressImage(file)
|
||||
thumbnail = buff.toString('base64')
|
||||
} else if(mediaType === 'video') {
|
||||
const imgFilename = join(tmpdir(), generateMessageID() + '.jpg')
|
||||
try {
|
||||
await extractVideoThumb(file, imgFilename, '00:00:00', { width: 48, height: 48 })
|
||||
const buff = await fs.readFile(imgFilename)
|
||||
thumbnail = buff.toString('base64')
|
||||
|
||||
await fs.unlink(imgFilename)
|
||||
} catch (err) {
|
||||
options.logger?.debug('could not generate video thumb: ' + err)
|
||||
}
|
||||
}
|
||||
return thumbnail
|
||||
}
|
||||
export const getGotStream = async(url: string | URL, options: Options & { isStream?: true } = {}) => {
|
||||
const fetched = got.stream(url, { ...options, isStream: true })
|
||||
await new Promise((resolve, reject) => {
|
||||
fetched.once('error', reject)
|
||||
fetched.once('response', ({ statusCode }: Response) => {
|
||||
if (statusCode >= 400) {
|
||||
reject(
|
||||
new Boom(
|
||||
'Invalid code (' + statusCode + ') returned',
|
||||
{ statusCode }
|
||||
))
|
||||
} else {
|
||||
resolve(undefined)
|
||||
}
|
||||
})
|
||||
})
|
||||
return fetched
|
||||
}
|
||||
export const encryptedStream = async(media: WAMediaUpload, mediaType: MediaType, saveOriginalFileIfRequired = true) => {
|
||||
const { stream, type } = await getStream(media)
|
||||
|
||||
const mediaKey = Crypto.randomBytes(32)
|
||||
const {cipherKey, iv, macKey} = getMediaKeys(mediaKey, mediaType)
|
||||
// random name
|
||||
const encBodyPath = join(tmpdir(), mediaType + generateMessageID() + '.enc')
|
||||
const encWriteStream = createWriteStream(encBodyPath)
|
||||
let bodyPath: string
|
||||
let writeStream: WriteStream
|
||||
let didSaveToTmpPath = false
|
||||
if(type === 'file') {
|
||||
bodyPath = (media as any).url
|
||||
} else if(saveOriginalFileIfRequired) {
|
||||
bodyPath = join(tmpdir(), mediaType + generateMessageID())
|
||||
writeStream = createWriteStream(bodyPath)
|
||||
didSaveToTmpPath = true
|
||||
}
|
||||
|
||||
let fileLength = 0
|
||||
const aes = Crypto.createCipheriv('aes-256-cbc', cipherKey, iv)
|
||||
let hmac = Crypto.createHmac('sha256', macKey).update(iv)
|
||||
let sha256Plain = Crypto.createHash('sha256')
|
||||
let sha256Enc = Crypto.createHash('sha256')
|
||||
|
||||
const onChunk = (buff: Buffer) => {
|
||||
sha256Enc = sha256Enc.update(buff)
|
||||
hmac = hmac.update(buff)
|
||||
encWriteStream.write(buff)
|
||||
}
|
||||
for await(const data of stream) {
|
||||
fileLength += data.length
|
||||
sha256Plain = sha256Plain.update(data)
|
||||
writeStream && writeStream.write(data)
|
||||
onChunk(aes.update(data))
|
||||
}
|
||||
onChunk(aes.final())
|
||||
|
||||
const mac = hmac.digest().slice(0, 10)
|
||||
sha256Enc = sha256Enc.update(mac)
|
||||
|
||||
const fileSha256 = sha256Plain.digest()
|
||||
const fileEncSha256 = sha256Enc.digest()
|
||||
|
||||
encWriteStream.write(mac)
|
||||
encWriteStream.close()
|
||||
|
||||
writeStream && writeStream.close()
|
||||
|
||||
return {
|
||||
mediaKey,
|
||||
encBodyPath,
|
||||
bodyPath,
|
||||
mac,
|
||||
fileEncSha256,
|
||||
fileSha256,
|
||||
fileLength,
|
||||
didSaveToTmpPath
|
||||
}
|
||||
}
|
||||
/**
|
||||
* Decode a media message (video, image, document, audio) & return decrypted buffer
|
||||
* @param message the media message you want to decode
|
||||
*/
|
||||
export async function decryptMediaMessageBuffer(message: WAMessageContent): Promise<Readable> {
|
||||
/*
|
||||
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 ||
|
||||
type === 'conversation' ||
|
||||
type === 'extendedTextMessage'
|
||||
) {
|
||||
throw new Boom(`no media message for "${type}"`, { statusCode: 400 })
|
||||
}
|
||||
if (type === 'locationMessage' || type === 'liveLocationMessage') {
|
||||
const buffer = Buffer.from(message[type].jpegThumbnail)
|
||||
const readable = new Readable({ read: () => {} })
|
||||
readable.push(buffer)
|
||||
readable.push(null)
|
||||
return readable
|
||||
}
|
||||
let messageContent: WAGenericMediaMessage
|
||||
if (message.productMessage) {
|
||||
const product = message.productMessage.product?.productImage
|
||||
if (!product) throw new Boom('product has no image', { statusCode: 400 })
|
||||
messageContent = product
|
||||
} else {
|
||||
messageContent = message[type]
|
||||
}
|
||||
// download the message
|
||||
const fetched = await getGotStream(messageContent.url, {
|
||||
headers: { Origin: DEFAULT_ORIGIN }
|
||||
})
|
||||
let remainingBytes = Buffer.from([])
|
||||
const { cipherKey, iv } = getMediaKeys(messageContent.mediaKey, type.replace('Message', '') as MediaType)
|
||||
const aes = Crypto.createDecipheriv("aes-256-cbc", cipherKey, iv)
|
||||
|
||||
const output = new Transform({
|
||||
transform(chunk, _, callback) {
|
||||
let data = Buffer.concat([remainingBytes, chunk])
|
||||
const decryptLength =
|
||||
Math.floor(data.length / 16) * 16
|
||||
remainingBytes = data.slice(decryptLength)
|
||||
data = data.slice(0, decryptLength)
|
||||
|
||||
try {
|
||||
this.push(aes.update(data))
|
||||
callback()
|
||||
} catch(error) {
|
||||
callback(error)
|
||||
}
|
||||
},
|
||||
final(callback) {
|
||||
try {
|
||||
this.push(aes.final())
|
||||
callback()
|
||||
} catch(error) {
|
||||
callback(error)
|
||||
}
|
||||
},
|
||||
})
|
||||
return fetched.pipe(output, { end: true })
|
||||
}
|
||||
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 === 'locationMessage' ||
|
||||
type === 'liveLocationMessage' ||
|
||||
type === 'productMessage'
|
||||
) {
|
||||
extension = '.jpeg'
|
||||
} else {
|
||||
const messageContent = message[type] as
|
||||
| WAMessageProto.VideoMessage
|
||||
| WAMessageProto.ImageMessage
|
||||
| WAMessageProto.AudioMessage
|
||||
| WAMessageProto.DocumentMessage
|
||||
extension = getExtension (messageContent.mimetype)
|
||||
}
|
||||
return extension
|
||||
}
|
||||
355
src/Utils/messages.ts
Normal file
355
src/Utils/messages.ts
Normal file
@@ -0,0 +1,355 @@
|
||||
import { Boom } from '@hapi/boom'
|
||||
import { createReadStream, promises as fs } from "fs"
|
||||
import got from "got"
|
||||
import { DEFAULT_ORIGIN, URL_REGEX, WA_DEFAULT_EPHEMERAL } from "../Defaults"
|
||||
import {
|
||||
AnyMediaMessageContent,
|
||||
AnyMessageContent,
|
||||
MediaGenerationOptions,
|
||||
MessageContentGenerationOptions,
|
||||
MessageGenerationOptions,
|
||||
MessageGenerationOptionsFromContent,
|
||||
MessageType,
|
||||
WAMediaUpload,
|
||||
WAMessage,
|
||||
WAMessageContent,
|
||||
WAMessageProto,
|
||||
WATextMessage,
|
||||
MediaType,
|
||||
WAMessageStatus
|
||||
} from "../Types"
|
||||
import { generateMessageID, unixTimestampSeconds, whatsappID } from "./generics"
|
||||
import { encryptedStream, generateThumbnail, getAudioDuration } from "./messages-media"
|
||||
|
||||
type MediaUploadData = {
|
||||
media: WAMediaUpload
|
||||
caption?: string
|
||||
ptt?: boolean
|
||||
seconds?: number
|
||||
gifPlayback?: boolean
|
||||
fileName?: string
|
||||
jpegThumbnail?: string
|
||||
mimetype?: string
|
||||
}
|
||||
|
||||
const MEDIA_PATH_MAP: { [T in MediaType]: string } = {
|
||||
image: '/mms/image',
|
||||
video: '/mms/video',
|
||||
document: '/mms/document',
|
||||
audio: '/mms/audio',
|
||||
sticker: '/mms/image',
|
||||
} as const
|
||||
|
||||
const MIMETYPE_MAP: { [T in MediaType]: string } = {
|
||||
image: 'image/jpeg',
|
||||
video: 'video/mp4',
|
||||
document: 'application/pdf',
|
||||
audio: 'audio/ogg; codecs=opus',
|
||||
sticker: 'image/webp',
|
||||
}
|
||||
|
||||
const MessageTypeProto = {
|
||||
'imageMessage': WAMessageProto.ImageMessage,
|
||||
'videoMessage': WAMessageProto.VideoMessage,
|
||||
'audioMessage': WAMessageProto.AudioMessage,
|
||||
'stickerMessage': WAMessageProto.StickerMessage,
|
||||
'documentMessage': WAMessageProto.DocumentMessage,
|
||||
}
|
||||
|
||||
const MEDIA_KEYS = Object.keys(MEDIA_PATH_MAP) as MediaType[]
|
||||
|
||||
export const prepareWAMessageMedia = async(
|
||||
message: AnyMediaMessageContent,
|
||||
options: MediaGenerationOptions
|
||||
) => {
|
||||
let mediaType: typeof MEDIA_KEYS[number]
|
||||
for(const key of MEDIA_KEYS) {
|
||||
if(key in message) {
|
||||
mediaType = key
|
||||
}
|
||||
}
|
||||
const uploadData: MediaUploadData = {
|
||||
...message,
|
||||
[mediaType]: undefined,
|
||||
media: message[mediaType]
|
||||
}
|
||||
if(mediaType === 'document' && !uploadData.fileName) {
|
||||
uploadData.fileName = 'file'
|
||||
}
|
||||
if(!uploadData.mimetype) {
|
||||
uploadData.mimetype = MIMETYPE_MAP[mediaType]
|
||||
}
|
||||
const requiresDurationComputation = mediaType === 'audio' && typeof uploadData.seconds === 'undefined'
|
||||
const requiresThumbnailComputation = (mediaType === 'image' || mediaType === 'video') &&
|
||||
!('jpegThumbnail' in uploadData)
|
||||
const requiresOriginalForSomeProcessing = requiresDurationComputation || requiresThumbnailComputation
|
||||
const {
|
||||
mediaKey,
|
||||
encBodyPath,
|
||||
bodyPath,
|
||||
fileEncSha256,
|
||||
fileSha256,
|
||||
fileLength,
|
||||
didSaveToTmpPath
|
||||
} = await encryptedStream(uploadData.media, mediaType, requiresOriginalForSomeProcessing)
|
||||
// url safe Base64 encode the SHA256 hash of the body
|
||||
const fileEncSha256B64 = encodeURIComponent(
|
||||
fileEncSha256.toString('base64')
|
||||
.replace(/\+/g, '-')
|
||||
.replace(/\//g, '_')
|
||||
.replace(/\=+$/, '')
|
||||
)
|
||||
try {
|
||||
if(requiresThumbnailComputation) {
|
||||
uploadData.jpegThumbnail = await generateThumbnail(bodyPath, mediaType as any, options)
|
||||
}
|
||||
if (requiresDurationComputation) {
|
||||
uploadData.seconds = await getAudioDuration(bodyPath)
|
||||
}
|
||||
} catch (error) {
|
||||
options.logger?.debug ({ error }, 'failed to obtain audio duration: ' + error.message)
|
||||
}
|
||||
// send a query JSON to obtain the url & auth token to upload our media
|
||||
let uploadInfo = await options.getMediaOptions(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: options.agent
|
||||
},
|
||||
body: createReadStream(encBodyPath)
|
||||
}
|
||||
)
|
||||
const result = JSON.parse(responseText)
|
||||
mediaUrl = result?.url
|
||||
|
||||
if (mediaUrl) break
|
||||
else {
|
||||
uploadInfo = await options.getMediaOptions(true)
|
||||
throw new Error(`upload failed, reason: ${JSON.stringify(result)}`)
|
||||
}
|
||||
} catch (error) {
|
||||
const isLast = host.hostname === uploadInfo.hosts[uploadInfo.hosts.length-1].hostname
|
||||
options.logger?.debug(`Error in uploading to ${host.hostname} (${error}) ${isLast ? '' : ', retrying...'}`)
|
||||
}
|
||||
}
|
||||
if (!mediaUrl) {
|
||||
throw new Boom(
|
||||
'Media upload failed on all hosts',
|
||||
{ statusCode: 500 }
|
||||
)
|
||||
}
|
||||
// remove tmp files
|
||||
await Promise.all(
|
||||
[
|
||||
fs.unlink(encBodyPath),
|
||||
didSaveToTmpPath && bodyPath && fs.unlink(bodyPath)
|
||||
]
|
||||
.filter(Boolean)
|
||||
)
|
||||
delete uploadData.media
|
||||
const content = {
|
||||
[mediaType]: MessageTypeProto[mediaType].fromObject(
|
||||
{
|
||||
url: mediaUrl,
|
||||
mediaKey,
|
||||
fileEncSha256,
|
||||
fileSha256,
|
||||
fileLength,
|
||||
...uploadData
|
||||
}
|
||||
)
|
||||
}
|
||||
return WAMessageProto.Message.fromObject(content)
|
||||
}
|
||||
export const prepareDisappearingMessageSettingContent = (ephemeralExpiration?: number) => {
|
||||
ephemeralExpiration = ephemeralExpiration || 0
|
||||
const content: WAMessageContent = {
|
||||
ephemeralMessage: {
|
||||
message: {
|
||||
protocolMessage: {
|
||||
type: WAMessageProto.ProtocolMessage.ProtocolMessageType.EPHEMERAL_SETTING,
|
||||
ephemeralExpiration
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return WAMessageProto.Message.fromObject(content)
|
||||
}
|
||||
/**
|
||||
* Generate forwarded message content like WA does
|
||||
* @param message the message to forward
|
||||
* @param options.forceForward will show the message as forwarded even if it is from you
|
||||
*/
|
||||
export const generateForwardMessageContent = (
|
||||
message: WAMessage,
|
||||
forceForward?: boolean
|
||||
) => {
|
||||
let content = message.message
|
||||
if (!content) throw new Boom('no content in message', { statusCode: 400 })
|
||||
content = JSON.parse(JSON.stringify(content)) // hacky copy
|
||||
|
||||
let key = Object.keys(content)[0] as MessageType
|
||||
|
||||
let score = content[key].contextInfo?.forwardingScore || 0
|
||||
score += message.key.fromMe && !forceForward ? 0 : 1
|
||||
if (key === 'conversation') {
|
||||
content.extendedTextMessage = { text: content[key] }
|
||||
delete content.conversation
|
||||
|
||||
key = 'extendedTextMessage'
|
||||
}
|
||||
if (score > 0) content[key].contextInfo = { forwardingScore: score, isForwarded: true }
|
||||
else content[key].contextInfo = {}
|
||||
|
||||
return content
|
||||
}
|
||||
export const generateWAMessageContent = async(
|
||||
message: AnyMessageContent,
|
||||
options: MessageContentGenerationOptions
|
||||
) => {
|
||||
let m: WAMessageContent = {}
|
||||
if(typeof message === 'string') {
|
||||
message = { text: message }
|
||||
}
|
||||
if('text' in message) {
|
||||
const extContent = { ...message } as WATextMessage
|
||||
if (!!options.getUrlInfo && message.text.match(URL_REGEX)) {
|
||||
try {
|
||||
const data = await options.getUrlInfo(message.text)
|
||||
extContent.canonicalUrl = data['canonical-url']
|
||||
extContent.matchedText = data['matched-text']
|
||||
extContent.jpegThumbnail = data.jpegThumbnail
|
||||
extContent.description = data.description
|
||||
extContent.title = data.title
|
||||
extContent.previewType = 0
|
||||
} catch (error) { // ignore if fails
|
||||
|
||||
}
|
||||
}
|
||||
m.extendedTextMessage = WAMessageProto.ExtendedTextMessage.fromObject(extContent)
|
||||
} else if('contacts' in message) {
|
||||
const contactLen = message.contacts.contacts.length
|
||||
if(!contactLen) {
|
||||
throw new Boom('require atleast 1 contact', { statusCode: 400 })
|
||||
}
|
||||
if(contactLen === 1) {
|
||||
m.contactMessage = WAMessageProto.ContactMessage.fromObject(message.contacts.contacts[0])
|
||||
}
|
||||
} else if('location' in message) {
|
||||
m.locationMessage = WAMessageProto.LocationMessage.fromObject(message.location)
|
||||
} else if('delete' in message) {
|
||||
m.protocolMessage = {
|
||||
key: message.delete,
|
||||
type: WAMessageProto.ProtocolMessage.ProtocolMessageType.REVOKE
|
||||
}
|
||||
} else if('forward' in message) {
|
||||
m = generateForwardMessageContent(
|
||||
message.forward,
|
||||
message.force
|
||||
)
|
||||
} else if('disappearingMessagesInChat' in message) {
|
||||
const exp = typeof message.disappearingMessagesInChat === 'boolean' ?
|
||||
(message.disappearingMessagesInChat ? WA_DEFAULT_EPHEMERAL : 0) :
|
||||
message.disappearingMessagesInChat
|
||||
m = prepareDisappearingMessageSettingContent(exp)
|
||||
} else {
|
||||
m = await prepareWAMessageMedia(
|
||||
message,
|
||||
options
|
||||
)
|
||||
}
|
||||
if('mentions' in message) {
|
||||
const [messageType] = Object.keys(message)
|
||||
message[messageType].contextInfo = message[messageType] || { }
|
||||
message[messageType].contextInfo.mentionedJid = message.mentions
|
||||
}
|
||||
return WAMessageProto.Message.fromObject(m)
|
||||
}
|
||||
export const generateWAMessageFromContent = (
|
||||
jid: string,
|
||||
message: WAMessageContent,
|
||||
options: MessageGenerationOptionsFromContent
|
||||
) => {
|
||||
if (!options.timestamp) options.timestamp = new Date() // set timestamp to now
|
||||
// prevent an annoying bug (WA doesn't accept sending messages with '@c.us')
|
||||
jid = whatsappID(jid)
|
||||
|
||||
const key = Object.keys(message)[0]
|
||||
const timestamp = unixTimestampSeconds(options.timestamp)
|
||||
const { quoted, userJid } = options
|
||||
|
||||
if (quoted) {
|
||||
const participant = quoted.key.fromMe ? userJid : (quoted.participant || quoted.key.participant || quoted.key.remoteJid)
|
||||
|
||||
message[key].contextInfo = message[key].contextInfo || { }
|
||||
message[key].contextInfo.participant = participant
|
||||
message[key].contextInfo.stanzaId = quoted.key.id
|
||||
message[key].contextInfo.quotedMessage = quoted.message
|
||||
|
||||
// if a participant is quoted, then it must be a group
|
||||
// hence, remoteJid of group must also be entered
|
||||
if (quoted.key.participant) {
|
||||
message[key].contextInfo.remoteJid = quoted.key.remoteJid
|
||||
}
|
||||
}
|
||||
if(
|
||||
// if we want to send a disappearing message
|
||||
!!options?.ephemeralOptions &&
|
||||
// and it's not a protocol message -- delete, toggle disappear message
|
||||
key !== 'protocolMessage' &&
|
||||
// already not converted to disappearing message
|
||||
key !== 'ephemeralMessage'
|
||||
) {
|
||||
message[key].contextInfo = {
|
||||
...(message[key].contextInfo || {}),
|
||||
expiration: options.ephemeralOptions.expiration || WA_DEFAULT_EPHEMERAL,
|
||||
ephemeralSettingTimestamp: options.ephemeralOptions.eph_setting_ts.toString()
|
||||
}
|
||||
message = {
|
||||
ephemeralMessage: {
|
||||
message
|
||||
}
|
||||
}
|
||||
}
|
||||
message = WAMessageProto.Message.fromObject (message)
|
||||
|
||||
const messageJSON = {
|
||||
key: {
|
||||
remoteJid: jid,
|
||||
fromMe: true,
|
||||
id: options?.messageId || generateMessageID(),
|
||||
},
|
||||
message: message,
|
||||
messageTimestamp: timestamp,
|
||||
messageStubParameters: [],
|
||||
participant: jid.includes('@g.us') ? userJid : undefined,
|
||||
status: WAMessageStatus.PENDING
|
||||
}
|
||||
return WAMessageProto.WebMessageInfo.fromObject (messageJSON)
|
||||
}
|
||||
export const generateWAMessage = async(
|
||||
jid: string,
|
||||
content: AnyMessageContent,
|
||||
options: MessageGenerationOptions,
|
||||
) => (
|
||||
generateWAMessageFromContent(
|
||||
jid,
|
||||
await generateWAMessageContent(
|
||||
content,
|
||||
options
|
||||
),
|
||||
options
|
||||
)
|
||||
)
|
||||
@@ -1,4 +1,4 @@
|
||||
import Boom from 'boom'
|
||||
import {Boom} from '@hapi/boom'
|
||||
import * as Curve from 'curve25519-js'
|
||||
import type { Contact } from '../Types/Contact'
|
||||
import type { AnyAuthenticationCredentials, AuthenticationCredentials, CurveKeyPair } from "../Types"
|
||||
Reference in New Issue
Block a user