mirror of
https://github.com/FranP-code/Baileys.git
synced 2025-10-13 00:32:22 +00:00
PTT Audio + Automatic link preview generation + Minor changes
This commit is contained in:
1
.gitignore
vendored
1
.gitignore
vendored
@@ -12,3 +12,4 @@ lib
|
||||
docs
|
||||
browser-token.json
|
||||
auth_info_messcat.json
|
||||
Proxy
|
||||
@@ -223,7 +223,11 @@ To note:
|
||||
mimetype: Mimetype.pdf, /* (for media messages) specify the type of media (optional for all media types except documents),
|
||||
import {Mimetype} from '@adiwajshing/baileys'
|
||||
*/
|
||||
filename: 'somefile.pdf' // (for media messages) file name for the media
|
||||
filename: 'somefile.pdf', // (for media messages) file name for the media
|
||||
/* will send audio messages as voice notes, if set to true */
|
||||
ptt: true,
|
||||
// will detect links & generate a link preview automatically (default true)
|
||||
detectLinks: true
|
||||
}
|
||||
```
|
||||
## Forwarding Messages
|
||||
|
||||
@@ -18,11 +18,11 @@ WAConnectionTest('Messages', conn => {
|
||||
assert.equal (content?.contextInfo?.isForwarded, true)
|
||||
})
|
||||
it('should send a link preview', async () => {
|
||||
const content = await conn.generateLinkPreview ('hello this is from https://www.github.com/adiwajshing/Baileys')
|
||||
const message = await sendAndRetreiveMessage(conn, content, MessageType.text)
|
||||
const text = 'hello this is from https://www.github.com/adiwajshing/Baileys'
|
||||
const message = await sendAndRetreiveMessage(conn, text, MessageType.text, { detectLinks: true })
|
||||
const received = message.message.extendedTextMessage
|
||||
|
||||
assert.strictEqual(received.text, content.text)
|
||||
assert.strictEqual(received.text, text)
|
||||
assert.ok (received.canonicalUrl)
|
||||
assert.ok (received.title)
|
||||
assert.ok (received.description)
|
||||
@@ -58,6 +58,13 @@ WAConnectionTest('Messages', conn => {
|
||||
|
||||
await conn.downloadAndSaveMediaMessage(message,'./Media/received_aud')
|
||||
})
|
||||
it('should send an audio as a voice note', async () => {
|
||||
const content = await fs.readFile('./Media/sonata.mp3')
|
||||
const message = await sendAndRetreiveMessage(conn, content, MessageType.audio, { mimetype: Mimetype.ogg, ptt: true })
|
||||
|
||||
assert.equal (message.message?.audioMessage?.ptt, true)
|
||||
await conn.downloadAndSaveMediaMessage(message,'./Media/received_aud')
|
||||
})
|
||||
it('should send an image', async () => {
|
||||
const content = await fs.readFile('./Media/meme.jpeg')
|
||||
const message = await sendAndRetreiveMessage(conn, content, MessageType.image)
|
||||
|
||||
@@ -26,7 +26,7 @@ import {
|
||||
} from './Constants'
|
||||
import { EventEmitter } from 'events'
|
||||
import KeyedDB from '@adiwajshing/keyed-db'
|
||||
import { STATUS_CODES } from 'http'
|
||||
import { STATUS_CODES, Agent } from 'http'
|
||||
|
||||
export class WAConnection extends EventEmitter {
|
||||
/** The version of WhatsApp Web we're telling the servers we are */
|
||||
@@ -341,6 +341,8 @@ export class WAConnection extends EventEmitter {
|
||||
}
|
||||
protected endConnection () {
|
||||
this.conn?.removeAllListeners ('close')
|
||||
this.conn?.removeAllListeners ('error')
|
||||
this.conn?.removeAllListeners ('open')
|
||||
this.conn?.removeAllListeners ('message')
|
||||
//this.conn?.close ()
|
||||
this.conn?.terminate()
|
||||
@@ -358,12 +360,12 @@ export class WAConnection extends EventEmitter {
|
||||
/**
|
||||
* Does a fetch request with the configuration of the connection
|
||||
*/
|
||||
protected fetchRequest = (endpoint: string, method: string = 'GET', body?: any) => (
|
||||
protected fetchRequest = (endpoint: string, method: string = 'GET', body?: any, agent?: Agent) => (
|
||||
fetch(endpoint, {
|
||||
method,
|
||||
body,
|
||||
headers: { Origin: DEFAULT_ORIGIN },
|
||||
agent: this.connectOptions.agent
|
||||
agent: agent || this.connectOptions.agent
|
||||
})
|
||||
)
|
||||
generateMessageTag (longTag: boolean = false) {
|
||||
|
||||
@@ -35,14 +35,15 @@ export class WAConnection extends Base {
|
||||
|
||||
const loggedOut = error instanceof BaileysError && UNAUTHORIZED_CODES.includes(error.status)
|
||||
const willReconnect = !loggedOut && (tries <= (options?.maxRetries || 5)) && this.state === 'connecting'
|
||||
const reason = loggedOut ? DisconnectReason.invalidSession : error.message
|
||||
|
||||
this.log (`connect attempt ${tries} failed: ${error}${ willReconnect ? ', retrying...' : ''}`, MessageLogLevel.info)
|
||||
|
||||
if ((this.state as string) !== 'close' && !willReconnect) {
|
||||
this.closeInternal (loggedOut ? DisconnectReason.invalidSession : error.message)
|
||||
this.closeInternal (reason)
|
||||
}
|
||||
|
||||
if (!willReconnect) throw error
|
||||
this.emit ('intermediate-close', {reason})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -67,19 +68,12 @@ export class WAConnection extends Base {
|
||||
|
||||
let cancel: () => void
|
||||
const task = Utils.promiseTimeout(timeoutMs, (resolve, reject) => {
|
||||
let task: Promise<any> = Promise.resolve ()
|
||||
cancel = () => reject (CancelledError())
|
||||
const checkIdleTime = () => {
|
||||
this.debounceTimeout && clearTimeout (this.debounceTimeout)
|
||||
this.debounceTimeout = setTimeout (() => rejectSafe (TimedOutError()), this.connectOptions.maxIdleTimeMs)
|
||||
}
|
||||
const debouncedTimeout = () => this.connectOptions.maxIdleTimeMs && this.conn.addEventListener ('message', checkIdleTime)
|
||||
|
||||
// add wait for chats promise if required
|
||||
if (typeof options?.waitForChats === 'undefined' ? true : options?.waitForChats) {
|
||||
const {waitForChats, cancelChats} = this.receiveChatsAndContacts(this.connectOptions.waitOnlyForLastMessage)
|
||||
task = waitForChats
|
||||
cancel = cancelChats
|
||||
}
|
||||
// determine whether reconnect should be used or not
|
||||
const shouldUseReconnect = this.lastDisconnectReason !== DisconnectReason.replaced &&
|
||||
this.lastDisconnectReason !== DisconnectReason.unknown &&
|
||||
@@ -93,38 +87,34 @@ export class WAConnection extends Base {
|
||||
|
||||
this.conn.on ('open', async () => {
|
||||
this.log(`connected to WhatsApp Web server, authenticating via ${reconnectID ? 'reconnect' : 'takeover'}`, MessageLogLevel.info)
|
||||
|
||||
let waitForChats: Promise<{[k: string]: Partial<WAChat>}>
|
||||
// add wait for chats promise if required
|
||||
if (typeof options?.waitForChats === 'undefined' ? true : options?.waitForChats) {
|
||||
const recv = this.receiveChatsAndContacts(this.connectOptions.waitOnlyForLastMessage)
|
||||
waitForChats = recv.waitForChats
|
||||
cancel = () => {
|
||||
reject (CancelledError())
|
||||
recv.cancelChats ()
|
||||
}
|
||||
}
|
||||
try {
|
||||
task = Promise.all ([
|
||||
task,
|
||||
// debounce the timeout once validated
|
||||
this.authenticate (debouncedTimeout, reconnectID)
|
||||
.then (
|
||||
() => {
|
||||
this.conn
|
||||
.removeAllListeners ('error')
|
||||
.removeAllListeners ('close')
|
||||
}
|
||||
)
|
||||
])
|
||||
const [result] = await task
|
||||
|
||||
await this.authenticate (debouncedTimeout, reconnectID)
|
||||
this.conn
|
||||
.removeAllListeners ('error')
|
||||
.removeAllListeners ('close')
|
||||
const result = waitForChats && (await waitForChats)
|
||||
this.conn.removeEventListener ('message', checkIdleTime)
|
||||
|
||||
resolve (result)
|
||||
} catch (error) {
|
||||
reject (error)
|
||||
}
|
||||
})
|
||||
const rejectSafe = error => {
|
||||
task = task.catch (() => {})
|
||||
reject (error)
|
||||
}
|
||||
const rejectSafe = error => reject (error)
|
||||
this.conn.on('error', rejectSafe)
|
||||
this.conn.on('close', () => rejectSafe(new Error('close')))
|
||||
}) as Promise<void | { [k: string]: Partial<WAChat> }>
|
||||
|
||||
return { promise: task, cancel }
|
||||
return { promise: task, cancel: cancel }
|
||||
}
|
||||
|
||||
let promise = Promise.resolve ()
|
||||
|
||||
@@ -321,6 +321,8 @@ export class WAConnection extends Base {
|
||||
on (event: 'connecting', listener: () => void): this
|
||||
/** when the connection has closed */
|
||||
on (event: 'close', listener: (err: {reason?: DisconnectReason | string, isReconnecting: boolean}) => void): this
|
||||
/** when the connection has closed */
|
||||
on (event: 'intermediate-close', listener: (err: {reason?: DisconnectReason | string}) => void): this
|
||||
/** when a new QR is generated, ready for scanning */
|
||||
on (event: 'qr', listener: (qr: string) => void): this
|
||||
/** when the connection to the phone changes */
|
||||
|
||||
@@ -9,7 +9,7 @@ import {
|
||||
WALocationMessage,
|
||||
WAContactMessage,
|
||||
WATextMessage,
|
||||
WAMessageContent, WAMetric, WAFlag, WAMessage, BaileysError, MessageLogLevel, WA_MESSAGE_STATUS_TYPE, WAMessageProto, MediaConnInfo, MessageTypeProto
|
||||
WAMessageContent, WAMetric, WAFlag, WAMessage, BaileysError, MessageLogLevel, WA_MESSAGE_STATUS_TYPE, WAMessageProto, MediaConnInfo, MessageTypeProto, URL_REGEX, WAUrlInfo
|
||||
} from './Constants'
|
||||
import { generateMessageID, sha256, hmacSign, aesEncrypWithIV, randomBytes, generateThumbnail, getMediaKeys, decodeMediaMessageBuffer, extensionForMediaMessage, whatsappID, unixTimestampSeconds } from './Utils'
|
||||
import { Mutex } from './Mutex'
|
||||
@@ -53,9 +53,14 @@ export class WAConnection extends Base {
|
||||
switch (type) {
|
||||
case MessageType.text:
|
||||
case MessageType.extendedText:
|
||||
if (typeof message === 'string') {
|
||||
m.extendedTextMessage = WAMessageProto.ExtendedTextMessage.create({text: message} as any)
|
||||
} else if ('text' in message) {
|
||||
if (typeof message === 'string') message = {text: message} as WATextMessage
|
||||
|
||||
if ('text' in message) {
|
||||
if (options.detectLinks !== false && message.text.match(URL_REGEX)) {
|
||||
try {
|
||||
message = await this.generateLinkPreview (message.text)
|
||||
} catch { } // ignore if fails
|
||||
}
|
||||
m.extendedTextMessage = WAMessageProto.ExtendedTextMessage.create(message as any)
|
||||
} else {
|
||||
throw new BaileysError ('message needs to be a string or object with property \'text\'', message)
|
||||
@@ -98,7 +103,8 @@ export class WAConnection extends Base {
|
||||
const body = Buffer.concat([enc, mac]) // body is enc + mac
|
||||
const fileSha256 = sha256(buffer)
|
||||
// url safe Base64 encode the SHA256 hash of the body
|
||||
const fileEncSha256B64 = sha256(body)
|
||||
const fileEncSha256 = sha256(body)
|
||||
const fileEncSha256B64 = fileEncSha256
|
||||
.toString('base64')
|
||||
.replace(/\+/g, '-')
|
||||
.replace(/\//g, '_')
|
||||
@@ -114,11 +120,14 @@ export class WAConnection extends Base {
|
||||
for (let host of json.hosts) {
|
||||
const hostname = `https://${host.hostname}${MediaPathMap[mediaType]}/${fileEncSha256B64}?auth=${auth}&token=${fileEncSha256B64}`
|
||||
try {
|
||||
const urlFetch = await this.fetchRequest(hostname, 'POST', body)
|
||||
const urlFetch = await this.fetchRequest(hostname, 'POST', body, options.uploadAgent)
|
||||
mediaUrl = (await urlFetch.json())?.url
|
||||
|
||||
if (mediaUrl) break
|
||||
else throw new Error (`upload failed`)
|
||||
else {
|
||||
await this.refreshMediaConn (true)
|
||||
throw new Error (`upload failed`)
|
||||
}
|
||||
} catch (error) {
|
||||
const isLast = host.hostname === json.hosts[json.hosts.length-1].hostname
|
||||
this.log (`Error in uploading to ${host.hostname}${isLast ? '' : ', retrying...'}`, MessageLogLevel.info)
|
||||
@@ -132,12 +141,13 @@ export class WAConnection extends Base {
|
||||
url: mediaUrl,
|
||||
mediaKey: mediaKey,
|
||||
mimetype: options.mimetype,
|
||||
fileEncSha256: sha256(body),//fileEncSha256B64,
|
||||
fileEncSha256: fileEncSha256,
|
||||
fileSha256: fileSha256,
|
||||
fileLength: buffer.length,
|
||||
fileName: options.filename || 'file',
|
||||
gifPlayback: isGIF || null,
|
||||
gifPlayback: isGIF || undefined,
|
||||
caption: options.caption,
|
||||
ptt: options.ptt
|
||||
}
|
||||
)
|
||||
}
|
||||
@@ -244,9 +254,26 @@ export class WAConnection extends Base {
|
||||
await fs.writeFile (trueFileName, buffer)
|
||||
return trueFileName
|
||||
}
|
||||
/** Query a string to check if it has a url, if it does, return required extended text message */
|
||||
async generateLinkPreview (text: string) {
|
||||
const query = ['query', {type: 'url', url: text, epoch: this.msgCount.toString()}, null]
|
||||
const response = await this.query ({json: query, binaryTags: [26, WAFlag.ignore], expect200: true})
|
||||
|
||||
protected async refreshMediaConn () {
|
||||
if (!this.mediaConn || (new Date().getTime()-this.mediaConn.fetchDate.getTime()) > this.mediaConn.ttl*1000) {
|
||||
if (response[1]) response[1].jpegThumbnail = response[2]
|
||||
const data = response[1] as WAUrlInfo
|
||||
|
||||
const content = {text} as WATextMessage
|
||||
content.canonicalUrl = data['canonical-url']
|
||||
content.matchedText = data['matched-text']
|
||||
content.jpegThumbnail = data.jpegThumbnail
|
||||
content.description = data.description
|
||||
content.title = data.title
|
||||
content.previewType = 0
|
||||
return content
|
||||
}
|
||||
|
||||
protected async refreshMediaConn (forceGet = false) {
|
||||
if (!this.mediaConn || (new Date().getTime()-this.mediaConn.fetchDate.getTime()) > this.mediaConn.ttl*1000 || forceGet) {
|
||||
const result = await this.query({json: ['query', 'mediaConn']})
|
||||
this.mediaConn = result.media_conn
|
||||
this.mediaConn.fetchDate = new Date()
|
||||
|
||||
@@ -1,12 +1,5 @@
|
||||
import {WAConnection as Base} from './6.MessagesSend'
|
||||
import {
|
||||
MessageType,
|
||||
WAMessageKey,
|
||||
MessageInfo,
|
||||
WATextMessage,
|
||||
WAUrlInfo,
|
||||
WAMessageContent, WAMetric, WAFlag, WANode, WAMessage, WAMessageProto, BaileysError, MessageLogLevel, WA_MESSAGE_STATUS_TYPE
|
||||
} from './Constants'
|
||||
import { MessageType, WAMessageKey, MessageInfo, WAMessageContent, WAMetric, WAFlag, WANode, WAMessage, WAMessageProto } from './Constants'
|
||||
import { whatsappID, delay, toNumber, unixTimestampSeconds } from './Utils'
|
||||
import { Mutex } from './Mutex'
|
||||
|
||||
@@ -211,23 +204,6 @@ export class WAConnection extends Base {
|
||||
const actual = await this.loadMessages (jid, 1, messages[0] && messages[0].key, false)
|
||||
return actual.messages[0]
|
||||
}
|
||||
/** Query a string to check if it has a url, if it does, return required extended text message */
|
||||
async generateLinkPreview (text: string) {
|
||||
const query = ['query', {type: 'url', url: text, epoch: this.msgCount.toString()}, null]
|
||||
const response = await this.query ({json: query, binaryTags: [26, WAFlag.ignore], expect200: true})
|
||||
|
||||
if (response[1]) response[1].jpegThumbnail = response[2]
|
||||
const data = response[1] as WAUrlInfo
|
||||
|
||||
const content = {text} as WATextMessage
|
||||
content.canonicalUrl = data['canonical-url']
|
||||
content.matchedText = data['matched-text']
|
||||
content.jpegThumbnail = data.jpegThumbnail
|
||||
content.description = data.description
|
||||
content.title = data.title
|
||||
content.previewType = 0
|
||||
return content
|
||||
}
|
||||
/**
|
||||
* Search WhatsApp messages with a given text string
|
||||
* @param txt the search string
|
||||
|
||||
@@ -82,6 +82,8 @@ export type WAConnectOptions = {
|
||||
/** agent which can be used for proxying connections */
|
||||
agent?: Agent
|
||||
}
|
||||
/** from: https://stackoverflow.com/questions/3809401/what-is-a-good-regular-expression-to-match-a-url */
|
||||
export const URL_REGEX = /[-a-zA-Z0-9@:%._+~#=]{1,256}\.[a-zA-Z0-9()]{1,6}\b([-a-zA-Z0-9()@:%_+.~#?&//=]*)?/gi
|
||||
|
||||
export type WAConnectionState = 'open' | 'connecting' | 'close'
|
||||
|
||||
@@ -166,6 +168,7 @@ export interface WAGroupModification {
|
||||
}
|
||||
|
||||
export interface WAContact {
|
||||
verify?: string
|
||||
/** name of the contact, the contact has set on their own on WA */
|
||||
notify?: string
|
||||
jid: string
|
||||
@@ -302,13 +305,30 @@ export enum Mimetype {
|
||||
webp = 'image/webp',
|
||||
}
|
||||
export interface MessageOptions {
|
||||
/** the message you want to quote */
|
||||
quoted?: WAMessage
|
||||
/** some random context info (can show a forwarded message with this too) */
|
||||
contextInfo?: WAContextInfo
|
||||
/** optional, if you want to manually set the timestamp of the message */
|
||||
timestamp?: Date
|
||||
/** (for media messages) the caption to send with the media (cannot be sent with stickers though) */
|
||||
caption?: string
|
||||
/**
|
||||
* For location & media messages -- has to be a base 64 encoded JPEG if you want to send a custom thumb,
|
||||
* or set to null if you don't want to send a thumbnail.
|
||||
* Do not enter this field if you want to automatically generate a thumb
|
||||
* */
|
||||
thumbnail?: string
|
||||
/** (for media messages) specify the type of media (optional for all media types except documents) */
|
||||
mimetype?: Mimetype | string
|
||||
/** (for media messages) file name for the media */
|
||||
filename?: string
|
||||
/** For audio messages, if set to true, will send as a `voice note` */
|
||||
ptt?: boolean
|
||||
/** Optional agent for media uploads */
|
||||
uploadAgent?: Agent
|
||||
/** If set to true (default), automatically detects if you're sending a link & attaches the preview*/
|
||||
detectLinks?: boolean
|
||||
}
|
||||
export interface WABroadcastListInfo {
|
||||
status: number
|
||||
@@ -388,6 +408,7 @@ export type BaileysEvent =
|
||||
'open' |
|
||||
'connecting' |
|
||||
'close' |
|
||||
'intermediate-close' |
|
||||
'qr' |
|
||||
'connection-phone-change' |
|
||||
'user-presence-update' |
|
||||
|
||||
Reference in New Issue
Block a user