Typescript update

This commit is contained in:
Adhiraj Singh
2020-07-01 13:25:08 +05:30
parent e50c1cbaf1
commit 4d83f6dd53
14 changed files with 144 additions and 124 deletions

1
.gitignore vendored
View File

@@ -6,3 +6,4 @@ output.csv
package-lock.json
*/.DS_Store
.DS_Store
.env

View File

@@ -17,7 +17,7 @@ export default class Encoder {
this.pushBytes([(value >> 16) & 0x0f, (value >> 8) & 0xff, value & 0xff])
}
pushBytes(bytes: Uint8Array | Array<number>) {
this.data.push(...bytes)
this.data.push.apply(this.data, bytes)
}
pushString(str: string) {
const bytes = new TextEncoder().encode(str)

View File

@@ -1,53 +1,40 @@
import {
WAClient,
AuthenticationCredentialsBase64,
getNotificationType,
MessageType,
decodeMediaMessage,
Presence,
MessageOptions,
Mimetype,
WALocationMessage,
MessageLogLevel,
} from '../WAClient/WAClient'
import fs from 'fs'
import * as fs from 'fs'
async function example() {
const client = new WAClient() // instantiate
client.autoReconnect = true // auto reconnect on disconnect
client.logUnhandledMessages = false // set to true to see what kind of stuff you can implement
client.logLevel = MessageLogLevel.none // set to unhandled to see what kind of stuff you can implement
let authInfo: AuthenticationCredentialsBase64 = null
try {
const file = fs.readFileSync('auth_info.json') // load a closed session back if it exists
authInfo = JSON.parse(file)
} catch {}
// connect or timeout in 20 seconds
const [user, chats, contacts, unread] = await client.connect(authInfo, 20 * 1000)
// connect or timeout in 20 seconds (loads the auth file credentials if present)
const [user, chats, contacts, unread] = await client.connect('./auth_info.json', 20 * 1000)
console.log('oh hello ' + user.name + ' (' + user.id + ')')
console.log('you have ' + unread.length + ' unread messages')
console.log('you have ' + chats.length + ' chats & ' + contacts.length + ' contacts')
authInfo = client.base64EncodedAuthInfo() // get all the auth info we need to restore this session
const authInfo = client.base64EncodedAuthInfo() // get all the auth info we need to restore this session
fs.writeFileSync('./auth_info.json', JSON.stringify(authInfo, null, '\t')) // save this info to a file
/* Note: one can take this auth_info.json file and login again from any computer without having to scan the QR code,
and get full access to one's WhatsApp. Despite the convenience, be careful with this file */
client.setOnPresenceUpdate((json) => console.log(json.id + ' presence is ' + json.type))
client.setOnMessageStatusChange((json) => {
client.setOnPresenceUpdate(json => console.log(json.id + ' presence is ' + json.type))
client.setOnMessageStatusChange(json => {
const participant = json.participant ? ' (' + json.participant + ')' : '' // participant exists when the message is from a group
console.log(
json.to +
participant +
' acknowledged message(s) ' +
json.ids +
' as ' +
json.type +
' at ' +
json.timestamp,
)
console.log(`${json.to}${participant} acknlowledged message(s) ${json.ids} as ${json.type}`)
})
client.setOnUnreadMessage(async (m) => {
// set to false to NOT relay your own sent messages
client.setOnUnreadMessage(true, async (m) => {
const [notificationType, messageType] = getNotificationType(m) // get what type of notification it is -- message, group add notification etc.
console.log('got notification of type: ' + notificationType)
@@ -74,32 +61,20 @@ async function example() {
const contact = m.message.contactMessage
console.log(sender + ' sent contact (' + contact.displayName + '): ' + contact.vcard)
} else if (messageType === MessageType.location || messageType === MessageType.liveLocation) {
const locMessage = m.message[messageType]
console.log(
sender +
' sent location (lat: ' +
locMessage.degreesLatitude +
', long: ' +
locMessage.degreesLongitude +
'), saving thumbnail...',
)
decodeMediaMessage(m.message, 'loc_thumb_in_' + m.key.id)
const locMessage = m.message[messageType] as WALocationMessage
console.log(`${sender} sent location (lat: ${locMessage.degreesLatitude}, long: ${locMessage.degreesLongitude})`)
decodeMediaMessage(m.message, './Media/loc_thumb_in_' + m.key.id) // save location thumbnail
if (messageType === MessageType.liveLocation) {
console.log(
sender +
' sent live location for duration: ' +
m.duration / 60 +
' minutes, seq number: ' +
locMessage.sequenceNumber,
)
console.log(`${sender} sent live location for duration: ${m.duration/60}`)
}
} else {
// if it is a media (audio, image, video) message
// decode, decrypt & save the media.
// The extension to the is applied automatically based on the media type
try {
const savedFile = await decodeMediaMessage(m.message, 'media_in_' + m.key.id)
const savedFile = await decodeMediaMessage(m.message, './Media/media_in_' + m.key.id)
console.log(sender + ' sent media, saved at: ' + savedFile)
} catch (err) {
console.log('error in decoding message: ' + err)
@@ -124,21 +99,22 @@ async function example() {
content = { degreesLatitude: 32.123123, degreesLongitude: 12.12123123 }
type = MessageType.location
} else {
content = fs.readFileSync('example/ma_gif.mp4') // load the gif
content = fs.readFileSync('./Media/ma_gif.mp4') // load the gif
options.mimetype = Mimetype.gif
type = MessageType.video
}
const response = await client.sendMessage(m.key.remoteJid, content, type, options)
console.log("sent message with ID '" + response.messageID + "' successfully: " + (response.status === 200))
}, 3 * 1000)
}, true) // set to false to not relay your own sent messages
})
/* example of custom functionality for tracking battery */
client.registerCallback(['action', null, 'battery'], (json) => {
client.registerCallback(['action', null, 'battery'], json => {
const batteryLevelStr = json[2][0][1].value
const batterylevel = parseInt(batteryLevelStr)
console.log('battery level: ' + batterylevel)
})
client.setOnUnexpectedDisconnect((err) => console.log('disconnected unexpectedly: ' + err))
client.setOnUnexpectedDisconnect(err => console.log('disconnected unexpectedly: ' + err))
}
example().catch((err) => console.log(`encountered error: ${err}`))

2
Media/.gitkeep Normal file
View File

@@ -0,0 +1,2 @@
ma_gif.mp4
meme.jpeg

View File

@@ -8,6 +8,7 @@ import {
WAGroupCreateResponse,
WAGroupMetadata,
WAGroupModification,
MessageLogLevel,
} from '../WAConnection/Constants'
import { generateMessageTag } from '../WAConnection/Utils'
@@ -51,7 +52,7 @@ export default class WhatsAppWebBase extends WAConnection {
if (!message.key.fromMe || callbackOnMyMessages) {
// if this message was sent to us, notify
callback(message as WAMessage)
} else if (this.logUnhandledMessages) {
} else if (this.logLevel >= MessageLogLevel.unhandled) {
this.log(`[Unhandled] message - ${JSON.stringify(message)}`)
}
})

View File

@@ -1,24 +1,27 @@
import { WAClient } from './WAClient'
import { MessageType, MessageOptions, Mimetype, Presence } from './Constants'
import fs from 'fs'
import * as fs from 'fs'
import * as assert from 'assert'
import assert from 'assert'
import { decodeMediaMessage } from './Utils'
import { promiseTimeout } from '../WAConnection/Utils'
const testJid = '919646328797@s.whatsapp.net'
const createTimeout = (timeout) => new Promise((resolve) => setTimeout(resolve, timeout))
require ('dotenv').config () // dotenv to load test jid
const testJid = process.env.TEST_JID // set TEST_JID=xyz@s.whatsapp.net in a .env file in the root directory
const createTimeout = (timeout) => new Promise(resolve => setTimeout(resolve, timeout))
async function sendAndRetreiveMessage(client: WAClient, content, type: MessageType, options: MessageOptions = {}) {
const response = await client.sendMessage(testJid, content, type, options)
assert.equal(response.status, 200)
assert.strictEqual(response.status, 200)
const messages = await client.loadConversation(testJid, 1, null, true)
assert.equal(messages[0].key.id, response.messageID)
assert.strictEqual(messages[0].key.id, response.messageID)
return messages[0]
}
function WAClientTest(name: string, func: (client: WAClient) => void) {
describe(name, () => {
const client = new WAClient()
console.log (`test jid: ${testJid}`)
before(async () => {
const file = './auth_info.json'
await client.connectSlim(file)
@@ -31,14 +34,14 @@ function WAClientTest(name: string, func: (client: WAClient) => void) {
WAClientTest('Messages', (client) => {
it('should send a text message', async () => {
const message = await sendAndRetreiveMessage(client, 'hello fren', MessageType.text)
assert.equal(message.message.conversation, 'hello fren')
assert.strictEqual(message.message.conversation, 'hello fren')
})
it('should quote a message', async () => {
const messages = await client.loadConversation(testJid, 2)
const message = await sendAndRetreiveMessage(client, 'hello fren 2', MessageType.extendedText, {
quoted: messages[0],
})
assert.equal(message.message.extendedTextMessage.contextInfo.stanzaId, messages[0].key.id)
assert.strictEqual(message.message.extendedTextMessage.contextInfo.stanzaId, messages[0].key.id)
})
it('should send a gif', async () => {
const content = fs.readFileSync('./Media/ma_gif.mp4')
@@ -56,7 +59,7 @@ WAClientTest('Messages', (client) => {
const content = fs.readFileSync('./Media/meme.jpeg')
const message = await sendAndRetreiveMessage(client, content, MessageType.image, { quoted: messages[0] })
const file = await decodeMediaMessage(message.message, './Media/received_img')
assert.equal(message.message.imageMessage.contextInfo.stanzaId, messages[0].key.id)
assert.strictEqual(message.message.imageMessage.contextInfo.stanzaId, messages[0].key.id)
})
})
WAClientTest('Presence', (client) => {
@@ -64,7 +67,7 @@ WAClientTest('Presence', (client) => {
const presences = Object.values(Presence)
for (const i in presences) {
const response = await client.updatePresence(testJid, presences[i])
assert.equal(response.status, 200)
assert.strictEqual(response.status, 200)
await createTimeout(1500)
}
@@ -73,15 +76,15 @@ WAClientTest('Presence', (client) => {
WAClientTest('Misc', (client) => {
it('should tell if someone has an account on WhatsApp', async () => {
const response = await client.isOnWhatsApp(testJid)
assert.equal(response, true)
assert.strictEqual(response, true)
const responseFail = await client.isOnWhatsApp('abcd@s.whatsapp.net')
assert.equal(responseFail, false)
assert.strictEqual(responseFail, false)
})
it('should return the status', async () => {
const response = await client.getStatus(testJid)
assert.ok(response.status)
assert.equal(typeof response.status, 'string')
assert.strictEqual(typeof response.status, 'string')
})
it('should return the profile picture', async () => {
const response = await client.getProfilePicture(testJid)
@@ -93,38 +96,38 @@ WAClientTest('Groups', (client) => {
let gid: string
it('should create a group', async () => {
const response = await client.groupCreate('Cool Test Group', [testJid])
assert.equal(response.status, 200)
assert.strictEqual(response.status, 200)
gid = response.gid
console.log('created group: ' + gid)
})
it('should retreive group invite code', async () => {
const code = await client.groupInviteCode(gid)
assert.ok(code)
assert.equal(typeof code, 'string')
assert.strictEqual(typeof code, 'string')
})
it('should retreive group metadata', async () => {
const metadata = await client.groupMetadata(gid)
assert.equal(metadata.id, gid)
assert.equal(metadata.participants.filter((obj) => obj.id.split('@')[0] === testJid.split('@')[0]).length, 1)
assert.strictEqual(metadata.id, gid)
assert.strictEqual(metadata.participants.filter((obj) => obj.id.split('@')[0] === testJid.split('@')[0]).length, 1)
})
it('should send a message on the group', async () => {
const r = await client.sendMessage(gid, 'hello', MessageType.text)
assert.equal(r.status, 200)
assert.strictEqual(r.status, 200)
})
it('should update the subject', async () => {
const subject = 'V Cool Title'
const r = await client.groupUpdateSubject(gid, subject)
assert.equal(r.status, 200)
assert.strictEqual(r.status, 200)
const metadata = await client.groupMetadata(gid)
assert.equal(metadata.subject, subject)
assert.strictEqual(metadata.subject, subject)
})
it('should remove someone from a group', async () => {
await client.groupRemove(gid, [testJid])
})
it('should leave the group', async () => {
const response = await client.groupLeave(gid)
assert.equal(response.status, 200)
assert.strictEqual(response.status, 200)
})
})
WAClientTest('Events', (client) => {
@@ -146,7 +149,12 @@ WAClientTest('Events', (client) => {
console.log (presence)
})
const response = await client.requestPresenceUpdate (client.userMetaData)
assert.equal (response.status, 200)
assert.strictEqual (response.status, 200)
await createTimeout (25000)
})*/
})
/*WAClientTest ('Testz', client => {
it ('should work', async () => {
})
})*/

View File

@@ -1,6 +1,6 @@
import { MessageType, HKDFInfoKeys, MessageOptions, MessageStubTypes } from './Constants'
import sharp from 'sharp'
import fs from 'fs'
import * as sharp from 'sharp'
import * as fs from 'fs'
import fetch from 'node-fetch'
import { WAMessage, WAMessageContent } from '../WAConnection/Constants'
import { hmacSign, aesDecryptWithIV, hkdf } from '../WAConnection/Utils'

View File

@@ -1,10 +1,11 @@
import WS from 'ws'
import QR from 'qrcode-terminal'
import fs from 'fs'
import * as QR from 'qrcode-terminal'
import * as fs from 'fs'
import * as WS from 'ws'
import * as Utils from './Utils'
import Encoder from '../Binary/Encoder'
import Decoder from '../Binary/Decoder'
import { AuthenticationCredentials, UserMetaData, WANode, AuthenticationCredentialsBase64, WATag } from './Constants'
import { AuthenticationCredentials, UserMetaData, WANode, AuthenticationCredentialsBase64, WATag, MessageLogLevel } from './Constants'
/** Generate a QR code from the ref & the curve public key. This is scanned by the phone */
const generateQRCode = function ([ref, publicKey, clientID]) {
@@ -23,7 +24,7 @@ export default class WAConnectionBase {
autoReconnect = true
lastSeen: Date = null
/** Log messages that are not handled, so you can debug & see what custom stuff you can implement */
logUnhandledMessages = false
logLevel: MessageLogLevel = MessageLogLevel.none
/** Data structure of tokens & IDs used to establish one's identiy to WhatsApp Web */
protected authInfo: AuthenticationCredentials = {
clientID: null,
@@ -259,6 +260,6 @@ export default class WAConnectionBase {
}
}
protected log(text) {
console.log(`[Baileys] ${text}`)
console.log(`[Baileys][${new Date().toLocaleString()}] ${text}`)
}
}

View File

@@ -1,7 +1,6 @@
import WS from 'ws'
import * as WS from 'ws'
import * as Utils from './Utils'
import { AuthenticationCredentialsBase64, UserMetaData, WAMessage, WAChat, WAContact } from './Constants'
import { AuthenticationCredentialsBase64, UserMetaData, WAMessage, WAChat, WAContact, MessageLogLevel } from './Constants'
import WAConnectionValidator from './Validation'
export default class WAConnectionConnector extends WAConnectionValidator {
@@ -13,7 +12,7 @@ export default class WAConnectionConnector extends WAConnectionValidator {
*/
async connect(authInfo: AuthenticationCredentialsBase64 | string = null, timeoutMs: number = null) {
const userInfo = await this.connectSlim(authInfo, timeoutMs)
const chats = await this.receiveChatsAndContacts()
const chats = await this.receiveChatsAndContacts(timeoutMs)
return [userInfo, ...chats] as [UserMetaData, WAChat[], WAContact[], WAMessage[]]
}
/**
@@ -31,7 +30,7 @@ export default class WAConnectionConnector extends WAConnectionValidator {
try {
this.loadAuthInfoFromBase64(authInfo)
} catch {}
this.conn = new WS('wss://web.whatsapp.com/ws', null, { origin: 'https://web.whatsapp.com' })
let promise: Promise<UserMetaData> = new Promise((resolve, reject) => {
@@ -52,8 +51,8 @@ export default class WAConnectionConnector extends WAConnectionValidator {
reject(error)
})
})
promise = timeoutMs ? Utils.promiseTimeout(timeoutMs, promise) : promise
return promise.catch((err) => {
promise = Utils.promiseTimeout(timeoutMs, promise)
return promise.catch(err => {
this.close()
throw err
})
@@ -61,25 +60,31 @@ export default class WAConnectionConnector extends WAConnectionValidator {
/**
* Sets up callbacks to receive chats, contacts & unread messages.
* Must be called immediately after connect
* [chats, contacts, unreadMessages]
* @returns [chats, contacts, unreadMessages]
*/
receiveChatsAndContacts() {
const chats: Array<WAChat> = []
async receiveChatsAndContacts(timeoutMs: number = null) {
let chats: Array<WAChat> = []
let contacts: Array<WAContact> = []
const unreadMessages: Array<WAMessage> = []
const unreadMap = {}
let unreadMessages: Array<WAMessage> = []
let unreadMap: Record<string, number> = {}
let encounteredAddBefore = false
let receivedContacts = false
let receivedMessages = false
let convoResolve
this.log('waiting for chats & contacts') // wait for the message with chats
const waitForConvos = () =>
new Promise((resolve, _) => {
convoResolve = resolve
const chatUpdate = (json) => {
new Promise(resolve => {
convoResolve = () => {
// de-register the callbacks, so that they don't get called again
this.deregisterCallback(['action', 'add:last'])
this.deregisterCallback(['action', 'add:before'])
this.deregisterCallback(['action', 'add:unread'])
resolve()
}
const chatUpdate = json => {
receivedMessages = true
const isLast = json[1].last
encounteredAddBefore = json[1].add === 'before' ? true : encounteredAddBefore
json = json[2]
if (json) {
for (let k = json.length - 1; k >= 0; k--) {
@@ -92,12 +97,8 @@ export default class WAConnectionConnector extends WAConnectionValidator {
}
}
}
if (isLast) {
// de-register the callbacks, so that they don't get called again
this.deregisterCallback(['action', 'add:last'])
this.deregisterCallback(['action', 'add:before'])
this.deregisterCallback(['action', 'add:unread'])
resolve()
if (isLast && receivedContacts) { // if received contacts before messages
convoResolve ()
}
}
// wait for actual messages to load, "last" is the most recent message, "before" contains prior messages
@@ -105,22 +106,27 @@ export default class WAConnectionConnector extends WAConnectionValidator {
this.registerCallback(['action', 'add:before'], chatUpdate)
this.registerCallback(['action', 'add:unread'], chatUpdate)
})
const waitForChats = this.registerCallbackOneTime(['response', 'type:chat']).then((json) => {
json[2].forEach((chat) => {
const waitForChats = async () => {
const json = await this.registerCallbackOneTime(['response', 'type:chat'])
json[2].forEach(chat => {
chats.push(chat[1]) // chats data (log json to see what it looks like)
// store the number of unread messages for each sender
unreadMap[chat[1].jid] = chat[1].count
})
if (chats && chats.length > 0) return waitForConvos()
})
const waitForContacts = this.registerCallbackOneTime(['response', 'type:contacts']).then((json) => {
contacts = json[2].map((item) => item[1])
// if no add:before messages are sent, and you receive contacts
if (chats.length > 0) return waitForConvos()
}
const waitForContacts = async () => {
const json = await this.registerCallbackOneTime(['response', 'type:contacts'])
contacts = json[2].map(item => item[1])
receivedContacts = true
// if you receive contacts after messages
// should probably resolve the promise
if (!encounteredAddBefore && convoResolve) convoResolve()
})
if (receivedMessages) convoResolve()
}
// wait for the chats & contacts to load
return Promise.all([waitForChats, waitForContacts]).then(() => [chats, contacts, unreadMessages])
const promise = Promise.all([waitForChats(), waitForContacts()])
await Utils.promiseTimeout (timeoutMs, promise)
return [chats, contacts, unreadMessages] as [WAChat[], WAContact[], WAMessage[]]
}
private onMessageRecieved(message) {
if (message[0] === '!') {
@@ -170,6 +176,9 @@ export default class WAConnectionConnector extends WAConnectionValidator {
// if we recieved a message that was encrypted but we don't have the keys, then there must be an error
throw [3, 'recieved encrypted message when auth creds not available', message]
}
if (this.logLevel === MessageLogLevel.all) {
this.log(messageTag + ', ' + JSON.stringify(json))
}
/*
Check if this is a response to a message we sent
*/
@@ -214,7 +223,7 @@ export default class WAConnectionConnector extends WAConnectionValidator {
return
}
}
if (this.logUnhandledMessages) {
if (this.logLevel === MessageLogLevel.unhandled) {
this.log('[Unhandled] ' + messageTag + ', ' + JSON.stringify(json))
}
}

View File

@@ -1,6 +1,11 @@
import { WA } from '../Binary/Constants'
import { proto } from '../Binary/WAMessage'
export enum MessageLogLevel {
none=0,
unhandled=1,
all=2
}
export interface AuthenticationCredentials {
clientID: string
serverToken: string

View File

@@ -1,4 +1,4 @@
import assert from 'assert'
import * as assert from 'assert'
import WAConnection from './WAConnection'
import { AuthenticationCredentialsBase64 } from './Constants'
@@ -22,17 +22,16 @@ describe('Test Connect', () => {
it('should connect', async () => {
console.log('please be ready to scan with your phone')
const conn = new WAConnection()
await assert.doesNotReject(async () => conn.connectSlim(null), 'initial connection failed')
assert.ok(conn.userMetaData)
assert.ok(conn.userMetaData.id)
const user = await conn.connectSlim(null)
assert.ok(user)
assert.ok(user.id)
conn.close()
auth = conn.base64EncodedAuthInfo()
})
it('should reconnect', async () => {
const conn = new WAConnection()
const [user, chats, contacts, unread] = await conn.connect('./auth_info.json', 20 * 1000)
const [user, chats, contacts, unread] = await conn.connect(auth, 20*1000)
assert.ok(user)
assert.ok(user.id)
@@ -55,4 +54,4 @@ describe('Test Connect', () => {
await assert.rejects(async () => conn.connectSlim(auth), 'reconnect should have failed')
})
})
})

View File

@@ -1,5 +1,5 @@
import Crypto from 'crypto'
import HKDF from 'futoin-hkdf'
import * as Crypto from 'crypto'
import * as HKDF from 'futoin-hkdf'
/** decrypt AES 256 CBC; where the IV is prefixed to the buffer */
@@ -38,6 +38,7 @@ export function randomBytes(length) {
return Crypto.randomBytes(length)
}
export function promiseTimeout<T>(ms: number, promise: Promise<T>) {
if (!ms) { return promise }
// Create a promise that rejects in <ms> milliseconds
const timeout = new Promise((_, reject) => {
const id = setTimeout(() => {

15
package-lock.json generated
View File

@@ -140,6 +140,15 @@
"integrity": "sha512-7NQmHra/JILCd1QqpSzl8+mJRc8ZHz3uDm8YV1Ks9IhK0epEiTw8aIErbvH9PI+6XbqhyIQy3462nEsn7UVzjQ==",
"dev": true
},
"@types/ws": {
"version": "7.2.6",
"resolved": "https://registry.npmjs.org/@types/ws/-/ws-7.2.6.tgz",
"integrity": "sha512-Q07IrQUSNpr+cXU4E4LtkSIBPie5GLZyyMC1QtQYRLWz701+XcoVygGUZgvLqElq1nU4ICldMYPnexlBsg3dqQ==",
"dev": true,
"requires": {
"@types/node": "*"
}
},
"@typescript-eslint/eslint-plugin": {
"version": "3.5.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-3.5.0.tgz",
@@ -722,6 +731,12 @@
"esutils": "^2.0.2"
}
},
"dotenv": {
"version": "8.2.0",
"resolved": "https://registry.npmjs.org/dotenv/-/dotenv-8.2.0.tgz",
"integrity": "sha512-8sJ78ElpbDJBHNeBzUbUVLsqKdccaa/BXF1uPTw3GrvQTBgrQrtObr2mUrE38vzYd8cEv+m/JBfDLioYcfXoaw==",
"dev": true
},
"dynamic-dedupe": {
"version": "0.3.0",
"resolved": "https://registry.npmjs.org/dynamic-dedupe/-/dynamic-dedupe-0.3.0.tgz",

View File

@@ -20,7 +20,7 @@
"lint": "eslint '*/*.ts' --quiet --fix",
"tsc": "tsc",
"dev": "ts-node-dev --respawn --transpileOnly ./app/app.ts",
"prod": "tsc && node ./build/app.js"
"example": "npx ts-node Example/example.ts"
},
"author": "Adhiraj Singh",
"license": "MIT",
@@ -40,9 +40,11 @@
"@types/mocha": "^7.0.2",
"@types/node": "^14.0.14",
"@types/sharp": "^0.25.0",
"@types/ws": "^7.2.6",
"@typescript-eslint/eslint-plugin": "^3.5.0",
"@typescript-eslint/parser": "^3.5.0",
"assert": "^2.0.0",
"dotenv": "^8.2.0",
"eslint": "^7.3.1",
"eslint-config-prettier": "^6.11.0",
"eslint-plugin-prettier": "^3.1.4",