├── .gitignore ├── .DS_Store ├── .gitattributes ├── binary_coding ├── .DS_Store ├── binary_coding_tests.js ├── binary_encoder.js ├── def.proto └── whatsapp_message_coding.json ├── package.json ├── WhatsAppWeb.Utils.js ├── test.js ├── WhatsAppWeb.js ├── WhatsAppWeb.Send.js ├── README.md ├── WhatsAppWeb.Session.js └── WhatsAppWeb.Recv.js /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | auth_info.json 3 | test_pvt.js 4 | -------------------------------------------------------------------------------- /.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sorke/Baileys/HEAD/.DS_Store -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | # Auto detect text files and perform LF normalization 2 | * text=auto 3 | -------------------------------------------------------------------------------- /binary_coding/.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sorke/Baileys/HEAD/binary_coding/.DS_Store -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "baileys", 3 | "version": "1.0.0", 4 | "description": "Whatsapp Web API", 5 | "main": "WhatsAppWeb.js", 6 | "scripts": { 7 | "test": "node test.js" 8 | }, 9 | "author": "Adhiraj Singh", 10 | "license": "ISC", 11 | "dependencies": { 12 | "curve25519-js": "0.0.4", 13 | "futoin-hkdf": "^1.3.1", 14 | "protobufjs": "^6.8.9", 15 | "qrcode-terminal": "^0.12.0", 16 | "ws": "^7.2.3" 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /WhatsAppWeb.Utils.js: -------------------------------------------------------------------------------- 1 | const Crypto = require("crypto") 2 | 3 | /* 4 | Basic cryptographic utilities to interact with WhatsApp servers 5 | */ 6 | module.exports = { 7 | // decrypt AES 256 CBC; where the IV is prefixed to the buffer 8 | aesDecrypt: function (buffer, key) { 9 | const aes = Crypto.createDecipheriv('aes-256-cbc', key, buffer.slice(0,16) ) // first 16 bytes of buffer is IV 10 | return Buffer.concat( [ aes.update(buffer.slice(16, buffer.length)), aes.final() ] ) 11 | }, 12 | // encrypt AES 256 CBC; where the IV is prefixed to the buffer 13 | aesEncrypt: function (buffer, key) { 14 | const IV = this.randomBytes(16) 15 | const aes = Crypto.createCipheriv('aes-256-cbc', key, IV) 16 | return Buffer.concat( [ IV, aes.update(buffer), aes.final() ] ) // prefix IV to the buffer 17 | }, 18 | // sign HMAC using SHA 256 19 | hmacSign: function (buffer, key) { 20 | return Crypto.createHmac('sha256', key).update(buffer).digest() 21 | }, 22 | // generate a buffer with random bytes of the specified length 23 | randomBytes: function (length) { return Crypto.randomBytes(length) }, 24 | 25 | // whatsapp requires a message tag for every message, we just use the timestamp as one 26 | generateMessageTag: function () { return new Date().getTime().toString() }, 27 | // generate a random 16 byte client ID 28 | generateClientID: function () { return this.randomBytes(16).toString('base64') }, 29 | // generate a random 10 byte ID to attach to a message 30 | generateMessageID: function () { return this.randomBytes(10).toString('hex').toUpperCase() } 31 | } -------------------------------------------------------------------------------- /test.js: -------------------------------------------------------------------------------- 1 | const WhatsAppWeb = require("./WhatsAppWeb") 2 | const fs = require("fs") 3 | 4 | let client = new WhatsAppWeb() // instantiate 5 | try { 6 | const file = fs.readFileSync("auth_info.json") // load a closed session back if it exists 7 | const authInfo = JSON.parse(file) 8 | client.login( authInfo ) // log back in using the info we just loaded 9 | } catch { 10 | // if no auth info exists, start a new session 11 | client.connect() // start a new session, with QR code scanning and what not 12 | } 13 | // called once the client connects successfully to the WhatsApp servers 14 | client.handlers.onConnected = () => { 15 | const authInfo = client.base64EncodedAuthInfo() // get all the auth info we need to restore this session 16 | fs.writeFileSync("auth_info.json", JSON.stringify(authInfo, null, "\t")) // save this info to a file 17 | /* 18 | Note: one can take this file and login again from any computer without having to scan the QR code, and get full access to one's WhatsApp 19 | Despite the convenience, be careful with this file 20 | */ 21 | } 22 | // called when you have a pending unread message or recieve a new message 23 | client.handlers.onUnreadMessage = (m) => { 24 | console.log("recieved message: " + JSON.stringify(m)) // log and see what the message looks like 25 | 26 | /* send a message after at least a 1 second timeout after recieving a message, otherwise WhatsApp will reject the message otherwise */ 27 | setTimeout(() => client.sendReadReceipt(m.key.remoteJid, m.key.id), 2*1000) // send a read reciept for the message in 2 seconds 28 | setTimeout(() => client.updatePresence(m.key.remoteJid, WhatsAppWeb.Presence.composing), 2.5*1000) // let them know you're typing in 2.5 seconds 29 | setTimeout(() => client.sendTextMessage(m.key.remoteJid, "hello!"), 4*1000) // send the actual message after 4 seconds 30 | } 31 | // called if an error occurs 32 | client.handlers.onError = (err) => console.log(err) 33 | client.handlers.onDisconnect = () => { /* internet got disconnected, save chats here or whatever; will reconnect automatically */ } 34 | 35 | 36 | const readline = require('readline').createInterface({ 37 | input: process.stdin, 38 | output: process.stdout 39 | }) 40 | readline.question("type exit to disconnect\n", (txt) => { 41 | if (txt === "exit") { 42 | client.close() 43 | process.exit(0) 44 | } 45 | }) -------------------------------------------------------------------------------- /binary_coding/binary_coding_tests.js: -------------------------------------------------------------------------------- 1 | const assert = require('assert').strict 2 | const BinaryCoding = require("./binary_encoder.js") 3 | 4 | const testingPairs = [ 5 | [ 6 | "f806092f5a0a10f804f80234fc6c0a350a1b39313735323938373131313740732e77686174736170702e6e657410011a143345423030393637354537454433374141424632122b0a292a7069616e6f20726f6f6d2074696d696e6773206172653a2a0a20363a3030414d2d31323a3030414d18b3faa7f3052003f80234fc4c0a410a1b39313735323938373131313740732e77686174736170702e6e657410001a20304643454335333330463634393239433645394132434646443242433845414418bdfaa7f305c00101f80234fc930a350a1b39313735323938373131313740732e77686174736170702e6e657410011a14334542303033433742353339414644303937353312520a50536f727279206672656e2c204920636f756c646e277420756e6465727374616e6420274c69627261272e2054797065202768656c702720746f206b6e6f77207768617420616c6c20492063616e20646f18c1faa7f3052003f80234fc540a410a1b39313735323938373131313740732e77686174736170702e6e657410001a20413132333042384436423041314437393345433241453245413043313638443812090a076c69627261727918c2faa7f305", 7 | ["action",{"last":"true","add":"before"},[["message",null,{"key":{"remoteJid":"917529871117@s.whatsapp.net","fromMe":true,"id":"3EB009675E7ED37AABF2"},"message":{"conversation":"*piano room timings are:*\n 6:00AM-12:00AM"},"messageTimestamp":"1584004403","status":"DELIVERY_ACK"}],["message",null,{"key":{"remoteJid":"917529871117@s.whatsapp.net","fromMe":false,"id":"0FCEC5330F64929C6E9A2CFFD2BC8EAD"},"messageTimestamp":"1584004413","messageStubType":"REVOKE"}],["message",null,{"key":{"remoteJid":"917529871117@s.whatsapp.net","fromMe":true,"id":"3EB003C7B539AFD09753"},"message":{"conversation":"Sorry fren, I couldn't understand 'Libra'. Type 'help' to know what all I can do"},"messageTimestamp":"1584004417","status":"DELIVERY_ACK"}],["message",null,{"key":{"remoteJid":"917529871117@s.whatsapp.net","fromMe":false,"id":"A1230B8D6B0A1D793EC2AE2EA0C168D8"},"message":{"conversation":"library"},"messageTimestamp":"1584004418"}]]] 8 | ] 9 | ] 10 | function testCoding () { 11 | const encoder = new BinaryCoding.Encoder() 12 | const decoder = new BinaryCoding.Decoder() 13 | 14 | testingPairs.forEach(pair => { 15 | const buff = Buffer.from(pair[0], 'hex') 16 | const decoded = decoder.read(buff) 17 | 18 | assert.deepEqual( JSON.stringify( decoded ), JSON.stringify( pair[1] )) 19 | 20 | const encoded = encoder.write(decoded) 21 | assert.deepEqual(encoded, buff) 22 | }) 23 | console.log("all coding tests passed") 24 | } 25 | testCoding() 26 | -------------------------------------------------------------------------------- /WhatsAppWeb.js: -------------------------------------------------------------------------------- 1 | const BinaryCoding = require('./binary_coding/binary_encoder.js') 2 | 3 | class WhatsAppWeb { 4 | 5 | static version = [0,4,1296] // the version of WhatsApp Web we're telling the servers we are 6 | static browserDescriptions = ["Baileys", "Baileys"] 7 | 8 | static Status = { 9 | notConnected: 0, 10 | connecting: 1, 11 | creatingNewConnection: 3, 12 | loggingIn: 4, 13 | connected: 5 14 | } 15 | 16 | // set of statuses visible to other people; see updatePresence() in WhatsAppWeb.Send 17 | static Presence = { 18 | available: "available", // "online" 19 | unavailable: "unavailable", // offline 20 | composing: "composing", // "typing..." 21 | recording: "recording", // "recording..." 22 | paused: "paused" // I have no clue 23 | } 24 | 25 | constructor() { 26 | this.conn = null // the websocket connection 27 | 28 | this.authInfo = null // the auth info used to extablish new connections & restore connections 29 | 30 | this.userMetaData = null // metadata of the user i.e. name, phone number, phone stats 31 | this.chats = {} // all chats of the user, mapped by the user ID 32 | this.handlers = {} // data for the event handlers 33 | this.msgCount = 0 // number of messages sent to the server; required field for sending messages etc. 34 | this.autoReconnect = true // reconnect automatically after an unexpected disconnect 35 | this.lastSeen = null // updated by sending a keep alive request to the server, and the server responds with our updated last seen 36 | 37 | this.queryCallbacks = [] 38 | 39 | this.encoder = new BinaryCoding.Encoder() 40 | this.decoder = new BinaryCoding.Decoder() 41 | 42 | this.status = WhatsAppWeb.Status.notConnected 43 | } 44 | // error is a json array: [errorCode, "error description", optionalDescription] 45 | gotError (error) { 46 | this.handlers.onError(error) // tell the handler, we got an error 47 | } 48 | // called when established a connection to the WhatsApp servers successfully 49 | didConnectSuccessfully () { 50 | console.log("connected successfully") 51 | 52 | this.status = WhatsAppWeb.Status.connected // update our status 53 | this.lastSeen = new Date() // set last seen to right now 54 | this.startKeepAliveRequest() // start sending keep alive requests (keeps the WebSocket alive & updates our last seen) 55 | 56 | if (this.reconnectLoop) { // if we connected after being disconnected 57 | clearInterval(this.reconnectLoop) // kill the loop to reconnect us 58 | } else { // if we connected for the first time, i.e. not after being disconnected 59 | if (this.handlers.onConnected) // tell the handler that we're connected 60 | this.handlers.onConnected() 61 | } 62 | } 63 | // base 64 encode the authentication credentials and return them, these can then be saved used to login again 64 | // see login () in WhatsAppWeb.Session 65 | base64EncodedAuthInfo () { 66 | return { 67 | clientID: this.authInfo.clientID, 68 | serverToken: this.authInfo.serverToken, 69 | clientToken: this.authInfo.clientToken, 70 | encKey: this.authInfo.encKey.toString('base64'), 71 | macKey: this.authInfo.macKey.toString('base64') 72 | } 73 | } 74 | } 75 | 76 | /* import the rest of the code */ 77 | require("./WhatsAppWeb.Session.js")(WhatsAppWeb) 78 | require("./WhatsAppWeb.Recv.js")(WhatsAppWeb) 79 | require("./WhatsAppWeb.Send.js")(WhatsAppWeb) 80 | 81 | module.exports = WhatsAppWeb -------------------------------------------------------------------------------- /WhatsAppWeb.Send.js: -------------------------------------------------------------------------------- 1 | const Utils = require("./WhatsAppWeb.Utils") 2 | 3 | /* 4 | Contains the code for sending stuff to WhatsApp 5 | */ 6 | module.exports = function(WhatsAppWeb) { 7 | 8 | // send a read receipt to the given ID on a certain message 9 | WhatsAppWeb.prototype.sendReadReceipt = function (jid, messageID) { 10 | const json = [ 11 | "action", 12 | { epoch: this.msgCount.toString(), type: "set" }, 13 | [ ["read", {count: "1", index: messageID, jid: jid, owner: "false"}, null] ] 14 | ] 15 | this.sendBinary(json, [10, 128]) // encrypt and send off 16 | 17 | if (this.chats[ jid ]) { 18 | this.chats[jid].user.count = 0 // reset read count 19 | } 20 | } 21 | // check if given number is registered on WhatsApp 22 | WhatsAppWeb.prototype.isOnWhatsApp = function (jid, callback) { 23 | const json = [ 24 | "query", 25 | "exist", 26 | jid 27 | ] 28 | this.sendJSON(json) // send 29 | 30 | this.queryCallbacks.push({queryJSON: json, callback: callback}) 31 | } 32 | // tell someone about your presence -- online, typing, offline etc. 33 | WhatsAppWeb.prototype.updatePresence = function (jid, type) { 34 | const json = [ 35 | "action", 36 | { epoch: this.msgCount.toString(), type: "set" }, 37 | [ ["presence", {type: type, to: jid}, null] ] 38 | ] 39 | this.sendBinary(json, [10, 128]) 40 | } 41 | // send a text message to someone, optionally you can provide the time at which you want the message to be sent 42 | WhatsAppWeb.prototype.sendTextMessage = function (id, txt, timestamp=null) { 43 | const message = {conversation: txt} 44 | return this.sendMessage(id, message, timestamp) 45 | } 46 | // generic send message construct 47 | WhatsAppWeb.prototype.sendMessage = function (id, message, timestamp=null) { 48 | if (!timestamp) { // if no timestamp was provided, 49 | timestamp = new Date() // set timestamp to now 50 | } 51 | timestamp = timestamp.getTime()/1000 52 | 53 | const messageJSON = { 54 | key: { 55 | remoteJid: id, 56 | fromMe: true, 57 | id: Utils.generateMessageID() 58 | }, 59 | message: message, 60 | messageTimestamp: timestamp, 61 | status: "ERROR" 62 | } 63 | 64 | const json = [ 65 | "action", 66 | {epoch: this.msgCount.toString(), type: "relay" }, 67 | [ ['message', null, messageJSON] ] 68 | ] 69 | return this.sendBinary(json, [16, 128]) 70 | } 71 | // send a binary message, the tags parameter tell WhatsApp what the message is all about 72 | WhatsAppWeb.prototype.sendBinary = function (json, tags) { 73 | const binary = this.encoder.write(json) // encode the JSON to the WhatsApp binary format 74 | 75 | var buff = Utils.aesEncrypt(binary, this.authInfo.encKey) // encrypt it using AES and our encKey 76 | const sign = Utils.hmacSign(buff, this.authInfo.macKey) // sign the message using HMAC and our macKey 77 | 78 | buff = Buffer.concat([ 79 | Buffer.from( Utils.generateMessageTag() + "," ), // generate & prefix the message tag 80 | Buffer.from(tags), // prefix some bytes that tell whatsapp what the message is about 81 | sign, // the HMAC sign of the message 82 | buff // the actual encrypted buffer 83 | ]) 84 | this.send(buff) // send it off 85 | } 86 | // send a JSON message to WhatsApp servers 87 | WhatsAppWeb.prototype.sendJSON = function (json) { 88 | const str = JSON.stringify(json) 89 | this.send( Utils.generateMessageTag() + "," + str ) 90 | } 91 | WhatsAppWeb.prototype.send = function (m) { 92 | this.msgCount += 1 // increment message count, it makes the 'epoch' field when sending binary messages 93 | this.conn.send( m ) 94 | } 95 | 96 | } -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Baileys 2 | Reverse Engineered WhatsApp Web API in Node.js. Baileys does not require Selenium or any other browser to be interface with WhatsApp Web, it does so directly using WebSockets. 3 | 4 | Thank you to [Sigalor](https://github.com/sigalor/whatsapp-web-reveng) for writing the guide reverse engineering WhatsApp Web and to the go reimplementation written by [Rhymen](https://github.com/Rhymen/go-whatsapp/tree/484cfe758705761d76724e01839d6fc473dc10c4) 5 | 6 | Baileys is super easy to use: 7 | 1. Install from npm using 8 | ``` npm install github:adiwajshing/Baileys ``` 9 | 2. Then import in your code using 10 | ``` javascript 11 | const WhatsAppWeb = require('Baileys') 12 | ``` 13 | 3. Create an instance of Baileys & connect using 14 | ``` javascript 15 | let client = new WhatsAppWeb() 16 | client.connect() 17 | ``` 18 | If the connection is successful, you will see a QR code printed on your terminal screen, scan it with WhatsApp on your phone and you'll be logged in! 19 | 4. Implement the following event handlers in your code: 20 | ``` javascript 21 | client.handlers.onConnected = () => { /* when you're successfully authenticated with the WhatsApp Web servers */ } 22 | ``` 23 | ``` javascript 24 | client.handlers.onUnreadMessage = (message) => { /* called when you have a pending unread message or recieve a new message */ } 25 | ``` 26 | ``` javascript 27 | client.handlers.onError = (error) => { /* called when there was an error */ } 28 | ``` 29 | ``` javascript 30 | client.handlers.onDisconnect = () => { /* called when internet gets disconnected */ } 31 | ``` 32 | 5. Send a text message using 33 | ``` javascript 34 | client.sendTextMessage(id, txtMessage) 35 | ``` 36 | The id is the phone number of the person the message is being sent to, it must be in the format '[country code][phone number]@s.whatsapp.net', for example '+19999999999@s.whatsapp.net' 37 | 6. Send a read reciept using 38 | ``` javascript 39 | client.sendReadReceipt(id, messageID) 40 | ``` 41 | The id is in the same format as mentioned earlier. The message ID is the unique identifier of the message that you are marking as read 42 | 7. Tell someone what your status is right now by using 43 | ``` javascript 44 | client.updatePresence(id, presence) 45 | ``` 46 | Presence can be one of the following: 47 | ``` javascript 48 | static Presence = { 49 | available: "available", // "online" 50 | unavailable: "unavailable", // offline 51 | composing: "composing", // "typing..." 52 | recording: "recording", // "recording..." 53 | paused: "paused" // I have no clue 54 | } 55 | ``` 56 | 8. Once you want to close your session, you can get your authentication credentials using: 57 | ``` javascript 58 | const authJSON = client.base64EncodedAuthInfo() 59 | ``` 60 | and then save this JSON to a file 61 | 9. If you want to restore your session (i.e. log back in without having to scan the QR code), simply retreive your previously saved credentials and use 62 | ``` javascript 63 | const authJSON = JSON.parse( fs.readFileSync("auth_info.json") ) 64 | client.login( authJSON ) 65 | ``` 66 | This will use the credentials to connect & log back in. No need to call ``` connect() ``` after calling this function 67 | 10. If you want to query whether a number is registered on WhatsApp, use: 68 | ``` javascript 69 | client.isOnWhatsApp ("[countrycode][some10digitnumber]@s.whatsapp.net", (exists, id) => { 70 | if (exists) { 71 | console.log(id + " is on WhatsApp") 72 | } else { 73 | console.log(id + " is not on WhatsApp :(") 74 | } 75 | }) 76 | ``` 77 | Of course, replace ``` [countrycode][some10digitnumber] ``` with an actual country code & number 78 | 79 | 80 | Do check out test.js to see example usage of all these functions. 81 | 82 | # Note 83 | I am in no way affiliated with WhatsApp. This was written for educational purposes. Use at your own discretion. -------------------------------------------------------------------------------- /WhatsAppWeb.Session.js: -------------------------------------------------------------------------------- 1 | const WebSocket = require('ws') 2 | const Curve = require ('curve25519-js') 3 | const HKDF = require('futoin-hkdf') 4 | const Utils = require('./WhatsAppWeb.Utils') 5 | const QR = require('qrcode-terminal') 6 | 7 | /* 8 | Contains the code for connecting to WhatsApp Web, establishing a new session & logging back in 9 | */ 10 | module.exports = function (WhatsAppWeb) { 11 | const Status = WhatsAppWeb.Status 12 | 13 | // connect to the WhatsApp Web servers 14 | WhatsAppWeb.prototype.connect = function () { 15 | if (this.status != Status.notConnected) { 16 | return this.gotError([1, "already connected or connecting"]) 17 | } 18 | 19 | this.status = Status.connecting 20 | 21 | this.conn = new WebSocket("wss://web.whatsapp.com/ws", {origin: "https://web.whatsapp.com"}) 22 | 23 | this.conn.on('open', () => this.onConnect()) 24 | this.conn.on('message', (m) => this.onMessageRecieved(m)) // in WhatsAppWeb.Recv.js 25 | this.conn.on('error', (error) => { // if there was an error in the WebSocket 26 | this.close() 27 | this.gotError([20, error]) 28 | }) 29 | this.conn.on('close', () => { }) 30 | } 31 | // once a connection has been successfully established 32 | WhatsAppWeb.prototype.onConnect = function () { 33 | console.log("connected to WhatsApp Web") 34 | 35 | this.status = Status.creatingNewConnection 36 | if (!this.authInfo) { // if no auth info is present, that is, a new session has to be established 37 | this.authInfo = { clientID: Utils.generateClientID() } // generate a client ID 38 | } 39 | 40 | const data = ["admin", "init", WhatsAppWeb.version, WhatsAppWeb.browserDescriptions, this.authInfo.clientID, true] 41 | 42 | this.sendJSON( data ) 43 | } 44 | // restore a previously closed session using the given authentication information 45 | WhatsAppWeb.prototype.login = function (authInfo) { 46 | this.authInfo = { 47 | clientID: authInfo.clientID, 48 | serverToken: authInfo.serverToken, 49 | clientToken: authInfo.clientToken, 50 | encKey: Buffer.from( authInfo.encKey, 'base64' ), 51 | macKey: Buffer.from( authInfo.macKey, 'base64' ) 52 | } 53 | 54 | this.connect() 55 | } 56 | // once the QR code is scanned and we can validate our connection, 57 | // or we resolved the challenge when logging back in 58 | WhatsAppWeb.prototype.validateNewConnection = function (json) { 59 | if (json.connected) { // only if we're connected 60 | if (!json.secret) { // if we didn't get a secret, that is we don't need it 61 | return this.didConnectSuccessfully() 62 | } 63 | const secret = Buffer.from(json.secret, 'base64') 64 | 65 | if (secret.length !== 144) { 66 | return this.gotError([4, "incorrect secret length: " + secret.length]) 67 | } 68 | // generate shared key from our private key & the secret shared by the server 69 | const sharedKey = Curve.sharedKey( this.curveKeys.private, secret.slice(0, 32) ) 70 | // expand the key to 80 bytes using HKDF 71 | const expandedKey = HKDF(sharedKey, 80, [ Buffer.alloc(32), '', 'SHA-256' ]) 72 | 73 | // perform HMAC validation. 74 | const hmacValidationKey = expandedKey.slice(32, 64) 75 | const hmacValidationMessage = Buffer.concat( [ secret.slice(0, 32), secret.slice(64, secret.length) ] ) 76 | 77 | const hmac = Utils.hmacSign(hmacValidationMessage, hmacValidationKey) 78 | 79 | if ( hmac.equals(secret.slice(32, 64)) ) { // computed HMAC should equal secret[32:64] 80 | // expandedKey[64:] + secret[64:] are the keys, encrypted using AES, that are used to encrypt/decrypt the messages recieved from WhatsApp 81 | // they are encrypted using key: expandedKey[0:32] 82 | const encryptedAESKeys = Buffer.concat([ expandedKey.slice(64, expandedKey.length), secret.slice(64, secret.length) ]) 83 | const decryptedKeys = Utils.aesDecrypt(encryptedAESKeys, expandedKey.slice(0,32)) 84 | 85 | // this data is required to restore closed sessions 86 | this.authInfo = { 87 | encKey: decryptedKeys.slice(0, 32), // first 32 bytes form the key to encrypt/decrypt messages 88 | macKey: decryptedKeys.slice(32, 64), // last 32 bytes from the key to sign messages 89 | clientToken: json.clientToken, 90 | serverToken: json.serverToken, 91 | clientID: this.authInfo.clientID 92 | } 93 | this.userMetaData = { 94 | id: json.wid, // one's WhatsApp ID [cc][number]@s.whatsapp.net 95 | name: json.pushname, // name set on whatsapp 96 | phone: json.phone // information about the phone one has logged in to 97 | } 98 | this.status = Status.CONNECTED 99 | 100 | this.didConnectSuccessfully() 101 | } else { // if the checksums didn't match 102 | this.close() 103 | this.gotError([5, "HMAC validation failed"]) 104 | } 105 | } else { // if we didn't get the connected field (usually we get this message when one opens WhatsApp on their phone) 106 | if (this.status !== Status.connected) { // and we're not already connected 107 | this.close() 108 | this.gotError([6, "json connection failed", json]) 109 | } 110 | } 111 | } 112 | /* 113 | when logging back in (restoring a previously closed session), WhatsApp may challenge one to check if one still has the encryption keys 114 | WhatsApp does that by asking for us to sign a string it sends with our macKey 115 | */ 116 | WhatsAppWeb.prototype.respondToChallenge = function (challenge) { 117 | const bytes = Buffer.from(challenge, 'base64') // decode the base64 encoded challenge string 118 | const signed = Utils.hmacSign(bytes, this.authInfo.macKey).toString('base64') // sign the challenge string with our macKey 119 | const data = ["admin", "challenge", signed, this.authInfo.serverToken, this.authInfo.clientID] // prepare to send this signed string with the serverToken & clientID 120 | 121 | console.log( "resolving challenge" ) 122 | 123 | this.sendJSON( data ) 124 | } 125 | /* 126 | when starting a new session, generate a QR code by generating a private/public key pair & the keys the server sends 127 | */ 128 | WhatsAppWeb.prototype.generateKeysForAuth = function (ref) { 129 | this.curveKeys = Curve.generateKeyPair( Utils.randomBytes(32) ) 130 | 131 | const publicKeyStr = Buffer.from(this.curveKeys.public).toString('base64') 132 | //console.log ("private key: " + Buffer.from(this.curveKeys.private) ) 133 | 134 | let str = ref + "," + publicKeyStr + "," + this.authInfo.clientID 135 | 136 | console.log("authenticating... Converting to QR: " + str) 137 | 138 | QR.generate(str, {small: true}) 139 | } 140 | // send a keep alive request every 25 seconds, server updates & responds with last seen 141 | WhatsAppWeb.prototype.startKeepAliveRequest = function () { 142 | this.keepAliveReq = setInterval(() => { 143 | const diff = (new Date().getTime()-this.lastSeen.getTime())/1000 144 | /* 145 | check if it's been a suspicious amount of time since the server responded with our last seen 146 | could be that the network is down, or the phone got disconnected or unpaired 147 | */ 148 | if (diff > 25+10) { 149 | console.log("disconnected") 150 | 151 | this.close() 152 | if (this.handlers.onDisconnect) 153 | this.handlers.onDisconnect() 154 | 155 | if (this.autoReconnect) { // attempt reconnecting if the user wants us to 156 | // keep trying to connect 157 | this.reconnectLoop = setInterval( () => { 158 | // only connect if we're not already in the prcoess of connectin 159 | if (this.status === Status.notConnected) { 160 | this.connect() 161 | } 162 | }, 10 * 1000) 163 | } 164 | } else { // if its all good, send a keep alive request 165 | this.send( "?,," ) 166 | } 167 | }, 25 * 1000) 168 | } 169 | // disconnect from the phone. Your auth credentials become invalid after sending a disconnect request. 170 | // use close() if you just want to close the connection 171 | WhatsAppWeb.prototype.disconnect = function () { 172 | if (this.status === Status.connected) { 173 | this.conn.send('goodbye,["admin","Conn","disconnect"]', null, () => { 174 | this.conn.close() 175 | if (this.handlers.onDisconnect) 176 | this.handlers.onDisconnect() 177 | }) 178 | } else if (this.conn) { 179 | this.close() 180 | } 181 | } 182 | // close the connection 183 | WhatsAppWeb.prototype.close = function () { 184 | this.conn.close() 185 | this.conn = null 186 | this.status = Status.notConnected 187 | this.msgCount = 0 188 | this.chats = {} 189 | 190 | if (this.keepAliveReq) { 191 | clearInterval(this.keepAliveReq) 192 | } 193 | } 194 | // request a new QR code from the server (HAVEN'T TESTED THIS OUT YET) 195 | WhatsAppWeb.prototype.requestNewQRCode = function () { 196 | if (this.status !== Status.creatingNewConnection) { // if we're not in the process of connecting 197 | return 198 | } 199 | const json = ["admin", "Conn", "reref"] 200 | this.sendJSON(json) 201 | } 202 | 203 | } -------------------------------------------------------------------------------- /WhatsAppWeb.Recv.js: -------------------------------------------------------------------------------- 1 | const Utils = require("./WhatsAppWeb.Utils") 2 | /* 3 | Contains the code for recieving messages and forwarding what to do with them to the correct functions 4 | */ 5 | module.exports = function(WhatsAppWeb) { 6 | 7 | const Status = WhatsAppWeb.Status 8 | 9 | WhatsAppWeb.prototype.onMessageRecieved = function (message) { 10 | 11 | if (message[0] === "!") { // when the first character in the message is an '!', the server is updating on the last seen 12 | const timestamp = message.slice(1,message.length) 13 | this.lastSeen = new Date( parseInt(timestamp) ) 14 | } else { 15 | const commaIndex = message.indexOf(",") // all whatsapp messages have a tag and a comma, followed by the actual message 16 | 17 | if (commaIndex < 0) { // if there was no comma, then this message must be not be valid 18 | return this.gotError([2, "invalid message", message]) 19 | } 20 | 21 | var data = message.slice(commaIndex+1, message.length) 22 | if (data.length === 0) { 23 | // got an empty message, usually get one after sending a message or something, just return then 24 | return 25 | } 26 | 27 | let json 28 | if (data[0] === "[" || data[0] === "{") { // if the first character is a "[", then the data must just be plain JSON array or object 29 | json = JSON.parse( data ) // simply parse the JSON 30 | console.log("JSON: " + data) 31 | } else if (this.status === Status.connected) { 32 | /* 33 | If the data recieved was not a JSON, then it must be an encrypted message. 34 | Such a message can only be decrypted if we're connected successfully to the servers & have encryption keys 35 | */ 36 | 37 | data = Buffer.from(data, 'binary') // convert the string to a buffer 38 | const checksum = data.slice(0, 32) // the first 32 bytes of the buffer are the HMAC sign of the message 39 | data = data.slice(32, data.length) // the actual message 40 | 41 | const computedChecksum = Utils.hmacSign(data, this.authInfo.macKey) // compute the sign of the message we recieved using our macKey 42 | 43 | if (checksum.equals(computedChecksum)) { // the checksum the server sent, must match the one we computed for the message to be valid 44 | const decrypted = Utils.aesDecrypt(data, this.authInfo.encKey) // decrypt using AES 45 | json = this.decoder.read( decrypted ) // decode the binary message into a JSON array 46 | } else { 47 | return this.gotError([7, "checksums don't match"]) 48 | } 49 | //console.log("enc_json: " + JSON.stringify(json)) 50 | } else { 51 | // if we recieved a message that was encrypted but we weren't conencted, then there must be an error 52 | return this.gotError([3, "recieved encrypted message when not connected: " + this.status, message]) 53 | } 54 | 55 | //console.log(json) 56 | // the first item in the recieved JSON, if it exists, it tells us what the message is about 57 | switch (json[0]) { 58 | case "Conn": 59 | /* 60 | we get this message when a new connection is established, 61 | whether we're starting a new session or are logging back in. 62 | Sometimes, we also recieve it when one opens their phone 63 | */ 64 | this.validateNewConnection(json[1]) 65 | return 66 | case "Cmd": 67 | /* 68 | WhatsApp usually sends this when we're trying to restore a closed session, 69 | WhatsApp will challenge us to see whether we still have the keys 70 | */ 71 | if (json[1].type === "challenge") { // if it really is a challenge 72 | this.respondToChallenge(json[1].challenge) 73 | } 74 | return 75 | case "action": 76 | /* 77 | this is when some action was taken on a chat or that we recieve a message. 78 | json[1] tells us more about the message, it can be null 79 | */ 80 | if (!json[1]) { // if json[1] is null 81 | json = json[2][0] // set json to the first element in json[2]; it contains the relevant part 82 | 83 | if (json[0] === "read") { // if one marked a chat as read or unread on the phone 84 | const id = json[1].jid.replace("@c.us", "@s.whatsapp.net") // format the sender's ID 85 | if (this.chats[id] && json[1].type === 'false') { // if it was marked unread 86 | this.chats[id].user.count = 1 // up the read count 87 | this.clearUnreadMessages(id) // send notification to the handler about the unread message 88 | } else { // if it was marked read 89 | this.chats[id].user.count = 0 // set the read count to zero 90 | } 91 | } 92 | 93 | } else if (json[1].add === "relay") { // if we just recieved a new message sent to us 94 | this.onNewMessage( json[2][0][2] ) // handle this new message 95 | } else if (json[1].add === "before" || json[1].add === "last") { 96 | /* 97 | if we're recieving a full chat log 98 | if json[1].add equals before: if its non-recent messages 99 | if json[1].add equals last: contains the last message of the conversation between the sender and us 100 | */ 101 | 102 | json = json[2] // json[2] is the relevant part 103 | /* reverse for loop, because messages are sent ordered by most recent 104 | I can order them by recency if I add them in reverse order */ 105 | for (var k = json.length-1;k >= 0;k--) { 106 | const message = json[k] 107 | const id = message[2].key.remoteJid 108 | if (!this.chats[ id ]) { // if we haven't added this ID before, add them now 109 | this.chats[ id ] = {user: { jid: id, count: 0 }, messages: []} 110 | } 111 | 112 | this.chats[id].messages.push(message[2]) // append this message to the array 113 | } 114 | 115 | const id = json[0][2].key.remoteJid // get the ID whose chats we just processed 116 | this.clearUnreadMessages(id) // forward to the handler any any unread messages 117 | } 118 | return 119 | case "response": 120 | // if it is the list of all the people the WhatsApp account has chats with 121 | if (json[1].type === "chat") { 122 | json[2].forEach (chat => { 123 | if (chat[0] === "chat" && chat[1].jid) { 124 | const jid = chat[1].jid.replace("@c.us", "@s.whatsapp.net") // format ID 125 | this.chats[ jid ] = { 126 | user: { 127 | jid: jid, // the ID of the person 128 | count: chat[1].count}, // number of unread messages we have from them 129 | messages: [ ] // empty messages, is filled by content in the previous section 130 | } 131 | } 132 | }) 133 | 134 | } 135 | return 136 | default: 137 | break 138 | } 139 | 140 | /* 141 | if the recieved JSON wasn't an array, then we must have recieved a status for a request we made 142 | this would include creating new sessions, logging in & queries 143 | */ 144 | // if we're connected and we had a pending query 145 | if (this.status === Status.connected) { 146 | if (json.status && this.queryCallbacks.length > 0) { 147 | for (var i in this.queryCallbacks) { 148 | if (this.queryCallbacks[i].queryJSON[1] === "exist") { 149 | this.queryCallbacks[i].callback(json.status == 200, this.queryCallbacks[i].queryJSON[2]) 150 | this.queryCallbacks.splice(i, 1) 151 | break 152 | } 153 | } 154 | } 155 | } else { 156 | // if we're trying to establish a new connection or are trying to log in 157 | switch (json.status) { 158 | case 200: // all good and we can procede to generate a QR code for new connection, or can now login given present auth info 159 | 160 | if (this.status === Status.creatingNewConnection){ // if we're trying to start a connection 161 | if (this.authInfo.encKey && this.authInfo.macKey) { // if we have the info to restore a closed session 162 | this.status = Status.loggingIn 163 | // create the login request 164 | const data = ["admin", "login", this.authInfo.clientToken, this.authInfo.serverToken, this.authInfo.clientID, "takeover"] 165 | this.sendJSON( data ) 166 | } else { 167 | this.generateKeysForAuth(json.ref) 168 | } 169 | } else if (this.queryCallbacks.length > 0) { 170 | for (var i in this.queryCallbacks) { 171 | if (this.queryCallbacks[i].queryJSON[1] == "query") { 172 | this.queryCallbacks[i].callback( ) 173 | } 174 | } 175 | } 176 | 177 | break 178 | case 401: // if the phone was unpaired 179 | this.close() 180 | return this.gotError([json.status, "unpaired from phone", message]) 181 | case 429: // request to login was denied, don't know why it happens 182 | this.close() 183 | return this.gotError([ json.status, "request denied, try reconnecting", message ]) 184 | case 304: // request to generate a new key for a QR code was denied 185 | console.log("reuse previous ref") 186 | return this.gotError([ json.status, "request for new key denied", message ]) 187 | default: 188 | break 189 | } 190 | } 191 | } 192 | } 193 | // shoot off notifications to the handler that new unread message are available 194 | WhatsAppWeb.prototype.clearUnreadMessages = function (id) { 195 | const chat = this.chats[id] // get the chat 196 | var j = 0 197 | let unreadMessages = chat.user.count 198 | while (unreadMessages > 0) { 199 | if (!chat.messages[j].key.fromMe) { // only forward if the message is from the sender 200 | this.handlers.onUnreadMessage( chat.messages[j] ) // send off the unread message 201 | unreadMessages -= 1 // reduce 202 | } 203 | j += 1 204 | } 205 | } 206 | // when a new message is recieved 207 | WhatsAppWeb.prototype.onNewMessage = function (message) { 208 | 209 | if (message && message.message) { // confirm that the message really is valid 210 | if (!this.chats[message.key.remoteJid]) { // if we don't have any chats from this ID before, add them to our DB 211 | this.chats[message.key.remoteJid] = { 212 | user: { jid: message.key.remoteJid, count: 0 }, 213 | messages: [ message ] 214 | } 215 | } else { 216 | // if the chat was already there, then insert the message at the front of the array 217 | this.chats[ message.key.remoteJid ].messages.splice(0, 0, message) 218 | } 219 | 220 | if (!message.key.fromMe) { // if this message was sent to us, notify the handler 221 | this.handlers.onUnreadMessage ( message ) 222 | } 223 | } 224 | } 225 | 226 | } -------------------------------------------------------------------------------- /binary_coding/binary_encoder.js: -------------------------------------------------------------------------------- 1 | const ProtoBuf = require("protobufjs") 2 | 3 | const WATags = { 4 | LIST_EMPTY: 0, 5 | STREAM_END: 2, 6 | DICTIONARY_0: 236, 7 | DICTIONARY_1: 237, 8 | DICTIONARY_2: 238, 9 | DICTIONARY_3: 239, 10 | LIST_8 : 248, 11 | LIST_16 : 249, 12 | JID_PAIR : 250, 13 | HEX_8 : 251, 14 | BINARY_8 : 252, 15 | BINARY_20 : 253, 16 | BINARY_32 : 254, 17 | NIBBLE_8 : 255, 18 | SINGLE_BYTE_MAX: 256, 19 | PACKED_MAX: 254 20 | } 21 | const WADoubleByteTokens = [] 22 | 23 | const WASingleByteTokens = [ 24 | null,null,null,"200","400","404","500","501","502","action","add", 25 | "after","archive","author","available","battery","before","body", 26 | "broadcast","chat","clear","code","composing","contacts","count", 27 | "create","debug","delete","demote","duplicate","encoding","error", 28 | "false","filehash","from","g.us","group","groups_v2","height","id", 29 | "image","in","index","invis","item","jid","kind","last","leave", 30 | "live","log","media","message","mimetype","missing","modify","name", 31 | "notification","notify","out","owner","participant","paused", 32 | "picture","played","presence","preview","promote","query","raw", 33 | "read","receipt","received","recipient","recording","relay", 34 | "remove","response","resume","retry","s.whatsapp.net","seconds", 35 | "set","size","status","subject","subscribe","t","text","to","true", 36 | "type","unarchive","unavailable","url","user","value","web","width", 37 | "mute","read_only","admin","creator","short","update","powersave", 38 | "checksum","epoch","block","previous","409","replaced","reason", 39 | "spam","modify_tag","message_info","delivery","emoji","title", 40 | "description","canonical-url","matched-text","star","unstar", 41 | "media_key","filename","identity","unread","page","page_count", 42 | "search","media_message","security","call_log","profile","ciphertext", 43 | "invite","gif","vcard","frequent","privacy","blacklist","whitelist", 44 | "verify","location","document","elapsed","revoke_invite","expiration", 45 | "unsubscribe","disable","vname","old_jid","new_jid","announcement", 46 | "locked","prop","label","color","call","offer","call-id", 47 | "quick_reply", "sticker", "pay_t", "accept", "reject", "sticker_pack", 48 | "invalid", "canceled", "missed", "connected", "result", "audio", 49 | "video", "recent" 50 | ] 51 | const WebMessageInfo = ProtoBuf.Root.fromJSON( require("./whatsapp_message_coding.json") ).lookupType("proto.WebMessageInfo") 52 | 53 | class WhatsAppBinaryEncoder { 54 | 55 | constructor () { 56 | this.data = [] 57 | } 58 | pushByte (value) { 59 | 60 | this.data.push((value & 0xFF)) 61 | } 62 | pushInt (value, n, littleEndian=false) { 63 | for (var i = 0; i < n;i++) { 64 | const curShift = littleEndian ? i : (n-1-i) 65 | this.data.push( (value>>(curShift*8)) & 0xFF ) 66 | } 67 | } 68 | pushInt20 (value) { 69 | this.pushBytes ( [(value >> 16) & 0x0F, (value >> 8) & 0xFF, value & 0xFF] ) 70 | } 71 | pushInt16 (value) { 72 | this.pushInt(value, 2) 73 | } 74 | pushInt32 (value) { 75 | this.pushInt(value, 4) 76 | } 77 | pushInt64 (value) { 78 | this.pushInt(value, 8) 79 | } 80 | pushBytes (bytes) { 81 | this.data.push.apply(this.data, bytes) 82 | } 83 | pushString (str) { 84 | const bytes = new TextEncoder('utf-8').encode(str) 85 | this.pushBytes(bytes) 86 | } 87 | writeByteLength (length) { 88 | if (length >= 4294967296) { 89 | throw "string too large to encode: " + length 90 | } 91 | 92 | if (length >= (1<<20)) { 93 | this.pushByte(WATags.BINARY_32) 94 | this.pushInt32(length) 95 | } else if (length >= 256) { 96 | this.pushByte(WATags.BINARY_20) 97 | this.pushInt20(length) 98 | } else { 99 | this.pushByte(WATags.BINARY_8) 100 | this.pushByte(length) 101 | } 102 | } 103 | writeStringRaw (string) { 104 | this.writeByteLength( string.length ) 105 | this.pushString(string) 106 | } 107 | writeJid(left,right) { 108 | this.pushByte(WATags.JID_PAIR) 109 | if (left && left.length > 0) { 110 | this.writeString(left) 111 | } else { 112 | this.writeToken(WATags.LIST_EMPTY) 113 | } 114 | this.writeString(right) 115 | } 116 | writeToken (token) { 117 | if (token < 245) { 118 | this.pushByte(token) 119 | } else if (token <= 500) { 120 | throw "invalid token" 121 | } 122 | } 123 | writeString(token, i=null) { 124 | if (typeof token !== "string") { 125 | throw "invalid string: " + token 126 | } 127 | 128 | if (token === "c.us") { 129 | token = "s.whatsapp.net" 130 | } 131 | 132 | 133 | const tokenIndex = WASingleByteTokens.indexOf(token) 134 | if (!i && token === "s.whatsapp.net") { 135 | this.writeToken( tokenIndex ) 136 | } else if ( tokenIndex >= 0 ) { 137 | if (tokenIndex < WATags.SINGLE_BYTE_MAX) { 138 | this.writeToken(tokenIndex) 139 | } else { 140 | const overflow = tokenIndex-WATags.SINGLE_BYTE_MAX 141 | const dictionaryIndex = overflow >> 8 142 | if (dictionaryIndex < 0 || dictionaryIndex > 3) { 143 | throw "double byte dict token out of range: " + token + ", " + tokenIndex 144 | } 145 | this.writeToken(WATags.DICTIONARY_0 + dictionaryIndex) 146 | this.writeToken(overflow % 256) 147 | } 148 | } else { 149 | const jidSepIndex = token.indexOf("@") 150 | 151 | if (jidSepIndex <= 0) { 152 | this.writeStringRaw(token) 153 | } else { 154 | this.writeJid(token.slice(0,jidSepIndex), token.slice(jidSepIndex+1, token.length)) 155 | } 156 | } 157 | } 158 | writeAttributes (attrs) { 159 | if (!attrs) { 160 | return 161 | } 162 | Object.keys(attrs).forEach (key => { 163 | this.writeString( key ) 164 | this.writeString( attrs[key] ) 165 | }) 166 | } 167 | writeListStart (listSize) { 168 | if (listSize === 0) { 169 | this.pushByte(WATags.LIST_EMPTY) 170 | } else if (listSize < 256) { 171 | this.pushBytes([WATags.LIST_8, listSize]) 172 | } else { 173 | this.pushByte([WATags.LIST_16, listSize]) 174 | } 175 | } 176 | writeChildren (children) { 177 | if (!children) { 178 | return 179 | } 180 | 181 | if (typeof children === "string") { 182 | this.writeString(children, true) 183 | } else if (typeof children === "Buffer" || typeof children === "Uint8Array") { 184 | this.writeByteLength(children.length) 185 | this.pushBytes(children) 186 | } else if (Array.isArray(children)) { 187 | this.writeListStart(children.length) 188 | children.forEach (c => { 189 | this.writeNode(c) 190 | }) 191 | } else if (typeof children === "object") { 192 | //console.log(children) 193 | const buff = WebMessageInfo.encode(children).finish() 194 | this.writeByteLength(buff.length) 195 | this.pushBytes(buff) 196 | } else { 197 | throw "invalid children: " + children + " (" + (typeof children) + ")" 198 | } 199 | } 200 | getNumValidKeys (arr) { 201 | return arr ? Object.keys(arr).length : 0 202 | } 203 | writeNode (node) { 204 | if (!node) { 205 | return 206 | } else if (!Array.isArray(node) || node.length !== 3) { 207 | throw "invalid node given: " + node 208 | } 209 | 210 | const numAttributes = this.getNumValidKeys(node[1]) 211 | 212 | this.writeListStart( 2*numAttributes + 1 + ( node[2] ? 1 : 0 ) ) 213 | this.writeString(node[0]) 214 | this.writeAttributes(node[1]) 215 | this.writeChildren(node[2]) 216 | } 217 | write (data) { 218 | this.data = new Array() 219 | this.writeNode(data) 220 | 221 | return Buffer.from(this.data) 222 | } 223 | } 224 | class WhatsAppBinaryDecoder { 225 | 226 | constructor () { 227 | this.buffer = null 228 | this.index = 0 229 | 230 | } 231 | checkEOS (length) { 232 | if (this.index+length > this.buffer.length) { 233 | throw "end of stream" 234 | } 235 | } 236 | next () { 237 | const value = this.buffer[this.index] 238 | this.index += 1 239 | return value 240 | } 241 | readByte () { 242 | this.checkEOS(1) 243 | return this.next() 244 | } 245 | 246 | readInt (n, littleEndian=false) { 247 | this.checkEOS(n) 248 | let val = 0 249 | for (var i = 0; i < n;i++) { 250 | const shift = (littleEndian) ? i : (n-1-i) 251 | val |= this.next() << (shift*8) 252 | } 253 | return val 254 | } 255 | readInt16 (littleEndian=false) { 256 | return this.readInt(2, littleEndian) 257 | } 258 | readInt20 () { 259 | this.checkEOS(3) 260 | return ( (this.next() & 15) << 16 ) + (this.next()<<8) + this.next() 261 | } 262 | readInt32 (littleEndian=false) { 263 | return this.readInt(4, littleEndian) 264 | } 265 | readInt64 (littleEndian=false) { 266 | return this.readInt(8, littleEndian) 267 | } 268 | unpackHex (value) { 269 | if (value >= 0 && value < 16) { 270 | return value<10 ? ('0'.charCodeAt(0)+value) : ('A'.charCodeAt(0)+value-10) 271 | } 272 | throw "invalid hex: " + value 273 | } 274 | unpackNibble(value) { 275 | if (value >= 0 && value <= 9) { 276 | return '0'.charCodeAt(0)+value 277 | } 278 | switch (value) { 279 | case 10: 280 | return '-'.charCodeAt(0) 281 | case 11: 282 | return '.'.charCodeAt(0) 283 | case 15: 284 | return '\0'.charCodeAt(0) 285 | default: 286 | throw "invalid nibble: " + value 287 | } 288 | } 289 | unpackByte (tag, value) { 290 | if (tag === WATags.NIBBLE_8) { 291 | return this.unpackNibble(value) 292 | } else if (tag === WATags.HEX_8) { 293 | return this.unpackHex(value) 294 | } else { 295 | throw "unknown tag: " + tag 296 | } 297 | } 298 | readPacked8(tag) { 299 | const startByte = this.readByte() 300 | let value = "" 301 | 302 | for (var i = 0; i < (startByte&127);i++) { 303 | let curByte = this.readByte() 304 | value += String.fromCharCode( this.unpackByte(tag, (curByte&0xF0) >> 4) ) 305 | value += String.fromCharCode( this.unpackByte(tag, curByte&0x0F) ) 306 | } 307 | if ((startByte >> 7) !== 0) { 308 | value = value.slice(0,-1) 309 | } 310 | return value 311 | 312 | } 313 | readRangedVarInt (min, max, description="unknown") { 314 | // value = 315 | throw "WTF" 316 | } 317 | isListTag (tag) { 318 | return tag === WATags.LIST_EMPTY || tag === WATags.LIST_8 || tag === WATags.LIST_16 319 | } 320 | readListSize (tag) { 321 | switch (tag) { 322 | case WATags.LIST_EMPTY: 323 | return 0 324 | case WATags.LIST_8: 325 | return this.readByte() 326 | case WATags.LIST_16: 327 | return this.readInt16() 328 | default: 329 | throw "invalid tag for list size: " + tag 330 | } 331 | } 332 | readStringFromChars (length) { 333 | this.checkEOS(length) 334 | const value = this.buffer.slice(this.index, this.index+length) 335 | 336 | this.index += length 337 | return new TextDecoder('utf-8').decode(value) 338 | } 339 | readString (tag) { 340 | if (tag >= 3 && tag <= 235) { 341 | const token = this.getToken(tag) 342 | return token === "s.whatsapp.net" ? "c.us" : token 343 | } 344 | 345 | switch (tag) { 346 | case WATags.DICTIONARY_0: 347 | case WATags.DICTIONARY_1: 348 | case WATags.DICTIONARY_2: 349 | case WATags.DICTIONARY_3: 350 | return this.getTokenDouble( tag - WATags.DICTIONARY_0, this.readByte() ) 351 | case WATags.LIST_EMPTY: 352 | return null 353 | case WATags.BINARY_8: 354 | return this.readStringFromChars( this.readByte() ) 355 | case WATags.BINARY_20: 356 | return this.readStringFromChars( this.readInt20() ) 357 | case WATags.BINARY_32: 358 | return this.readStringFromChars( this.readInt32() ) 359 | case WATags.JID_PAIR: 360 | const i = this.readString( this.readByte() ) 361 | const j = this.readString( this.readByte() ) 362 | if (i && j) { 363 | return i + "@" + j 364 | } 365 | throw "invalid jid pair: " + i + ", " + j 366 | case WATags.HEX_8: 367 | case WATags.NIBBLE_8: 368 | return this.readPacked8(tag) 369 | default: 370 | throw "invalid string with tag: " + tag 371 | } 372 | } 373 | readAttributes (n) { 374 | if (n !== 0) { 375 | let attributes = {} 376 | for (var i = 0;i < n;i++) { 377 | const index = this.readString(this.readByte()) 378 | const b = this.readByte() 379 | 380 | attributes[index] = this.readString(b) 381 | } 382 | return attributes 383 | } else { 384 | return null 385 | } 386 | } 387 | readList (tag) { 388 | let list = Array( this.readListSize(tag) ) 389 | for (var i = 0;i < list.length;i++) { 390 | list[i] = this.readNode() 391 | } 392 | return list 393 | } 394 | readBytes (n) { 395 | this.checkEOS(n) 396 | const value = this.buffer.slice(this.index, this.index+n) 397 | this.index += n 398 | return value 399 | } 400 | getToken (index) { 401 | if (index < 3 || index >= WASingleByteTokens.length) { 402 | throw "invalid token index: " + index 403 | } 404 | return WASingleByteTokens[index] 405 | } 406 | getTokenDouble (index1, index2) { 407 | const n = 256*index1 + index2 408 | if (n < 0 || n > WADoubleByteTokens.length) { 409 | throw "invalid double token index: " + n 410 | } 411 | return WADoubleByteTokens[n] 412 | } 413 | readNode () { 414 | const listSize = this.readListSize( this.readByte() ) 415 | const descrTag = this.readByte() 416 | 417 | if (descrTag === WATags.STREAM_END) { 418 | throw "unexpected stream end" 419 | } 420 | 421 | const descr = this.readString(descrTag) 422 | if (listSize === 0 || !descr) { 423 | throw "invalid node" 424 | } 425 | //console.log(descr + "," + listSize) 426 | 427 | let attrs = this.readAttributes( (listSize-1) >> 1 ) 428 | let content = null 429 | 430 | 431 | if (listSize%2 === 0) { 432 | const tag = this.readByte() 433 | 434 | if (this.isListTag(tag)) { 435 | content = this.readList(tag) 436 | } else { 437 | switch (tag) { 438 | case WATags.BINARY_8: 439 | content = this.readBytes( this.readByte() ) 440 | break 441 | case WATags.BINARY_20: 442 | content = this.readBytes( this.readInt20() ) 443 | break 444 | case WATags.BINARY_32: 445 | content = this.readBytes( this.readInt32() ) 446 | break 447 | default: 448 | content = this.readString(tag) 449 | break 450 | } 451 | } 452 | } 453 | //console.log( descr + "," + JSON.stringify(attrs) + ", " + content) 454 | return [descr, attrs, content] 455 | } 456 | 457 | read (buffer) { 458 | this.buffer = buffer 459 | this.index = 0 460 | 461 | let node = this.readNode() 462 | 463 | if (node[2]) { 464 | for (var i = 0; i < node[2].length;i++) { 465 | if (node[2][0][0] === "message") { 466 | node[2][i][2] = WebMessageInfo.decode( node[2][i][2] ) 467 | } 468 | } 469 | } 470 | 471 | return node 472 | } 473 | 474 | } 475 | 476 | module.exports = { Encoder: WhatsAppBinaryEncoder, Decoder: WhatsAppBinaryDecoder } -------------------------------------------------------------------------------- /binary_coding/def.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto2"; 2 | package proto; 3 | 4 | message HydratedQuickReplyButton { 5 | optional string displayText = 1; 6 | optional string id = 2; 7 | } 8 | 9 | message HydratedURLButton { 10 | optional string displayText = 1; 11 | optional string url = 2; 12 | } 13 | 14 | message HydratedCallButton { 15 | optional string displayText = 1; 16 | optional string phoneNumber = 2; 17 | } 18 | 19 | message HydratedTemplateButton { 20 | optional uint32 index = 4; 21 | oneof hydratedButton { 22 | HydratedQuickReplyButton quickReplyButton = 1; 23 | HydratedURLButton urlButton = 2; 24 | HydratedCallButton callButton = 3; 25 | } 26 | } 27 | 28 | message QuickReplyButton { 29 | optional HighlyStructuredMessage displayText = 1; 30 | optional string id = 2; 31 | } 32 | 33 | message URLButton { 34 | optional HighlyStructuredMessage displayText = 1; 35 | optional HighlyStructuredMessage url = 2; 36 | } 37 | 38 | message CallButton { 39 | optional HighlyStructuredMessage displayText = 1; 40 | optional HighlyStructuredMessage phoneNumber = 2; 41 | } 42 | 43 | message TemplateButton { 44 | optional uint32 index = 4; 45 | oneof button { 46 | QuickReplyButton quickReplyButton = 1; 47 | URLButton urlButton = 2; 48 | CallButton callButton = 3; 49 | } 50 | } 51 | 52 | message Location { 53 | optional double degreesLatitude = 1; 54 | optional double degreesLongitude = 2; 55 | optional string name = 3; 56 | } 57 | 58 | message Point { 59 | optional double x = 3; 60 | optional double y = 4; 61 | } 62 | 63 | message InteractiveAnnotation { 64 | repeated Point polygonVertices = 1; 65 | oneof action { 66 | Location location = 2; 67 | } 68 | } 69 | 70 | message AdReplyInfo { 71 | optional string advertiserName = 1; 72 | enum AD_REPLY_INFO_MEDIATYPE { 73 | NONE = 0; 74 | IMAGE = 1; 75 | VIDEO = 2; 76 | } 77 | optional AD_REPLY_INFO_MEDIATYPE mediaType = 2; 78 | optional bytes jpegThumbnail = 16; 79 | optional string caption = 17; 80 | } 81 | 82 | message ContextInfo { 83 | optional string stanzaId = 1; 84 | optional string participant = 2; 85 | optional Message quotedMessage = 3; 86 | optional string remoteJid = 4; 87 | repeated string mentionedJid = 15; 88 | optional string conversionSource = 18; 89 | optional bytes conversionData = 19; 90 | optional uint32 conversionDelaySeconds = 20; 91 | optional uint32 forwardingScore = 21; 92 | optional bool isForwarded = 22; 93 | optional AdReplyInfo quotedAd = 23; 94 | optional MessageKey placeholderKey = 24; 95 | optional uint32 expiration = 25; 96 | } 97 | 98 | message SenderKeyDistributionMessage { 99 | optional string groupId = 1; 100 | optional bytes axolotlSenderKeyDistributionMessage = 2; 101 | } 102 | 103 | message ImageMessage { 104 | optional string url = 1; 105 | optional string mimetype = 2; 106 | optional string caption = 3; 107 | optional bytes fileSha256 = 4; 108 | optional uint64 fileLength = 5; 109 | optional uint32 height = 6; 110 | optional uint32 width = 7; 111 | optional bytes mediaKey = 8; 112 | optional bytes fileEncSha256 = 9; 113 | repeated InteractiveAnnotation interactiveAnnotations = 10; 114 | optional string directPath = 11; 115 | optional int64 mediaKeyTimestamp = 12; 116 | optional bytes jpegThumbnail = 16; 117 | optional ContextInfo contextInfo = 17; 118 | optional bytes firstScanSidecar = 18; 119 | optional uint32 firstScanLength = 19; 120 | optional uint32 experimentGroupId = 20; 121 | optional bytes scansSidecar = 21; 122 | repeated uint32 scanLengths = 22; 123 | optional bytes midQualityFileSha256 = 23; 124 | optional bytes midQualityFileEncSha256 = 24; 125 | } 126 | 127 | message ContactMessage { 128 | optional string displayName = 1; 129 | optional string vcard = 16; 130 | optional ContextInfo contextInfo = 17; 131 | } 132 | 133 | message LocationMessage { 134 | optional double degreesLatitude = 1; 135 | optional double degreesLongitude = 2; 136 | optional string name = 3; 137 | optional string address = 4; 138 | optional string url = 5; 139 | optional bytes jpegThumbnail = 16; 140 | optional ContextInfo contextInfo = 17; 141 | } 142 | 143 | message ExtendedTextMessage { 144 | optional string text = 1; 145 | optional string matchedText = 2; 146 | optional string canonicalUrl = 4; 147 | optional string description = 5; 148 | optional string title = 6; 149 | optional fixed32 textArgb = 7; 150 | optional fixed32 backgroundArgb = 8; 151 | enum EXTENDED_TEXT_MESSAGE_FONTTYPE { 152 | SANS_SERIF = 0; 153 | SERIF = 1; 154 | NORICAN_REGULAR = 2; 155 | BRYNDAN_WRITE = 3; 156 | BEBASNEUE_REGULAR = 4; 157 | OSWALD_HEAVY = 5; 158 | } 159 | optional EXTENDED_TEXT_MESSAGE_FONTTYPE font = 9; 160 | enum EXTENDED_TEXT_MESSAGE_PREVIEWTYPE { 161 | NONE = 0; 162 | VIDEO = 1; 163 | } 164 | optional EXTENDED_TEXT_MESSAGE_PREVIEWTYPE previewType = 10; 165 | optional bytes jpegThumbnail = 16; 166 | optional ContextInfo contextInfo = 17; 167 | optional bool doNotPlayInline = 18; 168 | } 169 | 170 | message DocumentMessage { 171 | optional string url = 1; 172 | optional string mimetype = 2; 173 | optional string title = 3; 174 | optional bytes fileSha256 = 4; 175 | optional uint64 fileLength = 5; 176 | optional uint32 pageCount = 6; 177 | optional bytes mediaKey = 7; 178 | optional string fileName = 8; 179 | optional bytes fileEncSha256 = 9; 180 | optional string directPath = 10; 181 | optional int64 mediaKeyTimestamp = 11; 182 | optional bytes jpegThumbnail = 16; 183 | optional ContextInfo contextInfo = 17; 184 | } 185 | 186 | message AudioMessage { 187 | optional string url = 1; 188 | optional string mimetype = 2; 189 | optional bytes fileSha256 = 3; 190 | optional uint64 fileLength = 4; 191 | optional uint32 seconds = 5; 192 | optional bool ptt = 6; 193 | optional bytes mediaKey = 7; 194 | optional bytes fileEncSha256 = 8; 195 | optional string directPath = 9; 196 | optional int64 mediaKeyTimestamp = 10; 197 | optional ContextInfo contextInfo = 17; 198 | optional bytes streamingSidecar = 18; 199 | } 200 | 201 | message VideoMessage { 202 | optional string url = 1; 203 | optional string mimetype = 2; 204 | optional bytes fileSha256 = 3; 205 | optional uint64 fileLength = 4; 206 | optional uint32 seconds = 5; 207 | optional bytes mediaKey = 6; 208 | optional string caption = 7; 209 | optional bool gifPlayback = 8; 210 | optional uint32 height = 9; 211 | optional uint32 width = 10; 212 | optional bytes fileEncSha256 = 11; 213 | repeated InteractiveAnnotation interactiveAnnotations = 12; 214 | optional string directPath = 13; 215 | optional int64 mediaKeyTimestamp = 14; 216 | optional bytes jpegThumbnail = 16; 217 | optional ContextInfo contextInfo = 17; 218 | optional bytes streamingSidecar = 18; 219 | enum VIDEO_MESSAGE_ATTRIBUTION { 220 | NONE = 0; 221 | GIPHY = 1; 222 | TENOR = 2; 223 | } 224 | optional VIDEO_MESSAGE_ATTRIBUTION gifAttribution = 19; 225 | } 226 | 227 | message Call { 228 | optional bytes callKey = 1; 229 | } 230 | 231 | message Chat { 232 | optional string displayName = 1; 233 | optional string id = 2; 234 | } 235 | 236 | message ProtocolMessage { 237 | optional MessageKey key = 1; 238 | enum PROTOCOL_MESSAGE_TYPE { 239 | REVOKE = 0; 240 | EPHEMERAL_SETTING = 3; 241 | } 242 | optional PROTOCOL_MESSAGE_TYPE type = 2; 243 | optional uint32 ephemeralExpiration = 4; 244 | } 245 | 246 | message ContactsArrayMessage { 247 | optional string displayName = 1; 248 | repeated ContactMessage contacts = 2; 249 | optional ContextInfo contextInfo = 17; 250 | } 251 | 252 | message HSMCurrency { 253 | optional string currencyCode = 1; 254 | optional int64 amount1000 = 2; 255 | } 256 | 257 | message HSMDateTimeComponent { 258 | enum HSM_DATE_TIME_COMPONENT_DAYOFWEEKTYPE { 259 | MONDAY = 1; 260 | TUESDAY = 2; 261 | WEDNESDAY = 3; 262 | THURSDAY = 4; 263 | FRIDAY = 5; 264 | SATURDAY = 6; 265 | SUNDAY = 7; 266 | } 267 | optional HSM_DATE_TIME_COMPONENT_DAYOFWEEKTYPE dayOfWeek = 1; 268 | optional uint32 year = 2; 269 | optional uint32 month = 3; 270 | optional uint32 dayOfMonth = 4; 271 | optional uint32 hour = 5; 272 | optional uint32 minute = 6; 273 | enum HSM_DATE_TIME_COMPONENT_CALENDARTYPE { 274 | GREGORIAN = 1; 275 | SOLAR_HIJRI = 2; 276 | } 277 | optional HSM_DATE_TIME_COMPONENT_CALENDARTYPE calendar = 7; 278 | } 279 | 280 | message HSMDateTimeUnixEpoch { 281 | optional int64 timestamp = 1; 282 | } 283 | 284 | message HSMDateTime { 285 | oneof datetimeOneof { 286 | HSMDateTimeComponent component = 1; 287 | HSMDateTimeUnixEpoch unixEpoch = 2; 288 | } 289 | } 290 | 291 | message HSMLocalizableParameter { 292 | optional string default = 1; 293 | oneof paramOneof { 294 | HSMCurrency currency = 2; 295 | HSMDateTime dateTime = 3; 296 | } 297 | } 298 | 299 | message HighlyStructuredMessage { 300 | optional string namespace = 1; 301 | optional string elementName = 2; 302 | repeated string params = 3; 303 | optional string fallbackLg = 4; 304 | optional string fallbackLc = 5; 305 | repeated HSMLocalizableParameter localizableParams = 6; 306 | optional string deterministicLg = 7; 307 | optional string deterministicLc = 8; 308 | optional TemplateMessage hydratedHsm = 9; 309 | } 310 | 311 | message SendPaymentMessage { 312 | optional Message noteMessage = 2; 313 | optional MessageKey requestMessageKey = 3; 314 | } 315 | 316 | message RequestPaymentMessage { 317 | optional Message noteMessage = 4; 318 | optional string currencyCodeIso4217 = 1; 319 | optional uint64 amount1000 = 2; 320 | optional string requestFrom = 3; 321 | optional int64 expiryTimestamp = 5; 322 | } 323 | 324 | message DeclinePaymentRequestMessage { 325 | optional MessageKey key = 1; 326 | } 327 | 328 | message CancelPaymentRequestMessage { 329 | optional MessageKey key = 1; 330 | } 331 | 332 | message LiveLocationMessage { 333 | optional double degreesLatitude = 1; 334 | optional double degreesLongitude = 2; 335 | optional uint32 accuracyInMeters = 3; 336 | optional float speedInMps = 4; 337 | optional uint32 degreesClockwiseFromMagneticNorth = 5; 338 | optional string caption = 6; 339 | optional int64 sequenceNumber = 7; 340 | optional uint32 timeOffset = 8; 341 | optional bytes jpegThumbnail = 16; 342 | optional ContextInfo contextInfo = 17; 343 | } 344 | 345 | message StickerMessage { 346 | optional string url = 1; 347 | optional bytes fileSha256 = 2; 348 | optional bytes fileEncSha256 = 3; 349 | optional bytes mediaKey = 4; 350 | optional string mimetype = 5; 351 | optional uint32 height = 6; 352 | optional uint32 width = 7; 353 | optional string directPath = 8; 354 | optional uint64 fileLength = 9; 355 | optional int64 mediaKeyTimestamp = 10; 356 | optional uint32 firstFrameLength = 11; 357 | optional bytes firstFrameSidecar = 12; 358 | optional ContextInfo contextInfo = 17; 359 | } 360 | 361 | message FourRowTemplate { 362 | optional HighlyStructuredMessage content = 6; 363 | optional HighlyStructuredMessage footer = 7; 364 | repeated TemplateButton buttons = 8; 365 | oneof title { 366 | DocumentMessage documentMessage = 1; 367 | HighlyStructuredMessage highlyStructuredMessage = 2; 368 | ImageMessage imageMessage = 3; 369 | VideoMessage videoMessage = 4; 370 | LocationMessage locationMessage = 5; 371 | } 372 | } 373 | 374 | message HydratedFourRowTemplate { 375 | optional string hydratedContentText = 6; 376 | optional string hydratedFooterText = 7; 377 | repeated HydratedTemplateButton hydratedButtons = 8; 378 | optional string templateId = 9; 379 | oneof title { 380 | DocumentMessage documentMessage = 1; 381 | string hydratedTitleText = 2; 382 | ImageMessage imageMessage = 3; 383 | VideoMessage videoMessage = 4; 384 | LocationMessage locationMessage = 5; 385 | } 386 | } 387 | 388 | message TemplateMessage { 389 | optional ContextInfo contextInfo = 3; 390 | optional HydratedFourRowTemplate hydratedTemplate = 4; 391 | oneof format { 392 | FourRowTemplate fourRowTemplate = 1; 393 | HydratedFourRowTemplate hydratedFourRowTemplate = 2; 394 | } 395 | } 396 | 397 | message TemplateButtonReplyMessage { 398 | optional string selectedId = 1; 399 | optional string selectedDisplayText = 2; 400 | optional ContextInfo contextInfo = 3; 401 | optional uint32 selectedIndex = 4; 402 | } 403 | 404 | message ProductSnapshot { 405 | optional ImageMessage productImage = 1; 406 | optional string productId = 2; 407 | optional string title = 3; 408 | optional string description = 4; 409 | optional string currencyCode = 5; 410 | optional int64 priceAmount1000 = 6; 411 | optional string retailerId = 7; 412 | optional string url = 8; 413 | optional uint32 productImageCount = 9; 414 | optional string firstImageId = 11; 415 | } 416 | 417 | message ProductMessage { 418 | optional ProductSnapshot product = 1; 419 | optional string businessOwnerJid = 2; 420 | optional ContextInfo contextInfo = 17; 421 | } 422 | 423 | message GroupInviteMessage { 424 | optional string groupJid = 1; 425 | optional string inviteCode = 2; 426 | optional int64 inviteExpiration = 3; 427 | optional string groupName = 4; 428 | optional bytes jpegThumbnail = 5; 429 | optional string caption = 6; 430 | optional ContextInfo contextInfo = 7; 431 | } 432 | 433 | message DeviceSentMessage { 434 | optional string destinationJid = 1; 435 | optional Message message = 2; 436 | } 437 | 438 | message DeviceSyncMessage { 439 | optional bytes serializedXmlBytes = 1; 440 | } 441 | 442 | message Message { 443 | optional string conversation = 1; 444 | optional SenderKeyDistributionMessage senderKeyDistributionMessage = 2; 445 | optional ImageMessage imageMessage = 3; 446 | optional ContactMessage contactMessage = 4; 447 | optional LocationMessage locationMessage = 5; 448 | optional ExtendedTextMessage extendedTextMessage = 6; 449 | optional DocumentMessage documentMessage = 7; 450 | optional AudioMessage audioMessage = 8; 451 | optional VideoMessage videoMessage = 9; 452 | optional Call call = 10; 453 | optional Chat chat = 11; 454 | optional ProtocolMessage protocolMessage = 12; 455 | optional ContactsArrayMessage contactsArrayMessage = 13; 456 | optional HighlyStructuredMessage highlyStructuredMessage = 14; 457 | optional SenderKeyDistributionMessage fastRatchetKeySenderKeyDistributionMessage = 15; 458 | optional SendPaymentMessage sendPaymentMessage = 16; 459 | optional LiveLocationMessage liveLocationMessage = 18; 460 | optional RequestPaymentMessage requestPaymentMessage = 22; 461 | optional DeclinePaymentRequestMessage declinePaymentRequestMessage = 23; 462 | optional CancelPaymentRequestMessage cancelPaymentRequestMessage = 24; 463 | optional TemplateMessage templateMessage = 25; 464 | optional StickerMessage stickerMessage = 26; 465 | optional GroupInviteMessage groupInviteMessage = 28; 466 | optional TemplateButtonReplyMessage templateButtonReplyMessage = 29; 467 | optional ProductMessage productMessage = 30; 468 | optional DeviceSentMessage deviceSentMessage = 31; 469 | optional DeviceSyncMessage deviceSyncMessage = 32; 470 | } 471 | 472 | message MessageKey { 473 | optional string remoteJid = 1; 474 | optional bool fromMe = 2; 475 | optional string id = 3; 476 | optional string participant = 4; 477 | } 478 | 479 | message WebFeatures { 480 | enum WEB_FEATURES_FLAG { 481 | NOT_STARTED = 0; 482 | FORCE_UPGRADE = 1; 483 | DEVELOPMENT = 2; 484 | PRODUCTION = 3; 485 | } 486 | optional WEB_FEATURES_FLAG labelsDisplay = 1; 487 | optional WEB_FEATURES_FLAG voipIndividualOutgoing = 2; 488 | optional WEB_FEATURES_FLAG groupsV3 = 3; 489 | optional WEB_FEATURES_FLAG groupsV3Create = 4; 490 | optional WEB_FEATURES_FLAG changeNumberV2 = 5; 491 | optional WEB_FEATURES_FLAG queryStatusV3Thumbnail = 6; 492 | optional WEB_FEATURES_FLAG liveLocations = 7; 493 | optional WEB_FEATURES_FLAG queryVname = 8; 494 | optional WEB_FEATURES_FLAG voipIndividualIncoming = 9; 495 | optional WEB_FEATURES_FLAG quickRepliesQuery = 10; 496 | optional WEB_FEATURES_FLAG payments = 11; 497 | optional WEB_FEATURES_FLAG stickerPackQuery = 12; 498 | optional WEB_FEATURES_FLAG liveLocationsFinal = 13; 499 | optional WEB_FEATURES_FLAG labelsEdit = 14; 500 | optional WEB_FEATURES_FLAG mediaUpload = 15; 501 | optional WEB_FEATURES_FLAG mediaUploadRichQuickReplies = 18; 502 | optional WEB_FEATURES_FLAG vnameV2 = 19; 503 | optional WEB_FEATURES_FLAG videoPlaybackUrl = 20; 504 | optional WEB_FEATURES_FLAG statusRanking = 21; 505 | optional WEB_FEATURES_FLAG voipIndividualVideo = 22; 506 | optional WEB_FEATURES_FLAG thirdPartyStickers = 23; 507 | optional WEB_FEATURES_FLAG frequentlyForwardedSetting = 24; 508 | optional WEB_FEATURES_FLAG groupsV4JoinPermission = 25; 509 | optional WEB_FEATURES_FLAG recentStickers = 26; 510 | optional WEB_FEATURES_FLAG catalog = 27; 511 | optional WEB_FEATURES_FLAG starredStickers = 28; 512 | optional WEB_FEATURES_FLAG voipGroupCall = 29; 513 | optional WEB_FEATURES_FLAG templateMessage = 30; 514 | optional WEB_FEATURES_FLAG templateMessageInteractivity = 31; 515 | optional WEB_FEATURES_FLAG ephemeralMessages = 32; 516 | } 517 | 518 | message TabletNotificationsInfo { 519 | optional uint64 timestamp = 2; 520 | optional uint32 unreadChats = 3; 521 | optional uint32 notifyMessageCount = 4; 522 | repeated NotificationMessageInfo notifyMessage = 5; 523 | } 524 | 525 | message NotificationMessageInfo { 526 | optional MessageKey key = 1; 527 | optional Message message = 2; 528 | optional uint64 messageTimestamp = 3; 529 | optional string participant = 4; 530 | } 531 | 532 | message WebNotificationsInfo { 533 | optional uint64 timestamp = 2; 534 | optional uint32 unreadChats = 3; 535 | optional uint32 notifyMessageCount = 4; 536 | repeated WebMessageInfo notifyMessages = 5; 537 | } 538 | 539 | message PaymentInfo { 540 | optional uint64 amount1000 = 2; 541 | optional string receiverJid = 3; 542 | enum PAYMENT_INFO_STATUS { 543 | UNKNOWN_STATUS = 0; 544 | PROCESSING = 1; 545 | SENT = 2; 546 | NEED_TO_ACCEPT = 3; 547 | COMPLETE = 4; 548 | COULD_NOT_COMPLETE = 5; 549 | REFUNDED = 6; 550 | EXPIRED = 7; 551 | REJECTED = 8; 552 | CANCELLED = 9; 553 | WAITING_FOR_PAYER = 10; 554 | WAITING = 11; 555 | } 556 | optional PAYMENT_INFO_STATUS status = 4; 557 | optional uint64 transactionTimestamp = 5; 558 | optional MessageKey requestMessageKey = 6; 559 | optional uint64 expiryTimestamp = 7; 560 | optional bool futureproofed = 8; 561 | optional string currency = 9; 562 | } 563 | 564 | message WebMessageInfo { 565 | required MessageKey key = 1; 566 | optional Message message = 2; 567 | optional uint64 messageTimestamp = 3; 568 | enum WEB_MESSAGE_INFO_STATUS { 569 | ERROR = 0; 570 | PENDING = 1; 571 | SERVER_ACK = 2; 572 | DELIVERY_ACK = 3; 573 | READ = 4; 574 | PLAYED = 5; 575 | } 576 | optional WEB_MESSAGE_INFO_STATUS status = 4; 577 | optional string participant = 5; 578 | optional bool ignore = 16; 579 | optional bool starred = 17; 580 | optional bool broadcast = 18; 581 | optional string pushName = 19; 582 | optional bytes mediaCiphertextSha256 = 20; 583 | optional bool multicast = 21; 584 | optional bool urlText = 22; 585 | optional bool urlNumber = 23; 586 | enum WEB_MESSAGE_INFO_STUBTYPE { 587 | UNKNOWN = 0; 588 | REVOKE = 1; 589 | CIPHERTEXT = 2; 590 | FUTUREPROOF = 3; 591 | NON_VERIFIED_TRANSITION = 4; 592 | UNVERIFIED_TRANSITION = 5; 593 | VERIFIED_TRANSITION = 6; 594 | VERIFIED_LOW_UNKNOWN = 7; 595 | VERIFIED_HIGH = 8; 596 | VERIFIED_INITIAL_UNKNOWN = 9; 597 | VERIFIED_INITIAL_LOW = 10; 598 | VERIFIED_INITIAL_HIGH = 11; 599 | VERIFIED_TRANSITION_ANY_TO_NONE = 12; 600 | VERIFIED_TRANSITION_ANY_TO_HIGH = 13; 601 | VERIFIED_TRANSITION_HIGH_TO_LOW = 14; 602 | VERIFIED_TRANSITION_HIGH_TO_UNKNOWN = 15; 603 | VERIFIED_TRANSITION_UNKNOWN_TO_LOW = 16; 604 | VERIFIED_TRANSITION_LOW_TO_UNKNOWN = 17; 605 | VERIFIED_TRANSITION_NONE_TO_LOW = 18; 606 | VERIFIED_TRANSITION_NONE_TO_UNKNOWN = 19; 607 | GROUP_CREATE = 20; 608 | GROUP_CHANGE_SUBJECT = 21; 609 | GROUP_CHANGE_ICON = 22; 610 | GROUP_CHANGE_INVITE_LINK = 23; 611 | GROUP_CHANGE_DESCRIPTION = 24; 612 | GROUP_CHANGE_RESTRICT = 25; 613 | GROUP_CHANGE_ANNOUNCE = 26; 614 | GROUP_PARTICIPANT_ADD = 27; 615 | GROUP_PARTICIPANT_REMOVE = 28; 616 | GROUP_PARTICIPANT_PROMOTE = 29; 617 | GROUP_PARTICIPANT_DEMOTE = 30; 618 | GROUP_PARTICIPANT_INVITE = 31; 619 | GROUP_PARTICIPANT_LEAVE = 32; 620 | GROUP_PARTICIPANT_CHANGE_NUMBER = 33; 621 | BROADCAST_CREATE = 34; 622 | BROADCAST_ADD = 35; 623 | BROADCAST_REMOVE = 36; 624 | GENERIC_NOTIFICATION = 37; 625 | E2E_IDENTITY_CHANGED = 38; 626 | E2E_ENCRYPTED = 39; 627 | CALL_MISSED_VOICE = 40; 628 | CALL_MISSED_VIDEO = 41; 629 | INDIVIDUAL_CHANGE_NUMBER = 42; 630 | GROUP_DELETE = 43; 631 | GROUP_ANNOUNCE_MODE_MESSAGE_BOUNCE = 44; 632 | CALL_MISSED_GROUP_VOICE = 45; 633 | CALL_MISSED_GROUP_VIDEO = 46; 634 | PAYMENT_CIPHERTEXT = 47; 635 | PAYMENT_FUTUREPROOF = 48; 636 | PAYMENT_TRANSACTION_STATUS_UPDATE_FAILED = 49; 637 | PAYMENT_TRANSACTION_STATUS_UPDATE_REFUNDED = 50; 638 | PAYMENT_TRANSACTION_STATUS_UPDATE_REFUND_FAILED = 51; 639 | PAYMENT_TRANSACTION_STATUS_RECEIVER_PENDING_SETUP = 52; 640 | PAYMENT_TRANSACTION_STATUS_RECEIVER_SUCCESS_AFTER_HICCUP = 53; 641 | PAYMENT_ACTION_ACCOUNT_SETUP_REMINDER = 54; 642 | PAYMENT_ACTION_SEND_PAYMENT_REMINDER = 55; 643 | PAYMENT_ACTION_SEND_PAYMENT_INVITATION = 56; 644 | PAYMENT_ACTION_REQUEST_DECLINED = 57; 645 | PAYMENT_ACTION_REQUEST_EXPIRED = 58; 646 | PAYMENT_ACTION_REQUEST_CANCELLED = 59; 647 | BIZ_VERIFIED_TRANSITION_TOP_TO_BOTTOM = 60; 648 | BIZ_VERIFIED_TRANSITION_BOTTOM_TO_TOP = 61; 649 | BIZ_INTRO_TOP = 62; 650 | BIZ_INTRO_BOTTOM = 63; 651 | BIZ_NAME_CHANGE = 64; 652 | BIZ_MOVE_TO_CONSUMER_APP = 65; 653 | BIZ_TWO_TIER_MIGRATION_TOP = 66; 654 | BIZ_TWO_TIER_MIGRATION_BOTTOM = 67; 655 | OVERSIZED = 68; 656 | GROUP_CHANGE_NO_FREQUENTLY_FORWARDED = 69; 657 | GROUP_V4_ADD_INVITE_SENT = 70; 658 | GROUP_PARTICIPANT_ADD_REQUEST_JOIN = 71; 659 | CHANGE_EPHEMERAL_SETTING = 72; 660 | } 661 | optional WEB_MESSAGE_INFO_STUBTYPE messageStubType = 24; 662 | optional bool clearMedia = 25; 663 | repeated string messageStubParameters = 26; 664 | optional uint32 duration = 27; 665 | repeated string labels = 28; 666 | optional PaymentInfo paymentInfo = 29; 667 | optional LiveLocationMessage finalLiveLocation = 30; 668 | optional PaymentInfo quotedPaymentInfo = 31; 669 | optional uint64 ephemeralStartTimestamp = 32; 670 | optional uint32 ephemeralDuration = 33; 671 | } -------------------------------------------------------------------------------- /binary_coding/whatsapp_message_coding.json: -------------------------------------------------------------------------------- 1 | { 2 | "nested": { 3 | "proto": { 4 | "nested": { 5 | "HydratedQuickReplyButton": { 6 | "fields": { 7 | "displayText": { 8 | "type": "string", 9 | "id": 1 10 | }, 11 | "id": { 12 | "type": "string", 13 | "id": 2 14 | } 15 | } 16 | }, 17 | "HydratedURLButton": { 18 | "fields": { 19 | "displayText": { 20 | "type": "string", 21 | "id": 1 22 | }, 23 | "url": { 24 | "type": "string", 25 | "id": 2 26 | } 27 | } 28 | }, 29 | "HydratedCallButton": { 30 | "fields": { 31 | "displayText": { 32 | "type": "string", 33 | "id": 1 34 | }, 35 | "phoneNumber": { 36 | "type": "string", 37 | "id": 2 38 | } 39 | } 40 | }, 41 | "HydratedTemplateButton": { 42 | "oneofs": { 43 | "hydratedButton": { 44 | "oneof": [ 45 | "quickReplyButton", 46 | "urlButton", 47 | "callButton" 48 | ] 49 | } 50 | }, 51 | "fields": { 52 | "index": { 53 | "type": "uint32", 54 | "id": 4 55 | }, 56 | "quickReplyButton": { 57 | "type": "HydratedQuickReplyButton", 58 | "id": 1 59 | }, 60 | "urlButton": { 61 | "type": "HydratedURLButton", 62 | "id": 2 63 | }, 64 | "callButton": { 65 | "type": "HydratedCallButton", 66 | "id": 3 67 | } 68 | } 69 | }, 70 | "QuickReplyButton": { 71 | "fields": { 72 | "displayText": { 73 | "type": "HighlyStructuredMessage", 74 | "id": 1 75 | }, 76 | "id": { 77 | "type": "string", 78 | "id": 2 79 | } 80 | } 81 | }, 82 | "URLButton": { 83 | "fields": { 84 | "displayText": { 85 | "type": "HighlyStructuredMessage", 86 | "id": 1 87 | }, 88 | "url": { 89 | "type": "HighlyStructuredMessage", 90 | "id": 2 91 | } 92 | } 93 | }, 94 | "CallButton": { 95 | "fields": { 96 | "displayText": { 97 | "type": "HighlyStructuredMessage", 98 | "id": 1 99 | }, 100 | "phoneNumber": { 101 | "type": "HighlyStructuredMessage", 102 | "id": 2 103 | } 104 | } 105 | }, 106 | "TemplateButton": { 107 | "oneofs": { 108 | "button": { 109 | "oneof": [ 110 | "quickReplyButton", 111 | "urlButton", 112 | "callButton" 113 | ] 114 | } 115 | }, 116 | "fields": { 117 | "index": { 118 | "type": "uint32", 119 | "id": 4 120 | }, 121 | "quickReplyButton": { 122 | "type": "QuickReplyButton", 123 | "id": 1 124 | }, 125 | "urlButton": { 126 | "type": "URLButton", 127 | "id": 2 128 | }, 129 | "callButton": { 130 | "type": "CallButton", 131 | "id": 3 132 | } 133 | } 134 | }, 135 | "Location": { 136 | "fields": { 137 | "degreesLatitude": { 138 | "type": "double", 139 | "id": 1 140 | }, 141 | "degreesLongitude": { 142 | "type": "double", 143 | "id": 2 144 | }, 145 | "name": { 146 | "type": "string", 147 | "id": 3 148 | } 149 | } 150 | }, 151 | "Point": { 152 | "fields": { 153 | "x": { 154 | "type": "double", 155 | "id": 3 156 | }, 157 | "y": { 158 | "type": "double", 159 | "id": 4 160 | } 161 | } 162 | }, 163 | "InteractiveAnnotation": { 164 | "oneofs": { 165 | "action": { 166 | "oneof": [ 167 | "location" 168 | ] 169 | } 170 | }, 171 | "fields": { 172 | "polygonVertices": { 173 | "rule": "repeated", 174 | "type": "Point", 175 | "id": 1 176 | }, 177 | "location": { 178 | "type": "Location", 179 | "id": 2 180 | } 181 | } 182 | }, 183 | "AdReplyInfo": { 184 | "fields": { 185 | "advertiserName": { 186 | "type": "string", 187 | "id": 1 188 | }, 189 | "mediaType": { 190 | "type": "AD_REPLY_INFO_MEDIATYPE", 191 | "id": 2 192 | }, 193 | "jpegThumbnail": { 194 | "type": "bytes", 195 | "id": 16 196 | }, 197 | "caption": { 198 | "type": "string", 199 | "id": 17 200 | } 201 | }, 202 | "nested": { 203 | "AD_REPLY_INFO_MEDIATYPE": { 204 | "values": { 205 | "NONE": 0, 206 | "IMAGE": 1, 207 | "VIDEO": 2 208 | } 209 | } 210 | } 211 | }, 212 | "ContextInfo": { 213 | "fields": { 214 | "stanzaId": { 215 | "type": "string", 216 | "id": 1 217 | }, 218 | "participant": { 219 | "type": "string", 220 | "id": 2 221 | }, 222 | "quotedMessage": { 223 | "type": "Message", 224 | "id": 3 225 | }, 226 | "remoteJid": { 227 | "type": "string", 228 | "id": 4 229 | }, 230 | "mentionedJid": { 231 | "rule": "repeated", 232 | "type": "string", 233 | "id": 15 234 | }, 235 | "conversionSource": { 236 | "type": "string", 237 | "id": 18 238 | }, 239 | "conversionData": { 240 | "type": "bytes", 241 | "id": 19 242 | }, 243 | "conversionDelaySeconds": { 244 | "type": "uint32", 245 | "id": 20 246 | }, 247 | "forwardingScore": { 248 | "type": "uint32", 249 | "id": 21 250 | }, 251 | "isForwarded": { 252 | "type": "bool", 253 | "id": 22 254 | }, 255 | "quotedAd": { 256 | "type": "AdReplyInfo", 257 | "id": 23 258 | }, 259 | "placeholderKey": { 260 | "type": "MessageKey", 261 | "id": 24 262 | }, 263 | "expiration": { 264 | "type": "uint32", 265 | "id": 25 266 | } 267 | } 268 | }, 269 | "SenderKeyDistributionMessage": { 270 | "fields": { 271 | "groupId": { 272 | "type": "string", 273 | "id": 1 274 | }, 275 | "axolotlSenderKeyDistributionMessage": { 276 | "type": "bytes", 277 | "id": 2 278 | } 279 | } 280 | }, 281 | "ImageMessage": { 282 | "fields": { 283 | "url": { 284 | "type": "string", 285 | "id": 1 286 | }, 287 | "mimetype": { 288 | "type": "string", 289 | "id": 2 290 | }, 291 | "caption": { 292 | "type": "string", 293 | "id": 3 294 | }, 295 | "fileSha256": { 296 | "type": "bytes", 297 | "id": 4 298 | }, 299 | "fileLength": { 300 | "type": "uint64", 301 | "id": 5 302 | }, 303 | "height": { 304 | "type": "uint32", 305 | "id": 6 306 | }, 307 | "width": { 308 | "type": "uint32", 309 | "id": 7 310 | }, 311 | "mediaKey": { 312 | "type": "bytes", 313 | "id": 8 314 | }, 315 | "fileEncSha256": { 316 | "type": "bytes", 317 | "id": 9 318 | }, 319 | "interactiveAnnotations": { 320 | "rule": "repeated", 321 | "type": "InteractiveAnnotation", 322 | "id": 10 323 | }, 324 | "directPath": { 325 | "type": "string", 326 | "id": 11 327 | }, 328 | "mediaKeyTimestamp": { 329 | "type": "int64", 330 | "id": 12 331 | }, 332 | "jpegThumbnail": { 333 | "type": "bytes", 334 | "id": 16 335 | }, 336 | "contextInfo": { 337 | "type": "ContextInfo", 338 | "id": 17 339 | }, 340 | "firstScanSidecar": { 341 | "type": "bytes", 342 | "id": 18 343 | }, 344 | "firstScanLength": { 345 | "type": "uint32", 346 | "id": 19 347 | }, 348 | "experimentGroupId": { 349 | "type": "uint32", 350 | "id": 20 351 | }, 352 | "scansSidecar": { 353 | "type": "bytes", 354 | "id": 21 355 | }, 356 | "scanLengths": { 357 | "rule": "repeated", 358 | "type": "uint32", 359 | "id": 22, 360 | "options": { 361 | "packed": false 362 | } 363 | }, 364 | "midQualityFileSha256": { 365 | "type": "bytes", 366 | "id": 23 367 | }, 368 | "midQualityFileEncSha256": { 369 | "type": "bytes", 370 | "id": 24 371 | } 372 | } 373 | }, 374 | "ContactMessage": { 375 | "fields": { 376 | "displayName": { 377 | "type": "string", 378 | "id": 1 379 | }, 380 | "vcard": { 381 | "type": "string", 382 | "id": 16 383 | }, 384 | "contextInfo": { 385 | "type": "ContextInfo", 386 | "id": 17 387 | } 388 | } 389 | }, 390 | "LocationMessage": { 391 | "fields": { 392 | "degreesLatitude": { 393 | "type": "double", 394 | "id": 1 395 | }, 396 | "degreesLongitude": { 397 | "type": "double", 398 | "id": 2 399 | }, 400 | "name": { 401 | "type": "string", 402 | "id": 3 403 | }, 404 | "address": { 405 | "type": "string", 406 | "id": 4 407 | }, 408 | "url": { 409 | "type": "string", 410 | "id": 5 411 | }, 412 | "jpegThumbnail": { 413 | "type": "bytes", 414 | "id": 16 415 | }, 416 | "contextInfo": { 417 | "type": "ContextInfo", 418 | "id": 17 419 | } 420 | } 421 | }, 422 | "ExtendedTextMessage": { 423 | "fields": { 424 | "text": { 425 | "type": "string", 426 | "id": 1 427 | }, 428 | "matchedText": { 429 | "type": "string", 430 | "id": 2 431 | }, 432 | "canonicalUrl": { 433 | "type": "string", 434 | "id": 4 435 | }, 436 | "description": { 437 | "type": "string", 438 | "id": 5 439 | }, 440 | "title": { 441 | "type": "string", 442 | "id": 6 443 | }, 444 | "textArgb": { 445 | "type": "fixed32", 446 | "id": 7 447 | }, 448 | "backgroundArgb": { 449 | "type": "fixed32", 450 | "id": 8 451 | }, 452 | "font": { 453 | "type": "EXTENDED_TEXT_MESSAGE_FONTTYPE", 454 | "id": 9 455 | }, 456 | "previewType": { 457 | "type": "EXTENDED_TEXT_MESSAGE_PREVIEWTYPE", 458 | "id": 10 459 | }, 460 | "jpegThumbnail": { 461 | "type": "bytes", 462 | "id": 16 463 | }, 464 | "contextInfo": { 465 | "type": "ContextInfo", 466 | "id": 17 467 | }, 468 | "doNotPlayInline": { 469 | "type": "bool", 470 | "id": 18 471 | } 472 | }, 473 | "nested": { 474 | "EXTENDED_TEXT_MESSAGE_FONTTYPE": { 475 | "values": { 476 | "SANS_SERIF": 0, 477 | "SERIF": 1, 478 | "NORICAN_REGULAR": 2, 479 | "BRYNDAN_WRITE": 3, 480 | "BEBASNEUE_REGULAR": 4, 481 | "OSWALD_HEAVY": 5 482 | } 483 | }, 484 | "EXTENDED_TEXT_MESSAGE_PREVIEWTYPE": { 485 | "values": { 486 | "NONE": 0, 487 | "VIDEO": 1 488 | } 489 | } 490 | } 491 | }, 492 | "DocumentMessage": { 493 | "fields": { 494 | "url": { 495 | "type": "string", 496 | "id": 1 497 | }, 498 | "mimetype": { 499 | "type": "string", 500 | "id": 2 501 | }, 502 | "title": { 503 | "type": "string", 504 | "id": 3 505 | }, 506 | "fileSha256": { 507 | "type": "bytes", 508 | "id": 4 509 | }, 510 | "fileLength": { 511 | "type": "uint64", 512 | "id": 5 513 | }, 514 | "pageCount": { 515 | "type": "uint32", 516 | "id": 6 517 | }, 518 | "mediaKey": { 519 | "type": "bytes", 520 | "id": 7 521 | }, 522 | "fileName": { 523 | "type": "string", 524 | "id": 8 525 | }, 526 | "fileEncSha256": { 527 | "type": "bytes", 528 | "id": 9 529 | }, 530 | "directPath": { 531 | "type": "string", 532 | "id": 10 533 | }, 534 | "mediaKeyTimestamp": { 535 | "type": "int64", 536 | "id": 11 537 | }, 538 | "jpegThumbnail": { 539 | "type": "bytes", 540 | "id": 16 541 | }, 542 | "contextInfo": { 543 | "type": "ContextInfo", 544 | "id": 17 545 | } 546 | } 547 | }, 548 | "AudioMessage": { 549 | "fields": { 550 | "url": { 551 | "type": "string", 552 | "id": 1 553 | }, 554 | "mimetype": { 555 | "type": "string", 556 | "id": 2 557 | }, 558 | "fileSha256": { 559 | "type": "bytes", 560 | "id": 3 561 | }, 562 | "fileLength": { 563 | "type": "uint64", 564 | "id": 4 565 | }, 566 | "seconds": { 567 | "type": "uint32", 568 | "id": 5 569 | }, 570 | "ptt": { 571 | "type": "bool", 572 | "id": 6 573 | }, 574 | "mediaKey": { 575 | "type": "bytes", 576 | "id": 7 577 | }, 578 | "fileEncSha256": { 579 | "type": "bytes", 580 | "id": 8 581 | }, 582 | "directPath": { 583 | "type": "string", 584 | "id": 9 585 | }, 586 | "mediaKeyTimestamp": { 587 | "type": "int64", 588 | "id": 10 589 | }, 590 | "contextInfo": { 591 | "type": "ContextInfo", 592 | "id": 17 593 | }, 594 | "streamingSidecar": { 595 | "type": "bytes", 596 | "id": 18 597 | } 598 | } 599 | }, 600 | "VideoMessage": { 601 | "fields": { 602 | "url": { 603 | "type": "string", 604 | "id": 1 605 | }, 606 | "mimetype": { 607 | "type": "string", 608 | "id": 2 609 | }, 610 | "fileSha256": { 611 | "type": "bytes", 612 | "id": 3 613 | }, 614 | "fileLength": { 615 | "type": "uint64", 616 | "id": 4 617 | }, 618 | "seconds": { 619 | "type": "uint32", 620 | "id": 5 621 | }, 622 | "mediaKey": { 623 | "type": "bytes", 624 | "id": 6 625 | }, 626 | "caption": { 627 | "type": "string", 628 | "id": 7 629 | }, 630 | "gifPlayback": { 631 | "type": "bool", 632 | "id": 8 633 | }, 634 | "height": { 635 | "type": "uint32", 636 | "id": 9 637 | }, 638 | "width": { 639 | "type": "uint32", 640 | "id": 10 641 | }, 642 | "fileEncSha256": { 643 | "type": "bytes", 644 | "id": 11 645 | }, 646 | "interactiveAnnotations": { 647 | "rule": "repeated", 648 | "type": "InteractiveAnnotation", 649 | "id": 12 650 | }, 651 | "directPath": { 652 | "type": "string", 653 | "id": 13 654 | }, 655 | "mediaKeyTimestamp": { 656 | "type": "int64", 657 | "id": 14 658 | }, 659 | "jpegThumbnail": { 660 | "type": "bytes", 661 | "id": 16 662 | }, 663 | "contextInfo": { 664 | "type": "ContextInfo", 665 | "id": 17 666 | }, 667 | "streamingSidecar": { 668 | "type": "bytes", 669 | "id": 18 670 | }, 671 | "gifAttribution": { 672 | "type": "VIDEO_MESSAGE_ATTRIBUTION", 673 | "id": 19 674 | } 675 | }, 676 | "nested": { 677 | "VIDEO_MESSAGE_ATTRIBUTION": { 678 | "values": { 679 | "NONE": 0, 680 | "GIPHY": 1, 681 | "TENOR": 2 682 | } 683 | } 684 | } 685 | }, 686 | "Call": { 687 | "fields": { 688 | "callKey": { 689 | "type": "bytes", 690 | "id": 1 691 | } 692 | } 693 | }, 694 | "Chat": { 695 | "fields": { 696 | "displayName": { 697 | "type": "string", 698 | "id": 1 699 | }, 700 | "id": { 701 | "type": "string", 702 | "id": 2 703 | } 704 | } 705 | }, 706 | "ProtocolMessage": { 707 | "fields": { 708 | "key": { 709 | "type": "MessageKey", 710 | "id": 1 711 | }, 712 | "type": { 713 | "type": "PROTOCOL_MESSAGE_TYPE", 714 | "id": 2 715 | }, 716 | "ephemeralExpiration": { 717 | "type": "uint32", 718 | "id": 4 719 | } 720 | }, 721 | "nested": { 722 | "PROTOCOL_MESSAGE_TYPE": { 723 | "values": { 724 | "REVOKE": 0, 725 | "EPHEMERAL_SETTING": 3 726 | } 727 | } 728 | } 729 | }, 730 | "ContactsArrayMessage": { 731 | "fields": { 732 | "displayName": { 733 | "type": "string", 734 | "id": 1 735 | }, 736 | "contacts": { 737 | "rule": "repeated", 738 | "type": "ContactMessage", 739 | "id": 2 740 | }, 741 | "contextInfo": { 742 | "type": "ContextInfo", 743 | "id": 17 744 | } 745 | } 746 | }, 747 | "HSMCurrency": { 748 | "fields": { 749 | "currencyCode": { 750 | "type": "string", 751 | "id": 1 752 | }, 753 | "amount1000": { 754 | "type": "int64", 755 | "id": 2 756 | } 757 | } 758 | }, 759 | "HSMDateTimeComponent": { 760 | "fields": { 761 | "dayOfWeek": { 762 | "type": "HSM_DATE_TIME_COMPONENT_DAYOFWEEKTYPE", 763 | "id": 1 764 | }, 765 | "year": { 766 | "type": "uint32", 767 | "id": 2 768 | }, 769 | "month": { 770 | "type": "uint32", 771 | "id": 3 772 | }, 773 | "dayOfMonth": { 774 | "type": "uint32", 775 | "id": 4 776 | }, 777 | "hour": { 778 | "type": "uint32", 779 | "id": 5 780 | }, 781 | "minute": { 782 | "type": "uint32", 783 | "id": 6 784 | }, 785 | "calendar": { 786 | "type": "HSM_DATE_TIME_COMPONENT_CALENDARTYPE", 787 | "id": 7 788 | } 789 | }, 790 | "nested": { 791 | "HSM_DATE_TIME_COMPONENT_DAYOFWEEKTYPE": { 792 | "values": { 793 | "MONDAY": 1, 794 | "TUESDAY": 2, 795 | "WEDNESDAY": 3, 796 | "THURSDAY": 4, 797 | "FRIDAY": 5, 798 | "SATURDAY": 6, 799 | "SUNDAY": 7 800 | } 801 | }, 802 | "HSM_DATE_TIME_COMPONENT_CALENDARTYPE": { 803 | "values": { 804 | "GREGORIAN": 1, 805 | "SOLAR_HIJRI": 2 806 | } 807 | } 808 | } 809 | }, 810 | "HSMDateTimeUnixEpoch": { 811 | "fields": { 812 | "timestamp": { 813 | "type": "int64", 814 | "id": 1 815 | } 816 | } 817 | }, 818 | "HSMDateTime": { 819 | "oneofs": { 820 | "datetimeOneof": { 821 | "oneof": [ 822 | "component", 823 | "unixEpoch" 824 | ] 825 | } 826 | }, 827 | "fields": { 828 | "component": { 829 | "type": "HSMDateTimeComponent", 830 | "id": 1 831 | }, 832 | "unixEpoch": { 833 | "type": "HSMDateTimeUnixEpoch", 834 | "id": 2 835 | } 836 | } 837 | }, 838 | "HSMLocalizableParameter": { 839 | "oneofs": { 840 | "paramOneof": { 841 | "oneof": [ 842 | "currency", 843 | "dateTime" 844 | ] 845 | } 846 | }, 847 | "fields": { 848 | "default": { 849 | "type": "string", 850 | "id": 1 851 | }, 852 | "currency": { 853 | "type": "HSMCurrency", 854 | "id": 2 855 | }, 856 | "dateTime": { 857 | "type": "HSMDateTime", 858 | "id": 3 859 | } 860 | } 861 | }, 862 | "HighlyStructuredMessage": { 863 | "fields": { 864 | "namespace": { 865 | "type": "string", 866 | "id": 1 867 | }, 868 | "elementName": { 869 | "type": "string", 870 | "id": 2 871 | }, 872 | "params": { 873 | "rule": "repeated", 874 | "type": "string", 875 | "id": 3 876 | }, 877 | "fallbackLg": { 878 | "type": "string", 879 | "id": 4 880 | }, 881 | "fallbackLc": { 882 | "type": "string", 883 | "id": 5 884 | }, 885 | "localizableParams": { 886 | "rule": "repeated", 887 | "type": "HSMLocalizableParameter", 888 | "id": 6 889 | }, 890 | "deterministicLg": { 891 | "type": "string", 892 | "id": 7 893 | }, 894 | "deterministicLc": { 895 | "type": "string", 896 | "id": 8 897 | }, 898 | "hydratedHsm": { 899 | "type": "TemplateMessage", 900 | "id": 9 901 | } 902 | } 903 | }, 904 | "SendPaymentMessage": { 905 | "fields": { 906 | "noteMessage": { 907 | "type": "Message", 908 | "id": 2 909 | }, 910 | "requestMessageKey": { 911 | "type": "MessageKey", 912 | "id": 3 913 | } 914 | } 915 | }, 916 | "RequestPaymentMessage": { 917 | "fields": { 918 | "noteMessage": { 919 | "type": "Message", 920 | "id": 4 921 | }, 922 | "currencyCodeIso4217": { 923 | "type": "string", 924 | "id": 1 925 | }, 926 | "amount1000": { 927 | "type": "uint64", 928 | "id": 2 929 | }, 930 | "requestFrom": { 931 | "type": "string", 932 | "id": 3 933 | }, 934 | "expiryTimestamp": { 935 | "type": "int64", 936 | "id": 5 937 | } 938 | } 939 | }, 940 | "DeclinePaymentRequestMessage": { 941 | "fields": { 942 | "key": { 943 | "type": "MessageKey", 944 | "id": 1 945 | } 946 | } 947 | }, 948 | "CancelPaymentRequestMessage": { 949 | "fields": { 950 | "key": { 951 | "type": "MessageKey", 952 | "id": 1 953 | } 954 | } 955 | }, 956 | "LiveLocationMessage": { 957 | "fields": { 958 | "degreesLatitude": { 959 | "type": "double", 960 | "id": 1 961 | }, 962 | "degreesLongitude": { 963 | "type": "double", 964 | "id": 2 965 | }, 966 | "accuracyInMeters": { 967 | "type": "uint32", 968 | "id": 3 969 | }, 970 | "speedInMps": { 971 | "type": "float", 972 | "id": 4 973 | }, 974 | "degreesClockwiseFromMagneticNorth": { 975 | "type": "uint32", 976 | "id": 5 977 | }, 978 | "caption": { 979 | "type": "string", 980 | "id": 6 981 | }, 982 | "sequenceNumber": { 983 | "type": "int64", 984 | "id": 7 985 | }, 986 | "timeOffset": { 987 | "type": "uint32", 988 | "id": 8 989 | }, 990 | "jpegThumbnail": { 991 | "type": "bytes", 992 | "id": 16 993 | }, 994 | "contextInfo": { 995 | "type": "ContextInfo", 996 | "id": 17 997 | } 998 | } 999 | }, 1000 | "StickerMessage": { 1001 | "fields": { 1002 | "url": { 1003 | "type": "string", 1004 | "id": 1 1005 | }, 1006 | "fileSha256": { 1007 | "type": "bytes", 1008 | "id": 2 1009 | }, 1010 | "fileEncSha256": { 1011 | "type": "bytes", 1012 | "id": 3 1013 | }, 1014 | "mediaKey": { 1015 | "type": "bytes", 1016 | "id": 4 1017 | }, 1018 | "mimetype": { 1019 | "type": "string", 1020 | "id": 5 1021 | }, 1022 | "height": { 1023 | "type": "uint32", 1024 | "id": 6 1025 | }, 1026 | "width": { 1027 | "type": "uint32", 1028 | "id": 7 1029 | }, 1030 | "directPath": { 1031 | "type": "string", 1032 | "id": 8 1033 | }, 1034 | "fileLength": { 1035 | "type": "uint64", 1036 | "id": 9 1037 | }, 1038 | "mediaKeyTimestamp": { 1039 | "type": "int64", 1040 | "id": 10 1041 | }, 1042 | "firstFrameLength": { 1043 | "type": "uint32", 1044 | "id": 11 1045 | }, 1046 | "firstFrameSidecar": { 1047 | "type": "bytes", 1048 | "id": 12 1049 | }, 1050 | "contextInfo": { 1051 | "type": "ContextInfo", 1052 | "id": 17 1053 | } 1054 | } 1055 | }, 1056 | "FourRowTemplate": { 1057 | "oneofs": { 1058 | "title": { 1059 | "oneof": [ 1060 | "documentMessage", 1061 | "highlyStructuredMessage", 1062 | "imageMessage", 1063 | "videoMessage", 1064 | "locationMessage" 1065 | ] 1066 | } 1067 | }, 1068 | "fields": { 1069 | "content": { 1070 | "type": "HighlyStructuredMessage", 1071 | "id": 6 1072 | }, 1073 | "footer": { 1074 | "type": "HighlyStructuredMessage", 1075 | "id": 7 1076 | }, 1077 | "buttons": { 1078 | "rule": "repeated", 1079 | "type": "TemplateButton", 1080 | "id": 8 1081 | }, 1082 | "documentMessage": { 1083 | "type": "DocumentMessage", 1084 | "id": 1 1085 | }, 1086 | "highlyStructuredMessage": { 1087 | "type": "HighlyStructuredMessage", 1088 | "id": 2 1089 | }, 1090 | "imageMessage": { 1091 | "type": "ImageMessage", 1092 | "id": 3 1093 | }, 1094 | "videoMessage": { 1095 | "type": "VideoMessage", 1096 | "id": 4 1097 | }, 1098 | "locationMessage": { 1099 | "type": "LocationMessage", 1100 | "id": 5 1101 | } 1102 | } 1103 | }, 1104 | "HydratedFourRowTemplate": { 1105 | "oneofs": { 1106 | "title": { 1107 | "oneof": [ 1108 | "documentMessage", 1109 | "hydratedTitleText", 1110 | "imageMessage", 1111 | "videoMessage", 1112 | "locationMessage" 1113 | ] 1114 | } 1115 | }, 1116 | "fields": { 1117 | "hydratedContentText": { 1118 | "type": "string", 1119 | "id": 6 1120 | }, 1121 | "hydratedFooterText": { 1122 | "type": "string", 1123 | "id": 7 1124 | }, 1125 | "hydratedButtons": { 1126 | "rule": "repeated", 1127 | "type": "HydratedTemplateButton", 1128 | "id": 8 1129 | }, 1130 | "templateId": { 1131 | "type": "string", 1132 | "id": 9 1133 | }, 1134 | "documentMessage": { 1135 | "type": "DocumentMessage", 1136 | "id": 1 1137 | }, 1138 | "hydratedTitleText": { 1139 | "type": "string", 1140 | "id": 2 1141 | }, 1142 | "imageMessage": { 1143 | "type": "ImageMessage", 1144 | "id": 3 1145 | }, 1146 | "videoMessage": { 1147 | "type": "VideoMessage", 1148 | "id": 4 1149 | }, 1150 | "locationMessage": { 1151 | "type": "LocationMessage", 1152 | "id": 5 1153 | } 1154 | } 1155 | }, 1156 | "TemplateMessage": { 1157 | "oneofs": { 1158 | "format": { 1159 | "oneof": [ 1160 | "fourRowTemplate", 1161 | "hydratedFourRowTemplate" 1162 | ] 1163 | } 1164 | }, 1165 | "fields": { 1166 | "contextInfo": { 1167 | "type": "ContextInfo", 1168 | "id": 3 1169 | }, 1170 | "hydratedTemplate": { 1171 | "type": "HydratedFourRowTemplate", 1172 | "id": 4 1173 | }, 1174 | "fourRowTemplate": { 1175 | "type": "FourRowTemplate", 1176 | "id": 1 1177 | }, 1178 | "hydratedFourRowTemplate": { 1179 | "type": "HydratedFourRowTemplate", 1180 | "id": 2 1181 | } 1182 | } 1183 | }, 1184 | "TemplateButtonReplyMessage": { 1185 | "fields": { 1186 | "selectedId": { 1187 | "type": "string", 1188 | "id": 1 1189 | }, 1190 | "selectedDisplayText": { 1191 | "type": "string", 1192 | "id": 2 1193 | }, 1194 | "contextInfo": { 1195 | "type": "ContextInfo", 1196 | "id": 3 1197 | }, 1198 | "selectedIndex": { 1199 | "type": "uint32", 1200 | "id": 4 1201 | } 1202 | } 1203 | }, 1204 | "ProductSnapshot": { 1205 | "fields": { 1206 | "productImage": { 1207 | "type": "ImageMessage", 1208 | "id": 1 1209 | }, 1210 | "productId": { 1211 | "type": "string", 1212 | "id": 2 1213 | }, 1214 | "title": { 1215 | "type": "string", 1216 | "id": 3 1217 | }, 1218 | "description": { 1219 | "type": "string", 1220 | "id": 4 1221 | }, 1222 | "currencyCode": { 1223 | "type": "string", 1224 | "id": 5 1225 | }, 1226 | "priceAmount1000": { 1227 | "type": "int64", 1228 | "id": 6 1229 | }, 1230 | "retailerId": { 1231 | "type": "string", 1232 | "id": 7 1233 | }, 1234 | "url": { 1235 | "type": "string", 1236 | "id": 8 1237 | }, 1238 | "productImageCount": { 1239 | "type": "uint32", 1240 | "id": 9 1241 | }, 1242 | "firstImageId": { 1243 | "type": "string", 1244 | "id": 11 1245 | } 1246 | } 1247 | }, 1248 | "ProductMessage": { 1249 | "fields": { 1250 | "product": { 1251 | "type": "ProductSnapshot", 1252 | "id": 1 1253 | }, 1254 | "businessOwnerJid": { 1255 | "type": "string", 1256 | "id": 2 1257 | }, 1258 | "contextInfo": { 1259 | "type": "ContextInfo", 1260 | "id": 17 1261 | } 1262 | } 1263 | }, 1264 | "GroupInviteMessage": { 1265 | "fields": { 1266 | "groupJid": { 1267 | "type": "string", 1268 | "id": 1 1269 | }, 1270 | "inviteCode": { 1271 | "type": "string", 1272 | "id": 2 1273 | }, 1274 | "inviteExpiration": { 1275 | "type": "int64", 1276 | "id": 3 1277 | }, 1278 | "groupName": { 1279 | "type": "string", 1280 | "id": 4 1281 | }, 1282 | "jpegThumbnail": { 1283 | "type": "bytes", 1284 | "id": 5 1285 | }, 1286 | "caption": { 1287 | "type": "string", 1288 | "id": 6 1289 | }, 1290 | "contextInfo": { 1291 | "type": "ContextInfo", 1292 | "id": 7 1293 | } 1294 | } 1295 | }, 1296 | "DeviceSentMessage": { 1297 | "fields": { 1298 | "destinationJid": { 1299 | "type": "string", 1300 | "id": 1 1301 | }, 1302 | "message": { 1303 | "type": "Message", 1304 | "id": 2 1305 | } 1306 | } 1307 | }, 1308 | "DeviceSyncMessage": { 1309 | "fields": { 1310 | "serializedXmlBytes": { 1311 | "type": "bytes", 1312 | "id": 1 1313 | } 1314 | } 1315 | }, 1316 | "Message": { 1317 | "fields": { 1318 | "conversation": { 1319 | "type": "string", 1320 | "id": 1 1321 | }, 1322 | "senderKeyDistributionMessage": { 1323 | "type": "SenderKeyDistributionMessage", 1324 | "id": 2 1325 | }, 1326 | "imageMessage": { 1327 | "type": "ImageMessage", 1328 | "id": 3 1329 | }, 1330 | "contactMessage": { 1331 | "type": "ContactMessage", 1332 | "id": 4 1333 | }, 1334 | "locationMessage": { 1335 | "type": "LocationMessage", 1336 | "id": 5 1337 | }, 1338 | "extendedTextMessage": { 1339 | "type": "ExtendedTextMessage", 1340 | "id": 6 1341 | }, 1342 | "documentMessage": { 1343 | "type": "DocumentMessage", 1344 | "id": 7 1345 | }, 1346 | "audioMessage": { 1347 | "type": "AudioMessage", 1348 | "id": 8 1349 | }, 1350 | "videoMessage": { 1351 | "type": "VideoMessage", 1352 | "id": 9 1353 | }, 1354 | "call": { 1355 | "type": "Call", 1356 | "id": 10 1357 | }, 1358 | "chat": { 1359 | "type": "Chat", 1360 | "id": 11 1361 | }, 1362 | "protocolMessage": { 1363 | "type": "ProtocolMessage", 1364 | "id": 12 1365 | }, 1366 | "contactsArrayMessage": { 1367 | "type": "ContactsArrayMessage", 1368 | "id": 13 1369 | }, 1370 | "highlyStructuredMessage": { 1371 | "type": "HighlyStructuredMessage", 1372 | "id": 14 1373 | }, 1374 | "fastRatchetKeySenderKeyDistributionMessage": { 1375 | "type": "SenderKeyDistributionMessage", 1376 | "id": 15 1377 | }, 1378 | "sendPaymentMessage": { 1379 | "type": "SendPaymentMessage", 1380 | "id": 16 1381 | }, 1382 | "liveLocationMessage": { 1383 | "type": "LiveLocationMessage", 1384 | "id": 18 1385 | }, 1386 | "requestPaymentMessage": { 1387 | "type": "RequestPaymentMessage", 1388 | "id": 22 1389 | }, 1390 | "declinePaymentRequestMessage": { 1391 | "type": "DeclinePaymentRequestMessage", 1392 | "id": 23 1393 | }, 1394 | "cancelPaymentRequestMessage": { 1395 | "type": "CancelPaymentRequestMessage", 1396 | "id": 24 1397 | }, 1398 | "templateMessage": { 1399 | "type": "TemplateMessage", 1400 | "id": 25 1401 | }, 1402 | "stickerMessage": { 1403 | "type": "StickerMessage", 1404 | "id": 26 1405 | }, 1406 | "groupInviteMessage": { 1407 | "type": "GroupInviteMessage", 1408 | "id": 28 1409 | }, 1410 | "templateButtonReplyMessage": { 1411 | "type": "TemplateButtonReplyMessage", 1412 | "id": 29 1413 | }, 1414 | "productMessage": { 1415 | "type": "ProductMessage", 1416 | "id": 30 1417 | }, 1418 | "deviceSentMessage": { 1419 | "type": "DeviceSentMessage", 1420 | "id": 31 1421 | }, 1422 | "deviceSyncMessage": { 1423 | "type": "DeviceSyncMessage", 1424 | "id": 32 1425 | } 1426 | } 1427 | }, 1428 | "MessageKey": { 1429 | "fields": { 1430 | "remoteJid": { 1431 | "type": "string", 1432 | "id": 1 1433 | }, 1434 | "fromMe": { 1435 | "type": "bool", 1436 | "id": 2 1437 | }, 1438 | "id": { 1439 | "type": "string", 1440 | "id": 3 1441 | }, 1442 | "participant": { 1443 | "type": "string", 1444 | "id": 4 1445 | } 1446 | } 1447 | }, 1448 | "WebFeatures": { 1449 | "fields": { 1450 | "labelsDisplay": { 1451 | "type": "WEB_FEATURES_FLAG", 1452 | "id": 1 1453 | }, 1454 | "voipIndividualOutgoing": { 1455 | "type": "WEB_FEATURES_FLAG", 1456 | "id": 2 1457 | }, 1458 | "groupsV3": { 1459 | "type": "WEB_FEATURES_FLAG", 1460 | "id": 3 1461 | }, 1462 | "groupsV3Create": { 1463 | "type": "WEB_FEATURES_FLAG", 1464 | "id": 4 1465 | }, 1466 | "changeNumberV2": { 1467 | "type": "WEB_FEATURES_FLAG", 1468 | "id": 5 1469 | }, 1470 | "queryStatusV3Thumbnail": { 1471 | "type": "WEB_FEATURES_FLAG", 1472 | "id": 6 1473 | }, 1474 | "liveLocations": { 1475 | "type": "WEB_FEATURES_FLAG", 1476 | "id": 7 1477 | }, 1478 | "queryVname": { 1479 | "type": "WEB_FEATURES_FLAG", 1480 | "id": 8 1481 | }, 1482 | "voipIndividualIncoming": { 1483 | "type": "WEB_FEATURES_FLAG", 1484 | "id": 9 1485 | }, 1486 | "quickRepliesQuery": { 1487 | "type": "WEB_FEATURES_FLAG", 1488 | "id": 10 1489 | }, 1490 | "payments": { 1491 | "type": "WEB_FEATURES_FLAG", 1492 | "id": 11 1493 | }, 1494 | "stickerPackQuery": { 1495 | "type": "WEB_FEATURES_FLAG", 1496 | "id": 12 1497 | }, 1498 | "liveLocationsFinal": { 1499 | "type": "WEB_FEATURES_FLAG", 1500 | "id": 13 1501 | }, 1502 | "labelsEdit": { 1503 | "type": "WEB_FEATURES_FLAG", 1504 | "id": 14 1505 | }, 1506 | "mediaUpload": { 1507 | "type": "WEB_FEATURES_FLAG", 1508 | "id": 15 1509 | }, 1510 | "mediaUploadRichQuickReplies": { 1511 | "type": "WEB_FEATURES_FLAG", 1512 | "id": 18 1513 | }, 1514 | "vnameV2": { 1515 | "type": "WEB_FEATURES_FLAG", 1516 | "id": 19 1517 | }, 1518 | "videoPlaybackUrl": { 1519 | "type": "WEB_FEATURES_FLAG", 1520 | "id": 20 1521 | }, 1522 | "statusRanking": { 1523 | "type": "WEB_FEATURES_FLAG", 1524 | "id": 21 1525 | }, 1526 | "voipIndividualVideo": { 1527 | "type": "WEB_FEATURES_FLAG", 1528 | "id": 22 1529 | }, 1530 | "thirdPartyStickers": { 1531 | "type": "WEB_FEATURES_FLAG", 1532 | "id": 23 1533 | }, 1534 | "frequentlyForwardedSetting": { 1535 | "type": "WEB_FEATURES_FLAG", 1536 | "id": 24 1537 | }, 1538 | "groupsV4JoinPermission": { 1539 | "type": "WEB_FEATURES_FLAG", 1540 | "id": 25 1541 | }, 1542 | "recentStickers": { 1543 | "type": "WEB_FEATURES_FLAG", 1544 | "id": 26 1545 | }, 1546 | "catalog": { 1547 | "type": "WEB_FEATURES_FLAG", 1548 | "id": 27 1549 | }, 1550 | "starredStickers": { 1551 | "type": "WEB_FEATURES_FLAG", 1552 | "id": 28 1553 | }, 1554 | "voipGroupCall": { 1555 | "type": "WEB_FEATURES_FLAG", 1556 | "id": 29 1557 | }, 1558 | "templateMessage": { 1559 | "type": "WEB_FEATURES_FLAG", 1560 | "id": 30 1561 | }, 1562 | "templateMessageInteractivity": { 1563 | "type": "WEB_FEATURES_FLAG", 1564 | "id": 31 1565 | }, 1566 | "ephemeralMessages": { 1567 | "type": "WEB_FEATURES_FLAG", 1568 | "id": 32 1569 | } 1570 | }, 1571 | "nested": { 1572 | "WEB_FEATURES_FLAG": { 1573 | "values": { 1574 | "NOT_STARTED": 0, 1575 | "FORCE_UPGRADE": 1, 1576 | "DEVELOPMENT": 2, 1577 | "PRODUCTION": 3 1578 | } 1579 | } 1580 | } 1581 | }, 1582 | "TabletNotificationsInfo": { 1583 | "fields": { 1584 | "timestamp": { 1585 | "type": "uint64", 1586 | "id": 2 1587 | }, 1588 | "unreadChats": { 1589 | "type": "uint32", 1590 | "id": 3 1591 | }, 1592 | "notifyMessageCount": { 1593 | "type": "uint32", 1594 | "id": 4 1595 | }, 1596 | "notifyMessage": { 1597 | "rule": "repeated", 1598 | "type": "NotificationMessageInfo", 1599 | "id": 5 1600 | } 1601 | } 1602 | }, 1603 | "NotificationMessageInfo": { 1604 | "fields": { 1605 | "key": { 1606 | "type": "MessageKey", 1607 | "id": 1 1608 | }, 1609 | "message": { 1610 | "type": "Message", 1611 | "id": 2 1612 | }, 1613 | "messageTimestamp": { 1614 | "type": "uint64", 1615 | "id": 3 1616 | }, 1617 | "participant": { 1618 | "type": "string", 1619 | "id": 4 1620 | } 1621 | } 1622 | }, 1623 | "WebNotificationsInfo": { 1624 | "fields": { 1625 | "timestamp": { 1626 | "type": "uint64", 1627 | "id": 2 1628 | }, 1629 | "unreadChats": { 1630 | "type": "uint32", 1631 | "id": 3 1632 | }, 1633 | "notifyMessageCount": { 1634 | "type": "uint32", 1635 | "id": 4 1636 | }, 1637 | "notifyMessages": { 1638 | "rule": "repeated", 1639 | "type": "WebMessageInfo", 1640 | "id": 5 1641 | } 1642 | } 1643 | }, 1644 | "PaymentInfo": { 1645 | "fields": { 1646 | "amount1000": { 1647 | "type": "uint64", 1648 | "id": 2 1649 | }, 1650 | "receiverJid": { 1651 | "type": "string", 1652 | "id": 3 1653 | }, 1654 | "status": { 1655 | "type": "PAYMENT_INFO_STATUS", 1656 | "id": 4 1657 | }, 1658 | "transactionTimestamp": { 1659 | "type": "uint64", 1660 | "id": 5 1661 | }, 1662 | "requestMessageKey": { 1663 | "type": "MessageKey", 1664 | "id": 6 1665 | }, 1666 | "expiryTimestamp": { 1667 | "type": "uint64", 1668 | "id": 7 1669 | }, 1670 | "futureproofed": { 1671 | "type": "bool", 1672 | "id": 8 1673 | }, 1674 | "currency": { 1675 | "type": "string", 1676 | "id": 9 1677 | } 1678 | }, 1679 | "nested": { 1680 | "PAYMENT_INFO_STATUS": { 1681 | "values": { 1682 | "UNKNOWN_STATUS": 0, 1683 | "PROCESSING": 1, 1684 | "SENT": 2, 1685 | "NEED_TO_ACCEPT": 3, 1686 | "COMPLETE": 4, 1687 | "COULD_NOT_COMPLETE": 5, 1688 | "REFUNDED": 6, 1689 | "EXPIRED": 7, 1690 | "REJECTED": 8, 1691 | "CANCELLED": 9, 1692 | "WAITING_FOR_PAYER": 10, 1693 | "WAITING": 11 1694 | } 1695 | } 1696 | } 1697 | }, 1698 | "WebMessageInfo": { 1699 | "fields": { 1700 | "key": { 1701 | "rule": "required", 1702 | "type": "MessageKey", 1703 | "id": 1 1704 | }, 1705 | "message": { 1706 | "type": "Message", 1707 | "id": 2 1708 | }, 1709 | "messageTimestamp": { 1710 | "type": "uint64", 1711 | "id": 3 1712 | }, 1713 | "status": { 1714 | "type": "WEB_MESSAGE_INFO_STATUS", 1715 | "id": 4 1716 | }, 1717 | "participant": { 1718 | "type": "string", 1719 | "id": 5 1720 | }, 1721 | "ignore": { 1722 | "type": "bool", 1723 | "id": 16 1724 | }, 1725 | "starred": { 1726 | "type": "bool", 1727 | "id": 17 1728 | }, 1729 | "broadcast": { 1730 | "type": "bool", 1731 | "id": 18 1732 | }, 1733 | "pushName": { 1734 | "type": "string", 1735 | "id": 19 1736 | }, 1737 | "mediaCiphertextSha256": { 1738 | "type": "bytes", 1739 | "id": 20 1740 | }, 1741 | "multicast": { 1742 | "type": "bool", 1743 | "id": 21 1744 | }, 1745 | "urlText": { 1746 | "type": "bool", 1747 | "id": 22 1748 | }, 1749 | "urlNumber": { 1750 | "type": "bool", 1751 | "id": 23 1752 | }, 1753 | "messageStubType": { 1754 | "type": "WEB_MESSAGE_INFO_STUBTYPE", 1755 | "id": 24 1756 | }, 1757 | "clearMedia": { 1758 | "type": "bool", 1759 | "id": 25 1760 | }, 1761 | "messageStubParameters": { 1762 | "rule": "repeated", 1763 | "type": "string", 1764 | "id": 26 1765 | }, 1766 | "duration": { 1767 | "type": "uint32", 1768 | "id": 27 1769 | }, 1770 | "labels": { 1771 | "rule": "repeated", 1772 | "type": "string", 1773 | "id": 28 1774 | }, 1775 | "paymentInfo": { 1776 | "type": "PaymentInfo", 1777 | "id": 29 1778 | }, 1779 | "finalLiveLocation": { 1780 | "type": "LiveLocationMessage", 1781 | "id": 30 1782 | }, 1783 | "quotedPaymentInfo": { 1784 | "type": "PaymentInfo", 1785 | "id": 31 1786 | }, 1787 | "ephemeralStartTimestamp": { 1788 | "type": "uint64", 1789 | "id": 32 1790 | }, 1791 | "ephemeralDuration": { 1792 | "type": "uint32", 1793 | "id": 33 1794 | } 1795 | }, 1796 | "nested": { 1797 | "WEB_MESSAGE_INFO_STATUS": { 1798 | "values": { 1799 | "ERROR": 0, 1800 | "PENDING": 1, 1801 | "SERVER_ACK": 2, 1802 | "DELIVERY_ACK": 3, 1803 | "READ": 4, 1804 | "PLAYED": 5 1805 | } 1806 | }, 1807 | "WEB_MESSAGE_INFO_STUBTYPE": { 1808 | "values": { 1809 | "UNKNOWN": 0, 1810 | "REVOKE": 1, 1811 | "CIPHERTEXT": 2, 1812 | "FUTUREPROOF": 3, 1813 | "NON_VERIFIED_TRANSITION": 4, 1814 | "UNVERIFIED_TRANSITION": 5, 1815 | "VERIFIED_TRANSITION": 6, 1816 | "VERIFIED_LOW_UNKNOWN": 7, 1817 | "VERIFIED_HIGH": 8, 1818 | "VERIFIED_INITIAL_UNKNOWN": 9, 1819 | "VERIFIED_INITIAL_LOW": 10, 1820 | "VERIFIED_INITIAL_HIGH": 11, 1821 | "VERIFIED_TRANSITION_ANY_TO_NONE": 12, 1822 | "VERIFIED_TRANSITION_ANY_TO_HIGH": 13, 1823 | "VERIFIED_TRANSITION_HIGH_TO_LOW": 14, 1824 | "VERIFIED_TRANSITION_HIGH_TO_UNKNOWN": 15, 1825 | "VERIFIED_TRANSITION_UNKNOWN_TO_LOW": 16, 1826 | "VERIFIED_TRANSITION_LOW_TO_UNKNOWN": 17, 1827 | "VERIFIED_TRANSITION_NONE_TO_LOW": 18, 1828 | "VERIFIED_TRANSITION_NONE_TO_UNKNOWN": 19, 1829 | "GROUP_CREATE": 20, 1830 | "GROUP_CHANGE_SUBJECT": 21, 1831 | "GROUP_CHANGE_ICON": 22, 1832 | "GROUP_CHANGE_INVITE_LINK": 23, 1833 | "GROUP_CHANGE_DESCRIPTION": 24, 1834 | "GROUP_CHANGE_RESTRICT": 25, 1835 | "GROUP_CHANGE_ANNOUNCE": 26, 1836 | "GROUP_PARTICIPANT_ADD": 27, 1837 | "GROUP_PARTICIPANT_REMOVE": 28, 1838 | "GROUP_PARTICIPANT_PROMOTE": 29, 1839 | "GROUP_PARTICIPANT_DEMOTE": 30, 1840 | "GROUP_PARTICIPANT_INVITE": 31, 1841 | "GROUP_PARTICIPANT_LEAVE": 32, 1842 | "GROUP_PARTICIPANT_CHANGE_NUMBER": 33, 1843 | "BROADCAST_CREATE": 34, 1844 | "BROADCAST_ADD": 35, 1845 | "BROADCAST_REMOVE": 36, 1846 | "GENERIC_NOTIFICATION": 37, 1847 | "E2E_IDENTITY_CHANGED": 38, 1848 | "E2E_ENCRYPTED": 39, 1849 | "CALL_MISSED_VOICE": 40, 1850 | "CALL_MISSED_VIDEO": 41, 1851 | "INDIVIDUAL_CHANGE_NUMBER": 42, 1852 | "GROUP_DELETE": 43, 1853 | "GROUP_ANNOUNCE_MODE_MESSAGE_BOUNCE": 44, 1854 | "CALL_MISSED_GROUP_VOICE": 45, 1855 | "CALL_MISSED_GROUP_VIDEO": 46, 1856 | "PAYMENT_CIPHERTEXT": 47, 1857 | "PAYMENT_FUTUREPROOF": 48, 1858 | "PAYMENT_TRANSACTION_STATUS_UPDATE_FAILED": 49, 1859 | "PAYMENT_TRANSACTION_STATUS_UPDATE_REFUNDED": 50, 1860 | "PAYMENT_TRANSACTION_STATUS_UPDATE_REFUND_FAILED": 51, 1861 | "PAYMENT_TRANSACTION_STATUS_RECEIVER_PENDING_SETUP": 52, 1862 | "PAYMENT_TRANSACTION_STATUS_RECEIVER_SUCCESS_AFTER_HICCUP": 53, 1863 | "PAYMENT_ACTION_ACCOUNT_SETUP_REMINDER": 54, 1864 | "PAYMENT_ACTION_SEND_PAYMENT_REMINDER": 55, 1865 | "PAYMENT_ACTION_SEND_PAYMENT_INVITATION": 56, 1866 | "PAYMENT_ACTION_REQUEST_DECLINED": 57, 1867 | "PAYMENT_ACTION_REQUEST_EXPIRED": 58, 1868 | "PAYMENT_ACTION_REQUEST_CANCELLED": 59, 1869 | "BIZ_VERIFIED_TRANSITION_TOP_TO_BOTTOM": 60, 1870 | "BIZ_VERIFIED_TRANSITION_BOTTOM_TO_TOP": 61, 1871 | "BIZ_INTRO_TOP": 62, 1872 | "BIZ_INTRO_BOTTOM": 63, 1873 | "BIZ_NAME_CHANGE": 64, 1874 | "BIZ_MOVE_TO_CONSUMER_APP": 65, 1875 | "BIZ_TWO_TIER_MIGRATION_TOP": 66, 1876 | "BIZ_TWO_TIER_MIGRATION_BOTTOM": 67, 1877 | "OVERSIZED": 68, 1878 | "GROUP_CHANGE_NO_FREQUENTLY_FORWARDED": 69, 1879 | "GROUP_V4_ADD_INVITE_SENT": 70, 1880 | "GROUP_PARTICIPANT_ADD_REQUEST_JOIN": 71, 1881 | "CHANGE_EPHEMERAL_SETTING": 72 1882 | } 1883 | } 1884 | } 1885 | } 1886 | } 1887 | } 1888 | } 1889 | } --------------------------------------------------------------------------------