Better error handling

This commit is contained in:
Adhiraj
2020-07-24 14:17:39 +05:30
parent d8d7e7dff8
commit db1f62f102
8 changed files with 43 additions and 45 deletions

View File

@@ -59,7 +59,7 @@ export default class WhatsAppWebBase extends WAConnection {
* @param jid the ID of the person/group who you are updating * @param jid the ID of the person/group who you are updating
* @param type your presence * @param type your presence
*/ */
async updatePresence(jid: string, type: Presence) { async updatePresence(jid: string | null, type: Presence) {
const json = [ const json = [
'action', 'action',
{ epoch: this.msgCount.toString(), type: 'set' }, { epoch: this.msgCount.toString(), type: 'set' },

View File

@@ -17,7 +17,7 @@ import {
WAUrlInfo, WAUrlInfo,
} from './Constants' } from './Constants'
import { generateMessageID, sha256, hmacSign, aesEncrypWithIV, randomBytes } from '../WAConnection/Utils' 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 { validateJIDForSending, generateThumbnail, getMediaKeys, decodeMediaMessageBuffer, extensionForMediaMessage } from './Utils'
import { proto } from '../../WAMessage/WAMessage' import { proto } from '../../WAMessage/WAMessage'
@@ -173,11 +173,11 @@ export default class WhatsAppWebMessages extends WhatsAppWebGroups {
*/ */
async updateMediaMessage (message: WAMessage) { async updateMediaMessage (message: WAMessage) {
const content = message.message?.audioMessage || message.message?.videoMessage || message.message?.imageMessage || message.message?.stickerMessage || message.message?.documentMessage 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 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]) 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 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) { } else if ('text' in message) {
m.extendedTextMessage = message as WATextMessage m.extendedTextMessage = message as WATextMessage
} else { } 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 break
case MessageType.location: case MessageType.location:

View File

@@ -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 = {}) { async function sendAndRetreiveMessage(client: WAClient, content, type: MessageType, options: MessageOptions = {}) {
const response = await client.sendMessage(testJid, content, type, options) const response = await client.sendMessage(testJid, content, type, options)
assert.strictEqual(response.status, 200)
const messages = await client.loadConversation(testJid, 1, null, true) const messages = await client.loadConversation(testJid, 1, null, true)
assert.strictEqual(messages[0].key.id, response.messageID) assert.strictEqual(messages[0].key.id, response.messageID)
return messages[0] return messages[0]

View File

@@ -2,7 +2,7 @@ import { MessageType, HKDFInfoKeys, MessageOptions, WAMessageType } from './Cons
import Jimp from 'jimp' import Jimp from 'jimp'
import * as fs from 'fs' import * as fs from 'fs'
import fetch from 'node-fetch' 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 { hmacSign, aesDecryptWithIV, hkdf } from '../WAConnection/Utils'
import { proto } from '../../WAMessage/WAMessage' import { proto } from '../../WAMessage/WAMessage'
import { randomBytes } from 'crypto' import { randomBytes } from 'crypto'
@@ -55,11 +55,8 @@ const extractVideoThumb = async (
new Promise((resolve, reject) => { new Promise((resolve, reject) => {
const cmd = `ffmpeg -ss ${time} -i ${path} -y -s ${size.width}x${size.height} -vframes 1 -f image2 ${destPath}` const cmd = `ffmpeg -ss ${time} -i ${path} -y -s ${size.width}x${size.height} -vframes 1 -f image2 ${destPath}`
exec(cmd, (err) => { exec(cmd, (err) => {
if (err) { if (err) reject(err)
reject(err) else resolve()
} else {
resolve()
}
}) })
}) as Promise<void> }) as Promise<void>
@@ -112,10 +109,10 @@ export async function decodeMediaMessageBuffer(message: WAMessageContent) {
*/ */
const type = Object.keys(message)[0] as MessageType const type = Object.keys(message)[0] as MessageType
if (!type) { if (!type) {
throw new Error('unknown message type') throw new BaileysError('unknown message type', message)
} }
if (type === MessageType.text || type === MessageType.extendedText) { 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) { if (type === MessageType.location || type === MessageType.liveLocation) {
return new Buffer(message[type].jpegThumbnail) 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 let messageContent: proto.IVideoMessage | proto.IImageMessage | proto.IAudioMessage | proto.IDocumentMessage
if (message.productMessage) { if (message.productMessage) {
const product = message.productMessage.product?.productImage 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 messageContent = product
} else { } else {
messageContent = message[type] messageContent = message[type]
@@ -134,7 +131,7 @@ export async function decodeMediaMessageBuffer(message: WAMessageContent) {
const buffer = await fetched.buffer() const buffer = await fetched.buffer()
if (buffer.length <= 10) { 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) => { 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`) } 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) { export function extensionForMediaMessage(message: WAMessageContent) {
const getExtension = (mimetype: string) => mimetype.split(';')[0].split('/')[1] const getExtension = (mimetype: string) => mimetype.split(';')[0].split('/')[1]

View File

@@ -12,6 +12,7 @@ import {
WATag, WATag,
MessageLogLevel, MessageLogLevel,
AuthenticationCredentialsBrowser, AuthenticationCredentialsBrowser,
BaileysError,
} from './Constants' } from './Constants'
/** Generate a QR code from the ref & the curve public key. This is scanned by the phone */ /** 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, timeoutMs: number = null,
tag: string = 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 * Query something from the WhatsApp servers

View File

@@ -1,6 +1,19 @@
import { WA } from '../Binary/Constants' import { WA } from '../Binary/Constants'
import { proto } from '../../WAMessage/WAMessage' 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 { export enum MessageLogLevel {
none=0, none=0,
info=1, info=1,

View File

@@ -2,6 +2,7 @@ import * as Crypto from 'crypto'
import HKDF from 'futoin-hkdf' import HKDF from 'futoin-hkdf'
import Decoder from '../Binary/Decoder' import Decoder from '../Binary/Decoder'
import {platform, release} from 'os' import {platform, release} from 'os'
import { BaileysError } from './Constants'
const platformMap = { const platformMap = {
'aix': 'AIX', 'aix': 'AIX',
@@ -55,12 +56,7 @@ export const createTimeout = (timeout) => new Promise(resolve => setTimeout(reso
export function promiseTimeout<T>(ms: number, promise: Promise<T>) { export function promiseTimeout<T>(ms: number, promise: Promise<T>) {
if (!ms) return promise if (!ms) return promise
// Create a promise that rejects in <ms> milliseconds // Create a promise that rejects in <ms> milliseconds
const timeout = new Promise((_, reject) => { const timeout = createTimeout (ms).then (() => { throw new BaileysError ('Timed out', promise) })
const id = setTimeout(() => {
clearTimeout(id)
reject('Timed out')
}, ms)
})
return Promise.race([promise, timeout]) as Promise<T> return Promise.race([promise, timeout]) as Promise<T>
} }
// whatsapp requires a message tag for every message, we just use the timestamp as one // 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() { export function generateMessageID() {
return randomBytes(10).toString('hex').toUpperCase() return randomBytes(10).toString('hex').toUpperCase()
} }
export function errorOnNon200Status(p: Promise<any>) {
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]?] { 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 let commaIndex = message.indexOf(',') // all whatsapp messages have a tag and a comma, followed by the actual message

View File

@@ -1,11 +1,9 @@
import * as Curve from 'curve25519-js' import * as Curve from 'curve25519-js'
import * as Utils from './Utils' import * as Utils from './Utils'
import WAConnectionBase from './Base' import WAConnectionBase from './Base'
import { MessageLogLevel, WAMetric, WAFlag } from './Constants' import { MessageLogLevel, WAMetric, WAFlag, BaileysError } from './Constants'
import { Presence } from '../WAClient/WAClient' 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 { export default class WAConnectionValidator extends WAConnectionBase {
/** Authenticate the connection */ /** Authenticate the connection */
protected async authenticate() { 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 return this.generateKeysForAuth(json.ref) // generate keys which will in turn be the QR
}) })
.then(json => { .then(async json => {
if ('status' in json) { if ('status' in json) {
switch (json.status) { switch (json.status) {
case 401: // if the phone was unpaired 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 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: default:
throw StatusError (json) throw new BaileysError ('unexpected status', json)
} }
} }
// if its a challenge request (we get it when logging in) // if its a challenge request (we get it when logging in)
if (json[1]?.challenge) { if (json[1]?.challenge) {
return this.respondToChallenge(json[1].challenge) await this.respondToChallenge(json[1].challenge)
.then (() => this.waitForMessage('s2', [])) return this.waitForMessage('s2', [])
} }
// otherwise just chain the promise further // otherwise just chain the promise further
return json return json
@@ -147,11 +145,11 @@ export default class WAConnectionValidator extends WAConnectionBase {
return onValidationSuccess() return onValidationSuccess()
} else { } else {
// if the checksums didn't match // if the checksums didn't match
throw new Error ('HMAC validation failed') throw new BaileysError ('HMAC validation failed', json)
} }
} else { } else {
// if we didn't get the connected field (usually we get this message when one opens WhatsApp on their phone) // 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)
} }
} }
/** /**