├── LICENSE ├── commiter.yml ├── whatsapp.png ├── js ├── injects │ ├── 02_utility.js │ ├── 03_storage.js │ ├── 05_panel.js │ ├── 04_main.js │ └── 01_md5.min.js ├── make.rb ├── whatsapp.template.js └── whatsapp.js ├── README.txt ├── manifest.json ├── css └── style.css └── .gitignore /LICENSE: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /commiter.yml: -------------------------------------------------------------------------------- 1 | convention: symphony 2 | -------------------------------------------------------------------------------- /whatsapp.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/skyme5/whatsapp-pp/master/whatsapp.png -------------------------------------------------------------------------------- /js/injects/02_utility.js: -------------------------------------------------------------------------------- 1 | function formatDate() { 2 | return new Date().toJSON().replace(/:/g, "-").split(".")[0]; 3 | } 4 | -------------------------------------------------------------------------------- /js/make.rb: -------------------------------------------------------------------------------- 1 | template = File.read('whatsapp.template.js') 2 | 3 | injects = Dir.entries('injects')[2..-1].map { |e| "// File: #{e}\n#{File.read(File.join('injects', e))}" } 4 | 5 | out = File.open('whatsapp.js', 'w') 6 | out.puts template.gsub('{{INJECT_CONTENT}}', injects.join("\n")) 7 | out.close 8 | -------------------------------------------------------------------------------- /README.txt: -------------------------------------------------------------------------------- 1 | Steps to Install the Extension 2 | 3 | 1. Open this url in Chrome: "chrome://extensions/" 4 | 2. Enable "Developer mode" by clicking toggle switch at top-right corner 5 | 3. Click "Load Unpacked" and choose the folder this extension is extracted to. 6 | 4. The extension will be installed. 7 | 5. Chrome will ask for permissin to download multiple files, allow the permission to download the pictures. 8 | 6. The images are downloaded only once when it detects a change. The filename will contain phone number of the person image belongs to and the timestamp it was downloaded. -------------------------------------------------------------------------------- /manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "background": { 3 | "scripts": [ 4 | ] 5 | }, 6 | "content_scripts": [ 7 | { 8 | "matches": ["https://web.whatsapp.com/*"], 9 | "css": ["css/style.css"], 10 | "js": ["js/whatsapp.js"], 11 | "run_at": "document_start" 12 | } 13 | ], 14 | "description": "WhatsApp Profile Picture Downloader", 15 | "icons": { 16 | "192": "whatsapp.png" 17 | }, 18 | "manifest_version": 2, 19 | "minimum_chrome_version": "47", 20 | "name": "WhatsApp Profile Picture by Sky", 21 | "permissions": ["downloads", "storage"], 22 | "version": "1.0.1" 23 | } 24 | -------------------------------------------------------------------------------- /css/style.css: -------------------------------------------------------------------------------- 1 | #web-pp-panel { 2 | all: unset; 3 | position: fixed; 4 | top: 0; 5 | right: 0; 6 | background: rgb(189, 189, 189); 7 | z-index: 999; 8 | border-radius: 0 0 0 4px; 9 | display: flex; 10 | flex-direction: row; 11 | } 12 | 13 | #web-pp-panel label, 14 | #web-pp-panel button { 15 | all: unset; 16 | padding: 3px; 17 | font-size: 14px; 18 | } 19 | 20 | #web-pp-panel input { 21 | margin-right: 5px; 22 | } 23 | 24 | #web-pp-panel button:hover, 25 | #web-pp-panel label:hover { 26 | cursor: pointer; 27 | transition: all .3s ease-in-out; 28 | background-color: rgb(255, 255, 255); 29 | } 30 | 31 | #whatsapp-pp-config { 32 | visibility: hidden; 33 | } -------------------------------------------------------------------------------- /js/whatsapp.template.js: -------------------------------------------------------------------------------- 1 | var url = chrome.runtime.getURL("numbers.txt"); 2 | 3 | function injectInterceptScript(numbers) { 4 | var xhrOverrideScript = document.createElement("script"); 5 | xhrOverrideScript.type = "text/javascript"; 6 | xhrOverrideScript.innerHTML = ` 7 | {{INJECT_CONTENT}} 8 | `; 9 | document.head.prepend(xhrOverrideScript); 10 | } 11 | 12 | function checkForDOM() { 13 | if (document.head) { 14 | injectInterceptScript(); 15 | } else { 16 | setTimeout(() => { 17 | checkForDOM(); 18 | }, 1); 19 | } 20 | } 21 | 22 | function saveConfig() { 23 | // Get a value saved in a form. 24 | var theValue = document.querySelector('#whatsapp-pp-config').value; 25 | // Check that there's some code there. 26 | if (!JSON.parse(theValue)) { 27 | message('Error: No value specified'); 28 | return; 29 | } 30 | // Save it using the Chrome extension storage API. 31 | chrome.storage.sync.set({'sky': theValue}, function() { 32 | // Notify that we saved. 33 | message('Settings saved'); 34 | }); 35 | } 36 | 37 | setInterval(() => {saveConfig();}, 5000); 38 | 39 | checkForDOM(); 40 | -------------------------------------------------------------------------------- /js/injects/03_storage.js: -------------------------------------------------------------------------------- 1 | const STORAGE = "sky"; 2 | 3 | if (localStorage.getItem(STORAGE) === null) { 4 | localStorage.setItem( 5 | STORAGE, 6 | JSON.stringify({ cellToMD5: {}, downloadOnly: [], downloadAll: false }) 7 | ); 8 | } 9 | 10 | var syncExt = document.createElement('input'); 11 | syncExt.id = 'whatsapp-pp-config'; 12 | 13 | document.body.appendChild(syncExt); 14 | 15 | var saved = getStorage(); 16 | 17 | function commitStorage() { 18 | var settings = JSON.stringify(saved); 19 | localStorage.setItem(STORAGE, settings); 20 | syncExt.value = settings; 21 | } 22 | 23 | function cellToMD5PutStorage(md5Cell, md5hah) { 24 | saved.cellToMD5[md5Cell] = md5hah; 25 | commitStorage(); 26 | } 27 | 28 | function getStorage() { 29 | return JSON.parse(localStorage.getItem(STORAGE)); 30 | } 31 | 32 | function exportStorage() { 33 | var storage = localStorage.getItem(STORAGE); 34 | 35 | var a = document.createElement("a"); 36 | a.download = "WhatsApp Profile Picture_" + formatDate() + ".json"; 37 | a.href = "data:base64," + storage; 38 | a.click(); 39 | } 40 | 41 | function restoreStorage(dataStr) { 42 | localStorage.setItem(STORAGE, dataStr); 43 | } 44 | 45 | function addCellForSync(cellNumber) { 46 | saved.downloadOnly.push(cellNumber); 47 | commitStorage(); 48 | } 49 | 50 | function syncDownloadAll(downloadAll) { 51 | saved.downloadAll = downloadAll; 52 | commitStorage(); 53 | } 54 | -------------------------------------------------------------------------------- /js/injects/05_panel.js: -------------------------------------------------------------------------------- 1 | function panelInit() { 2 | var panel = document.createElement("DIV"); 3 | panel.id = "web-pp-panel"; 4 | 5 | var inputWrapper = document.createElement("label"); 6 | inputWrapper.innerText = "Download All"; 7 | var downloadAll = document.createElement("INPUT"); 8 | downloadAll.type = "checkbox"; 9 | downloadAll.id = "download"; 10 | downloadAll.checked = saved.downloadAll; 11 | downloadAll.onclick = function() { 12 | syncDownloadAll(this.checked); 13 | }; 14 | inputWrapper.prepend(downloadAll); 15 | 16 | panel.appendChild(inputWrapper); 17 | 18 | var addCellNumber = document.createElement("button"); 19 | addCellNumber.innerText = "+ Cell"; 20 | addCellNumber.onclick = function () { 21 | var cell = prompt("Add Number in the format: +[country code][10 digits]"); 22 | if (/^\+\d+/.test(cell)){ 23 | addCellForSync(cell); 24 | } 25 | }; 26 | 27 | panel.appendChild(addCellNumber); 28 | 29 | var backup = document.createElement("button"); 30 | backup.innerText = "Export"; 31 | backup.onclick = function () { 32 | exportStorage(); 33 | }; 34 | 35 | panel.appendChild(backup); 36 | 37 | var restore = document.createElement("button"); 38 | restore.innerText = "Restore"; 39 | restore.onclick = function () { 40 | var data = prompt("Enter data to restore"); 41 | if (JSON.parse(data)) { 42 | if (confirm("Restore this date:" + data)) { 43 | restoreStorage(data); 44 | } 45 | } else { 46 | alert("Invalid JSON data"); 47 | } 48 | }; 49 | 50 | panel.appendChild(restore); 51 | 52 | document.body.appendChild(panel); 53 | } 54 | 55 | panelInit(); 56 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | lerna-debug.log* 8 | 9 | # Diagnostic reports (https://nodejs.org/api/report.html) 10 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 11 | 12 | # Runtime data 13 | pids 14 | *.pid 15 | *.seed 16 | *.pid.lock 17 | 18 | # Directory for instrumented libs generated by jscoverage/JSCover 19 | lib-cov 20 | 21 | # Coverage directory used by tools like istanbul 22 | coverage 23 | *.lcov 24 | 25 | # nyc test coverage 26 | .nyc_output 27 | 28 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 29 | .grunt 30 | 31 | # Bower dependency directory (https://bower.io/) 32 | bower_components 33 | 34 | # node-waf configuration 35 | .lock-wscript 36 | 37 | # Compiled binary addons (https://nodejs.org/api/addons.html) 38 | build/Release 39 | 40 | # Dependency directories 41 | node_modules/ 42 | jspm_packages/ 43 | 44 | # TypeScript v1 declaration files 45 | typings/ 46 | 47 | # TypeScript cache 48 | *.tsbuildinfo 49 | 50 | # Optional npm cache directory 51 | .npm 52 | 53 | # Optional eslint cache 54 | .eslintcache 55 | 56 | # Microbundle cache 57 | .rpt2_cache/ 58 | .rts2_cache_cjs/ 59 | .rts2_cache_es/ 60 | .rts2_cache_umd/ 61 | 62 | # Optional REPL history 63 | .node_repl_history 64 | 65 | # Output of 'npm pack' 66 | *.tgz 67 | 68 | # Yarn Integrity file 69 | .yarn-integrity 70 | 71 | # dotenv environment variables file 72 | .env 73 | .env.test 74 | 75 | # parcel-bundler cache (https://parceljs.org/) 76 | .cache 77 | 78 | # Next.js build output 79 | .next 80 | 81 | # Nuxt.js build / generate output 82 | .nuxt 83 | dist 84 | 85 | # Gatsby files 86 | .cache/ 87 | # Comment in the public line in if your project uses Gatsby and *not* Next.js 88 | # https://nextjs.org/blog/next-9-1#public-directory-support 89 | # public 90 | 91 | # vuepress build output 92 | .vuepress/dist 93 | 94 | # Serverless directories 95 | .serverless/ 96 | 97 | # FuseBox cache 98 | .fusebox/ 99 | 100 | # DynamoDB Local files 101 | .dynamodb/ 102 | 103 | # TernJS port file 104 | .tern-port 105 | -------------------------------------------------------------------------------- /js/injects/04_main.js: -------------------------------------------------------------------------------- 1 | function getMD5OfImageSrc(wPPUrl) { 2 | var imgUrl = new URL(decodeURIComponent(wPPUrl)); 3 | var imgSrc = new URL(imgUrl.searchParams.get("e")).pathname.split("/").pop(); 4 | 5 | if (imgSrc == null) { 6 | alert("Update WhatsApp Profile Picture ?"); 7 | } 8 | 9 | return md5(imgSrc); 10 | } 11 | 12 | function download(name, src, index = 10) { 13 | setTimeout(() => { 14 | var imgI = new Image(); 15 | imgI.setAttribute("crossOrigin", "anonymous"); 16 | imgI.onload = function () { 17 | var canvas = document.createElement("canvas"); 18 | canvas.style.opacity = 0; 19 | canvas.style.position = "fixed"; 20 | canvas.style.top = "1000px"; 21 | canvas.width = this.width; 22 | canvas.height = this.height; 23 | 24 | var body = document.getElementsByTagName("body")[0]; 25 | body.appendChild(canvas); 26 | 27 | var ctx = canvas.getContext("2d"); 28 | ctx.drawImage(imgI, 0, 0); 29 | 30 | var dataURL = canvas.toDataURL("image/jpg"); 31 | canvas.className = "done"; 32 | imgI = null; 33 | 34 | var link = document.createElement("a"); 35 | link.download = ["+" + name, formatDate(), "WPP"].join("_"); 36 | link.href = dataURL; 37 | link.click(); 38 | }; 39 | imgI.src = src; 40 | }, 41 | 100 + index * 250, 42 | name, 43 | src 44 | ); 45 | } 46 | 47 | var list = []; 48 | 49 | function get_images() { 50 | document.querySelectorAll("img").forEach((img, index) => { 51 | if (img.src.indexOf("https://web.whatsapp.com/pp?e=") < 0) return; 52 | 53 | var cellNumber = img.src.split("&u=").pop().split("%40")[0]; 54 | 55 | if (cellNumber === null) return; 56 | if (!saved.downloadAll && !saved.downloadOnly.includes(cellNumber)) return; 57 | 58 | var md5Sum = getMD5OfImageSrc(img.src); 59 | var md5Cell = md5(cellNumber); 60 | console.log(md5Sum, md5Cell, saved.cellToMD5[md5Cell] !== md5Sum) 61 | if (saved.cellToMD5[md5Cell] !== md5Sum) { 62 | cellToMD5PutStorage(md5Cell, md5Sum); 63 | download(cellNumber, img.src, index); 64 | } 65 | }); 66 | } 67 | 68 | setInterval(function () { 69 | console.info('Scanning for Profile Pic'); 70 | document.querySelectorAll("canvas.done").forEach((e) => e.remove()); 71 | get_images(); 72 | }, 500); 73 | -------------------------------------------------------------------------------- /js/injects/01_md5.min.js: -------------------------------------------------------------------------------- 1 | !function(n){"use strict";function d(n,t){var r=(65535&n)+(65535&t);return(n>>16)+(t>>16)+(r>>16)<<16|65535&r}function f(n,t,r,e,o,u){return d(function(n,t){return n<>>32-t}(d(d(t,n),d(e,u)),o),r)}function l(n,t,r,e,o,u,c){return f(t&r|~t&e,n,t,o,u,c)}function g(n,t,r,e,o,u,c){return f(t&e|r&~e,n,t,o,u,c)}function v(n,t,r,e,o,u,c){return f(t^r^e,n,t,o,u,c)}function m(n,t,r,e,o,u,c){return f(r^(t|~e),n,t,o,u,c)}function i(n,t){var r,e,o,u,c;n[t>>5]|=128<>>9<<4)]=t;var f=1732584193,i=-271733879,a=-1732584194,h=271733878;for(r=0;r>5]>>>t%32&255);return r}function h(n){var t,r=[];for(r[(n.length>>2)-1]=void 0,t=0;t>5]|=(255&n.charCodeAt(t/8))<>>4&15)+e.charAt(15&t);return o}function r(n){return unescape(encodeURIComponent(n))}function o(n){return function(n){return a(i(h(n),8*n.length))}(r(n))}function u(n,t){return function(n,t){var r,e,o=h(n),u=[],c=[];for(u[15]=c[15]=void 0,16>16)+(t>>16)+(r>>16)<<16|65535&r}function f(n,t,r,e,o,u){return d(function(n,t){return n<>>32-t}(d(d(t,n),d(e,u)),o),r)}function l(n,t,r,e,o,u,c){return f(t&r|~t&e,n,t,o,u,c)}function g(n,t,r,e,o,u,c){return f(t&e|r&~e,n,t,o,u,c)}function v(n,t,r,e,o,u,c){return f(t^r^e,n,t,o,u,c)}function m(n,t,r,e,o,u,c){return f(r^(t|~e),n,t,o,u,c)}function i(n,t){var r,e,o,u,c;n[t>>5]|=128<>>9<<4)]=t;var f=1732584193,i=-271733879,a=-1732584194,h=271733878;for(r=0;r>5]>>>t%32&255);return r}function h(n){var t,r=[];for(r[(n.length>>2)-1]=void 0,t=0;t>5]|=(255&n.charCodeAt(t/8))<>>4&15)+e.charAt(15&t);return o}function r(n){return unescape(encodeURIComponent(n))}function o(n){return function(n){return a(i(h(n),8*n.length))}(r(n))}function u(n,t){return function(n,t){var r,e,o=h(n),u=[],c=[];for(u[15]=c[15]=void 0,16 { 84 | var imgI = new Image(); 85 | imgI.setAttribute("crossOrigin", "anonymous"); 86 | imgI.onload = function () { 87 | var canvas = document.createElement("canvas"); 88 | canvas.style.opacity = 0; 89 | canvas.style.position = "fixed"; 90 | canvas.style.top = "1000px"; 91 | canvas.width = this.width; 92 | canvas.height = this.height; 93 | 94 | var body = document.getElementsByTagName("body")[0]; 95 | body.appendChild(canvas); 96 | 97 | var ctx = canvas.getContext("2d"); 98 | ctx.drawImage(imgI, 0, 0); 99 | 100 | var dataURL = canvas.toDataURL("image/jpg"); 101 | canvas.className = "done"; 102 | imgI = null; 103 | 104 | var link = document.createElement("a"); 105 | link.download = ["+" + name, formatDate(), "WPP"].join("_"); 106 | link.href = dataURL; 107 | link.click(); 108 | }; 109 | imgI.src = src; 110 | }, 111 | 100 + index * 250, 112 | name, 113 | src 114 | ); 115 | } 116 | 117 | var list = []; 118 | 119 | function get_images() { 120 | document.querySelectorAll("img").forEach((img, index) => { 121 | if (img.src.indexOf("https://web.whatsapp.com/pp?e=") < 0) return; 122 | 123 | var cellNumber = img.src.split("&u=").pop().split("%40")[0]; 124 | 125 | if (cellNumber === null) return; 126 | if (!saved.downloadAll && !saved.downloadOnly.includes(cellNumber)) return; 127 | 128 | var md5Sum = getMD5OfImageSrc(img.src); 129 | var md5Cell = md5(cellNumber); 130 | console.log(md5Sum, md5Cell, saved.cellToMD5[md5Cell] !== md5Sum) 131 | if (saved.cellToMD5[md5Cell] !== md5Sum) { 132 | cellToMD5PutStorage(md5Cell, md5Sum); 133 | download(cellNumber, img.src, index); 134 | } 135 | }); 136 | } 137 | 138 | setInterval(function () { 139 | console.info('Scanning for Profile Pic'); 140 | document.querySelectorAll("canvas.done").forEach((e) => e.remove()); 141 | get_images(); 142 | }, 500); 143 | 144 | // File: 05_panel.js 145 | function panelInit() { 146 | var panel = document.createElement("DIV"); 147 | panel.id = "web-pp-panel"; 148 | 149 | var inputWrapper = document.createElement("label"); 150 | inputWrapper.innerText = "Download All"; 151 | var downloadAll = document.createElement("INPUT"); 152 | downloadAll.type = "checkbox"; 153 | downloadAll.id = "download"; 154 | downloadAll.checked = saved.downloadAll; 155 | downloadAll.onclick = function() { 156 | syncDownloadAll(this.checked); 157 | }; 158 | inputWrapper.prepend(downloadAll); 159 | 160 | panel.appendChild(inputWrapper); 161 | 162 | var addCellNumber = document.createElement("button"); 163 | addCellNumber.innerText = "+ Cell"; 164 | addCellNumber.onclick = function () { 165 | var cell = prompt("Add Number in the format: +[country code][10 digits]"); 166 | if (/^\d+/.test(cell)){ 167 | addCellForSync(cell); 168 | } 169 | }; 170 | 171 | panel.appendChild(addCellNumber); 172 | 173 | var backup = document.createElement("button"); 174 | backup.innerText = "Export"; 175 | backup.onclick = function () { 176 | exportStorage(); 177 | }; 178 | 179 | panel.appendChild(backup); 180 | 181 | var restore = document.createElement("button"); 182 | restore.innerText = "Restore"; 183 | restore.onclick = function () { 184 | var data = prompt("Enter data to restore"); 185 | if (JSON.parse(data)) { 186 | if (confirm("Restore this date:" + data)) { 187 | restoreStorage(data); 188 | } 189 | } else { 190 | alert("Invalid JSON data"); 191 | } 192 | }; 193 | 194 | panel.appendChild(restore); 195 | 196 | document.body.appendChild(panel); 197 | } 198 | 199 | panelInit(); 200 | 201 | `; 202 | document.head.prepend(xhrOverrideScript); 203 | } 204 | 205 | function checkForDOM() { 206 | if (document.head) { 207 | injectInterceptScript(); 208 | } else { 209 | setTimeout(() => { 210 | checkForDOM(); 211 | }, 1); 212 | } 213 | } 214 | 215 | function saveConfig() { 216 | // Get a value saved in a form. 217 | var theValue = document.querySelector('#whatsapp-pp-config').value; 218 | // Check that there's some code there. 219 | if (!JSON.parse(theValue)) { 220 | message('Error: No value specified'); 221 | return; 222 | } 223 | // Save it using the Chrome extension storage API. 224 | chrome.storage.sync.set({'sky': theValue}, function() { 225 | // Notify that we saved. 226 | message('Settings saved'); 227 | }); 228 | } 229 | 230 | checkForDOM(); 231 | --------------------------------------------------------------------------------