diff --git a/src/WAClient/Base.ts b/src/WAClient/Base.ts index 0634ca1..52356c9 100644 --- a/src/WAClient/Base.ts +++ b/src/WAClient/Base.ts @@ -59,7 +59,7 @@ export default class WhatsAppWebBase extends WAConnection { * @param jid the ID of the person/group who you are updating * @param type your presence */ - async updatePresence(jid: string, type: Presence) { + async updatePresence(jid: string | null, type: Presence) { const json = [ 'action', { epoch: this.msgCount.toString(), type: 'set' }, diff --git a/src/WAClient/Messages.ts b/src/WAClient/Messages.ts index ef4ad81..dcdc3e0 100644 --- a/src/WAClient/Messages.ts +++ b/src/WAClient/Messages.ts @@ -17,7 +17,7 @@ import { WAUrlInfo, } from './Constants' import { generateMessageID, sha256, hmacSign, aesEncrypWithIV, randomBytes } from '../WAConnection/Utils' -import { WAMessageContent, WAMetric, WAFlag, WANode, WAMessage, WAMessageProto } from '../WAConnection/Constants' +import { WAMessageContent, WAMetric, WAFlag, WANode, WAMessage, WAMessageProto, BaileysError } from '../WAConnection/Constants' import { validateJIDForSending, generateThumbnail, getMediaKeys, decodeMediaMessageBuffer, extensionForMediaMessage } from './Utils' import { proto } from '../../WAMessage/WAMessage' @@ -173,11 +173,11 @@ export default class WhatsAppWebMessages extends WhatsAppWebGroups { */ async updateMediaMessage (message: WAMessage) { const content = message.message?.audioMessage || message.message?.videoMessage || message.message?.imageMessage || message.message?.stickerMessage || message.message?.documentMessage - if (!content) throw new Error (`given message ${message.key.id} is not a media message`) + if (!content) throw new BaileysError (`given message ${message.key.id} is not a media message`, message) const query = ['query',{type: 'media', index: message.key.id, owner: message.key.fromMe ? 'true' : 'false', jid: message.key.remoteJid, epoch: this.msgCount.toString()},null] const response = await this.query (query, [WAMetric.queryMedia, WAFlag.ignore]) - if (parseInt(response[1].code) !== 200) throw new Error ('unexpected status ' + response[1].code) + if (parseInt(response[1].code) !== 200) throw new BaileysError ('unexpected status ' + response[1].code, response) Object.keys (response[1]).forEach (key => content[key] = response[1][key]) // update message } @@ -213,7 +213,7 @@ export default class WhatsAppWebMessages extends WhatsAppWebGroups { } else if ('text' in message) { m.extendedTextMessage = message as WATextMessage } else { - throw new Error ('message needs to be a string or object with property \'text\'') + throw new BaileysError ('message needs to be a string or object with property \'text\'', message) } break case MessageType.location: diff --git a/src/WAClient/Tests.ts b/src/WAClient/Tests.ts index c9f9bdf..518241f 100644 --- a/src/WAClient/Tests.ts +++ b/src/WAClient/Tests.ts @@ -12,7 +12,6 @@ const testJid = process.env.TEST_JID || '1234@s.whatsapp.net' // set TEST_JID=xy async function sendAndRetreiveMessage(client: WAClient, content, type: MessageType, options: MessageOptions = {}) { const response = await client.sendMessage(testJid, content, type, options) - assert.strictEqual(response.status, 200) const messages = await client.loadConversation(testJid, 1, null, true) assert.strictEqual(messages[0].key.id, response.messageID) return messages[0] diff --git a/src/WAClient/Utils.ts b/src/WAClient/Utils.ts index b7d326f..924bbb7 100644 --- a/src/WAClient/Utils.ts +++ b/src/WAClient/Utils.ts @@ -2,7 +2,7 @@ import { MessageType, HKDFInfoKeys, MessageOptions, WAMessageType } from './Cons import Jimp from 'jimp' import * as fs from 'fs' import fetch from 'node-fetch' -import { WAMessage, WAMessageContent } from '../WAConnection/Constants' +import { WAMessage, WAMessageContent, BaileysError } from '../WAConnection/Constants' import { hmacSign, aesDecryptWithIV, hkdf } from '../WAConnection/Utils' import { proto } from '../../WAMessage/WAMessage' import { randomBytes } from 'crypto' @@ -55,11 +55,8 @@ const extractVideoThumb = async ( 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() - } + if (err) reject(err) + else resolve() }) }) as Promise @@ -112,10 +109,10 @@ export async function decodeMediaMessageBuffer(message: WAMessageContent) { */ const type = Object.keys(message)[0] as MessageType if (!type) { - throw new Error('unknown message type') + throw new BaileysError('unknown message type', message) } if (type === MessageType.text || type === MessageType.extendedText) { - throw new Error('cannot decode text message') + throw new BaileysError('cannot decode text message', message) } if (type === MessageType.location || type === MessageType.liveLocation) { return new Buffer(message[type].jpegThumbnail) @@ -123,7 +120,7 @@ export async function decodeMediaMessageBuffer(message: WAMessageContent) { let messageContent: proto.IVideoMessage | proto.IImageMessage | proto.IAudioMessage | proto.IDocumentMessage if (message.productMessage) { const product = message.productMessage.product?.productImage - if (!product) throw new Error ('product has no image') + if (!product) throw new BaileysError ('product has no image', message) messageContent = product } else { messageContent = message[type] @@ -134,7 +131,7 @@ export async function decodeMediaMessageBuffer(message: WAMessageContent) { const buffer = await fetched.buffer() if (buffer.length <= 10) { - throw new Error ('Empty buffer returned. File has possibly been deleted from WA servers. Run `client.updateMediaMessage()` to refresh the url') + 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) => { @@ -163,7 +160,7 @@ export async function decodeMediaMessageBuffer(message: WAMessageContent) { if (i === 0) { console.log (`decryption of ${type} media with original HKDF key failed`) } } } - throw new Error('Decryption failed, HMAC sign does not match') + 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] diff --git a/src/WAConnection/Base.ts b/src/WAConnection/Base.ts index 1bb61f7..11a0490 100644 --- a/src/WAConnection/Base.ts +++ b/src/WAConnection/Base.ts @@ -12,6 +12,7 @@ import { WATag, MessageLogLevel, AuthenticationCredentialsBrowser, + BaileysError, } from './Constants' /** Generate a QR code from the ref & the curve public key. This is scanned by the phone */ @@ -213,7 +214,11 @@ export default class WAConnectionBase { timeoutMs: number = null, tag: string = null, ) { - return Utils.errorOnNon200Status(this.query(json, binaryTags, timeoutMs, tag)) + const response = await this.query(json, binaryTags, timeoutMs, tag) + if (response.status && Math.floor(+response.status / 100) !== 2) { + throw new BaileysError(`Unexpected status code: ${response.status}`, {query: json}) + } + return response } /** * Query something from the WhatsApp servers diff --git a/src/WAConnection/Constants.ts b/src/WAConnection/Constants.ts index 3a66740..1b2d33c 100644 --- a/src/WAConnection/Constants.ts +++ b/src/WAConnection/Constants.ts @@ -1,6 +1,19 @@ import { WA } from '../Binary/Constants' import { proto } from '../../WAMessage/WAMessage' + +export class BaileysError extends Error { + status?: number + context: any + + constructor (message: string, context: any) { + super (message) + this.name = 'BaileysError' + this.status = context.status + this.context = context + } +} + export enum MessageLogLevel { none=0, info=1, diff --git a/src/WAConnection/Utils.ts b/src/WAConnection/Utils.ts index bf81c52..d1ca5c0 100644 --- a/src/WAConnection/Utils.ts +++ b/src/WAConnection/Utils.ts @@ -2,6 +2,7 @@ import * as Crypto from 'crypto' import HKDF from 'futoin-hkdf' import Decoder from '../Binary/Decoder' import {platform, release} from 'os' +import { BaileysError } from './Constants' const platformMap = { 'aix': 'AIX', @@ -55,12 +56,7 @@ export const createTimeout = (timeout) => new Promise(resolve => setTimeout(reso export function promiseTimeout(ms: number, promise: Promise) { if (!ms) return promise // Create a promise that rejects in milliseconds - const timeout = new Promise((_, reject) => { - const id = setTimeout(() => { - clearTimeout(id) - reject('Timed out') - }, ms) - }) + const timeout = createTimeout (ms).then (() => { throw new BaileysError ('Timed out', promise) }) return Promise.race([promise, timeout]) as Promise } // whatsapp requires a message tag for every message, we just use the timestamp as one @@ -77,16 +73,6 @@ export function generateClientID() { export function generateMessageID() { return randomBytes(10).toString('hex').toUpperCase() } - -export function errorOnNon200Status(p: Promise) { - return p.then(json => { - if (json.status && typeof json.status === 'number' && Math.floor(json.status / 100) !== 2) { - throw new Error(`Unexpected status code: ${json.status}`) - } - return json - }) -} - export function decryptWA (message: any, macKey: Buffer, encKey: Buffer, decoder: Decoder, fromMe: boolean=false): [string, Object, [number, number]?] { let commaIndex = message.indexOf(',') // all whatsapp messages have a tag and a comma, followed by the actual message diff --git a/src/WAConnection/Validation.ts b/src/WAConnection/Validation.ts index c48674a..cf16b9f 100644 --- a/src/WAConnection/Validation.ts +++ b/src/WAConnection/Validation.ts @@ -1,11 +1,9 @@ import * as Curve from 'curve25519-js' import * as Utils from './Utils' import WAConnectionBase from './Base' -import { MessageLogLevel, WAMetric, WAFlag } from './Constants' +import { MessageLogLevel, WAMetric, WAFlag, BaileysError } from './Constants' import { Presence } from '../WAClient/WAClient' -const StatusError = (message: any, description: string='unknown error') => new Error (`unexpected status: ${message.status} on JSON: ${JSON.stringify(message)}`) - export default class WAConnectionValidator extends WAConnectionBase { /** Authenticate the connection */ protected async authenticate() { @@ -40,21 +38,21 @@ export default class WAConnectionValidator extends WAConnectionBase { } return this.generateKeysForAuth(json.ref) // generate keys which will in turn be the QR }) - .then(json => { + .then(async json => { if ('status' in json) { switch (json.status) { case 401: // if the phone was unpaired - throw StatusError (json, 'unpaired from phone') + throw new BaileysError ('unpaired from phone', json) case 429: // request to login was denied, don't know why it happens - throw StatusError (json, 'request denied, try reconnecting') + throw new BaileysError ('request denied, try reconnecting', json) default: - throw StatusError (json) + throw new BaileysError ('unexpected status', json) } } // if its a challenge request (we get it when logging in) if (json[1]?.challenge) { - return this.respondToChallenge(json[1].challenge) - .then (() => this.waitForMessage('s2', [])) + await this.respondToChallenge(json[1].challenge) + return this.waitForMessage('s2', []) } // otherwise just chain the promise further return json @@ -147,11 +145,11 @@ export default class WAConnectionValidator extends WAConnectionBase { return onValidationSuccess() } else { // if the checksums didn't match - throw new Error ('HMAC validation failed') + throw new BaileysError ('HMAC validation failed', json) } } else { // if we didn't get the connected field (usually we get this message when one opens WhatsApp on their phone) - throw new Error (`incorrect JSON: ${JSON.stringify(json)}`) + throw new BaileysError (`invalid JSON`, json) } } /**