├── .DS_Store ├── doc ├── img │ ├── app-architecture1000.png │ └── app-architecture.svg └── spec │ └── protobuf-extractor │ ├── package.json │ ├── index.js │ └── package-lock.json ├── client ├── lib │ ├── font-awesome │ │ └── fonts │ │ │ ├── FontAwesome.otf │ │ │ ├── fontawesome-webfont.eot │ │ │ ├── fontawesome-webfont.ttf │ │ │ ├── fontawesome-webfont.woff │ │ │ └── fontawesome-webfont.woff2 │ ├── bootstrap │ │ ├── fonts │ │ │ ├── glyphicons-halflings-regular.eot │ │ │ ├── glyphicons-halflings-regular.ttf │ │ │ ├── glyphicons-halflings-regular.woff │ │ │ └── glyphicons-halflings-regular.woff2 │ │ └── js │ │ │ └── bootbox-4.4.0.min.js │ ├── jsonTree │ │ ├── icons.svg │ │ └── jsonTree.css │ ├── jquery-colResizable │ │ └── js │ │ │ └── jquery-colResizable-1.6.min.js │ └── qrcode │ │ └── js │ │ └── qrcode.min.js ├── js │ ├── UpdaterPromise.js │ ├── BootstrapStep.js │ ├── WebSocketClient.js │ └── main.js ├── css │ ├── main.css │ └── main.scss ├── index.html └── login-via-js-demo.html ├── requirements.txt ├── session.json ├── Dockerfile ├── index_jsdemo.js ├── LICENSE ├── .gitignore ├── shell.nix ├── backend ├── utilities.py ├── whatsapp_web_backend.py ├── whatsapp_defines.py ├── whatsapp_binary_writer.py ├── whatsapp_binary_reader.py └── whatsapp.py ├── package.json ├── windows └── stdint.h └── index.js /.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sigalor/whatsapp-web-reveng/HEAD/.DS_Store -------------------------------------------------------------------------------- /doc/img/app-architecture1000.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sigalor/whatsapp-web-reveng/HEAD/doc/img/app-architecture1000.png -------------------------------------------------------------------------------- /client/lib/font-awesome/fonts/FontAwesome.otf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sigalor/whatsapp-web-reveng/HEAD/client/lib/font-awesome/fonts/FontAwesome.otf -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | websocket-client 2 | git+https://github.com/dpallot/simple-websocket-server.git 3 | curve25519-donna 4 | pycryptodome 5 | pyqrcode 6 | protobuf 7 | -------------------------------------------------------------------------------- /client/lib/font-awesome/fonts/fontawesome-webfont.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sigalor/whatsapp-web-reveng/HEAD/client/lib/font-awesome/fonts/fontawesome-webfont.eot -------------------------------------------------------------------------------- /client/lib/font-awesome/fonts/fontawesome-webfont.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sigalor/whatsapp-web-reveng/HEAD/client/lib/font-awesome/fonts/fontawesome-webfont.ttf -------------------------------------------------------------------------------- /client/lib/font-awesome/fonts/fontawesome-webfont.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sigalor/whatsapp-web-reveng/HEAD/client/lib/font-awesome/fonts/fontawesome-webfont.woff -------------------------------------------------------------------------------- /client/lib/font-awesome/fonts/fontawesome-webfont.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sigalor/whatsapp-web-reveng/HEAD/client/lib/font-awesome/fonts/fontawesome-webfont.woff2 -------------------------------------------------------------------------------- /client/lib/bootstrap/fonts/glyphicons-halflings-regular.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sigalor/whatsapp-web-reveng/HEAD/client/lib/bootstrap/fonts/glyphicons-halflings-regular.eot -------------------------------------------------------------------------------- /client/lib/bootstrap/fonts/glyphicons-halflings-regular.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sigalor/whatsapp-web-reveng/HEAD/client/lib/bootstrap/fonts/glyphicons-halflings-regular.ttf -------------------------------------------------------------------------------- /client/lib/bootstrap/fonts/glyphicons-halflings-regular.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sigalor/whatsapp-web-reveng/HEAD/client/lib/bootstrap/fonts/glyphicons-halflings-regular.woff -------------------------------------------------------------------------------- /client/lib/bootstrap/fonts/glyphicons-halflings-regular.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sigalor/whatsapp-web-reveng/HEAD/client/lib/bootstrap/fonts/glyphicons-halflings-regular.woff2 -------------------------------------------------------------------------------- /session.json: -------------------------------------------------------------------------------- 1 | {"macKey": "\u00dc]\u00c4\u00aaG\f\u0003G\u00fa\u00a3.\u00e6xK;\u00e6\u00de}y\u001e\u0017\u00ae\u0017\u00d3\rH\u00c1R\u0004\u00a4\u0019\u0011", "serverToken": "1@B6whfXk6pYOR7Yt6GjLoQST3RfXAZpp19dsQz3aUvKo87z7Xpw5ceI2ZbeApDaeu+DjohY+ZqMOVJQ==", "encKey": "\u00e4\u0083In\u007f1\u001f\u00f1\n\u00965\u001c\u00cbn\u0085hM<\u0082\n\u001dT\u00f4\u00b5\u00b9y\u00ee\u00f3{\u0085\bN", "clientId": "5svFWIYYQdBGTxqFyhcsUg==", "clientToken": "InUzqzrXdq5ZwqLHwMMjNSmsV3ZzTHLmHwLPvh1YiME="} -------------------------------------------------------------------------------- /doc/spec/protobuf-extractor/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "whatsapp-web-protobuf-extractor", 3 | "version": "1.0.0", 4 | "description": "A utility program for extracting the protobuf definitions of WhatsApp Web.", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "node index.js" 8 | }, 9 | "author": "sigalor", 10 | "license": "ISC", 11 | "dependencies": { 12 | "acorn": "^6.4.1", 13 | "acorn-walk": "^6.1.1", 14 | "request": "^2.88.0", 15 | "request-promise-core": "^1.1.2", 16 | "request-promise-native": "^1.0.7" 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node 2 | 3 | # Install pip 4 | RUN apt-get update && apt-get install -y \ 5 | python-pip 6 | 7 | 8 | # Create app dir 9 | RUN mkdir /app 10 | WORKDIR /app 11 | 12 | # COPY project in app dir 13 | COPY . . 14 | 15 | # Install dependencies 16 | ## JS Dep 17 | ### Using Yarn 18 | #RUN yarn 19 | #RUN yarn global add concurrently 20 | ### Using NPM 21 | RUN npm install 22 | RUN npm install -g concurrently 23 | 24 | # Pip requirements 25 | RUN pip install -r requirements.txt 26 | 27 | 28 | # Command 29 | ## Yarn 30 | #CMD yarn __run_in_docker 31 | CMD npm run __run_in_docker 32 | -------------------------------------------------------------------------------- /index_jsdemo.js: -------------------------------------------------------------------------------- 1 | let WebSocket = require("ws"); 2 | 3 | let wss = new WebSocket.Server({ port: 2021 }); 4 | console.log("whatsapp-web-reveng jsdemo server listening on port 2021"); 5 | 6 | wss.on("connection", function(ws, req) { 7 | let whatsapp = new WebSocket("wss://web.whatsapp.com/ws", { headers: { "Origin": "https://web.whatsapp.com" } }); 8 | 9 | ws.onmessage = function(e) { whatsapp.send(e.data); } 10 | ws.onclose = function(e) { whatsapp.close(); } 11 | whatsapp.onopen = function(e) { ws.send("whatsapp_open"); } 12 | whatsapp.onmessage = function(e) { ws.send(e.data); } 13 | whatsapp.onclose = function(e) { ws.close(); } 14 | }); 15 | -------------------------------------------------------------------------------- /client/lib/jsonTree/icons.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /client/js/UpdaterPromise.js: -------------------------------------------------------------------------------- 1 | //just a promise class where the '.then(...)'-callback can be called any number of times 2 | class UpdaterPromise { 3 | constructor(executor) { 4 | this.onFulfilled = () => {}; 5 | this.onRejected = (...args) => console.error("unhandled promise rejection: ", ...args); 6 | this.executor = executor; 7 | } 8 | 9 | then(callback) { 10 | this.onFulfilled = callback; 11 | return this; 12 | } 13 | 14 | catch(callback) { 15 | this.onRejected = callback; 16 | return this; 17 | } 18 | 19 | run() { 20 | try { this.executor(this.onFulfilled, this.onRejected); } 21 | catch(e) { this.onRejected(e); } 22 | return this; 23 | } 24 | } 25 | 26 | if(typeof window === 'undefined') //from https://stackoverflow.com/a/4224668 27 | exports.UpdaterPromise = UpdaterPromise; 28 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 sigalor 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | 8 | # Runtime data 9 | pids 10 | *.pid 11 | *.seed 12 | *.pid.lock 13 | 14 | # Directory for instrumented libs generated by jscoverage/JSCover 15 | lib-cov 16 | 17 | # Coverage directory used by tools like istanbul 18 | coverage 19 | 20 | # nyc test coverage 21 | .nyc_output 22 | 23 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 24 | .grunt 25 | 26 | # Bower dependency directory (https://bower.io/) 27 | bower_components 28 | 29 | # node-waf configuration 30 | .lock-wscript 31 | 32 | # Compiled binary addons (http://nodejs.org/api/addons.html) 33 | build/Release 34 | 35 | # Dependency directories 36 | node_modules/ 37 | jspm_packages/ 38 | 39 | # Typescript v1 declaration files 40 | typings/ 41 | 42 | # Optional npm cache directory 43 | .npm 44 | 45 | # Optional eslint cache 46 | .eslintcache 47 | 48 | # Optional REPL history 49 | .node_repl_history 50 | 51 | # Output of 'npm pack' 52 | *.tgz 53 | 54 | # Yarn Integrity file 55 | .yarn-integrity 56 | 57 | # dotenv environment variables file 58 | .env 59 | 60 | misc/ 61 | .vscode/ 62 | .sass-cache/ 63 | backend/decodable_msgs 64 | backend/undecodable_msgs 65 | log.txt 66 | doc/spec/protobuf-extractor/node_modules 67 | # JetBrainsIDEs 68 | .idea 69 | .iml 70 | # direnv environment 71 | .envrc 72 | -------------------------------------------------------------------------------- /shell.nix: -------------------------------------------------------------------------------- 1 | with import {}; 2 | let 3 | pythonEnv = python27.withPackages (ps: [ 4 | ps.websocket_client 5 | ps.curve25519-donna 6 | ps.pycrypto 7 | ps.pyqrcode 8 | ps.protobuf 9 | ps.simple-websocket-server 10 | ]); 11 | in mkShell { 12 | buildInputs = [ 13 | pythonEnv 14 | nodejs-13_x 15 | ]; 16 | shellHook = '' 17 | echo "Installing node modules" 18 | npm ci 19 | echo "Done." 20 | 21 | echo ' 22 | $$\ $$\ $$\ $$\ 23 | $$ | $\ $$ |$$ | $$ | 24 | $$ |$$$\ $$ |$$$$$$$\ $$$$$$\ $$$$$$\ $$$$$$$\ $$$$$$\ $$$$$$\ $$$$$$\ 25 | $$ $$ $$\$$ |$$ __$$\ \____$$\\_$$ _| $$ _____| \____$$\ $$ __$$\ $$ __$$\ 26 | $$$$ _$$$$ |$$ | $$ | $$$$$$$ | $$ | \$$$$$$\ $$$$$$$ |$$ / $$ |$$ / $$ | 27 | $$$ / \$$$ |$$ | $$ |$$ __$$ | $$ |$$\ \____$$\ $$ __$$ |$$ | $$ |$$ | $$ | 28 | $$ / \$$ |$$ | $$ |\$$$$$$$ | \$$$$ |$$$$$$$ |\$$$$$$$ |$$$$$$$ |$$$$$$$ | 29 | \__/ \__|\__| \__| \_______| \____/ \_______/ \_______|$$ ____/ $$ ____/ 30 | $$ | $$ | 31 | $$ | $$ | 32 | \__| \__|' 33 | echo "Node $(node --version)" 34 | echo "$(python --version)" 35 | echo "Try running server with: npm start" 36 | ''; 37 | } 38 | -------------------------------------------------------------------------------- /backend/utilities.py: -------------------------------------------------------------------------------- 1 | from __future__ import print_function; 2 | 3 | import sys; 4 | import time; 5 | 6 | 7 | 8 | def eprint(*args, **kwargs): # from https://stackoverflow.com/a/14981125 9 | print(*args, file=sys.stderr, **kwargs); 10 | 11 | def getTimestamp(): 12 | return int(time.time()); 13 | 14 | def getTimestampMs(): 15 | return int(round(time.time() * 1000)); 16 | 17 | 18 | 19 | def mergeDicts(x, y): # from https://stackoverflow.com/a/26853961 20 | if x is None and y is None: 21 | return; 22 | z = (y if x is None else x).copy(); 23 | if x is not None and y is not None: 24 | z.update(y); 25 | return z; 26 | 27 | def getAttr(obj, key, alt=None): 28 | return obj[key] if isinstance(obj, dict) and key in obj else alt; 29 | 30 | def filterNone(obj): 31 | if isinstance(obj, dict): 32 | return dict((k, filterNone(v)) for k, v in obj.iteritems() if v is not None); 33 | elif isinstance(obj, list): 34 | return [filterNone(entry) for entry in obj]; 35 | else: 36 | return obj; 37 | 38 | def getNumValidKeys(obj): 39 | return len(filter(lambda x: obj[x] is not None, list(obj.keys()))); 40 | 41 | def encodeUTF8(s): 42 | if not isinstance(s, str): 43 | s = strng.encode("utf-8"); 44 | return s; 45 | 46 | 47 | 48 | def ceil(n): # from https://stackoverflow.com/a/32559239 49 | res = int(n); 50 | return res if res == n or n < 0 else res+1; 51 | 52 | def floor(n): 53 | res = int(n); 54 | return res if res == 0 or n >= 0 else res-1; 55 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "whatsapp-web-reveng", 3 | "version": "1.0.0", 4 | "description": "A graphical, web-based client for WhatsApp Web, using the reverse engineered Python code as backend.", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "concurrently --kill-others \"node index.js\" \"./backend/whatsapp-web-backend.py\"", 8 | "start": "npm run dev", 9 | "__run_in_docker": "concurrently \"node index.js\" \"python backend/whatsapp_web_backend.py\" ", 10 | "dev": "concurrently --kill-others \"./node_modules/.bin/nodemon index.js -i client -e js\" \"./node_modules/.bin/nodemon --exec python ./backend/whatsapp_web_backend.py -i client -e py\" \"sass --watch client/css/main.scss:client/css/main.css\"", 11 | "win": "concurrently --kill-others \"node_modules\\.bin\\nodemon index.js -i client -e js\" \"node_modules\\.bin\\nodemon --exec python ./backend/whatsapp_web_backend.py -i client -e py\" \"sass --watch client/css/main.scss:client/css/main.css\"" 12 | }, 13 | "author": "sigalor", 14 | "license": "MIT", 15 | "repository": { 16 | "type": "git", 17 | "url": "https://github.com/sigalor/whatsapp-web-reveng.git" 18 | }, 19 | "dependencies": { 20 | "express": "^4.17.1", 21 | "fs": "0.0.1-security", 22 | "lodash": "^4.17.21", 23 | "path": "^0.12.7", 24 | "string_decoder": "^1.3.0", 25 | "ws": "^5.2.3" 26 | }, 27 | "devDependencies": { 28 | "concurrently": "^3.6.1", 29 | "nodemon": "^1.19.4", 30 | "sass": "^1.25.0" 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /client/js/BootstrapStep.js: -------------------------------------------------------------------------------- 1 | class BootstrapStep { 2 | constructor({websocket, texts, actor, request}) { 3 | this.websocket = websocket; 4 | this.texts = texts; 5 | this.actor = actor; 6 | this.request = request; 7 | } 8 | 9 | run(timeout) { 10 | return new Promise((resolve, reject) => { 11 | if(this.actor != undefined) 12 | this.actor(this.websocket); 13 | 14 | let promise; 15 | switch(this.request.type) { 16 | case "waitForMessage": { 17 | promise = this.websocket.waitForMessage({ 18 | condition: this.request.condition, 19 | keepWhenHit: false, 20 | timeout: timeout 21 | }); 22 | break; 23 | } 24 | 25 | case "call": { 26 | promise = this.websocket.call({ 27 | callArgs: this.request.callArgs, 28 | ignoreCondition: this.request.ignoreCondition, 29 | successCondition: this.request.successCondition, 30 | successActor: this.request.successActor, 31 | failureActor: this.request.failureActor, 32 | timeoutCondition: this.request.timeoutCondition, 33 | timeout: timeout 34 | }); 35 | break; 36 | } 37 | } 38 | if(promise != undefined) { 39 | return promise.then((...args) => { 40 | resolve(...args); 41 | }) 42 | .catch((...args) => { 43 | reject(...args); 44 | }); 45 | } 46 | }); 47 | } 48 | } 49 | 50 | if(typeof window === 'undefined') 51 | exports.BootstrapStep = BootstrapStep; 52 | -------------------------------------------------------------------------------- /client/lib/jsonTree/jsonTree.css: -------------------------------------------------------------------------------- 1 | /* 2 | * JSON Tree Viewer 3 | * http://github.com/summerstyle/jsonTreeViewer 4 | * 5 | * Copyright 2017 Vera Lobacheva (http://iamvera.com) 6 | * Released under the MIT license (LICENSE.txt) 7 | */ 8 | 9 | /* Background for the tree. May use for element */ 10 | .jsontree_bg { 11 | background: #FFF; 12 | } 13 | 14 | /* Styles for the container of the tree (e.g. fonts, margins etc.) */ 15 | .jsontree_tree { 16 | margin-left: 30px; 17 | font-family: 'PT Mono', monospace; 18 | font-size: 14px; 19 | } 20 | 21 | /* Styles for a list of child nodes */ 22 | .jsontree_child-nodes { 23 | display: none; 24 | margin-left: 35px; 25 | margin-bottom: 5px; 26 | line-height: 2; 27 | } 28 | .jsontree_node_expanded > .jsontree_value-wrapper > .jsontree_value > .jsontree_child-nodes { 29 | display: block; 30 | } 31 | 32 | /* Styles for labels */ 33 | .jsontree_label-wrapper { 34 | float: left; 35 | margin-right: 8px; 36 | } 37 | .jsontree_label { 38 | font-weight: normal; 39 | vertical-align: top; 40 | color: #000; 41 | position: relative; 42 | padding: 1px; 43 | border-radius: 4px; 44 | cursor: default; 45 | } 46 | .jsontree_node_marked > .jsontree_label-wrapper > .jsontree_label { 47 | background: #fff2aa; 48 | } 49 | 50 | /* Styles for values */ 51 | .jsontree_value-wrapper { 52 | display: block; 53 | overflow: hidden; 54 | } 55 | .jsontree_node_complex > .jsontree_value-wrapper { 56 | overflow: inherit; 57 | } 58 | .jsontree_value { 59 | vertical-align: top; 60 | display: inline; 61 | } 62 | .jsontree_value_null { 63 | color: #777; 64 | font-weight: bold; 65 | } 66 | .jsontree_value_string { 67 | color: #025900; 68 | font-weight: bold; 69 | } 70 | .jsontree_value_number { 71 | color: #000E59; 72 | font-weight: bold; 73 | } 74 | .jsontree_value_boolean { 75 | color: #600100; 76 | font-weight: bold; 77 | } 78 | 79 | /* Styles for active elements */ 80 | .jsontree_expand-button { 81 | position: absolute; 82 | top: 3px; 83 | left: -15px; 84 | display: block; 85 | width: 11px; 86 | height: 11px; 87 | background-image: url('icons.svg'); 88 | } 89 | .jsontree_node_expanded > .jsontree_label-wrapper > .jsontree_label > .jsontree_expand-button { 90 | background-position: 0 -11px; 91 | } 92 | .jsontree_show-more { 93 | cursor: pointer; 94 | } 95 | .jsontree_node_expanded > .jsontree_value-wrapper > .jsontree_value > .jsontree_show-more { 96 | display: none; 97 | } 98 | .jsontree_node_empty > .jsontree_label-wrapper > .jsontree_label > .jsontree_expand-button, 99 | .jsontree_node_empty > .jsontree_value-wrapper > .jsontree_value > .jsontree_show-more { 100 | display: none !important; 101 | } 102 | .jsontree_node_complex > .jsontree_label-wrapper > .jsontree_label { 103 | cursor: pointer; 104 | } 105 | .jsontree_node_empty > .jsontree_label-wrapper > .jsontree_label { 106 | cursor: default !important; 107 | } 108 | -------------------------------------------------------------------------------- /client/css/main.css: -------------------------------------------------------------------------------- 1 | *, *:before, *:after { 2 | -webkit-box-sizing: border-box; 3 | -moz-box-sizing: border-box; 4 | box-sizing: border-box; 5 | } 6 | 7 | body { 8 | font-family: "Josefin Sans", Arial, sans-serif; 9 | padding: 0; 10 | margin: 0; 11 | width: 100vw; 12 | height: 100vh; 13 | background: radial-gradient(#fff 40%, #ddd); 14 | background-attachment: fixed; 15 | overflow-x: hidden; 16 | overflow-y: auto; 17 | display: flex; 18 | justify-content: center; 19 | align-items: center; 20 | } 21 | 22 | .hidden { 23 | display: none; 24 | } 25 | 26 | #bootstrap-container { 27 | height: 350px; 28 | text-align: center; 29 | } 30 | #bootstrap-container img { 31 | padding-top: 1.5vh; 32 | } 33 | 34 | #main-container { 35 | position: absolute; 36 | top: 0%; 37 | left: 0%; 38 | width: 100%; 39 | height: 100%; 40 | } 41 | #main-container > #main-container-heading { 42 | position: absolute; 43 | top: 0%; 44 | left: 1.5%; 45 | display: flex; 46 | flex-direction: row; 47 | align-items: baseline; 48 | } 49 | #main-container > #main-container-heading > h1 { 50 | position: relative; 51 | top: 1.5vh; 52 | font-weight: 100; 53 | font-size: 8vh; 54 | text-transform: uppercase; 55 | padding-bottom: -10px; 56 | margin-right: 3vh; 57 | } 58 | #main-container > #main-container-content { 59 | position: absolute; 60 | top: 15%; 61 | left: 1.5%; 62 | width: 97%; 63 | height: 85%; 64 | } 65 | #main-container > #main-container-content > #messages-list-table { 66 | border-collapse: collapse; 67 | } 68 | #main-container > #main-container-content > #messages-list-table th, #main-container > #main-container-content > #messages-list-table td { 69 | vertical-align: middle; 70 | } 71 | #main-container > #main-container-content > #messages-list-table td:not(.no-monospace) { 72 | font-family: monospace; 73 | } 74 | #main-container > #main-container-content > #messages-list-table th:not(.fill), #main-container > #main-container-content > #messages-list-table td:not(.fill) { 75 | width: 1px; 76 | white-space: nowrap; 77 | padding: 0px 25px; 78 | text-align: center; 79 | } 80 | 81 | .bootbox.modal { 82 | line-height: 0.8; 83 | } 84 | .bootbox.modal ul { 85 | list-style-type: none; 86 | } 87 | .bootbox.modal li { 88 | line-height: 1.5; 89 | } 90 | .bootbox.modal .jsontree_tree { 91 | margin-left: 0; 92 | padding-left: 0; 93 | } 94 | .bootbox.modal .jsontree_child-nodes { 95 | margin-left: 0; 96 | } 97 | .bootbox.modal .modal-dialog { 98 | width: 100vw; 99 | } 100 | .bootbox.modal .modal-dialog .modal-content { 101 | width: 90vw; 102 | left: 5vw; 103 | } 104 | 105 | #console-arrow { 106 | position: absolute; 107 | top: 2.5vh; 108 | right: 0vw; 109 | transition: right 0.2s; 110 | } 111 | #console-arrow > button { 112 | font-size: 2.5vh; 113 | } 114 | #console-arrow.extended { 115 | right: 20vw; 116 | } 117 | 118 | #console { 119 | position: absolute; 120 | top: 0vh; 121 | left: 100vw; 122 | width: 20vw; 123 | height: 100vh; 124 | padding: 3vh; 125 | transition: left 0.2s; 126 | font-size: 10pt; 127 | font-family: monospace; 128 | background-color: rgba(200, 200, 200, 0.5); 129 | } 130 | #console.extended { 131 | left: 80vw; 132 | } 133 | -------------------------------------------------------------------------------- /client/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | WhatsApp Web 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 |
35 |

WhatsApp Web

36 |
37 | 38 | 39 |
40 |
41 | 42 | 61 | 62 |
63 | 64 |
65 |
66 | Hello 67 |
68 | 69 | 70 | -------------------------------------------------------------------------------- /client/css/main.scss: -------------------------------------------------------------------------------- 1 | *, *:before, *:after { 2 | -webkit-box-sizing: border-box; 3 | -moz-box-sizing: border-box; 4 | box-sizing: border-box; 5 | } 6 | 7 | body { 8 | font-family: 'Josefin Sans', Arial, sans-serif; 9 | padding: 0; 10 | margin: 0; 11 | width: 100vw; 12 | height: 100vh; 13 | background: radial-gradient(#fff 40%, #ddd); 14 | background-attachment: fixed; 15 | overflow-x: hidden; 16 | overflow-y: auto; 17 | display: flex; 18 | justify-content: center; 19 | align-items: center; 20 | } 21 | 22 | .hidden { 23 | display: none; 24 | } 25 | 26 | #bootstrap-container { 27 | height: 350px; 28 | text-align: center; 29 | 30 | img { 31 | padding-top: 1.5vh; 32 | } 33 | } 34 | 35 | #main-container { 36 | position: absolute; 37 | top: 0%; 38 | left: 0%; 39 | width: 100%; 40 | height: 100%; 41 | 42 | & > #main-container-heading { 43 | position: absolute; 44 | top: 0%; 45 | left: 1.5%; 46 | 47 | display: flex; 48 | flex-direction: row; 49 | align-items: baseline; 50 | 51 | & > h1 { 52 | position: relative; 53 | top: 1.5vh; 54 | font-weight: 100; 55 | font-size: 8vh; 56 | text-transform: uppercase; 57 | padding-bottom: -10px; 58 | margin-right: 3vh; 59 | } 60 | } 61 | 62 | & > #main-container-content { 63 | position: absolute; 64 | top: 15%; 65 | left: 1.5%; 66 | width: 97%; 67 | height: 85%; 68 | 69 | & > #messages-list-table { 70 | border-collapse: collapse; 71 | 72 | th, td { 73 | vertical-align: middle; 74 | } 75 | 76 | td:not(.no-monospace) { 77 | font-family: monospace; 78 | } 79 | 80 | th:not(.fill), td:not(.fill) { 81 | width: 1px; 82 | white-space: nowrap; 83 | padding: 0px 25px; 84 | text-align: center; 85 | } 86 | } 87 | 88 | //display: flex; 89 | //flex-direction: column; 90 | } 91 | } 92 | 93 | .bootbox.modal { 94 | line-height: 0.8; 95 | 96 | ul { 97 | list-style-type: none; 98 | } 99 | 100 | li { 101 | line-height: 1.5; 102 | } 103 | 104 | .jsontree_tree { 105 | margin-left: 0; 106 | padding-left: 0; 107 | } 108 | 109 | .jsontree_child-nodes { 110 | margin-left: 0; 111 | } 112 | 113 | .modal-dialog { 114 | width: 100vw; 115 | 116 | .modal-content { 117 | width: 90vw; 118 | left: 5vw; 119 | } 120 | } 121 | } 122 | 123 | #console-arrow { 124 | position: absolute; 125 | top: 2.5vh; 126 | right: 0vw; 127 | transition: right 0.2s; 128 | 129 | & > button { 130 | font-size: 2.5vh; 131 | } 132 | 133 | &.extended { 134 | right: 20vw; 135 | } 136 | } 137 | 138 | #console { 139 | position: absolute; 140 | top: 0vh; 141 | left: 100vw; 142 | width: 20vw; 143 | height: 100vh; 144 | padding: 3vh; 145 | transition: left 0.2s; 146 | font-size: 10pt; 147 | font-family: monospace; 148 | background-color: rgba(200, 200, 200, 0.5); 149 | 150 | &.extended { 151 | left: 80vw; 152 | } 153 | } 154 | -------------------------------------------------------------------------------- /backend/whatsapp_web_backend.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | 4 | from __future__ import print_function; 5 | import sys; 6 | sys.dont_write_bytecode = True; 7 | 8 | import os; 9 | import base64; 10 | import time; 11 | import json; 12 | import uuid; 13 | import traceback; 14 | 15 | from SimpleWebSocketServer import SimpleWebSocketServer, WebSocket; 16 | from whatsapp import WhatsAppWebClient; 17 | from utilities import *; 18 | 19 | reload(sys); 20 | sys.setdefaultencoding("utf-8"); 21 | 22 | 23 | 24 | def eprint(*args, **kwargs): # from https://stackoverflow.com/a/14981125 25 | print(*args, file=sys.stderr, **kwargs); 26 | 27 | 28 | 29 | class WhatsAppWeb(WebSocket): 30 | clientInstances = {}; 31 | 32 | def sendJSON(self, obj, tag=None): 33 | if "from" not in obj: 34 | obj["from"] = "backend"; 35 | eprint("sending " + json.dumps(obj)); 36 | if tag is None: 37 | tag = str(getTimestampMs()); 38 | self.sendMessage(tag + "," + json.dumps(obj)); 39 | 40 | def sendError(self, reason, tag=None): 41 | eprint("sending error: " + reason); 42 | self.sendJSON({ "type": "error", "reason": reason }, tag); 43 | 44 | def handleMessage(self): 45 | try: 46 | eprint(self.data); 47 | tag = self.data.split(",", 1)[0]; 48 | obj = json.loads(self.data[len(tag)+1:]); 49 | 50 | eprint(obj); 51 | if "from" not in obj or obj["from"] != "api2backend" or "type" not in obj or not (("command" in obj and obj["command"] == "backend-connectWhatsApp") or "whatsapp_instance_id" in obj): 52 | self.sendError("Invalid request"); 53 | return; 54 | 55 | if obj["type"] == "call": 56 | if "command" not in obj: 57 | self.sendError("Invalid request"); 58 | return; 59 | 60 | if obj["command"] == "backend-connectWhatsApp": 61 | clientInstanceId = uuid.uuid4().hex; 62 | onOpenCallback = { 63 | "func": lambda cbSelf: self.sendJSON(mergeDicts({ "type": "resource_connected", "resource": "whatsapp" }, getAttr(cbSelf, "args")), getAttr(cbSelf, "tag")), 64 | "tag": tag, 65 | "args": { "resource_instance_id": clientInstanceId } 66 | }; 67 | onMessageCallback = { 68 | "func": lambda obj, cbSelf, moreArgs=None: self.sendJSON(mergeDicts(mergeDicts({ "type": "whatsapp_message_received", "message": obj, "timestamp": getTimestampMs() }, getAttr(cbSelf, "args")), moreArgs), getAttr(cbSelf, "tag")), 69 | "args": { "resource_instance_id": clientInstanceId } 70 | }; 71 | onCloseCallback = { 72 | "func": lambda cbSelf: self.sendJSON(mergeDicts({ "type": "resource_gone", "resource": "whatsapp" }, getAttr(cbSelf, "args")), getAttr(cbSelf, "tag")), 73 | "args": { "resource_instance_id": clientInstanceId } 74 | }; 75 | self.clientInstances[clientInstanceId] = WhatsAppWebClient(onOpenCallback, onMessageCallback, onCloseCallback); 76 | else: 77 | currWhatsAppInstance = self.clientInstances[obj["whatsapp_instance_id"]]; 78 | callback = { 79 | "func": lambda obj, cbSelf: self.sendJSON(mergeDicts(obj, getAttr(cbSelf, "args")), getAttr(cbSelf, "tag")), 80 | "tag": tag, 81 | "args": { "resource_instance_id": obj["whatsapp_instance_id"] } 82 | }; 83 | if currWhatsAppInstance.activeWs is None: 84 | self.sendError("No WhatsApp server connected to backend."); 85 | return; 86 | 87 | cmd = obj["command"]; 88 | if cmd == "backend-generateQRCode": 89 | currWhatsAppInstance.generateQRCode(callback); 90 | elif cmd == "backend-restoreSession": 91 | currWhatsAppInstance.restoreSession(callback); 92 | elif cmd == "backend-getLoginInfo": 93 | currWhatsAppInstance.getLoginInfo(callback); 94 | elif cmd == "backend-getConnectionInfo": 95 | currWhatsAppInstance.getConnectionInfo(callback); 96 | elif cmd == "backend-disconnectWhatsApp": 97 | currWhatsAppInstance.disconnect(); 98 | self.sendJSON({ "type": "resource_disconnected", "resource": "whatsapp", "resource_instance_id": obj["whatsapp_instance_id"] }, tag); 99 | except: 100 | eprint(traceback.format_exc()); 101 | 102 | def handleConnected(self): 103 | self.sendJSON({ "from": "backend", "type": "connected" }); 104 | eprint(self.address, "connected to backend"); 105 | 106 | def handleClose(self): 107 | whatsapp.disconnect(); 108 | eprint(self.address, "closed connection to backend"); 109 | 110 | server = SimpleWebSocketServer("", 2020, WhatsAppWeb); 111 | eprint("whatsapp-web-backend listening on port 2020"); 112 | server.serveforever(); 113 | -------------------------------------------------------------------------------- /client/lib/jquery-colResizable/js/jquery-colResizable-1.6.min.js: -------------------------------------------------------------------------------- 1 | // colResizable 1.6 - a jQuery plugin by Alvaro Prieto Lauroba http://www.bacubacu.com/colresizable/ 2 | 3 | !function(t){var e,i=t(document),r=t("head"),o=null,s={},d=0,n="id",a="px",l="JColResizer",c="JCLRFlex",f=parseInt,h=Math,p=navigator.userAgent.indexOf("Trident/4.0")>0;try{e=sessionStorage}catch(g){}r.append("");var u=function(e,i){var o=t(e);if(o.opt=i,o.mode=i.resizeMode,o.dc=o.opt.disabledColumns,o.opt.disable)return w(o);var a=o.id=o.attr(n)||l+d++;o.p=o.opt.postbackSafe,!o.is("table")||s[a]&&!o.opt.partialRefresh||("e-resize"!==o.opt.hoverCursor&&r.append(""),o.addClass(l).attr(n,a).before('
'),o.g=[],o.c=[],o.w=o.width(),o.gc=o.prev(),o.f=o.opt.fixed,i.marginLeft&&o.gc.css("marginLeft",i.marginLeft),i.marginRight&&o.gc.css("marginRight",i.marginRight),o.cs=f(p?e.cellSpacing||e.currentStyle.borderSpacing:o.css("border-spacing"))||2,o.b=f(p?e.border||e.currentStyle.borderLeftWidth:o.css("border-left-width"))||1,s[a]=o,v(o))},w=function(t){var e=t.attr(n),t=s[e];t&&t.is("table")&&(t.removeClass(l+" "+c).gc.remove(),delete s[e])},v=function(i){var r=i.find(">thead>tr:first>th,>thead>tr:first>td");r.length||(r=i.find(">tbody>tr:first>th,>tr:first>th,>tbody>tr:first>td, >tr:first>td")),r=r.filter(":visible"),i.cg=i.find("col"),i.ln=r.length,i.p&&e&&e[i.id]&&m(i,r),r.each(function(e){var r=t(this),o=-1!=i.dc.indexOf(e),s=t(i.gc.append('
')[0].lastChild);s.append(o?"":i.opt.gripInnerHtml).append('
'),e==i.ln-1&&(s.addClass("JCLRLastGrip"),i.f&&s.html("")),s.bind("touchstart mousedown",J),o?s.addClass("JCLRdisabledGrip"):s.removeClass("JCLRdisabledGrip").bind("touchstart mousedown",J),s.t=i,s.i=e,s.c=r,r.w=r.width(),i.g.push(s),i.c.push(r),r.width(r.w).removeAttr("width"),s.data(l,{i:e,t:i.attr(n),last:e==i.ln-1})}),i.cg.removeAttr("width"),i.find("td, th").not(r).not("table th, table td").each(function(){t(this).removeAttr("width")}),i.f||i.removeAttr("width").addClass(c),C(i)},m=function(t,i){var r,o,s=0,d=0,n=[];if(i){if(t.cg.removeAttr("width"),t.opt.flush)return void(e[t.id]="");for(r=e[t.id].split(";"),o=r[t.ln+1],!t.f&&o&&(t.width(o*=1),t.opt.overflow&&(t.css("min-width",o+a),t.w=o));d*{cursor:"+n.opt.dragCursor+"!important}"),a.addClass(n.opt.draggingClass),o=a,n.c[d.i].l)for(var f,h=0;h { 24 | let actualMsg = this.constructorConfig.getOnMessageData ? this.constructorConfig.getOnMessageData(msg) : msg; 25 | let tag = actualMsg.split(",")[0]; 26 | let obj = JSON.parse(actualMsg.substr(tag.length + 1)); 27 | console.log("got message ", obj); 28 | 29 | let idx = this.expectedMsgs.findIndex(e => e.condition(obj, tag)); 30 | if(idx != -1) { 31 | let currMsg = this.expectedMsgs[idx].keepWhenHit ? this.expectedMsgs[idx] : this.expectedMsgs.splice(idx, 1)[0]; 32 | //if(!currMsg.keepWhenHit) 33 | // console.log("just removed ", currMsg.condition.toString(), " from expectedMsgs"); 34 | //console.log("resolving ", obj, " to index ", idx); 35 | currMsg.resolve({ 36 | data: obj, 37 | respond: obj => this.send(obj, tag) 38 | }); 39 | } 40 | }; 41 | this.ws.onopen = () => { 42 | this.simulateMsg({ from: "meta", type: "opened" }); 43 | this.isOpen = true; 44 | }; 45 | this.ws.onclose = () => { 46 | this.simulateMsg({ from: "meta", type: "closed" }); 47 | this.isOpen = false; 48 | } 49 | } 50 | 51 | initialize(url, whoami, constructorConfig) { 52 | this.expectedMsgs = []; 53 | this.whoami = whoami; 54 | this.constructorConfig = constructorConfig; 55 | try { 56 | this.ws = new this.constructorConfig.func(url, ...(this.constructorConfig.args || [])); 57 | } 58 | catch(e) { 59 | throw this.info.errors.basic.connectionFailed; 60 | } 61 | this._initializeWebSocketListeners(); 62 | return this; 63 | } 64 | 65 | initializeFromRaw(ws, whoami, constructorConfig) { 66 | this.expectedMsgs = []; 67 | this.whoami = whoami; 68 | this.constructorConfig = constructorConfig; 69 | this.ws = ws; 70 | this._initializeWebSocketListeners(); 71 | return this; 72 | } 73 | 74 | simulateMsg(obj) { 75 | let msgTag = +new Date(); 76 | this.ws.onmessage({ data: `${msgTag},${JSON.stringify(obj)}` }) 77 | } 78 | 79 | send(obj, tag) { 80 | return new Promise((resolve, reject) => { 81 | let msgTag = tag==undefined ? (+new Date()) : tag; 82 | this.waitForMessage({ 83 | condition: (obj, tag) => tag == msgTag, 84 | keepWhenHit: false 85 | }) 86 | .then((...args) => { resolve(...args); }) 87 | .catch((...args) => reject(...args)); 88 | if(obj.from == undefined) 89 | obj.from = this.whoami; 90 | this.ws.send(`${msgTag},${JSON.stringify(obj)}`); 91 | }); 92 | } 93 | 94 | waitForMessage({condition, keepWhenHit, timeoutCondition, timeout}) { 95 | let executor = (resolve, reject) => { 96 | let timedOut = false, currTimeout; 97 | if(timeout != undefined) { 98 | currTimeout = setTimeout(() => { 99 | timedOut = true; 100 | if(this.isOpen && (timeoutCondition == undefined || timeoutCondition(this))) 101 | reject(this.info.errors.basic.timeout); 102 | }, timeout); 103 | } 104 | 105 | this.expectedMsgs.push({ 106 | condition: condition, 107 | keepWhenHit: keepWhenHit, 108 | resolve: (...args) => { 109 | if(timedOut) 110 | return; 111 | clearTimeout(currTimeout); 112 | resolve(...args); 113 | }, 114 | reject: (...args) => { 115 | if(timedOut) 116 | return; 117 | clearTimeout(currTimeout); 118 | reject(...args); 119 | } 120 | }); 121 | //console.log("index ", this.expectedMsgs.length-1, ": registered waitForMessage on", this.whoami, "with condition ", condition.toString()); 122 | } 123 | return (keepWhenHit ? new UpdaterPromise(executor) : new Promise(executor)); 124 | } 125 | 126 | call({callArgs, successCondition, successActor, failureActor, timeoutCondition, timeout}) { 127 | return new Promise((resolve, reject) => { 128 | let msgTag = +new Date(); 129 | this.waitForMessage({ 130 | condition: (obj, tag) => tag == msgTag, 131 | keepWhenHit: false, 132 | timeoutCondition: timeoutCondition, 133 | timeout: timeout 134 | }) 135 | .then((...args) => { 136 | if(successCondition(args[0].data)) { 137 | if(successActor != undefined) 138 | successActor(this, args[0].data); 139 | resolve(...args); 140 | } 141 | else 142 | reject(args[0].data.type == "error" ? args[0].data.reason : this.info.errors.basic.invalidResponse); 143 | }) 144 | .catch((...args) => { 145 | if(failureActor != undefined) 146 | failureActor(this, args[0]); 147 | reject(...args); 148 | }); 149 | 150 | let obj = Object.assign(callArgs, { from: this.whoami, type: "call" }); 151 | this.ws.send(`${msgTag},${JSON.stringify(obj)}`); 152 | }); 153 | } 154 | 155 | onClose(callback) { 156 | this.waitForMessage({ 157 | condition: obj => obj.from == "meta" && obj.type == "closed", 158 | keepWhenHit: false 159 | }).then((...args) => callback(...args)); 160 | } 161 | 162 | disconnect() { 163 | console.log("disconnecting " + this.whoami, this.isOpen); 164 | if(this.isOpen) 165 | this.ws.close(); 166 | } 167 | } 168 | 169 | if(typeof window === 'undefined') 170 | exports.WebSocketClient = WebSocketClient; 171 | -------------------------------------------------------------------------------- /backend/whatsapp_binary_writer.py: -------------------------------------------------------------------------------- 1 | from whatsapp_defines import WATags, WASingleByteTokens, WADoubleByteTokens, WAWebMessageInfo; 2 | from utilities import getNumValidKeys, encodeUTF8, ceil; 3 | 4 | 5 | 6 | class WABinaryWriter: 7 | def __init__(self): 8 | self.data = []; 9 | 10 | def getData(self): 11 | return "".join(map(chr, self.data)); 12 | 13 | def pushByte(self, value): 14 | self.data.append(value & 0xFF); 15 | 16 | def pushIntN(self, value, n, littleEndian): 17 | for i in range(n): 18 | currShift = i if littleEndian else n-1-i; 19 | self.data.append((value >> (currShift*8)) & 0xFF); 20 | 21 | def pushInt20(self, value): 22 | self.pushBytes([(value >> 16) & 0x0F, (value >> 8) & 0xFF, value & 0xFF]); 23 | 24 | def pushInt16(self, value): 25 | self.pushIntN(value, 2); 26 | 27 | def pushInt32(self, value): 28 | self.pushIntN(value, 4); 29 | 30 | def pushInt64(self, value): 31 | self.pushIntN(value, 8); 32 | 33 | def pushBytes(self, bytes): 34 | self.data += bytes; 35 | 36 | def pushString(self, str): 37 | self.data += map(ord, encodeUTF8(str)); 38 | 39 | def writeByteLength(self, length): 40 | if length >= 4294967296: 41 | raise ValueError("string too large to encode (len = " + str(length) + "): " + str); 42 | 43 | if length >= (1 << 20): 44 | self.pushByte(WATags.BINARY_32); 45 | self.pushInt32(length); 46 | elif length >= 256: 47 | self.pushByte(WATags.BINARY_20); 48 | self.pushInt20(length); 49 | else: 50 | self.pushByte(WATags.BINARY_8); 51 | self.pushByte(length); 52 | 53 | def writeNode(self, node): 54 | if node is None: 55 | return; 56 | if not isinstance(node, list) or len(node) != 3: 57 | raise ValueError("invalid node"); 58 | numAttributes = getNumValidKeys(node[1]) if bool(node[1]) else 0; 59 | 60 | self.writeListStart(2*numAttributes + 1 + (1 if bool(node[2]) else 0)); 61 | self.writeString(node[0]); 62 | self.writeAttributes(node[1]); 63 | self.writeChildren(node[2]); 64 | 65 | def writeString(self, token, i=None): 66 | if not isinstance(token, str): 67 | raise ValueError("invalid string"); 68 | 69 | if not bool(i) and token == "c.us": 70 | self.writeToken(WASingleByteTokens.index("s.whatsapp.net")); 71 | return; 72 | 73 | if token not in WASingleByteTokens: 74 | jidSepIndex = token.index("@") if "@" in token else -1; 75 | if jidSepIndex < 1: 76 | self.writeStringRaw(token); 77 | else: 78 | self.writeJid(token[:jidSepIndex], token[jidSepIndex+1:]); 79 | else: 80 | tokenIndex = WASingleByteTokens.index(token); 81 | if tokenIndex < WATags.SINGLE_BYTE_MAX: 82 | self.writeToken(tokenIndex); 83 | else: 84 | singleByteOverflow = tokenIndex - WATags.SINGLE_BYTE_MAX; 85 | dictionaryIndex = singleByteOverflow >> 8; 86 | if dictionaryIndex < 0 or dictionaryIndex > 3: 87 | raise ValueError("double byte dictionary token out of range: " + token + " " + str(tokenIndex)); 88 | self.writeToken(WATags.DICTIONARY_0 + dictionaryIndex); 89 | self.writeToken(singleByteOverflow % 256); 90 | 91 | def writeStringRaw(self, strng): 92 | strng = encodeUTF8(strng); 93 | self.writeByteLength(len(strng)); 94 | self.pushString(strng); 95 | 96 | def writeJid(self, jidLeft, jidRight): 97 | self.pushByte(WATags.JID_PAIR); 98 | if jidLeft is not None and len(jidLeft) > 0: 99 | self.writeString(jidLeft); 100 | else: 101 | self.writeToken(WATags.LIST_EMPTY); 102 | self.writeString(jidRight); 103 | 104 | def writeToken(self, token): 105 | if(token < 245): 106 | self.pushByte(token); 107 | elif token <= 500: 108 | raise ValueError("invalid token"); 109 | 110 | def writeAttributes(self, attrs): 111 | if attrs is None: 112 | return; 113 | for key, value in attrs.iteritems(): 114 | if value is not None: 115 | self.writeString(key); 116 | self.writeString(value); 117 | 118 | def writeChildren(self, children): 119 | if children is None: 120 | return; 121 | 122 | if isinstance(children, str): 123 | self.writeString(children, True); 124 | elif isinstance(children, bytes): 125 | self.writeByteLength(len(children)); 126 | self.pushBytes(children); 127 | else: 128 | if not isinstance(children, list): 129 | raise ValueError("invalid children"); 130 | self.writeListStart(len(children)); 131 | for c in children: 132 | self.writeNode(c); 133 | 134 | def writeListStart(self, listSize): 135 | if listSize == 0: 136 | self.pushByte(WATags.LIST_EMPTY); 137 | elif listSize < 256: 138 | self.pushBytes([ WATags.LIST_8, listSize ]); 139 | else: 140 | self.pushBytes([ WATags.LIST_16, listSize ]); 141 | 142 | def writePackedBytes(self, strng): 143 | try: 144 | self.writePackedBytesImpl(strng, WATags.NIBBLE_8); 145 | except e: 146 | self.writePackedBytesImpl(strng, WATags.HEX_8); 147 | 148 | def writePackedBytesImpl(self, strng, dataType): 149 | strng = encodeUTF8(strng); 150 | numBytes = len(strng); 151 | if numBytes > WATags.PACKED_MAX: 152 | raise ValueError("too many bytes to nibble-encode: len = " + str(numBytes)); 153 | 154 | self.pushByte(dataType); 155 | self.pushByte((128 if (numBytes%2)>0 else 0) | ceil(numBytes/2)); 156 | 157 | for i in range(numBytes // 2): 158 | self.pushByte(self.packBytePair(dataType, strng[2*i], str[2*i + 1])); 159 | if (numBytes % 2) != 0: 160 | self.pushByte(self.packBytePair(dataType, strng[numBytes - 1], "\x00")); 161 | 162 | def packBytePair(self, packType, part1, part2): 163 | if packType == WATags.NIBBLE_8: 164 | return (self.packNibble(part1) << 4) | self.packNibble(part2); 165 | elif packType == WATags.HEX_8: 166 | return (self.packHex(part1) << 4) | self.packHex(part2); 167 | else: 168 | raise ValueError("invalid byte pack type: " + str(packType)); 169 | 170 | def packNibble(self, value): 171 | if value >= "0" and value <= "9": 172 | return int(value); 173 | elif value == "-": 174 | return 10; 175 | elif value == ".": 176 | return 11; 177 | elif value == "\x00": 178 | return 15; 179 | raise ValueError("invalid byte to pack as nibble: " + str(value)); 180 | 181 | def packHex(self, value): 182 | if (value >= "0" and value <= "9") or (value >= "A" and value <= "F") or (value >= "a" and value <= "f"): 183 | return int(value, 16); 184 | elif value == "\x00": 185 | return 15; 186 | raise ValueError("invalid byte to pack as hex: " + str(value)); 187 | 188 | 189 | 190 | def whatsappWriteBinary(node): 191 | stream = WABinaryWriter(); 192 | stream.writeNode(node); 193 | return stream.getData(); 194 | -------------------------------------------------------------------------------- /backend/whatsapp_binary_reader.py: -------------------------------------------------------------------------------- 1 | from whatsapp_defines import WATags, WASingleByteTokens, WADoubleByteTokens, WAWebMessageInfo; 2 | 3 | 4 | 5 | class WABinaryReader: 6 | def __init__(self, data): 7 | self.data = data; 8 | self.index = 0; 9 | 10 | def checkEOS(self, length): 11 | if self.index + length > len(self.data): 12 | raise EOFError("end of stream reached"); 13 | 14 | def readByte(self): 15 | self.checkEOS(1); 16 | ret = ord(self.data[self.index]); 17 | self.index += 1; 18 | return ret; 19 | 20 | def readIntN(self, n, littleEndian=False): 21 | self.checkEOS(n); 22 | ret = 0; 23 | for i in range(n): 24 | currShift = i if littleEndian else n-1-i; 25 | ret |= ord(self.data[self.index + i]) << (currShift*8); 26 | self.index += n; 27 | return ret; 28 | 29 | def readInt16(self, littleEndian=False): 30 | return self.readIntN(2, littleEndian); 31 | 32 | def readInt20(self): 33 | self.checkEOS(3); 34 | ret = ((ord(self.data[self.index]) & 15) << 16) + (ord(self.data[self.index+1]) << 8) + ord(self.data[self.index+2]); 35 | self.index += 3; 36 | return ret; 37 | 38 | def readInt32(self, littleEndian=False): 39 | return self.readIntN(4, littleEndian); 40 | 41 | def readInt64(self, littleEndian=False): 42 | return self.readIntN(8, littleEndian); 43 | 44 | def readPacked8(self, tag): 45 | startByte = self.readByte(); 46 | ret = ""; 47 | for i in range(startByte & 127): 48 | currByte = self.readByte(); 49 | ret += self.unpackByte(tag, (currByte & 0xF0) >> 4) + self.unpackByte(tag, currByte & 0x0F); 50 | if (startByte >> 7) != 0: 51 | ret = ret[:len(ret)-1]; 52 | return ret; 53 | 54 | def unpackByte(self, tag, value): 55 | if tag == WATags.NIBBLE_8: 56 | return self.unpackNibble(value); 57 | elif tag == WATags.HEX_8: 58 | return self.unpackHex(value); 59 | 60 | def unpackNibble(self, value): 61 | if value >= 0 and value <= 9: 62 | return chr(ord('0') + value); 63 | elif value == 10: 64 | return "-"; 65 | elif value == 11: 66 | return "."; 67 | elif value == 15: 68 | return "\0"; 69 | raise ValueError("invalid nibble to unpack: " + value); 70 | 71 | def unpackHex(self, value): 72 | if value < 0 or value > 15: 73 | raise ValueError("invalid hex to unpack: " + str(value)); 74 | if value < 10: 75 | return chr(ord('0') + value); 76 | else: 77 | return chr(ord('A') + value - 10); 78 | 79 | def readRangedVarInt(self, minVal, maxVal, desc="unknown"): 80 | ret = self.readVarInt(); 81 | if ret < minVal or ret >= maxVal: 82 | raise ValueError("varint for " + desc + " is out of bounds: " + str(ret)); 83 | return ret; 84 | 85 | 86 | def isListTag(self, tag): 87 | return tag == WATags.LIST_EMPTY or tag == WATags.LIST_8 or tag == WATags.LIST_16; 88 | 89 | def readListSize(self, tag): 90 | if(tag == WATags.LIST_EMPTY): 91 | return 0; 92 | elif(tag == WATags.LIST_8): 93 | return self.readByte(); 94 | elif(tag == WATags.LIST_16): 95 | return self.readInt16(); 96 | raise ValueError("invalid tag for list size: " + str(tag)); 97 | 98 | def readString(self, tag): 99 | if tag >= 3 and tag <= 235: 100 | token = self.getToken(tag); 101 | if token == "s.whatsapp.net": 102 | token = "c.us"; 103 | return token; 104 | 105 | if tag == WATags.DICTIONARY_0 or tag == WATags.DICTIONARY_1 or tag == WATags.DICTIONARY_2 or tag == WATags.DICTIONARY_3: 106 | return self.getTokenDouble(tag - WATags.DICTIONARY_0, self.readByte()); 107 | elif tag == WATags.LIST_EMPTY: 108 | return; 109 | elif tag == WATags.BINARY_8: 110 | return self.readStringFromChars(self.readByte()); 111 | elif tag == WATags.BINARY_20: 112 | return self.readStringFromChars(self.readInt20()); 113 | elif tag == WATags.BINARY_32: 114 | return self.readStringFromChars(self.readInt32()); 115 | elif tag == WATags.JID_PAIR: 116 | i = self.readString(self.readByte()); 117 | j = self.readString(self.readByte()); 118 | if i is None or j is None: 119 | raise ValueError("invalid jid pair: " + str(i) + ", " + str(j)); 120 | return i + "@" + j; 121 | elif tag == WATags.NIBBLE_8 or tag == WATags.HEX_8: 122 | return self.readPacked8(tag); 123 | else: 124 | raise ValueError("invalid string with tag " + str(tag)); 125 | 126 | def readStringFromChars(self, length): 127 | self.checkEOS(length); 128 | ret = self.data[self.index:self.index+length]; 129 | self.index += length; 130 | return ret; 131 | 132 | def readAttributes(self, n): 133 | ret = {}; 134 | if n == 0: 135 | return; 136 | for i in range(n): 137 | index = self.readString(self.readByte()); 138 | ret[index] = self.readString(self.readByte()); 139 | return ret; 140 | 141 | def readList(self, tag): 142 | ret = []; 143 | for i in range(self.readListSize(tag)): 144 | ret.append(self.readNode()); 145 | return ret; 146 | 147 | def readNode(self): 148 | listSize = self.readListSize(self.readByte()); 149 | descrTag = self.readByte(); 150 | if descrTag == WATags.STREAM_END: 151 | raise ValueError("unexpected stream end"); 152 | descr = self.readString(descrTag); 153 | if listSize == 0 or not descr: 154 | raise ValueError("invalid node"); 155 | attrs = self.readAttributes((listSize-1) >> 1); 156 | if listSize % 2 == 1: 157 | return [descr, attrs, None]; 158 | 159 | tag = self.readByte(); 160 | if self.isListTag(tag): 161 | content = self.readList(tag); 162 | elif tag == WATags.BINARY_8: 163 | content = self.readBytes(self.readByte()); 164 | elif tag == WATags.BINARY_20: 165 | content = self.readBytes(self.readInt20()); 166 | elif tag == WATags.BINARY_32: 167 | content = self.readBytes(self.readInt32()); 168 | else: 169 | content = self.readString(tag); 170 | return [descr, attrs, content]; 171 | 172 | def readBytes(self, n): 173 | ret = ""; 174 | for i in range(n): 175 | ret += chr(self.readByte()); 176 | return ret; 177 | 178 | def getToken(self, index): 179 | if index < 3 or index >= len(WASingleByteTokens): 180 | raise ValueError("invalid token index: " + str(index)); 181 | return WASingleByteTokens[index]; 182 | 183 | def getTokenDouble(self, index1, index2): 184 | n = 256 * index1 + index2; 185 | if n < 0 or n >= len(WADoubleByteTokens): 186 | raise ValueError("invalid token index: " + str(n)); 187 | return WADoubleByteTokens[n]; 188 | 189 | 190 | 191 | def whatsappReadMessageArray(msgs): 192 | if not isinstance(msgs, list): 193 | return msgs; 194 | ret = []; 195 | for x in msgs: 196 | ret.append(WAWebMessageInfo.decode(x[2]) if isinstance(x, list) and x[0]=="message" else x); 197 | return ret; 198 | 199 | def whatsappReadBinary(data, withMessages=False): 200 | node = WABinaryReader(data).readNode(); 201 | if withMessages and node is not None and isinstance(node, list) and node[1] is not None: 202 | node[2] = whatsappReadMessageArray(node[2]); 203 | return node; 204 | -------------------------------------------------------------------------------- /windows/stdint.h: -------------------------------------------------------------------------------- 1 | // ISO C9x compliant stdint.h for Microsoft Visual Studio 2 | // Based on ISO/IEC 9899:TC2 Committee draft (May 6, 2005) WG14/N1124 3 | // 4 | // Copyright (c) 2006-2008 Alexander Chemeris 5 | // 6 | // Redistribution and use in source and binary forms, with or without 7 | // modification, are permitted provided that the following conditions are met: 8 | // 9 | // 1. Redistributions of source code must retain the above copyright notice, 10 | // this list of conditions and the following disclaimer. 11 | // 12 | // 2. Redistributions in binary form must reproduce the above copyright 13 | // notice, this list of conditions and the following disclaimer in the 14 | // documentation and/or other materials provided with the distribution. 15 | // 16 | // 3. The name of the author may be used to endorse or promote products 17 | // derived from this software without specific prior written permission. 18 | // 19 | // THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR IMPLIED 20 | // WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF 21 | // MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO 22 | // EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, 23 | // SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, 24 | // PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; 25 | // OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, 26 | // WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR 27 | // OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF 28 | // ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 29 | // 30 | /////////////////////////////////////////////////////////////////////////////// 31 | 32 | #ifndef _MSC_VER // [ 33 | #error "Use this header only with Microsoft Visual C++ compilers!" 34 | #endif // _MSC_VER ] 35 | 36 | #ifndef _MSC_STDINT_H_ // [ 37 | #define _MSC_STDINT_H_ 38 | 39 | #if _MSC_VER > 1000 40 | #pragma once 41 | #endif 42 | 43 | #include 44 | 45 | // For Visual Studio 6 in C++ mode and for many Visual Studio versions when 46 | // compiling for ARM we should wrap include with 'extern "C++" {}' 47 | // or compiler give many errors like this: 48 | // error C2733: second C linkage of overloaded function 'wmemchr' not allowed 49 | #ifdef __cplusplus 50 | extern "C" { 51 | #endif 52 | # include 53 | #ifdef __cplusplus 54 | } 55 | #endif 56 | 57 | // Define _W64 macros to mark types changing their size, like intptr_t. 58 | #ifndef _W64 59 | # if !defined(__midl) && (defined(_X86_) || defined(_M_IX86)) && _MSC_VER >= 1300 60 | # define _W64 __w64 61 | # else 62 | # define _W64 63 | # endif 64 | #endif 65 | 66 | 67 | // 7.18.1 Integer types 68 | 69 | // 7.18.1.1 Exact-width integer types 70 | 71 | // Visual Studio 6 and Embedded Visual C++ 4 doesn't 72 | // realize that, e.g. char has the same size as __int8 73 | // so we give up on __intX for them. 74 | #if (_MSC_VER < 1300) 75 | typedef signed char int8_t; 76 | typedef signed short int16_t; 77 | typedef signed int int32_t; 78 | typedef unsigned char uint8_t; 79 | typedef unsigned short uint16_t; 80 | typedef unsigned int uint32_t; 81 | #else 82 | typedef signed __int8 int8_t; 83 | typedef signed __int16 int16_t; 84 | typedef signed __int32 int32_t; 85 | typedef unsigned __int8 uint8_t; 86 | typedef unsigned __int16 uint16_t; 87 | typedef unsigned __int32 uint32_t; 88 | #endif 89 | typedef signed __int64 int64_t; 90 | typedef unsigned __int64 uint64_t; 91 | 92 | 93 | // 7.18.1.2 Minimum-width integer types 94 | typedef int8_t int_least8_t; 95 | typedef int16_t int_least16_t; 96 | typedef int32_t int_least32_t; 97 | typedef int64_t int_least64_t; 98 | typedef uint8_t uint_least8_t; 99 | typedef uint16_t uint_least16_t; 100 | typedef uint32_t uint_least32_t; 101 | typedef uint64_t uint_least64_t; 102 | 103 | // 7.18.1.3 Fastest minimum-width integer types 104 | typedef int8_t int_fast8_t; 105 | typedef int16_t int_fast16_t; 106 | typedef int32_t int_fast32_t; 107 | typedef int64_t int_fast64_t; 108 | typedef uint8_t uint_fast8_t; 109 | typedef uint16_t uint_fast16_t; 110 | typedef uint32_t uint_fast32_t; 111 | typedef uint64_t uint_fast64_t; 112 | 113 | // 7.18.1.4 Integer types capable of holding object pointers 114 | #ifdef _WIN64 // [ 115 | typedef signed __int64 intptr_t; 116 | typedef unsigned __int64 uintptr_t; 117 | #else // _WIN64 ][ 118 | typedef _W64 signed int intptr_t; 119 | typedef _W64 unsigned int uintptr_t; 120 | #endif // _WIN64 ] 121 | 122 | // 7.18.1.5 Greatest-width integer types 123 | typedef int64_t intmax_t; 124 | typedef uint64_t uintmax_t; 125 | 126 | 127 | // 7.18.2 Limits of specified-width integer types 128 | 129 | #if !defined(__cplusplus) || defined(__STDC_LIMIT_MACROS) // [ See footnote 220 at page 257 and footnote 221 at page 259 130 | 131 | // 7.18.2.1 Limits of exact-width integer types 132 | #define INT8_MIN ((int8_t)_I8_MIN) 133 | #define INT8_MAX _I8_MAX 134 | #define INT16_MIN ((int16_t)_I16_MIN) 135 | #define INT16_MAX _I16_MAX 136 | #define INT32_MIN ((int32_t)_I32_MIN) 137 | #define INT32_MAX _I32_MAX 138 | #define INT64_MIN ((int64_t)_I64_MIN) 139 | #define INT64_MAX _I64_MAX 140 | #define UINT8_MAX _UI8_MAX 141 | #define UINT16_MAX _UI16_MAX 142 | #define UINT32_MAX _UI32_MAX 143 | #define UINT64_MAX _UI64_MAX 144 | 145 | // 7.18.2.2 Limits of minimum-width integer types 146 | #define INT_LEAST8_MIN INT8_MIN 147 | #define INT_LEAST8_MAX INT8_MAX 148 | #define INT_LEAST16_MIN INT16_MIN 149 | #define INT_LEAST16_MAX INT16_MAX 150 | #define INT_LEAST32_MIN INT32_MIN 151 | #define INT_LEAST32_MAX INT32_MAX 152 | #define INT_LEAST64_MIN INT64_MIN 153 | #define INT_LEAST64_MAX INT64_MAX 154 | #define UINT_LEAST8_MAX UINT8_MAX 155 | #define UINT_LEAST16_MAX UINT16_MAX 156 | #define UINT_LEAST32_MAX UINT32_MAX 157 | #define UINT_LEAST64_MAX UINT64_MAX 158 | 159 | // 7.18.2.3 Limits of fastest minimum-width integer types 160 | #define INT_FAST8_MIN INT8_MIN 161 | #define INT_FAST8_MAX INT8_MAX 162 | #define INT_FAST16_MIN INT16_MIN 163 | #define INT_FAST16_MAX INT16_MAX 164 | #define INT_FAST32_MIN INT32_MIN 165 | #define INT_FAST32_MAX INT32_MAX 166 | #define INT_FAST64_MIN INT64_MIN 167 | #define INT_FAST64_MAX INT64_MAX 168 | #define UINT_FAST8_MAX UINT8_MAX 169 | #define UINT_FAST16_MAX UINT16_MAX 170 | #define UINT_FAST32_MAX UINT32_MAX 171 | #define UINT_FAST64_MAX UINT64_MAX 172 | 173 | // 7.18.2.4 Limits of integer types capable of holding object pointers 174 | #ifdef _WIN64 // [ 175 | # define INTPTR_MIN INT64_MIN 176 | # define INTPTR_MAX INT64_MAX 177 | # define UINTPTR_MAX UINT64_MAX 178 | #else // _WIN64 ][ 179 | # define INTPTR_MIN INT32_MIN 180 | # define INTPTR_MAX INT32_MAX 181 | # define UINTPTR_MAX UINT32_MAX 182 | #endif // _WIN64 ] 183 | 184 | // 7.18.2.5 Limits of greatest-width integer types 185 | #define INTMAX_MIN INT64_MIN 186 | #define INTMAX_MAX INT64_MAX 187 | #define UINTMAX_MAX UINT64_MAX 188 | 189 | // 7.18.3 Limits of other integer types 190 | 191 | #ifdef _WIN64 // [ 192 | # define PTRDIFF_MIN _I64_MIN 193 | # define PTRDIFF_MAX _I64_MAX 194 | #else // _WIN64 ][ 195 | # define PTRDIFF_MIN _I32_MIN 196 | # define PTRDIFF_MAX _I32_MAX 197 | #endif // _WIN64 ] 198 | 199 | #define SIG_ATOMIC_MIN INT_MIN 200 | #define SIG_ATOMIC_MAX INT_MAX 201 | 202 | #ifndef SIZE_MAX // [ 203 | # ifdef _WIN64 // [ 204 | # define SIZE_MAX _UI64_MAX 205 | # else // _WIN64 ][ 206 | # define SIZE_MAX _UI32_MAX 207 | # endif // _WIN64 ] 208 | #endif // SIZE_MAX ] 209 | 210 | // WCHAR_MIN and WCHAR_MAX are also defined in 211 | #ifndef WCHAR_MIN // [ 212 | # define WCHAR_MIN 0 213 | #endif // WCHAR_MIN ] 214 | #ifndef WCHAR_MAX // [ 215 | # define WCHAR_MAX _UI16_MAX 216 | #endif // WCHAR_MAX ] 217 | 218 | #define WINT_MIN 0 219 | #define WINT_MAX _UI16_MAX 220 | 221 | #endif // __STDC_LIMIT_MACROS ] 222 | 223 | 224 | // 7.18.4 Limits of other integer types 225 | 226 | #if !defined(__cplusplus) || defined(__STDC_CONSTANT_MACROS) // [ See footnote 224 at page 260 227 | 228 | // 7.18.4.1 Macros for minimum-width integer constants 229 | 230 | #define INT8_C(val) val##i8 231 | #define INT16_C(val) val##i16 232 | #define INT32_C(val) val##i32 233 | #define INT64_C(val) val##i64 234 | 235 | #define UINT8_C(val) val##ui8 236 | #define UINT16_C(val) val##ui16 237 | #define UINT32_C(val) val##ui32 238 | #define UINT64_C(val) val##ui64 239 | 240 | // 7.18.4.2 Macros for greatest-width integer constants 241 | #define INTMAX_C INT64_C 242 | #define UINTMAX_C UINT64_C 243 | 244 | #endif // __STDC_CONSTANT_MACROS ] 245 | 246 | 247 | #endif // _MSC_STDINT_H_ ] -------------------------------------------------------------------------------- /client/lib/bootstrap/js/bootbox-4.4.0.min.js: -------------------------------------------------------------------------------- 1 | /** 2 | * bootbox.js v4.4.0 3 | * 4 | * http://bootboxjs.com/license.txt 5 | */ 6 | !function(a,b){"use strict";"function"==typeof define&&define.amd?define(["jquery"],b):"object"==typeof exports?module.exports=b(require("jquery")):a.bootbox=b(a.jQuery)}(this,function a(b,c){"use strict";function d(a){var b=q[o.locale];return b?b[a]:q.en[a]}function e(a,c,d){a.stopPropagation(),a.preventDefault();var e=b.isFunction(d)&&d.call(c,a)===!1;e||c.modal("hide")}function f(a){var b,c=0;for(b in a)c++;return c}function g(a,c){var d=0;b.each(a,function(a,b){c(a,b,d++)})}function h(a){var c,d;if("object"!=typeof a)throw new Error("Please supply an object of options");if(!a.message)throw new Error("Please specify a message");return a=b.extend({},o,a),a.buttons||(a.buttons={}),c=a.buttons,d=f(c),g(c,function(a,e,f){if(b.isFunction(e)&&(e=c[a]={callback:e}),"object"!==b.type(e))throw new Error("button with key "+a+" must be an object");e.label||(e.label=a),e.className||(e.className=2>=d&&f===d-1?"btn-primary":"btn-default")}),a}function i(a,b){var c=a.length,d={};if(1>c||c>2)throw new Error("Invalid argument length");return 2===c||"string"==typeof a[0]?(d[b[0]]=a[0],d[b[1]]=a[1]):d=a[0],d}function j(a,c,d){return b.extend(!0,{},a,i(c,d))}function k(a,b,c,d){var e={className:"bootbox-"+a,buttons:l.apply(null,b)};return m(j(e,d,c),b)}function l(){for(var a={},b=0,c=arguments.length;c>b;b++){var e=arguments[b],f=e.toLowerCase(),g=e.toUpperCase();a[f]={label:d(g)}}return a}function m(a,b){var d={};return g(b,function(a,b){d[b]=!0}),g(a.buttons,function(a){if(d[a]===c)throw new Error("button key "+a+" is not allowed (options are "+b.join("\n")+")")}),a}var n={dialog:"",header:"",footer:"",closeButton:"",form:"
",inputs:{text:"",textarea:"",email:"",select:"",checkbox:"
",date:"",time:"",number:"",password:""}},o={locale:"en",backdrop:"static",animate:!0,className:null,closeButton:!0,show:!0,container:"body"},p={};p.alert=function(){var a;if(a=k("alert",["ok"],["message","callback"],arguments),a.callback&&!b.isFunction(a.callback))throw new Error("alert requires callback property to be a function when provided");return a.buttons.ok.callback=a.onEscape=function(){return b.isFunction(a.callback)?a.callback.call(this):!0},p.dialog(a)},p.confirm=function(){var a;if(a=k("confirm",["cancel","confirm"],["message","callback"],arguments),a.buttons.cancel.callback=a.onEscape=function(){return a.callback.call(this,!1)},a.buttons.confirm.callback=function(){return a.callback.call(this,!0)},!b.isFunction(a.callback))throw new Error("confirm requires a callback");return p.dialog(a)},p.prompt=function(){var a,d,e,f,h,i,k;if(f=b(n.form),d={className:"bootbox-prompt",buttons:l("cancel","confirm"),value:"",inputType:"text"},a=m(j(d,arguments,["title","callback"]),["cancel","confirm"]),i=a.show===c?!0:a.show,a.message=f,a.buttons.cancel.callback=a.onEscape=function(){return a.callback.call(this,null)},a.buttons.confirm.callback=function(){var c;switch(a.inputType){case"text":case"textarea":case"email":case"select":case"date":case"time":case"number":case"password":c=h.val();break;case"checkbox":var d=h.find("input:checked");c=[],g(d,function(a,d){c.push(b(d).val())})}return a.callback.call(this,c)},a.show=!1,!a.title)throw new Error("prompt requires a title");if(!b.isFunction(a.callback))throw new Error("prompt requires a callback");if(!n.inputs[a.inputType])throw new Error("invalid prompt type");switch(h=b(n.inputs[a.inputType]),a.inputType){case"text":case"textarea":case"email":case"date":case"time":case"number":case"password":h.val(a.value);break;case"select":var o={};if(k=a.inputOptions||[],!b.isArray(k))throw new Error("Please pass an array of input options");if(!k.length)throw new Error("prompt with select requires options");g(k,function(a,d){var e=h;if(d.value===c||d.text===c)throw new Error("given options in wrong format");d.group&&(o[d.group]||(o[d.group]=b("").attr("label",d.group)),e=o[d.group]),e.append("")}),g(o,function(a,b){h.append(b)}),h.val(a.value);break;case"checkbox":var q=b.isArray(a.value)?a.value:[a.value];if(k=a.inputOptions||[],!k.length)throw new Error("prompt with checkbox requires options");if(!k[0].value||!k[0].text)throw new Error("given options in wrong format");h=b("
"),g(k,function(c,d){var e=b(n.inputs[a.inputType]);e.find("input").attr("value",d.value),e.find("label").append(d.text),g(q,function(a,b){b===d.value&&e.find("input").prop("checked",!0)}),h.append(e)})}return a.placeholder&&h.attr("placeholder",a.placeholder),a.pattern&&h.attr("pattern",a.pattern),a.maxlength&&h.attr("maxlength",a.maxlength),f.append(h),f.on("submit",function(a){a.preventDefault(),a.stopPropagation(),e.find(".btn-primary").click()}),e=p.dialog(a),e.off("shown.bs.modal"),e.on("shown.bs.modal",function(){h.focus()}),i===!0&&e.modal("show"),e},p.dialog=function(a){a=h(a);var d=b(n.dialog),f=d.find(".modal-dialog"),i=d.find(".modal-body"),j=a.buttons,k="",l={onEscape:a.onEscape};if(b.fn.modal===c)throw new Error("$.fn.modal is not defined; please double check you have included the Bootstrap JavaScript library. See http://getbootstrap.com/javascript/ for more details.");if(g(j,function(a,b){k+="",l[a]=b.callback}),i.find(".bootbox-body").html(a.message),a.animate===!0&&d.addClass("fade"),a.className&&d.addClass(a.className),"large"===a.size?f.addClass("modal-lg"):"small"===a.size&&f.addClass("modal-sm"),a.title&&i.before(n.header),a.closeButton){var m=b(n.closeButton);a.title?d.find(".modal-header").prepend(m):m.css("margin-top","-10px").prependTo(i)}return a.title&&d.find(".modal-title").html(a.title),k.length&&(i.after(n.footer),d.find(".modal-footer").html(k)),d.on("hidden.bs.modal",function(a){a.target===this&&d.remove()}),d.on("shown.bs.modal",function(){d.find(".btn-primary:first").focus()}),"static"!==a.backdrop&&d.on("click.dismiss.bs.modal",function(a){d.children(".modal-backdrop").length&&(a.currentTarget=d.children(".modal-backdrop").get(0)),a.target===a.currentTarget&&d.trigger("escape.close.bb")}),d.on("escape.close.bb",function(a){l.onEscape&&e(a,d,l.onEscape)}),d.on("click",".modal-footer button",function(a){var c=b(this).data("bb-handler");e(a,d,l[c])}),d.on("click",".bootbox-close-button",function(a){e(a,d,l.onEscape)}),d.on("keyup",function(a){27===a.which&&d.trigger("escape.close.bb")}),b(a.container).append(d),d.modal({backdrop:a.backdrop?"static":!1,keyboard:!1,show:!1}),a.show&&d.modal("show"),d},p.setDefaults=function(){var a={};2===arguments.length?a[arguments[0]]=arguments[1]:a=arguments[0],b.extend(o,a)},p.hideAll=function(){return b(".bootbox").modal("hide"),p};var q={bg_BG:{OK:"Ок",CANCEL:"Отказ",CONFIRM:"Потвърждавам"},br:{OK:"OK",CANCEL:"Cancelar",CONFIRM:"Sim"},cs:{OK:"OK",CANCEL:"Zrušit",CONFIRM:"Potvrdit"},da:{OK:"OK",CANCEL:"Annuller",CONFIRM:"Accepter"},de:{OK:"OK",CANCEL:"Abbrechen",CONFIRM:"Akzeptieren"},el:{OK:"Εντάξει",CANCEL:"Ακύρωση",CONFIRM:"Επιβεβαίωση"},en:{OK:"OK",CANCEL:"Cancel",CONFIRM:"OK"},es:{OK:"OK",CANCEL:"Cancelar",CONFIRM:"Aceptar"},et:{OK:"OK",CANCEL:"Katkesta",CONFIRM:"OK"},fa:{OK:"قبول",CANCEL:"لغو",CONFIRM:"تایید"},fi:{OK:"OK",CANCEL:"Peruuta",CONFIRM:"OK"},fr:{OK:"OK",CANCEL:"Annuler",CONFIRM:"D'accord"},he:{OK:"אישור",CANCEL:"ביטול",CONFIRM:"אישור"},hu:{OK:"OK",CANCEL:"Mégsem",CONFIRM:"Megerősít"},hr:{OK:"OK",CANCEL:"Odustani",CONFIRM:"Potvrdi"},id:{OK:"OK",CANCEL:"Batal",CONFIRM:"OK"},it:{OK:"OK",CANCEL:"Annulla",CONFIRM:"Conferma"},ja:{OK:"OK",CANCEL:"キャンセル",CONFIRM:"確認"},lt:{OK:"Gerai",CANCEL:"Atšaukti",CONFIRM:"Patvirtinti"},lv:{OK:"Labi",CANCEL:"Atcelt",CONFIRM:"Apstiprināt"},nl:{OK:"OK",CANCEL:"Annuleren",CONFIRM:"Accepteren"},no:{OK:"OK",CANCEL:"Avbryt",CONFIRM:"OK"},pl:{OK:"OK",CANCEL:"Anuluj",CONFIRM:"Potwierdź"},pt:{OK:"OK",CANCEL:"Cancelar",CONFIRM:"Confirmar"},ru:{OK:"OK",CANCEL:"Отмена",CONFIRM:"Применить"},sq:{OK:"OK",CANCEL:"Anulo",CONFIRM:"Prano"},sv:{OK:"OK",CANCEL:"Avbryt",CONFIRM:"OK"},th:{OK:"ตกลง",CANCEL:"ยกเลิก",CONFIRM:"ยืนยัน"},tr:{OK:"Tamam",CANCEL:"İptal",CONFIRM:"Onayla"},zh_CN:{OK:"OK",CANCEL:"取消",CONFIRM:"确认"},zh_TW:{OK:"OK",CANCEL:"取消",CONFIRM:"確認"}};return p.addLocale=function(a,c){return b.each(["OK","CANCEL","CONFIRM"],function(a,b){if(!c[b])throw new Error("Please supply a translation for '"+b+"'")}),q[a]={OK:c.OK,CANCEL:c.CANCEL,CONFIRM:c.CONFIRM},p},p.removeLocale=function(a){return delete q[a],p},p.setLocale=function(a){return p.setDefaults("locale",a)},p.init=function(c){return a(c||b)},p}); -------------------------------------------------------------------------------- /client/login-via-js-demo.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | WhatsApp Web: Login via JavaScript demo 9 | 10 | 11 | 12 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 161 | 162 | 163 |

whatsapp-web-reveng (JS demo)

164 |
165 | 166 | 167 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | let _ = require("lodash"); 2 | let fs = require("fs"); 3 | let path = require("path"); 4 | let {StringDecoder} = require("string_decoder"); 5 | let express = require("express"); 6 | let WebSocket = require("ws"); 7 | let app = express(); 8 | 9 | let {WebSocketClient} = require("./client/js/WebSocketClient.js"); 10 | let {BootstrapStep} = require("./client/js/BootstrapStep.js"); 11 | 12 | 13 | 14 | let wss = new WebSocket.Server({ port: 2019 }); 15 | console.log("whatsapp-web-reveng API server listening on port 2019"); 16 | 17 | let backendInfo = { 18 | url: "ws://localhost:2020", 19 | timeout: 10000 20 | }; 21 | 22 | wss.on("connection", function(clientWebsocketRaw, req) { 23 | let backendWebsocket = new WebSocketClient(); 24 | let clientWebsocket = new WebSocketClient().initializeFromRaw(clientWebsocketRaw, "api2client", {getOnMessageData: msg => new StringDecoder("utf-8").write(msg.data)}); 25 | clientWebsocket.send({ type: "connected" }); 26 | //clientWebsocket.onClose(() => backendWebsocket.disconnect()); 27 | 28 | clientWebsocket.waitForMessage({ 29 | condition: obj => obj.from == "client" && obj.type == "call" && obj.command == "api-connectBackend", 30 | keepWhenHit: true 31 | }).then(clientCallRequest => { 32 | if(backendWebsocket.isOpen) 33 | return; 34 | new BootstrapStep({ 35 | websocket: backendWebsocket, 36 | actor: websocket => { 37 | websocket.initialize(backendInfo.url, "api2backend", {func: WebSocket, args: [{ perMessageDeflate: false }], getOnMessageData: msg => new StringDecoder("utf-8").write(msg.data)}); 38 | websocket.onClose(() => { 39 | clientWebsocket.send({ type: "resource_gone", resource: "backend" }); 40 | }); 41 | }, 42 | request: { 43 | type: "waitForMessage", 44 | condition: obj => obj.from == "backend" && obj.type == "connected" 45 | } 46 | }).run(backendInfo.timeout).then(backendResponse => { 47 | clientCallRequest.respond({ type: "resource_connected", resource: "backend" }); 48 | }).catch(reason => { 49 | clientCallRequest.respond({ type: "error", reason: reason }); 50 | }); 51 | }).run(); 52 | 53 | clientWebsocket.waitForMessage({ 54 | condition: obj => obj.from == "client" && obj.type == "call" && obj.command == "backend-connectWhatsApp", 55 | keepWhenHit: true 56 | }).then(clientCallRequest => { 57 | if(!backendWebsocket.isOpen) { 58 | clientCallRequest.respond({ type: "error", reason: "No backend connected." }); 59 | return; 60 | } 61 | new BootstrapStep({ 62 | websocket: backendWebsocket, 63 | request: { 64 | type: "call", 65 | callArgs: { command: "backend-connectWhatsApp" }, 66 | successCondition: obj => obj.type == "resource_connected" && obj.resource == "whatsapp" && obj.resource_instance_id 67 | } 68 | }).run(backendInfo.timeout).then(backendResponse => { 69 | backendWebsocket.activeWhatsAppInstanceId = backendResponse.data.resource_instance_id; 70 | backendWebsocket.waitForMessage({ 71 | condition: obj => obj.type == "resource_gone" && obj.resource == "whatsapp", 72 | keepWhenHit: false 73 | }).then(() => { 74 | delete backendWebsocket.activeWhatsAppInstanceId; 75 | clientWebsocket.send({ type: "resource_gone", resource: "whatsapp" }); 76 | }); 77 | clientCallRequest.respond({ type: "resource_connected", resource: "whatsapp" }); 78 | }).catch(reason => { 79 | clientCallRequest.respond({ type: "error", reason: reason }); 80 | }); 81 | }).run(); 82 | 83 | clientWebsocket.waitForMessage({ 84 | condition: obj => obj.from == "client" && obj.type == "call" && obj.command == "backend-disconnectWhatsApp", 85 | keepWhenHit: true 86 | }).then(clientCallRequest => { 87 | if(!backendWebsocket.isOpen) { 88 | clientCallRequest.respond({ type: "error", reason: "No backend connected." }); 89 | return; 90 | } 91 | new BootstrapStep({ 92 | websocket: backendWebsocket, 93 | request: { 94 | type: "call", 95 | callArgs: { command: "backend-disconnectWhatsApp", whatsapp_instance_id: backendWebsocket.activeWhatsAppInstanceId }, 96 | successCondition: obj => obj.type == "resource_disconnected" && obj.resource == "whatsapp" && obj.resource_instance_id == backendWebsocket.activeWhatsAppInstanceId 97 | } 98 | }).run(backendInfo.timeout).then(backendResponse => { 99 | clientCallRequest.respond({ type: "resource_disconnected", resource: "whatsapp" }); 100 | }).catch(reason => { 101 | clientCallRequest.respond({ type: "error", reason: reason }); 102 | }); 103 | }).run(); 104 | 105 | clientWebsocket.waitForMessage({ 106 | condition: obj => obj.from == "client" && obj.type == "call" && obj.command == "backend-generateQRCode", 107 | keepWhenHit: true 108 | }).then(clientCallRequest => { 109 | if(!backendWebsocket.isOpen) { 110 | clientCallRequest.respond({ type: "error", reason: "No backend connected." }); 111 | return; 112 | } 113 | new BootstrapStep({ 114 | websocket: backendWebsocket, 115 | request: { 116 | type: "call", 117 | callArgs: { command: "backend-generateQRCode", whatsapp_instance_id: backendWebsocket.activeWhatsAppInstanceId }, 118 | successCondition: obj => obj.from == "backend" && obj.type == "generated_qr_code" && obj.image && obj.content 119 | } 120 | }).run(backendInfo.timeout).then(backendResponse => { 121 | clientCallRequest.respond({ type: "generated_qr_code", image: backendResponse.data.image }) 122 | 123 | backendWebsocket.waitForMessage({ 124 | condition: obj => obj.type == "whatsapp_message_received" && obj.message && obj.message_type && obj.timestamp && obj.resource_instance_id == backendWebsocket.activeWhatsAppInstanceId, 125 | keepWhenHit: true 126 | }).then(whatsAppMessage => { 127 | let d = whatsAppMessage.data; 128 | clientWebsocket.send({ type: "whatsapp_message_received", message: d.message, message_type: d.message_type, timestamp: d.timestamp }); 129 | }).run(); 130 | }).catch(reason => { 131 | clientCallRequest.respond({ type: "error", reason: reason }); 132 | }) 133 | }).run(); 134 | clientWebsocket.waitForMessage({ 135 | condition: obj => 136 | { 137 | last_session_data = obj.last_session 138 | return obj.from == "client" && obj.type == "call" && obj.command == "backend-restoreSession" 139 | }, 140 | keepWhenHit: true 141 | }).then(clientCallRequest => { 142 | if(!backendWebsocket.isOpen) { 143 | clientCallRequest.respond({ type: "error", reason: "No backend connected." }); 144 | clientWebsocket.send({ data: backendResponse }); 145 | 146 | return; 147 | } 148 | new BootstrapStep({ 149 | websocket: backendWebsocket, 150 | request: { 151 | type: "call", 152 | callArgs: { command: "backend-restoreSession", last_session: last_session_data, whatsapp_instance_id: backendWebsocket.activeWhatsAppInstanceId }, 153 | successCondition: obj => obj.from == "backend" && obj.type == "restore_session" 154 | } 155 | }).run(backendInfo.timeout).then(backendResponse => { 156 | clientWebsocket.send({ data: backendResponse }); 157 | clientCallRequest.respond({ type: "restore_session", res: backendResponse }) 158 | 159 | backendWebsocket.waitForMessage({ 160 | condition: obj => obj.type == "whatsapp_message_received" && obj.message && obj.message_type && obj.timestamp && obj.resource_instance_id == backendWebsocket.activeWhatsAppInstanceId, 161 | keepWhenHit: true 162 | }).then(whatsAppMessage => { 163 | let d = whatsAppMessage.data; 164 | clientWebsocket.send({ type: "whatsapp_message_received", message: d.message, message_type: d.message_type, timestamp: d.timestamp }); 165 | }).run(); 166 | }).catch(reason => { 167 | clientCallRequest.respond({ type: "error", reason: reason }); 168 | }) 169 | }).run(); 170 | 171 | //TODO: 172 | // - designated backend call function to make everything shorter 173 | // - allow client to call "backend-getLoginInfo" and "backend-getConnectionInfo" 174 | // - add buttons for that to client 175 | // - look for handlers in "decoder.py" and add them to output information 176 | // - when decoding fails, write packet to file for further investigation later 177 | 178 | 179 | 180 | /*let send = (obj, tag) => { 181 | let msgTag = tag==undefined ? (+new Date()) : tag; 182 | if(obj.from == undefined) 183 | obj.from = "api"; 184 | clientWebsocket.send(`${msgTag},${JSON.stringify(obj)}`); 185 | }; 186 | 187 | send({ type: "connected" });*/ 188 | /*let backendCall = command => { 189 | if(!waBackendValid) { 190 | send({ type: "error", msg: "No backend connected." }); 191 | return; 192 | } 193 | waBackend.onmessage = msg => { 194 | let data = JSON.parse(msg.data); 195 | if(data.status == 200) 196 | send(data); 197 | }; 198 | waBackend.send(command); 199 | };*/ 200 | 201 | /*clientWebsocket.on("message", function(msg) { 202 | let tag = msg.split(",")[0]; 203 | let obj = JSON.parse(msg.substr(tag.length + 1)); 204 | 205 | switch(obj.command) { 206 | case "api-connectBackend": {*/ 207 | 208 | 209 | //backendWebsocket = new WebSocketClient("ws://localhost:2020", true); 210 | //backendWebsocket.onClose 211 | 212 | /*waBackend = new WebSocket("ws://localhost:2020", { perMessageDeflate: false }); 213 | waBackend.onclose = () => { 214 | waBackendValid = false; 215 | waBackend = undefined; 216 | send({ type: "resource_gone", resource: "backend" }); 217 | }; 218 | waBackend.onopen = () => { 219 | waBackendValid = true; 220 | send({ type: "resource_connected", resource: "backend" }, tag); 221 | };*/ 222 | //break; 223 | //} 224 | 225 | /*case "backend-connectWhatsApp": 226 | case "backend-generateQRCode": { 227 | backendCall(msg); 228 | break; 229 | }*/ 230 | // } 231 | //}); 232 | 233 | //clientWebsocket.on("close", function() { 234 | /*if(waBackend != undefined) { 235 | waBackend.onclose = () => {}; 236 | waBackend.close(); 237 | waBackend = undefined; 238 | }*/ 239 | //}); 240 | }) 241 | 242 | 243 | 244 | 245 | app.use(express.static("client")); 246 | 247 | app.listen(2018, function() { 248 | console.log("whatsapp-web-reveng HTTP server listening on port 2018"); 249 | }); 250 | -------------------------------------------------------------------------------- /doc/spec/protobuf-extractor/index.js: -------------------------------------------------------------------------------- 1 | const request = require("request-promise-native"); 2 | const acorn = require("acorn"); 3 | const walk = require("acorn-walk"); 4 | 5 | const objectToArray = obj => Object.keys(obj).map(k => [k, obj[k]]); 6 | const indent = (lines, n) => lines.map(l => " ".repeat(n) + l); 7 | 8 | async function findAppModules(mods) { 9 | const ua = { headers: { "User-Agent": "Mozilla/5.0 (X11; Linux x86_64; rv:75.0) Gecko/20100101 Firefox/75.0" } } 10 | const WAWebMain = "https://web.whatsapp.com"; 11 | const index = await request.get(WAWebMain, ua); 12 | const appPathId = index.match(/data-app="\/app.([0-9a-z]{10,}).js"/)[1] 13 | const data = await request.get(WAWebMain + "/app." + appPathId + ".js", ua) 14 | const appModules = acorn.parse(data).body[0].expression.argument.arguments[0].properties; 15 | return appModules.filter(m => mods.indexOf(m.key.name) != -1); 16 | } 17 | 18 | (async () => { 19 | const wantedModules = ['bgiachiigg', 'bfifcddbbg', 'bbcaggdbc']; 20 | const unsortedModules = await findAppModules(wantedModules); 21 | if(unsortedModules.length !== wantedModules.length) 22 | throw "did not find all wanted modules"; 23 | // Sort modules so that whatsapp module id changes don't change the order in the output protobuf schema 24 | const modules = [] 25 | for (const mod of wantedModules) { 26 | modules.push(unsortedModules.find(node => node.key.name === mod)) 27 | } 28 | 29 | // find aliases of cross references between the wanted modules 30 | let modulesInfo = {}; 31 | modules.forEach(({key, value}) => { 32 | const requiringParam = value.params[2].name; 33 | modulesInfo[key.name] = { crossRefs: [] }; 34 | walk.simple(value, { 35 | VariableDeclarator(node) { 36 | if(node.init && node.init.type === "CallExpression" && node.init.callee.name === requiringParam && node.init.arguments.length == 1 && wantedModules.indexOf(node.init.arguments[0].value) != -1) 37 | modulesInfo[key.name].crossRefs.push({ alias: node.id.name, module: node.init.arguments[0].value }); 38 | } 39 | }); 40 | }); 41 | 42 | // find all identifiers and, for enums, their array of values 43 | for(const mod of modules) { 44 | let modInfo = modulesInfo[mod.key.name]; 45 | 46 | // all identifiers will be initialized to "void 0" (i.e. "undefined") at the start, so capture them here 47 | walk.ancestor(mod, { 48 | UnaryExpression(node, anc) { 49 | if(!modInfo.identifiers && node.operator === "void") { 50 | let assignments = [], i = 1; 51 | anc.reverse(); 52 | while(anc[i].type === "AssignmentExpression") 53 | assignments.push(anc[i++].left); 54 | modInfo.identifiers = assignments.map(a => a.property.name).reverse() 55 | .reduce((prev, curr) => (prev[curr] = {}, prev), {}); 56 | } 57 | } 58 | }); 59 | const enumAliases = {} 60 | // enums are defined directly, and both enums and messages get a one-letter alias 61 | walk.simple(mod, { 62 | AssignmentExpression(node) { 63 | if (node.left.type === "MemberExpression" && modInfo.identifiers[node.left.property.name]) { 64 | let ident = modInfo.identifiers[node.left.property.name]; 65 | ident.alias = node.right.name; 66 | ident.enumValues = enumAliases[ident.alias]; 67 | } 68 | }, 69 | VariableDeclarator(node) { 70 | if(node.init && node.init.type === "CallExpression" && node.init.callee.type === "MemberExpression" && node.init.arguments.length === 1 && node.init.arguments[0].type === "ObjectExpression") { 71 | enumAliases[node.id.name] = node.init.arguments[0].properties.map(p => ({ name: p.key.name, id: p.value.value })); 72 | } 73 | } 74 | }); 75 | }; 76 | 77 | // find the contents for all protobuf messages 78 | for(const mod of modules) { 79 | let modInfo = modulesInfo[mod.key.name]; 80 | 81 | // message specifications are stored in a "_spec" attribute of the respective identifier alias 82 | walk.simple(mod, { 83 | AssignmentExpression(node) { 84 | if(node.left.type === "MemberExpression" && node.left.property.name === "internalSpec" && node.right.type === "ObjectExpression") { 85 | let targetIdentName = Object.keys(modInfo.identifiers).find(k => modInfo.identifiers[k].alias == node.left.object.name); 86 | if(!targetIdentName) { 87 | console.warn(`found message specification for unknown identifier alias: ${node.left.object.name}`); 88 | return; 89 | } 90 | 91 | // partition spec properties by normal members and constraints (like "__oneofs__") which will be processed afterwards 92 | let targetIdent = modInfo.identifiers[targetIdentName]; 93 | let constraints = [], members = []; 94 | for(let p of node.right.properties) { 95 | p.key.name = p.key.type === "Identifier" ? p.key.name : p.key.value; 96 | (p.key.name.substr(0, 2) === "__" ? constraints : members).push(p); 97 | } 98 | 99 | members = members.map(({key: {name}, value: {elements}}) => { 100 | let type, flags = []; 101 | let unwrapBinaryOr = n => (n.type === "BinaryExpression" && n.operator === "|") ? [].concat(unwrapBinaryOr(n.left), unwrapBinaryOr(n.right)) : [n]; 102 | 103 | // find type and flags 104 | unwrapBinaryOr(elements[1]).forEach(m => { 105 | if(m.type === "MemberExpression" && m.object.type === "MemberExpression") { 106 | if(m.object.property.name === "TYPES") 107 | type = m.property.name.toLowerCase(); 108 | else if(m.object.property.name === "FLAGS") 109 | flags.push(m.property.name.toLowerCase()); 110 | } 111 | }); 112 | 113 | // determine cross reference name from alias if this member has type "message" or "enum" 114 | if(type === "message" || type === "enum") { 115 | const currLoc = ` from member '${name}' of message '${targetIdentName}'`; 116 | if(elements[2].type === "Identifier") { 117 | type = objectToArray(modInfo.identifiers).find(i => i[1].alias === elements[2].name); 118 | type ? (type = type[0]) : console.warn(`unable to find reference of alias '${elements[2].name}'` + currLoc); 119 | } else if(elements[2].type === "MemberExpression") { 120 | let crossRef = modInfo.crossRefs.find(r => r.alias === elements[2].object.name); 121 | if(crossRef && modulesInfo[crossRef.module].identifiers[elements[2].property.name]) 122 | type = elements[2].property.name; 123 | else 124 | console.warn(`unable to find reference of alias to other module '${elements[2].object.name}' or to message ${elements[2].property.name} of this module` + currLoc) 125 | } 126 | } 127 | 128 | return { name, id: elements[0].value, type, flags }; 129 | }); 130 | 131 | // resolve constraints for members 132 | constraints.forEach(c => { 133 | if(c.key.name === "__oneofs__" && c.value.type === "ObjectExpression") { 134 | let newOneOfs = c.value.properties.map(p => ({ 135 | name: p.key.name, 136 | type: "__oneof__", 137 | members: p.value.elements.map(e => { 138 | let idx = members.findIndex(m => m.name == e.value); 139 | let member = members[idx]; 140 | members.splice(idx, 1); 141 | return member; 142 | }) 143 | })); 144 | members = members.concat(newOneOfs); 145 | } 146 | }); 147 | 148 | targetIdent.members = members; 149 | } 150 | } 151 | }); 152 | }; 153 | 154 | // make all enums only one message uses be local to that message 155 | for(const mod of modules) { 156 | let idents = modulesInfo[mod.key.name].identifiers; 157 | let identsArr = objectToArray(idents); 158 | 159 | identsArr.filter(i => !!i[1].enumValues).forEach(e => { 160 | // count number of occurrences of this enumeration and store these identifiers 161 | let occurrences = []; 162 | identsArr.forEach(i => { 163 | if(i[1].members && i[1].members.find(m => m.type === e[0])) 164 | occurrences.push(i[0]); 165 | }); 166 | 167 | // if there's only one occurrence, add the enum to that message. Also remove enums that do not occur anywhere 168 | if(occurrences.length <= 1) { 169 | if(occurrences.length == 1) 170 | idents[occurrences[0]].members.find(m => m.type === e[0]).enumValues = e[1].enumValues; 171 | delete idents[e[0]]; 172 | } 173 | }); 174 | } 175 | 176 | console.log('syntax = "proto2";') 177 | console.log('package proto;') 178 | console.log('') 179 | for(const mod of modules) { 180 | let modInfo = modulesInfo[mod.key.name]; 181 | let spacesPerIndentLevel = 4; 182 | 183 | // enum stringifying function 184 | let stringifyEnum = (name, values) => 185 | [].concat( 186 | [`enum ${name} {`], 187 | indent(values.map(v => `${v.name} = ${v.id};`), spacesPerIndentLevel), 188 | ["}"] 189 | ); 190 | 191 | // message specification member stringifying function 192 | let stringifyMessageSpecMember = (info, completeFlags = true) => { 193 | if(info.type === "__oneof__") { 194 | return [].concat( 195 | [`oneof ${info.name} {`], 196 | indent([].concat(...info.members.map(m => stringifyMessageSpecMember(m, false))), spacesPerIndentLevel), 197 | ["}"] 198 | ); 199 | } else { 200 | if(completeFlags && info.flags.length == 0) 201 | info.flags.push("optional"); 202 | let ret = info.enumValues ? stringifyEnum(info.type, info.enumValues) : []; 203 | ret.push(`${info.flags.join(" ") + (info.flags.length == 0 ? "" : " ")}${info.type} ${info.name} = ${info.id};`); 204 | return ret; 205 | } 206 | }; 207 | 208 | // message specification stringifying function 209 | let stringifyMessageSpec = (name, members) => 210 | [].concat( 211 | [`message ${name} {`], 212 | indent([].concat(...members.map(m => stringifyMessageSpecMember(m))), spacesPerIndentLevel), 213 | ["}", ""] 214 | ); 215 | 216 | let lines = [].concat(...objectToArray(modInfo.identifiers).map(i => i[1].members ? stringifyMessageSpec(i[0], i[1].members) : stringifyEnum(i[0], i[1].enumValues))); 217 | console.log(lines.join("\n")); 218 | } 219 | 220 | //console.log(JSON.stringify(modulesInfo, null, 2)); 221 | })(); 222 | -------------------------------------------------------------------------------- /doc/spec/protobuf-extractor/package-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "whatsapp-web-protobuf-extractor", 3 | "version": "1.0.0", 4 | "lockfileVersion": 1, 5 | "requires": true, 6 | "dependencies": { 7 | "acorn": { 8 | "version": "6.4.1", 9 | "resolved": "https://registry.npmjs.org/acorn/-/acorn-6.4.1.tgz", 10 | "integrity": "sha512-ZVA9k326Nwrj3Cj9jlh3wGFutC2ZornPNARZwsNYqQYgN0EsV2d53w5RN/co65Ohn4sUAUtb1rSUAOD6XN9idA==" 11 | }, 12 | "acorn-walk": { 13 | "version": "6.1.1", 14 | "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-6.1.1.tgz", 15 | "integrity": "sha512-OtUw6JUTgxA2QoqqmrmQ7F2NYqiBPi/L2jqHyFtllhOUvXYQXf0Z1CYUinIfyT4bTCGmrA7gX9FvHA81uzCoVw==" 16 | }, 17 | "ajv": { 18 | "version": "6.12.6", 19 | "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", 20 | "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", 21 | "requires": { 22 | "fast-deep-equal": "^3.1.1", 23 | "fast-json-stable-stringify": "^2.0.0", 24 | "json-schema-traverse": "^0.4.1", 25 | "uri-js": "^4.2.2" 26 | }, 27 | "dependencies": { 28 | "fast-deep-equal": { 29 | "version": "3.1.3", 30 | "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", 31 | "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==" 32 | } 33 | } 34 | }, 35 | "asn1": { 36 | "version": "0.2.4", 37 | "resolved": "https://registry.npmjs.org/asn1/-/asn1-0.2.4.tgz", 38 | "integrity": "sha512-jxwzQpLQjSmWXgwaCZE9Nz+glAG01yF1QnWgbhGwHI5A6FRIEY6IVqtHhIepHqI7/kyEyQEagBC5mBEFlIYvdg==", 39 | "requires": { 40 | "safer-buffer": "~2.1.0" 41 | } 42 | }, 43 | "assert-plus": { 44 | "version": "1.0.0", 45 | "resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz", 46 | "integrity": "sha1-8S4PPF13sLHN2RRpQuTpbB5N1SU=" 47 | }, 48 | "asynckit": { 49 | "version": "0.4.0", 50 | "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", 51 | "integrity": "sha1-x57Zf380y48robyXkLzDZkdLS3k=" 52 | }, 53 | "aws-sign2": { 54 | "version": "0.7.0", 55 | "resolved": "https://registry.npmjs.org/aws-sign2/-/aws-sign2-0.7.0.tgz", 56 | "integrity": "sha1-tG6JCTSpWR8tL2+G1+ap8bP+dqg=" 57 | }, 58 | "aws4": { 59 | "version": "1.8.0", 60 | "resolved": "https://registry.npmjs.org/aws4/-/aws4-1.8.0.tgz", 61 | "integrity": "sha512-ReZxvNHIOv88FlT7rxcXIIC0fPt4KZqZbOlivyWtXLt8ESx84zd3kMC6iK5jVeS2qt+g7ftS7ye4fi06X5rtRQ==" 62 | }, 63 | "bcrypt-pbkdf": { 64 | "version": "1.0.2", 65 | "resolved": "https://registry.npmjs.org/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.2.tgz", 66 | "integrity": "sha1-pDAdOJtqQ/m2f/PKEaP2Y342Dp4=", 67 | "requires": { 68 | "tweetnacl": "^0.14.3" 69 | } 70 | }, 71 | "caseless": { 72 | "version": "0.12.0", 73 | "resolved": "https://registry.npmjs.org/caseless/-/caseless-0.12.0.tgz", 74 | "integrity": "sha1-G2gcIf+EAzyCZUMJBolCDRhxUdw=" 75 | }, 76 | "combined-stream": { 77 | "version": "1.0.7", 78 | "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.7.tgz", 79 | "integrity": "sha512-brWl9y6vOB1xYPZcpZde3N9zDByXTosAeMDo4p1wzo6UMOX4vumB+TP1RZ76sfE6Md68Q0NJSrE/gbezd4Ul+w==", 80 | "requires": { 81 | "delayed-stream": "~1.0.0" 82 | } 83 | }, 84 | "core-util-is": { 85 | "version": "1.0.2", 86 | "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz", 87 | "integrity": "sha1-tf1UIgqivFq1eqtxQMlAdUUDwac=" 88 | }, 89 | "dashdash": { 90 | "version": "1.14.1", 91 | "resolved": "https://registry.npmjs.org/dashdash/-/dashdash-1.14.1.tgz", 92 | "integrity": "sha1-hTz6D3y+L+1d4gMmuN1YEDX24vA=", 93 | "requires": { 94 | "assert-plus": "^1.0.0" 95 | } 96 | }, 97 | "delayed-stream": { 98 | "version": "1.0.0", 99 | "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", 100 | "integrity": "sha1-3zrhmayt+31ECqrgsp4icrJOxhk=" 101 | }, 102 | "ecc-jsbn": { 103 | "version": "0.1.2", 104 | "resolved": "https://registry.npmjs.org/ecc-jsbn/-/ecc-jsbn-0.1.2.tgz", 105 | "integrity": "sha1-OoOpBOVDUyh4dMVkt1SThoSamMk=", 106 | "requires": { 107 | "jsbn": "~0.1.0", 108 | "safer-buffer": "^2.1.0" 109 | } 110 | }, 111 | "extend": { 112 | "version": "3.0.2", 113 | "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", 114 | "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==" 115 | }, 116 | "extsprintf": { 117 | "version": "1.3.0", 118 | "resolved": "https://registry.npmjs.org/extsprintf/-/extsprintf-1.3.0.tgz", 119 | "integrity": "sha1-lpGEQOMEGnpBT4xS48V06zw+HgU=" 120 | }, 121 | "fast-json-stable-stringify": { 122 | "version": "2.0.0", 123 | "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.0.0.tgz", 124 | "integrity": "sha1-1RQsDK7msRifh9OnYREGT4bIu/I=" 125 | }, 126 | "forever-agent": { 127 | "version": "0.6.1", 128 | "resolved": "https://registry.npmjs.org/forever-agent/-/forever-agent-0.6.1.tgz", 129 | "integrity": "sha1-+8cfDEGt6zf5bFd60e1C2P2sypE=" 130 | }, 131 | "form-data": { 132 | "version": "2.3.3", 133 | "resolved": "https://registry.npmjs.org/form-data/-/form-data-2.3.3.tgz", 134 | "integrity": "sha512-1lLKB2Mu3aGP1Q/2eCOx0fNbRMe7XdwktwOruhfqqd0rIJWwN4Dh+E3hrPSlDCXnSR7UtZ1N38rVXm+6+MEhJQ==", 135 | "requires": { 136 | "asynckit": "^0.4.0", 137 | "combined-stream": "^1.0.6", 138 | "mime-types": "^2.1.12" 139 | } 140 | }, 141 | "getpass": { 142 | "version": "0.1.7", 143 | "resolved": "https://registry.npmjs.org/getpass/-/getpass-0.1.7.tgz", 144 | "integrity": "sha1-Xv+OPmhNVprkyysSgmBOi6YhSfo=", 145 | "requires": { 146 | "assert-plus": "^1.0.0" 147 | } 148 | }, 149 | "har-schema": { 150 | "version": "2.0.0", 151 | "resolved": "https://registry.npmjs.org/har-schema/-/har-schema-2.0.0.tgz", 152 | "integrity": "sha1-qUwiJOvKwEeCoNkDVSHyRzW37JI=" 153 | }, 154 | "har-validator": { 155 | "version": "5.1.3", 156 | "resolved": "https://registry.npmjs.org/har-validator/-/har-validator-5.1.3.tgz", 157 | "integrity": "sha512-sNvOCzEQNr/qrvJgc3UG/kD4QtlHycrzwS+6mfTrrSq97BvaYcPZZI1ZSqGSPR73Cxn4LKTD4PttRwfU7jWq5g==", 158 | "requires": { 159 | "ajv": "^6.5.5", 160 | "har-schema": "^2.0.0" 161 | } 162 | }, 163 | "http-signature": { 164 | "version": "1.2.0", 165 | "resolved": "https://registry.npmjs.org/http-signature/-/http-signature-1.2.0.tgz", 166 | "integrity": "sha1-muzZJRFHcvPZW2WmCruPfBj7rOE=", 167 | "requires": { 168 | "assert-plus": "^1.0.0", 169 | "jsprim": "^1.2.2", 170 | "sshpk": "^1.7.0" 171 | } 172 | }, 173 | "is-typedarray": { 174 | "version": "1.0.0", 175 | "resolved": "https://registry.npmjs.org/is-typedarray/-/is-typedarray-1.0.0.tgz", 176 | "integrity": "sha1-5HnICFjfDBsR3dppQPlgEfzaSpo=" 177 | }, 178 | "isstream": { 179 | "version": "0.1.2", 180 | "resolved": "https://registry.npmjs.org/isstream/-/isstream-0.1.2.tgz", 181 | "integrity": "sha1-R+Y/evVa+m+S4VAOaQ64uFKcCZo=" 182 | }, 183 | "jsbn": { 184 | "version": "0.1.1", 185 | "resolved": "https://registry.npmjs.org/jsbn/-/jsbn-0.1.1.tgz", 186 | "integrity": "sha1-peZUwuWi3rXyAdls77yoDA7y9RM=" 187 | }, 188 | "json-schema": { 189 | "version": "0.2.3", 190 | "resolved": "https://registry.npmjs.org/json-schema/-/json-schema-0.2.3.tgz", 191 | "integrity": "sha1-tIDIkuWaLwWVTOcnvT8qTogvnhM=" 192 | }, 193 | "json-schema-traverse": { 194 | "version": "0.4.1", 195 | "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", 196 | "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==" 197 | }, 198 | "json-stringify-safe": { 199 | "version": "5.0.1", 200 | "resolved": "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz", 201 | "integrity": "sha1-Epai1Y/UXxmg9s4B1lcB4sc1tus=" 202 | }, 203 | "jsprim": { 204 | "version": "1.4.1", 205 | "resolved": "https://registry.npmjs.org/jsprim/-/jsprim-1.4.1.tgz", 206 | "integrity": "sha1-MT5mvB5cwG5Di8G3SZwuXFastqI=", 207 | "requires": { 208 | "assert-plus": "1.0.0", 209 | "extsprintf": "1.3.0", 210 | "json-schema": "0.2.3", 211 | "verror": "1.10.0" 212 | } 213 | }, 214 | "lodash": { 215 | "version": "4.17.21", 216 | "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", 217 | "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==" 218 | }, 219 | "mime-db": { 220 | "version": "1.37.0", 221 | "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.37.0.tgz", 222 | "integrity": "sha512-R3C4db6bgQhlIhPU48fUtdVmKnflq+hRdad7IyKhtFj06VPNVdk2RhiYL3UjQIlso8L+YxAtFkobT0VK+S/ybg==" 223 | }, 224 | "mime-types": { 225 | "version": "2.1.21", 226 | "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.21.tgz", 227 | "integrity": "sha512-3iL6DbwpyLzjR3xHSFNFeb9Nz/M8WDkX33t1GFQnFOllWk8pOrh/LSrB5OXlnlW5P9LH73X6loW/eogc+F5lJg==", 228 | "requires": { 229 | "mime-db": "~1.37.0" 230 | } 231 | }, 232 | "oauth-sign": { 233 | "version": "0.9.0", 234 | "resolved": "https://registry.npmjs.org/oauth-sign/-/oauth-sign-0.9.0.tgz", 235 | "integrity": "sha512-fexhUFFPTGV8ybAtSIGbV6gOkSv8UtRbDBnAyLQw4QPKkgNlsH2ByPGtMUqdWkos6YCRmAqViwgZrJc/mRDzZQ==" 236 | }, 237 | "performance-now": { 238 | "version": "2.1.0", 239 | "resolved": "https://registry.npmjs.org/performance-now/-/performance-now-2.1.0.tgz", 240 | "integrity": "sha1-Ywn04OX6kT7BxpMHrjZLSzd8nns=" 241 | }, 242 | "psl": { 243 | "version": "1.1.31", 244 | "resolved": "https://registry.npmjs.org/psl/-/psl-1.1.31.tgz", 245 | "integrity": "sha512-/6pt4+C+T+wZUieKR620OpzN/LlnNKuWjy1iFLQ/UG35JqHlR/89MP1d96dUfkf6Dne3TuLQzOYEYshJ+Hx8mw==" 246 | }, 247 | "punycode": { 248 | "version": "2.1.1", 249 | "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.1.1.tgz", 250 | "integrity": "sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A==" 251 | }, 252 | "qs": { 253 | "version": "6.5.2", 254 | "resolved": "https://registry.npmjs.org/qs/-/qs-6.5.2.tgz", 255 | "integrity": "sha512-N5ZAX4/LxJmF+7wN74pUD6qAh9/wnvdQcjq9TZjevvXzSUo7bfmw91saqMjzGS2xq91/odN2dW/WOl7qQHNDGA==" 256 | }, 257 | "request": { 258 | "version": "2.88.0", 259 | "resolved": "https://registry.npmjs.org/request/-/request-2.88.0.tgz", 260 | "integrity": "sha512-NAqBSrijGLZdM0WZNsInLJpkJokL72XYjUpnB0iwsRgxh7dB6COrHnTBNwN0E+lHDAJzu7kLAkDeY08z2/A0hg==", 261 | "requires": { 262 | "aws-sign2": "~0.7.0", 263 | "aws4": "^1.8.0", 264 | "caseless": "~0.12.0", 265 | "combined-stream": "~1.0.6", 266 | "extend": "~3.0.2", 267 | "forever-agent": "~0.6.1", 268 | "form-data": "~2.3.2", 269 | "har-validator": "~5.1.0", 270 | "http-signature": "~1.2.0", 271 | "is-typedarray": "~1.0.0", 272 | "isstream": "~0.1.2", 273 | "json-stringify-safe": "~5.0.1", 274 | "mime-types": "~2.1.19", 275 | "oauth-sign": "~0.9.0", 276 | "performance-now": "^2.1.0", 277 | "qs": "~6.5.2", 278 | "safe-buffer": "^5.1.2", 279 | "tough-cookie": "~2.4.3", 280 | "tunnel-agent": "^0.6.0", 281 | "uuid": "^3.3.2" 282 | } 283 | }, 284 | "request-promise-core": { 285 | "version": "1.1.2", 286 | "resolved": "https://registry.npmjs.org/request-promise-core/-/request-promise-core-1.1.2.tgz", 287 | "integrity": "sha512-UHYyq1MO8GsefGEt7EprS8UrXsm1TxEvFUX1IMTuSLU2Rh7fTIdFtl8xD7JiEYiWU2dl+NYAjCTksTehQUxPag==", 288 | "requires": { 289 | "lodash": "^4.17.11" 290 | } 291 | }, 292 | "request-promise-native": { 293 | "version": "1.0.7", 294 | "resolved": "https://registry.npmjs.org/request-promise-native/-/request-promise-native-1.0.7.tgz", 295 | "integrity": "sha512-rIMnbBdgNViL37nZ1b3L/VfPOpSi0TqVDQPAvO6U14lMzOLrt5nilxCQqtDKhZeDiW0/hkCXGoQjhgJd/tCh6w==", 296 | "requires": { 297 | "request-promise-core": "1.1.2", 298 | "stealthy-require": "^1.1.1", 299 | "tough-cookie": "^2.3.3" 300 | } 301 | }, 302 | "safe-buffer": { 303 | "version": "5.1.2", 304 | "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", 305 | "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==" 306 | }, 307 | "safer-buffer": { 308 | "version": "2.1.2", 309 | "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", 310 | "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==" 311 | }, 312 | "sshpk": { 313 | "version": "1.15.2", 314 | "resolved": "https://registry.npmjs.org/sshpk/-/sshpk-1.15.2.tgz", 315 | "integrity": "sha512-Ra/OXQtuh0/enyl4ETZAfTaeksa6BXks5ZcjpSUNrjBr0DvrJKX+1fsKDPpT9TBXgHAFsa4510aNVgI8g/+SzA==", 316 | "requires": { 317 | "asn1": "~0.2.3", 318 | "assert-plus": "^1.0.0", 319 | "bcrypt-pbkdf": "^1.0.0", 320 | "dashdash": "^1.12.0", 321 | "ecc-jsbn": "~0.1.1", 322 | "getpass": "^0.1.1", 323 | "jsbn": "~0.1.0", 324 | "safer-buffer": "^2.0.2", 325 | "tweetnacl": "~0.14.0" 326 | } 327 | }, 328 | "stealthy-require": { 329 | "version": "1.1.1", 330 | "resolved": "https://registry.npmjs.org/stealthy-require/-/stealthy-require-1.1.1.tgz", 331 | "integrity": "sha1-NbCYdbT/SfJqd35QmzCQoyJr8ks=" 332 | }, 333 | "tough-cookie": { 334 | "version": "2.4.3", 335 | "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-2.4.3.tgz", 336 | "integrity": "sha512-Q5srk/4vDM54WJsJio3XNn6K2sCG+CQ8G5Wz6bZhRZoAe/+TxjWB/GlFAnYEbkYVlON9FMk/fE3h2RLpPXo4lQ==", 337 | "requires": { 338 | "psl": "^1.1.24", 339 | "punycode": "^1.4.1" 340 | }, 341 | "dependencies": { 342 | "punycode": { 343 | "version": "1.4.1", 344 | "resolved": "https://registry.npmjs.org/punycode/-/punycode-1.4.1.tgz", 345 | "integrity": "sha1-wNWmOycYgArY4esPpSachN1BhF4=" 346 | } 347 | } 348 | }, 349 | "tunnel-agent": { 350 | "version": "0.6.0", 351 | "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz", 352 | "integrity": "sha1-J6XeoGs2sEoKmWZ3SykIaPD8QP0=", 353 | "requires": { 354 | "safe-buffer": "^5.0.1" 355 | } 356 | }, 357 | "tweetnacl": { 358 | "version": "0.14.5", 359 | "resolved": "https://registry.npmjs.org/tweetnacl/-/tweetnacl-0.14.5.tgz", 360 | "integrity": "sha1-WuaBd/GS1EViadEIr6k/+HQ/T2Q=" 361 | }, 362 | "uri-js": { 363 | "version": "4.2.2", 364 | "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.2.2.tgz", 365 | "integrity": "sha512-KY9Frmirql91X2Qgjry0Wd4Y+YTdrdZheS8TFwvkbLWf/G5KNJDCh6pKL5OZctEW4+0Baa5idK2ZQuELRwPznQ==", 366 | "requires": { 367 | "punycode": "^2.1.0" 368 | } 369 | }, 370 | "uuid": { 371 | "version": "3.3.2", 372 | "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.3.2.tgz", 373 | "integrity": "sha512-yXJmeNaw3DnnKAOKJE51sL/ZaYfWJRl1pK9dr19YFCu0ObS231AB1/LbqTKRAQ5kw8A90rA6fr4riOUpTZvQZA==" 374 | }, 375 | "verror": { 376 | "version": "1.10.0", 377 | "resolved": "https://registry.npmjs.org/verror/-/verror-1.10.0.tgz", 378 | "integrity": "sha1-OhBcoXBTr1XW4nDB+CiGguGNpAA=", 379 | "requires": { 380 | "assert-plus": "^1.0.0", 381 | "core-util-is": "1.0.2", 382 | "extsprintf": "^1.2.0" 383 | } 384 | } 385 | } 386 | } 387 | -------------------------------------------------------------------------------- /backend/whatsapp.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | 4 | import sys; 5 | sys.dont_write_bytecode = True; 6 | 7 | import os; 8 | import signal; 9 | import base64; 10 | from threading import Thread, Timer 11 | import math; 12 | import time; 13 | import datetime; 14 | import json; 15 | import io; 16 | from time import sleep; 17 | from threading import Thread; 18 | from Crypto.Cipher import AES; 19 | from Crypto.Hash import SHA256; 20 | import hashlib; 21 | import hmac; 22 | import traceback; 23 | import binascii 24 | from Crypto import Random 25 | from whatsapp_defines import WATags, WASingleByteTokens, WADoubleByteTokens, WAWebMessageInfo; 26 | from whatsapp_binary_writer import whatsappWriteBinary, WASingleByteTokens, WADoubleByteTokens, WAWebMessageInfo; 27 | from whatsapp_defines import WAMetrics; 28 | import websocket; 29 | import curve25519; 30 | import pyqrcode; 31 | from utilities import *; 32 | from whatsapp_binary_reader import whatsappReadBinary; 33 | 34 | WHATSAPP_WEB_VERSION="2,2121,6" 35 | 36 | reload(sys); 37 | sys.setdefaultencoding("utf-8"); 38 | 39 | 40 | 41 | def HmacSha256(key, sign): 42 | return hmac.new(key, sign, hashlib.sha256).digest(); 43 | 44 | def HKDF(key, length, appInfo=""): # implements RFC 5869, some parts from https://github.com/MirkoDziadzka/pyhkdf 45 | key = HmacSha256("\0"*32, key); 46 | keyStream = ""; 47 | keyBlock = ""; 48 | blockIndex = 1; 49 | while len(keyStream) < length: 50 | keyBlock = hmac.new(key, msg=keyBlock+appInfo+chr(blockIndex), digestmod=hashlib.sha256).digest(); 51 | blockIndex += 1; 52 | keyStream += keyBlock; 53 | return keyStream[:length]; 54 | 55 | def AESPad(s): 56 | bs = AES.block_size; 57 | return s + (bs - len(s) % bs) * chr(bs - len(s) % bs); 58 | 59 | def to_bytes(n, length, endianess='big'): 60 | h = '%x' % n 61 | s = ('0'*(len(h) % 2) + h).zfill(length*2).decode('hex') 62 | return s if endianess == 'big' else s[::-1] 63 | 64 | def AESUnpad(s): 65 | return s[:-ord(s[len(s)-1:])]; 66 | 67 | def AESEncrypt(key, plaintext): # like "AESPad"/"AESUnpad" from https://stackoverflow.com/a/21928790 68 | plaintext = AESPad(plaintext); 69 | iv = os.urandom(AES.block_size); 70 | cipher = AES.new(key, AES.MODE_CBC, iv); 71 | return iv + cipher.encrypt(plaintext); 72 | 73 | def WhatsAppEncrypt(encKey, macKey, plaintext): 74 | enc = AESEncrypt(encKey, plaintext) 75 | return HmacSha256(macKey, enc) + enc; # this may need padding to 64 byte boundary 76 | 77 | def AESDecrypt(key, ciphertext): # from https://stackoverflow.com/a/20868265 78 | iv = ciphertext[:AES.block_size]; 79 | cipher = AES.new(key, AES.MODE_CBC, iv); 80 | plaintext = cipher.decrypt(ciphertext[AES.block_size:]); 81 | return AESUnpad(plaintext); 82 | 83 | 84 | 85 | class WhatsAppWebClient: 86 | websocketIsOpened = False; 87 | onOpenCallback = None; 88 | onMessageCallback = None; 89 | onCloseCallback = None; 90 | activeWs = None; 91 | messageSentCount = 0; 92 | websocketThread = None; 93 | messageQueue = {}; # maps message tags (provided by WhatsApp) to more information (description and callback) 94 | loginInfo = { 95 | "clientId": None, 96 | "serverRef": None, 97 | "privateKey": None, 98 | "publicKey": None, 99 | "key": { 100 | "encKey": None, 101 | "macKey": None 102 | } 103 | }; 104 | connInfo = { 105 | "clientToken": None, 106 | "serverToken": None, 107 | "browserToken": None, 108 | "secret": None, 109 | "sharedSecret": None, 110 | "me": None 111 | }; 112 | 113 | def __init__(self, onOpenCallback, onMessageCallback, onCloseCallback): 114 | self.onOpenCallback = onOpenCallback; 115 | self.onMessageCallback = onMessageCallback; 116 | self.onCloseCallback = onCloseCallback; 117 | websocket.enableTrace(True); 118 | self.connect(); 119 | 120 | 121 | 122 | def onOpen(self, ws): 123 | try: 124 | self.websocketIsOpened = True; 125 | if self.onOpenCallback is not None and "func" in self.onOpenCallback: 126 | self.onOpenCallback["func"](self.onOpenCallback); 127 | eprint("WhatsApp backend Websocket opened."); 128 | except: 129 | eprint(traceback.format_exc()); 130 | 131 | def onError(self, ws, error): 132 | eprint(error); 133 | 134 | def onClose(self, ws): 135 | self.websocketIsOpened = False; 136 | if self.onCloseCallback is not None and "func" in self.onCloseCallback: 137 | self.onCloseCallback["func"](self.onCloseCallback); 138 | eprint("WhatsApp backend Websocket closed."); 139 | def keepAlive(self): 140 | if self.activeWs is not None: 141 | self.activeWs.send("?,,") 142 | Timer(20.0, self.keepAlive).start() 143 | 144 | def onMessage(self, ws, message): 145 | try: 146 | messageSplit = message.split(",", 1); 147 | messageTag = messageSplit[0]; 148 | messageContent = messageSplit[1]; 149 | 150 | if messageTag in self.messageQueue: # when the server responds to a client's message 151 | pend = self.messageQueue[messageTag]; 152 | if pend["desc"] == "_status": 153 | if messageContent[0] == 'Pong' and messageContent[1] == True: 154 | pend["callback"]({"Connected": True,"user":self.connInfo["me"],"pushname":self.connInfo["pushname"]}) 155 | elif pend["desc"] == "_restoresession": 156 | pend["callback"]["func"]({ "type": "restore_session" }, pend["callback"]); 157 | 158 | elif pend["desc"] == "_login": 159 | eprint("Message after login: ", message); 160 | self.loginInfo["serverRef"] = json.loads(messageContent)["ref"]; 161 | eprint("set server id: " + self.loginInfo["serverRef"]); 162 | self.loginInfo["privateKey"] = curve25519.Private(); 163 | self.loginInfo["publicKey"] = self.loginInfo["privateKey"].get_public(); 164 | qrCodeContents = self.loginInfo["serverRef"] + "," + base64.b64encode(self.loginInfo["publicKey"].serialize()) + "," + self.loginInfo["clientId"]; 165 | eprint("qr code contents: " + qrCodeContents); 166 | 167 | svgBuffer = io.BytesIO(); # from https://github.com/mnooner256/pyqrcode/issues/39#issuecomment-207621532 168 | pyqrcode.create(qrCodeContents, error='L').svg(svgBuffer, scale=6, background="rgba(0,0,0,0.0)", module_color="#122E31", quiet_zone=0); 169 | if "callback" in pend and pend["callback"] is not None and "func" in pend["callback"] and pend["callback"]["func"] is not None and "tag" in pend["callback"] and pend["callback"]["tag"] is not None: 170 | pend["callback"]["func"]({ "type": "generated_qr_code", "image": "data:image/svg+xml;base64," + base64.b64encode(svgBuffer.getvalue()), "content": qrCodeContents }, pend["callback"]); 171 | else: 172 | try: 173 | jsonObj = json.loads(messageContent); # try reading as json 174 | except ValueError, e: 175 | if messageContent != "": 176 | hmacValidation = HmacSha256(self.loginInfo["key"]["macKey"], messageContent[32:]); 177 | if hmacValidation != messageContent[:32]: 178 | raise ValueError("Hmac mismatch"); 179 | 180 | decryptedMessage = AESDecrypt(self.loginInfo["key"]["encKey"], messageContent[32:]); 181 | try: 182 | processedData = whatsappReadBinary(decryptedMessage, True); 183 | messageType = "binary"; 184 | except: 185 | processedData = { "traceback": traceback.format_exc().splitlines() }; 186 | messageType = "error"; 187 | finally: 188 | self.onMessageCallback["func"](processedData, self.onMessageCallback, { "message_type": messageType }); 189 | else: 190 | self.onMessageCallback["func"](jsonObj, self.onMessageCallback, { "message_type": "json" }); 191 | if isinstance(jsonObj, list) and len(jsonObj) > 0: # check if the result is an array 192 | eprint(json.dumps(jsonObj)); 193 | if jsonObj[0] == "Conn": 194 | Timer(20.0, self.keepAlive).start() # Keepalive Request 195 | self.connInfo["clientToken"] = jsonObj[1]["clientToken"]; 196 | self.connInfo["serverToken"] = jsonObj[1]["serverToken"]; 197 | self.connInfo["browserToken"] = jsonObj[1]["browserToken"]; 198 | self.connInfo["me"] = jsonObj[1]["wid"]; 199 | 200 | self.connInfo["secret"] = base64.b64decode(jsonObj[1]["secret"]); 201 | self.connInfo["sharedSecret"] = self.loginInfo["privateKey"].get_shared_key(curve25519.Public(self.connInfo["secret"][:32]), lambda a: a); 202 | sse = self.connInfo["sharedSecretExpanded"] = HKDF(self.connInfo["sharedSecret"], 80); 203 | hmacValidation = HmacSha256(sse[32:64], self.connInfo["secret"][:32] + self.connInfo["secret"][64:]); 204 | if hmacValidation != self.connInfo["secret"][32:64]: 205 | raise ValueError("Hmac mismatch"); 206 | 207 | keysEncrypted = sse[64:] + self.connInfo["secret"][64:]; 208 | keysDecrypted = AESDecrypt(sse[:32], keysEncrypted); 209 | self.loginInfo["key"]["encKey"] = keysDecrypted[:32]; 210 | self.loginInfo["key"]["macKey"] = keysDecrypted[32:64]; 211 | 212 | self.save_session(); 213 | # eprint("private key : ", base64.b64encode(self.loginInfo["privateKey"].serialize())); 214 | # eprint("secret : ", base64.b64encode(self.connInfo["secret"])); 215 | # eprint("shared secret : ", base64.b64encode(self.connInfo["sharedSecret"])); 216 | # eprint("shared secret expanded : ", base64.b64encode(self.connInfo["sharedSecretExpanded"])); 217 | # eprint("hmac validation : ", base64.b64encode(hmacValidation)); 218 | # eprint("keys encrypted : ", base64.b64encode(keysEncrypted)); 219 | # eprint("keys decrypted : ", base64.b64encode(keysDecrypted)); 220 | 221 | eprint("set connection info: client, server and browser token; secret, shared secret, enc key, mac key"); 222 | eprint("logged in as " + jsonObj[1]["pushname"] + " (" + jsonObj[1]["wid"] + ")"); 223 | elif jsonObj[0] == "Cmd": 224 | if jsonObj[1]["type"] == "challenge": # Do challenge 225 | challenge = WhatsAppEncrypt(self.loginInfo["key"]["encKey"], self.loginInfo["key"]["macKey"], base64.b64decode(jsonObj[1]["challenge"])) 226 | 227 | challenge = base64.b64encode(challenge) 228 | messageTag = str(getTimestamp()); 229 | eprint(json.dumps( [messageTag,["admin","challenge",challenge,self.connInfo["serverToken"],self.loginInfo["clientId"]]])) 230 | self.activeWs.send(json.dumps( [messageTag,["admin","challenge",challenge,self.connInfo["serverToken"],self.loginInfo["clientId"]]])); 231 | elif jsonObj[0] == "Stream": 232 | pass; 233 | elif jsonObj[0] == "Props": 234 | pass; 235 | except: 236 | eprint(traceback.format_exc()); 237 | 238 | 239 | 240 | def connect(self): 241 | self.activeWs = websocket.WebSocketApp("wss://web.whatsapp.com/ws", 242 | on_message = lambda ws, message: self.onMessage(ws, message), 243 | on_error = lambda ws, error: self.onError(ws, error), 244 | on_open = lambda ws: self.onOpen(ws), 245 | on_close = lambda ws: self.onClose(ws), 246 | header = { "Origin: https://web.whatsapp.com" }); 247 | 248 | self.websocketThread = Thread(target = self.activeWs.run_forever); 249 | self.websocketThread.daemon = True; 250 | self.websocketThread.start(); 251 | 252 | def generateQRCode(self, callback=None): 253 | self.loginInfo["clientId"] = base64.b64encode(os.urandom(16)); 254 | messageTag = str(getTimestamp()); 255 | self.messageQueue[messageTag] = { "desc": "_login", "callback": callback }; 256 | message = messageTag + ',["admin","init",['+ WHATSAPP_WEB_VERSION + '],["Chromium at ' + datetime.datetime.now().isoformat() + '","Chromium"],"' + self.loginInfo["clientId"] + '",true]'; 257 | self.activeWs.send(message); 258 | 259 | def restoreSession(self, callback=None): 260 | with open("session.json","r") as f: 261 | session_file = f.read() 262 | session = json.loads(session_file) 263 | self.connInfo["clientToken"] = session['clientToken'] 264 | self.connInfo["serverToken"] = session['serverToken'] 265 | self.loginInfo["clientId"] = session['clientId'] 266 | self.loginInfo["key"]["macKey"] = session['macKey'].encode("latin_1") 267 | self.loginInfo["key"]["encKey"] = session['encKey'].encode("latin_1") 268 | 269 | messageTag = str(getTimestamp()) 270 | message = messageTag + ',["admin","init",['+ WHATSAPP_WEB_VERSION + '],["StatusDownloader","Chromium"],"' + self.loginInfo["clientId"] + '",true]' 271 | self.activeWs.send(message) 272 | 273 | messageTag = str(getTimestamp()) 274 | self.messageQueue[messageTag] = {"desc": "_restoresession","callback": callback} 275 | message = messageTag + ',["admin","login","' + self.connInfo["clientToken"] + '", "' + self.connInfo[ 276 | "serverToken"] + '", "' + self.loginInfo["clientId"] + '", "takeover"]' 277 | 278 | self.activeWs.send(message) 279 | def save_session(self): 280 | session = {"clientToken":self.connInfo["clientToken"],"serverToken":self.connInfo["serverToken"], 281 | "clientId":self.loginInfo["clientId"],"macKey": self.loginInfo["key"]["macKey"].decode("latin_1") 282 | ,"encKey": self.loginInfo["key"]["encKey"].decode("latin_1")}; 283 | f = open("./session.json","w") 284 | f.write(json.dumps(session)) 285 | f.close() 286 | 287 | def getLoginInfo(self, callback): 288 | callback["func"]({ "type": "login_info", "data": self.loginInfo }, callback); 289 | 290 | def getConnectionInfo(self, callback): 291 | callback["func"]({ "type": "connection_info", "data": self.connInfo }, callback); 292 | 293 | def sendTextMessage(self, number, text): 294 | messageId = "3EB0"+binascii.hexlify(Random.get_random_bytes(8)).upper() 295 | messageTag = str(getTimestamp()) 296 | messageParams = {"key": {"fromMe": True, "remoteJid": number + "@s.whatsapp.net", "id": messageId},"messageTimestamp": getTimestamp(), "status": 1, "message": {"conversation": text}} 297 | msgData = ["action", {"type": "relay", "epoch": str(self.messageSentCount)},[["message", None, WAWebMessageInfo.encode(messageParams)]]] 298 | encryptedMessage = WhatsAppEncrypt(self.loginInfo["key"]["encKey"], self.loginInfo["key"]["macKey"],whatsappWriteBinary(msgData)) 299 | payload = bytearray(messageId) + bytearray(",") + bytearray(to_bytes(WAMetrics.MESSAGE, 1)) + bytearray([0x80]) + encryptedMessage 300 | self.messageSentCount = self.messageSentCount + 1 301 | self.messageQueue[messageId] = {"desc": "__sending"} 302 | self.activeWs.send(payload, websocket.ABNF.OPCODE_BINARY) 303 | 304 | def status(self, callback=None): 305 | if self.activeWs is not None: 306 | messageTag = str(getTimestamp()) 307 | self.messageQueue[messageTag] = {"desc": "_status", "callback": callback} 308 | message = messageTag + ',["admin", "test"]' 309 | self.activeWs.send(message) 310 | 311 | def disconnect(self): 312 | self.activeWs.send('goodbye,,["admin","Conn","disconnect"]'); # WhatsApp server closes connection automatically when client wants to disconnect 313 | #time.sleep(0.5); 314 | #self.activeWs.close(); 315 | -------------------------------------------------------------------------------- /client/js/main.js: -------------------------------------------------------------------------------- 1 | let consoleShown = false; 2 | let apiWebsocket = new WebSocketClient(); 3 | 4 | function sleep(ms) { 5 | return new Promise((resolve, reject) => { 6 | setTimeout(() => resolve(), ms); 7 | }); 8 | } 9 | 10 | $(document).ready(function() { 11 | $("#console-arrow-button").click(() => { 12 | if(consoleShown) { 13 | $("#console-arrow").removeClass("extended").find("i.fa").removeClass("fa-angle-right").addClass("fa-angle-left"); 14 | $("#console").removeClass("extended"); 15 | } 16 | else { 17 | $("#console-arrow").addClass("extended").find("i.fa").removeClass("fa-angle-left").addClass("fa-angle-right"); 18 | $("#console").addClass("extended"); 19 | } 20 | consoleShown = !consoleShown; 21 | }); 22 | 23 | const responseTimeout = 10000; 24 | let bootstrapState = 0; 25 | 26 | 27 | 28 | let apiInfo = { 29 | url: "ws://localhost:2019", 30 | timeout: 10000, 31 | errors: { 32 | basic: { 33 | timeout: "Timeout", 34 | invalidResponse: "Invalid response" 35 | } 36 | } 37 | }; 38 | 39 | 40 | 41 | let allWhatsAppMessages = []; 42 | let bootstrapInfo = { 43 | activateButton: (text, buttonEnabled) => { 44 | let container = $("#bootstrap-container").removeClass("hidden").children("#bootstrap-container-content"); 45 | container.children("img").detach(); 46 | container.children("button").removeClass("hidden").html(text).attr("disabled", !buttonEnabled); 47 | $("#main-container").addClass("hidden"); 48 | 49 | allWhatsAppMessages = []; 50 | $("#messages-list-table-body").empty(); 51 | $("#restore-session").addClass("hidden"); 52 | 53 | }, 54 | activateQRCode: image => { 55 | let container = $("#bootstrap-container").removeClass("hidden").children("#bootstrap-container-content"); 56 | container.children("button").addClass("hidden") 57 | container.append($("").attr("src", image)); 58 | $("#main-container").addClass("hidden"); 59 | }, 60 | deactivate: () => { 61 | $("#bootstrap-container").addClass("hidden"); 62 | $("#main-container").removeClass("hidden"); 63 | $("#button-disconnect").html("Disconnect").attr("disabled", false); 64 | }, 65 | restoreSession: () => { 66 | $("#restore-session").removeClass("hidden"); 67 | $("#restore-session").html("Restore Session"); 68 | 69 | }, 70 | steps: [ 71 | new BootstrapStep({ 72 | websocket: apiWebsocket, 73 | texts: { 74 | handling: "Connecting to API...", 75 | success: "Connected to API after %1 ms. Click to let API connect to backend.", 76 | failure: "Connection to API failed: %1. Click to try again.", 77 | connLost: "Connection to API closed. Click to reconnect." 78 | }, 79 | actor: websocket => { 80 | websocket.initialize(apiInfo.url, "client", {func: WebSocket, getOnMessageData: msg => msg.data}); 81 | websocket.onClose(() => { 82 | bootstrapInfo.activateButton(bootstrapInfo.steps[0].texts.connLost, true); 83 | bootstrapState = 0; 84 | }); 85 | }, 86 | request: { 87 | type: "waitForMessage", 88 | condition: obj => obj.type == "connected" 89 | } 90 | }), 91 | new BootstrapStep({ 92 | websocket: apiWebsocket, 93 | texts: { 94 | handling: "Connecting to backend...", 95 | success: "Connected API to backend after %1 ms. Click to let backend connect to WhatsApp.", 96 | failure: "Connection of API to backend failed: %1. Click to try again.", 97 | connLost: "Connection of API to backend closed. Click to reconnect." 98 | }, 99 | actor: websocket => { 100 | websocket.waitForMessage({ 101 | condition: obj => obj.type == "resource_gone" && obj.resource == "backend", 102 | keepWhenHit: false 103 | }).then(() => { 104 | bootstrapInfo.activateButton(bootstrapInfo.steps[1].texts.connLost, true); 105 | bootstrapState = 1; 106 | websocket.apiConnectedToBackend = false; 107 | websocket.backendConnectedToWhatsApp = false; 108 | }); 109 | }, 110 | request: { 111 | type: "call", 112 | callArgs: { command: "api-connectBackend" }, 113 | successCondition: obj => obj.type == "resource_connected" && obj.resource == "backend", 114 | successActor: websocket => websocket.apiConnectedToBackend = true 115 | } 116 | }), 117 | new BootstrapStep({ 118 | websocket: apiWebsocket, 119 | texts: { 120 | handling: "Connecting to WhatsApp...", 121 | success: "Connected backend to WhatsApp after %1 ms. Click to generate QR code.", 122 | failure: "Connection of backend to WhatsApp failed: %1. Click to try again.", 123 | connLost: "Connection of backend to WhatsApp closed. Click to reconnect." 124 | }, 125 | actor: websocket => { 126 | bootstrapInfo.restoreSession(); 127 | 128 | websocket.waitForMessage({ 129 | condition: obj => obj.type == "resource_gone" && obj.resource == "whatsapp", 130 | keepWhenHit: false 131 | }).then(() => { 132 | bootstrapInfo.activateButton(bootstrapInfo.steps[2].texts.connLost, true); 133 | bootstrapState = 2; 134 | websocket.backendConnectedToWhatsApp = false; 135 | }); 136 | }, 137 | request: { 138 | type: "call", 139 | callArgs: { command: "backend-connectWhatsApp" }, 140 | successCondition: obj => obj.type == "resource_connected" && obj.resource == "whatsapp", 141 | successActor: (websocket, obj) => websocket.backendConnectedToWhatsApp = true, 142 | timeoutCondition: websocket => websocket.apiConnectedToBackend //condition for the timeout to be possible at all (if connection to backend is closed, a timeout for connecting to WhatsApp shall not override this issue message) 143 | } 144 | }), 145 | new BootstrapStep({ 146 | websocket: apiWebsocket, 147 | texts: { 148 | handling: "Generating QR code...", 149 | success: "Generated QR code after %1 ms.", 150 | failure: "Generating QR code failed: %1. Click to try again." 151 | }, 152 | request: { 153 | type: "call", 154 | callArgs: { command: "backend-generateQRCode" }, 155 | successCondition: obj => obj.type == "generated_qr_code" && obj.image, 156 | successActor: (websocket, {image}) => { 157 | bootstrapInfo.activateQRCode(image); 158 | 159 | websocket.waitForMessage({ 160 | condition: obj => obj.type == "whatsapp_message_received" && obj.message, 161 | keepWhenHit: true 162 | }).then(whatsAppMessage => { 163 | bootstrapInfo.deactivate(); 164 | /* 165 | 1 166 | Do., 21.12.2017, 22:59:09.123 167 | Binary 168 | 169 | */ 170 | 171 | let d = whatsAppMessage.data; 172 | let viewJSONButton = $("").addClass("btn").html("View").click(function() { 173 | let messageIndex = parseInt($(this).parent().parent().attr("data-message-index")); 174 | let jsonData = allWhatsAppMessages[messageIndex]; 175 | let tree, collapse = false; 176 | let dialog = bootbox.dialog({ 177 | title: `WhatsApp message #${messageIndex+1}`, 178 | message: "

Loading JSON...

", 179 | buttons: { 180 | noclose: { 181 | label: "Collapse/Expand All", 182 | className: "btn-info", 183 | callback: function () { 184 | if (!tree) 185 | return true; 186 | 187 | if (collapse === false) 188 | tree.expand(); 189 | else 190 | tree.collapse(); 191 | 192 | collapse = !collapse; 193 | 194 | return false; 195 | } 196 | } 197 | } 198 | }); 199 | dialog.init(() => { 200 | tree = jsonTree.create(jsonData, dialog.find(".bootbox-body").empty()[0]); 201 | }); 202 | }); 203 | 204 | let tableRow = $("").attr("data-message-index", allWhatsAppMessages.length); 205 | tableRow.append($("").attr("scope", "row").html(allWhatsAppMessages.length+1)); 206 | tableRow.append($("").html(moment.unix(d.timestamp/1000.0).format("ddd, DD.MM.YYYY, HH:mm:ss.SSS"))); 207 | tableRow.append($("").html(d.message_type)); 208 | tableRow.append($("").addClass("fill no-monospace").append(viewJSONButton)); 209 | $("#messages-list-table-body").append(tableRow); 210 | allWhatsAppMessages.push(d.message); 211 | 212 | //$("#main-container-content").empty(); 213 | //jsonTree.create(whatsAppMessage.data.message, $("#main-container-content")[0]); 214 | }).run(); 215 | }, 216 | timeoutCondition: websocket => websocket.backendConnectedToWhatsApp 217 | } 218 | }), 219 | new BootstrapStep({ 220 | websocket: apiWebsocket, 221 | texts: { 222 | handling: "Restoring...", 223 | success: "Restored in %1 ms.", 224 | failure: "Restore failed: %1. Click to try again." 225 | }, 226 | request: { 227 | type: "call", 228 | callArgs: { command: "backend-restoreSession" }, 229 | successCondition: obj => obj.type == "restore_session" , 230 | successActor: (websocket) => { 231 | websocket.waitForMessage({ 232 | condition: obj => obj.type == "whatsapp_message_received" && obj.message, 233 | keepWhenHit: true 234 | }).then(whatsAppMessage => { 235 | 236 | bootstrapInfo.deactivate(); 237 | /* 238 | 1 239 | Do., 21.12.2017, 22:59:09.123 240 | Binary 241 | 242 | */ 243 | 244 | let d = whatsAppMessage.data; 245 | let viewJSONButton = $("").addClass("btn").html("View").click(function() { 246 | let messageIndex = parseInt($(this).parent().parent().attr("data-message-index")); 247 | let jsonData = allWhatsAppMessages[messageIndex]; 248 | let tree, collapse = false; 249 | let dialog = bootbox.dialog({ 250 | title: `WhatsApp message #${messageIndex+1}`, 251 | message: "

Loading JSON...

", 252 | buttons: { 253 | noclose: { 254 | label: "Collapse/Expand All", 255 | className: "btn-info", 256 | callback: function () { 257 | if (!tree) 258 | return true; 259 | 260 | if (collapse === false) 261 | tree.expand(); 262 | else 263 | tree.collapse(); 264 | 265 | collapse = !collapse; 266 | 267 | return false; 268 | } 269 | } 270 | } 271 | }); 272 | dialog.init(() => { 273 | tree = jsonTree.create(jsonData, dialog.find(".bootbox-body").empty()[0]); 274 | }); 275 | }); 276 | 277 | let tableRow = $("").attr("data-message-index", allWhatsAppMessages.length); 278 | tableRow.append($("").attr("scope", "row").html(allWhatsAppMessages.length+1)); 279 | tableRow.append($("").html(moment.unix(d.timestamp/1000.0).format("ddd, DD.MM.YYYY, HH:mm:ss.SSS"))); 280 | tableRow.append($("").html(d.message_type)); 281 | tableRow.append($("").addClass("fill no-monospace").append(viewJSONButton)); 282 | $("#messages-list-table-body").append(tableRow); 283 | allWhatsAppMessages.push(d.message); 284 | 285 | //$("#main-container-content").empty(); 286 | //jsonTree.create(whatsAppMessage.data.message, $("#main-container-content")[0]); 287 | }).run(); 288 | }, 289 | timeoutCondition: websocket => websocket.backendConnectedToWhatsApp 290 | } 291 | }) 292 | 293 | ] 294 | }; 295 | $("#restore-session").addClass("hidden"); 296 | 297 | $("#restore-session").click(function() { 298 | bootstrapInfo.steps[4].run(apiInfo.timeout).then(() => { 299 | let text = currStep.texts.success.replace("%1", Math.round(performance.now() - stepStartTime)); 300 | $(this).html(text).attr("disabled", false); 301 | bootstrapState++; 302 | }) 303 | .catch(reason => { 304 | let text = currStep.texts.failure.replace("%1", reason); 305 | $(this).html(text).attr("disabled", false); 306 | }); 307 | }); 308 | $("#bootstrap-button").click(function() { 309 | let currStep = bootstrapInfo.steps[bootstrapState]; 310 | let stepStartTime = performance.now(); 311 | $(this).html(currStep.texts.handling).attr("disabled", "true"); 312 | currStep.run(apiInfo.timeout) 313 | .then(() => { 314 | let text = currStep.texts.success.replace("%1", Math.round(performance.now() - stepStartTime)); 315 | $(this).html(text).attr("disabled", false); 316 | bootstrapState++; 317 | }) 318 | .catch(reason => { 319 | let text = currStep.texts.failure.replace("%1", reason); 320 | $(this).html(text).attr("disabled", false); 321 | }); 322 | }); 323 | 324 | $("#button-disconnect").click(function() { 325 | if(!apiWebsocket.backendConnectedToWhatsApp) 326 | return; 327 | 328 | $(this).attr("disabled", true).html("Disconnecting..."); 329 | new BootstrapStep({ 330 | websocket: apiWebsocket, 331 | request: { 332 | type: "call", 333 | callArgs: { command: "backend-disconnectWhatsApp" }, 334 | successCondition: obj => obj.type == "resource_disconnected" && obj.resource == "whatsapp" 335 | } 336 | }).run(apiInfo.timeout) 337 | .then(() => { 338 | apiWebsocket.backendConnectedToWhatsApp = false; 339 | $(this).html("Disconnected."); 340 | }).catch(reason => $(this).html(`Disconnecting failed: ${reason}. Click to try again.`).attr("disabled", false)); 341 | }); 342 | }); 343 | -------------------------------------------------------------------------------- /client/lib/qrcode/js/qrcode.min.js: -------------------------------------------------------------------------------- 1 | var QRCode;!function(){function a(a){this.mode=c.MODE_8BIT_BYTE,this.data=a,this.parsedData=[];for(var b=[],d=0,e=this.data.length;e>d;d++){var f=this.data.charCodeAt(d);f>65536?(b[0]=240|(1835008&f)>>>18,b[1]=128|(258048&f)>>>12,b[2]=128|(4032&f)>>>6,b[3]=128|63&f):f>2048?(b[0]=224|(61440&f)>>>12,b[1]=128|(4032&f)>>>6,b[2]=128|63&f):f>128?(b[0]=192|(1984&f)>>>6,b[1]=128|63&f):b[0]=f,this.parsedData=this.parsedData.concat(b)}this.parsedData.length!=this.data.length&&(this.parsedData.unshift(191),this.parsedData.unshift(187),this.parsedData.unshift(239))}function b(a,b){this.typeNumber=a,this.errorCorrectLevel=b,this.modules=null,this.moduleCount=0,this.dataCache=null,this.dataList=[]}function i(a,b){if(void 0==a.length)throw new Error(a.length+"/"+b);for(var c=0;c=f;f++){var h=0;switch(b){case d.L:h=l[f][0];break;case d.M:h=l[f][1];break;case d.Q:h=l[f][2];break;case d.H:h=l[f][3]}if(h>=e)break;c++}if(c>l.length)throw new Error("Too long data");return c}function s(a){var b=encodeURI(a).toString().replace(/\%[0-9a-fA-F]{2}/g,"a");return b.length+(b.length!=a?3:0)}a.prototype={getLength:function(){return this.parsedData.length},write:function(a){for(var b=0,c=this.parsedData.length;c>b;b++)a.put(this.parsedData[b],8)}},b.prototype={addData:function(b){var c=new a(b);this.dataList.push(c),this.dataCache=null},isDark:function(a,b){if(0>a||this.moduleCount<=a||0>b||this.moduleCount<=b)throw new Error(a+","+b);return this.modules[a][b]},getModuleCount:function(){return this.moduleCount},make:function(){this.makeImpl(!1,this.getBestMaskPattern())},makeImpl:function(a,c){this.moduleCount=4*this.typeNumber+17,this.modules=new Array(this.moduleCount);for(var d=0;d=7&&this.setupTypeNumber(a),null==this.dataCache&&(this.dataCache=b.createData(this.typeNumber,this.errorCorrectLevel,this.dataList)),this.mapData(this.dataCache,c)},setupPositionProbePattern:function(a,b){for(var c=-1;7>=c;c++)if(!(-1>=a+c||this.moduleCount<=a+c))for(var d=-1;7>=d;d++)-1>=b+d||this.moduleCount<=b+d||(this.modules[a+c][b+d]=c>=0&&6>=c&&(0==d||6==d)||d>=0&&6>=d&&(0==c||6==c)||c>=2&&4>=c&&d>=2&&4>=d?!0:!1)},getBestMaskPattern:function(){for(var a=0,b=0,c=0;8>c;c++){this.makeImpl(!0,c);var d=f.getLostPoint(this);(0==c||a>d)&&(a=d,b=c)}return b},createMovieClip:function(a,b,c){var d=a.createEmptyMovieClip(b,c),e=1;this.make();for(var f=0;f=g;g++)for(var h=-2;2>=h;h++)this.modules[d+g][e+h]=-2==g||2==g||-2==h||2==h||0==g&&0==h?!0:!1}},setupTypeNumber:function(a){for(var b=f.getBCHTypeNumber(this.typeNumber),c=0;18>c;c++){var d=!a&&1==(1&b>>c);this.modules[Math.floor(c/3)][c%3+this.moduleCount-8-3]=d}for(var c=0;18>c;c++){var d=!a&&1==(1&b>>c);this.modules[c%3+this.moduleCount-8-3][Math.floor(c/3)]=d}},setupTypeInfo:function(a,b){for(var c=this.errorCorrectLevel<<3|b,d=f.getBCHTypeInfo(c),e=0;15>e;e++){var g=!a&&1==(1&d>>e);6>e?this.modules[e][8]=g:8>e?this.modules[e+1][8]=g:this.modules[this.moduleCount-15+e][8]=g}for(var e=0;15>e;e++){var g=!a&&1==(1&d>>e);8>e?this.modules[8][this.moduleCount-e-1]=g:9>e?this.modules[8][15-e-1+1]=g:this.modules[8][15-e-1]=g}this.modules[this.moduleCount-8][8]=!a},mapData:function(a,b){for(var c=-1,d=this.moduleCount-1,e=7,g=0,h=this.moduleCount-1;h>0;h-=2)for(6==h&&h--;;){for(var i=0;2>i;i++)if(null==this.modules[d][h-i]){var j=!1;g>>e));var k=f.getMask(b,d,h-i);k&&(j=!j),this.modules[d][h-i]=j,e--,-1==e&&(g++,e=7)}if(d+=c,0>d||this.moduleCount<=d){d-=c,c=-c;break}}}},b.PAD0=236,b.PAD1=17,b.createData=function(a,c,d){for(var e=j.getRSBlocks(a,c),g=new k,h=0;h8*l)throw new Error("code length overflow. ("+g.getLengthInBits()+">"+8*l+")");for(g.getLengthInBits()+4<=8*l&&g.put(0,4);0!=g.getLengthInBits()%8;)g.putBit(!1);for(;;){if(g.getLengthInBits()>=8*l)break;if(g.put(b.PAD0,8),g.getLengthInBits()>=8*l)break;g.put(b.PAD1,8)}return b.createBytes(g,e)},b.createBytes=function(a,b){for(var c=0,d=0,e=0,g=new Array(b.length),h=new Array(b.length),j=0;j=0?p.get(q):0}}for(var r=0,m=0;mm;m++)for(var j=0;jm;m++)for(var j=0;j=0;)b^=f.G15<=0;)b^=f.G18<>>=1;return b},getPatternPosition:function(a){return f.PATTERN_POSITION_TABLE[a-1]},getMask:function(a,b,c){switch(a){case e.PATTERN000:return 0==(b+c)%2;case e.PATTERN001:return 0==b%2;case e.PATTERN010:return 0==c%3;case e.PATTERN011:return 0==(b+c)%3;case e.PATTERN100:return 0==(Math.floor(b/2)+Math.floor(c/3))%2;case e.PATTERN101:return 0==b*c%2+b*c%3;case e.PATTERN110:return 0==(b*c%2+b*c%3)%2;case e.PATTERN111:return 0==(b*c%3+(b+c)%2)%2;default:throw new Error("bad maskPattern:"+a)}},getErrorCorrectPolynomial:function(a){for(var b=new i([1],0),c=0;a>c;c++)b=b.multiply(new i([1,g.gexp(c)],0));return b},getLengthInBits:function(a,b){if(b>=1&&10>b)switch(a){case c.MODE_NUMBER:return 10;case c.MODE_ALPHA_NUM:return 9;case c.MODE_8BIT_BYTE:return 8;case c.MODE_KANJI:return 8;default:throw new Error("mode:"+a)}else if(27>b)switch(a){case c.MODE_NUMBER:return 12;case c.MODE_ALPHA_NUM:return 11;case c.MODE_8BIT_BYTE:return 16;case c.MODE_KANJI:return 10;default:throw new Error("mode:"+a)}else{if(!(41>b))throw new Error("type:"+b);switch(a){case c.MODE_NUMBER:return 14;case c.MODE_ALPHA_NUM:return 13;case c.MODE_8BIT_BYTE:return 16;case c.MODE_KANJI:return 12;default:throw new Error("mode:"+a)}}},getLostPoint:function(a){for(var b=a.getModuleCount(),c=0,d=0;b>d;d++)for(var e=0;b>e;e++){for(var f=0,g=a.isDark(d,e),h=-1;1>=h;h++)if(!(0>d+h||d+h>=b))for(var i=-1;1>=i;i++)0>e+i||e+i>=b||(0!=h||0!=i)&&g==a.isDark(d+h,e+i)&&f++;f>5&&(c+=3+f-5)}for(var d=0;b-1>d;d++)for(var e=0;b-1>e;e++){var j=0;a.isDark(d,e)&&j++,a.isDark(d+1,e)&&j++,a.isDark(d,e+1)&&j++,a.isDark(d+1,e+1)&&j++,(0==j||4==j)&&(c+=3)}for(var d=0;b>d;d++)for(var e=0;b-6>e;e++)a.isDark(d,e)&&!a.isDark(d,e+1)&&a.isDark(d,e+2)&&a.isDark(d,e+3)&&a.isDark(d,e+4)&&!a.isDark(d,e+5)&&a.isDark(d,e+6)&&(c+=40);for(var e=0;b>e;e++)for(var d=0;b-6>d;d++)a.isDark(d,e)&&!a.isDark(d+1,e)&&a.isDark(d+2,e)&&a.isDark(d+3,e)&&a.isDark(d+4,e)&&!a.isDark(d+5,e)&&a.isDark(d+6,e)&&(c+=40);for(var k=0,e=0;b>e;e++)for(var d=0;b>d;d++)a.isDark(d,e)&&k++;var l=Math.abs(100*k/b/b-50)/5;return c+=10*l}},g={glog:function(a){if(1>a)throw new Error("glog("+a+")");return g.LOG_TABLE[a]},gexp:function(a){for(;0>a;)a+=255;for(;a>=256;)a-=255;return g.EXP_TABLE[a]},EXP_TABLE:new Array(256),LOG_TABLE:new Array(256)},h=0;8>h;h++)g.EXP_TABLE[h]=1<h;h++)g.EXP_TABLE[h]=g.EXP_TABLE[h-4]^g.EXP_TABLE[h-5]^g.EXP_TABLE[h-6]^g.EXP_TABLE[h-8];for(var h=0;255>h;h++)g.LOG_TABLE[g.EXP_TABLE[h]]=h;i.prototype={get:function(a){return this.num[a]},getLength:function(){return this.num.length},multiply:function(a){for(var b=new Array(this.getLength()+a.getLength()-1),c=0;cf;f++)for(var g=c[3*f+0],h=c[3*f+1],i=c[3*f+2],k=0;g>k;k++)e.push(new j(h,i));return e},j.getRsBlockTable=function(a,b){switch(b){case d.L:return j.RS_BLOCK_TABLE[4*(a-1)+0];case d.M:return j.RS_BLOCK_TABLE[4*(a-1)+1];case d.Q:return j.RS_BLOCK_TABLE[4*(a-1)+2];case d.H:return j.RS_BLOCK_TABLE[4*(a-1)+3];default:return void 0}},k.prototype={get:function(a){var b=Math.floor(a/8);return 1==(1&this.buffer[b]>>>7-a%8)},put:function(a,b){for(var c=0;b>c;c++)this.putBit(1==(1&a>>>b-c-1))},getLengthInBits:function(){return this.length},putBit:function(a){var b=Math.floor(this.length/8);this.buffer.length<=b&&this.buffer.push(0),a&&(this.buffer[b]|=128>>>this.length%8),this.length++}};var l=[[17,14,11,7],[32,26,20,14],[53,42,32,24],[78,62,46,34],[106,84,60,44],[134,106,74,58],[154,122,86,64],[192,152,108,84],[230,180,130,98],[271,213,151,119],[321,251,177,137],[367,287,203,155],[425,331,241,177],[458,362,258,194],[520,412,292,220],[586,450,322,250],[644,504,364,280],[718,560,394,310],[792,624,442,338],[858,666,482,382],[929,711,509,403],[1003,779,565,439],[1091,857,611,461],[1171,911,661,511],[1273,997,715,535],[1367,1059,751,593],[1465,1125,805,625],[1528,1190,868,658],[1628,1264,908,698],[1732,1370,982,742],[1840,1452,1030,790],[1952,1538,1112,842],[2068,1628,1168,898],[2188,1722,1228,958],[2303,1809,1283,983],[2431,1911,1351,1051],[2563,1989,1423,1093],[2699,2099,1499,1139],[2809,2213,1579,1219],[2953,2331,1663,1273]],o=function(){var a=function(a,b){this._el=a,this._htOption=b};return a.prototype.draw=function(a){function g(a,b){var c=document.createElementNS("http://www.w3.org/2000/svg",a);for(var d in b)b.hasOwnProperty(d)&&c.setAttribute(d,b[d]);return c}var b=this._htOption,c=this._el,d=a.getModuleCount();Math.floor(b.width/d),Math.floor(b.height/d),this.clear();var h=g("svg",{viewBox:"0 0 "+String(d)+" "+String(d),width:"100%",height:"100%",fill:b.colorLight});h.setAttributeNS("http://www.w3.org/2000/xmlns/","xmlns:xlink","http://www.w3.org/1999/xlink"),c.appendChild(h),h.appendChild(g("rect",{fill:b.colorDark,width:"1",height:"1",id:"template"}));for(var i=0;d>i;i++)for(var j=0;d>j;j++)if(a.isDark(i,j)){var k=g("use",{x:String(i),y:String(j)});k.setAttributeNS("http://www.w3.org/1999/xlink","href","#template"),h.appendChild(k)}},a.prototype.clear=function(){for(;this._el.hasChildNodes();)this._el.removeChild(this._el.lastChild)},a}(),p="svg"===document.documentElement.tagName.toLowerCase(),q=p?o:m()?function(){function a(){this._elImage.src=this._elCanvas.toDataURL("image/png"),this._elImage.style.display="block",this._elCanvas.style.display="none"}function d(a,b){var c=this;if(c._fFail=b,c._fSuccess=a,null===c._bSupportDataURI){var d=document.createElement("img"),e=function(){c._bSupportDataURI=!1,c._fFail&&_fFail.call(c)},f=function(){c._bSupportDataURI=!0,c._fSuccess&&c._fSuccess.call(c)};return d.onabort=e,d.onerror=e,d.onload=f,d.src="data:image/gif;base64,iVBORw0KGgoAAAANSUhEUgAAAAUAAAAFCAYAAACNbyblAAAAHElEQVQI12P4//8/w38GIAXDIBKE0DHxgljNBAAO9TXL0Y4OHwAAAABJRU5ErkJggg==",void 0}c._bSupportDataURI===!0&&c._fSuccess?c._fSuccess.call(c):c._bSupportDataURI===!1&&c._fFail&&c._fFail.call(c)}if(this._android&&this._android<=2.1){var b=1/window.devicePixelRatio,c=CanvasRenderingContext2D.prototype.drawImage;CanvasRenderingContext2D.prototype.drawImage=function(a,d,e,f,g,h,i,j){if("nodeName"in a&&/img/i.test(a.nodeName))for(var l=arguments.length-1;l>=1;l--)arguments[l]=arguments[l]*b;else"undefined"==typeof j&&(arguments[1]*=b,arguments[2]*=b,arguments[3]*=b,arguments[4]*=b);c.apply(this,arguments)}}var e=function(a,b){this._bIsPainted=!1,this._android=n(),this._htOption=b,this._elCanvas=document.createElement("canvas"),this._elCanvas.width=b.width,this._elCanvas.height=b.height,a.appendChild(this._elCanvas),this._el=a,this._oContext=this._elCanvas.getContext("2d"),this._bIsPainted=!1,this._elImage=document.createElement("img"),this._elImage.style.display="none",this._el.appendChild(this._elImage),this._bSupportDataURI=null};return e.prototype.draw=function(a){var b=this._elImage,c=this._oContext,d=this._htOption,e=a.getModuleCount(),f=d.width/e,g=d.height/e,h=Math.round(f),i=Math.round(g);b.style.display="none",this.clear();for(var j=0;e>j;j++)for(var k=0;e>k;k++){var l=a.isDark(j,k),m=k*f,n=j*g;c.strokeStyle=l?d.colorDark:d.colorLight,c.lineWidth=1,c.fillStyle=l?d.colorDark:d.colorLight,c.fillRect(m,n,f,g),c.strokeRect(Math.floor(m)+.5,Math.floor(n)+.5,h,i),c.strokeRect(Math.ceil(m)-.5,Math.ceil(n)-.5,h,i)}this._bIsPainted=!0},e.prototype.makeImage=function(){this._bIsPainted&&d.call(this,a)},e.prototype.isPainted=function(){return this._bIsPainted},e.prototype.clear=function(){this._oContext.clearRect(0,0,this._elCanvas.width,this._elCanvas.height),this._bIsPainted=!1},e.prototype.round=function(a){return a?Math.floor(1e3*a)/1e3:a},e}():function(){var a=function(a,b){this._el=a,this._htOption=b};return a.prototype.draw=function(a){for(var b=this._htOption,c=this._el,d=a.getModuleCount(),e=Math.floor(b.width/d),f=Math.floor(b.height/d),g=[''],h=0;d>h;h++){g.push("");for(var i=0;d>i;i++)g.push('');g.push("")}g.push("
"),c.innerHTML=g.join("");var j=c.childNodes[0],k=(b.width-j.offsetWidth)/2,l=(b.height-j.offsetHeight)/2;k>0&&l>0&&(j.style.margin=l+"px "+k+"px")},a.prototype.clear=function(){this._el.innerHTML=""},a}();QRCode=function(a,b){if(this._htOption={width:256,height:256,typeNumber:4,colorDark:"#000000",colorLight:"#ffffff",correctLevel:d.H},"string"==typeof b&&(b={text:b}),b)for(var c in b)this._htOption[c]=b[c];"string"==typeof a&&(a=document.getElementById(a)),this._android=n(),this._el=a,this._oQRCode=null,this._oDrawing=new q(this._el,this._htOption),this._htOption.text&&this.makeCode(this._htOption.text)},QRCode.prototype.makeCode=function(a){this._oQRCode=new b(r(a,this._htOption.correctLevel),this._htOption.correctLevel),this._oQRCode.addData(a),this._oQRCode.make(),this._el.title=a,this._oDrawing.draw(this._oQRCode),this.makeImage()},QRCode.prototype.makeImage=function(){"function"==typeof this._oDrawing.makeImage&&(!this._android||this._android>=3)&&this._oDrawing.makeImage()},QRCode.prototype.clear=function(){this._oDrawing.clear()},QRCode.CorrectLevel=d}(); -------------------------------------------------------------------------------- /doc/img/app-architecture.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 20 | 22 | 30 | 35 | 36 | 44 | 49 | 50 | 59 | 64 | 65 | 74 | 79 | 80 | 89 | 94 | 95 | 104 | 109 | 110 | 112 | 116 | 120 | 121 | 124 | 128 | 129 | 130 | 153 | 155 | 156 | 158 | image/svg+xml 159 | 161 | 162 | 163 | 164 | 165 | 170 | 175 | WhatsApp Web serverswss://w[1-8].web.whatsapp.com/ws 192 | 195 | 201 | 207 | 213 | 219 | 220 | Python backendws://localhost:2020 237 | 241 | 245 | 249 | 254 | 255 | 256 | Node.js API serverws://localhost:2019 273 | 276 | 280 | 284 | 288 | 292 | 293 | 296 | 299 | 302 | 305 | 308 | 311 | 314 | 317 | 320 | 323 | 326 | 329 | 332 | 335 | 338 | Client frontendhttp://localhost:2018 355 | 360 | 365 | 370 | 371 | 372 | --------------------------------------------------------------------------------