mirror of
https://github.com/FranP-code/Baileys.git
synced 2025-10-13 00:32:22 +00:00
330 lines
9.6 KiB
TypeScript
330 lines
9.6 KiB
TypeScript
import { Boom } from '@hapi/boom'
|
|
import { createReadStream, promises as fs } from "fs"
|
|
import { proto } from '../../WAMessage'
|
|
import { MEDIA_KEYS, 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 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 = {
|
|
'image': WAMessageProto.ImageMessage,
|
|
'video': WAMessageProto.VideoMessage,
|
|
'audio': WAMessageProto.AudioMessage,
|
|
'sticker': WAMessageProto.StickerMessage,
|
|
'document': WAMessageProto.DocumentMessage,
|
|
} as const
|
|
|
|
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]
|
|
}
|
|
// check for cache hit
|
|
if(typeof uploadData.media === 'object' && 'url' in uploadData.media) {
|
|
const result = !!options.mediaCache && await options.mediaCache!(uploadData.media.url?.toString())
|
|
if(result) {
|
|
return WAMessageProto.Message.fromObject({
|
|
[`${mediaType}Message`]: result
|
|
})
|
|
}
|
|
}
|
|
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') &&
|
|
(typeof uploadData['jpegThumbnail'] === 'undefined')
|
|
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)
|
|
}
|
|
const {mediaUrl} = await options.upload(
|
|
createReadStream(encBodyPath),
|
|
{ fileEncSha256B64, mediaType }
|
|
)
|
|
// remove tmp files
|
|
await Promise.all(
|
|
[
|
|
fs.unlink(encBodyPath),
|
|
didSaveToTmpPath && bodyPath && fs.unlink(bodyPath)
|
|
]
|
|
.filter(Boolean)
|
|
)
|
|
delete uploadData.media
|
|
const content = {
|
|
[`${mediaType}Message`]: 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('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 = 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('buttons' in message && !!message.buttons) {
|
|
const buttonsMessage: proto.IButtonsMessage = {
|
|
buttons: message.buttons!,
|
|
text: ''
|
|
}
|
|
if('text' in message) {
|
|
buttonsMessage.contentText = message.text
|
|
} else {
|
|
Object.assign(buttonsMessage, m)
|
|
}
|
|
m = { buttonsMessage }
|
|
}
|
|
if('viewOnce' in message && !!message.viewOnce) {
|
|
m = { viewOnceMessage: { message: m } }
|
|
}
|
|
if('mentions' in message && message.mentions?.length) {
|
|
const [messageType] = Object.keys(m)
|
|
m[messageType].contextInfo = m[messageType] || { }
|
|
m[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
|
|
)
|
|
) |