PTT Audio + Automatic link preview generation + Minor changes

This commit is contained in:
Adhiraj
2020-09-22 22:50:29 +05:30
parent e880556115
commit 30cee92758
9 changed files with 104 additions and 74 deletions

1
.gitignore vendored
View File

@@ -12,3 +12,4 @@ lib
docs
browser-token.json
auth_info_messcat.json
Proxy

View File

@@ -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

View File

@@ -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)

View File

@@ -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) {

View File

@@ -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 ()

View File

@@ -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 */

View File

@@ -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()

View File

@@ -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

View File

@@ -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' |