Skip to content

Instantly share code, notes, and snippets.

@shaneapen
Last active November 19, 2023 06:03
Show Gist options
  • Star 23 You must be signed in to star a gist
  • Fork 20 You must be signed in to fork a gist
  • Save shaneapen/3406477b9f946855d02e3f33ec121975 to your computer and use it in GitHub Desktop.
Save shaneapen/3406477b9f946855d02e3f33ec121975 to your computer and use it in GitHub Desktop.
WhatsApp Group Contacts Exporter

WhatsApp Group Contacts Exporter (WAXP) by codegena.com

  1. Go to web.whatsapp.com and select the group from which you want to export contacts.
  2. Copy-paste the code into your browser console.
  3. Type WAXP and hit enter to see the available methods.
  4. Execute WAXP.quickExport() to export unsaved phone numbers alone quickly or WAXP.start() to export contacts with names and status.

Side Note : This code snippet is shared only for educational purpose and the code is not maintained anymore. Pro users who requires contact exports on a daily basis can check WAXP chrome extension

WAXP = (function(){
MutationObserver = window.MutationObserver || window.WebKitMutationObserver;
var SCROLL_INTERVAL = 600,
SCROLL_INCREMENT = 450,
AUTO_SCROLL = true,
NAME_PREFIX = '',
UNKNOWN_CONTACTS_ONLY = false,
MEMBERS_QUEUE = {},
TOTAL_MEMBERS;
var scrollInterval, observer, membersList, header;
console.log("%c WhatsApp Group Contacts Exporter ","font-size:24px;font-weight:bold;color:white;background:green;");
var start = function(){
membersList = document.querySelectorAll('span[title=You]')[0]?.parentNode?.parentNode?.parentNode?.parentNode?.parentNode?.parentNode?.parentNode;
header = document.getElementsByTagName('header')[0];
if(!membersList){
document.querySelector("#main > header").firstChild.click();
membersList = document.querySelectorAll('span[title=You]')[0]?.parentNode?.parentNode?.parentNode?.parentNode?.parentNode?.parentNode?.parentNode;
header = document.getElementsByTagName('header')[0];
}
observer = new MutationObserver(function (mutations, observer) {
scrapeData(); // fired when a mutation occurs
});
// the div to watch for mutations
observer.observe(membersList, {
childList: true,
subtree: true
});
TOTAL_MEMBERS = membersList.parentElement.parentElement.querySelector('span').innerText.match(/\d+/)[0]*1;
// click the `n more` button to show all members
document.querySelector("span[data-icon=down]")?.click()
//scroll to top before beginning
header.nextSibling.scrollTop = 100;
scrapeData();
if(AUTO_SCROLL) scrollInterval = setInterval(autoScroll, SCROLL_INTERVAL);
}
/**
* Function to autoscroll the div
*/
var autoScroll = function (){
if(!utils.scrollEndReached(header.nextSibling))
header.nextSibling.scrollTop += SCROLL_INCREMENT;
else
stop();
};
/**
* Stops the current scrape instance
*/
var stop = function(){
window.clearInterval(scrollInterval);
observer.disconnect();
console.log(`%c Extracted [${utils.queueLength()} / ${TOTAL_MEMBERS}] Members. Starting Download..`,`font-size:13px;color:white;background:green;border-radius:10px;`)
downloadAsCSV(['Name','Phone','Status']);
}
/**
* Function to scrape member data
*/
var scrapeData = function () {
var contact, status, name;
var memberCard = membersList.querySelectorAll(':scope > div');
for (let i = 0; i < memberCard.length; i++) {
status = memberCard[i].querySelectorAll('span[title]')[1] ? memberCard[i].querySelectorAll('span[title]')[1].title : "";
contact = scrapePhoneNum(memberCard[i]);
name = scrapeName(memberCard[i]);
if (contact.phone!='NIL' && !MEMBERS_QUEUE[contact.phone]) {
if (contact.isUnsaved) {
MEMBERS_QUEUE[contact.phone] = { 'Name': NAME_PREFIX + name,'Status': status };
continue;
} else if (!UNKNOWN_CONTACTS_ONLY) {
MEMBERS_QUEUE[contact.phone] = { 'Name': name, 'Status': status };
}
}else if(MEMBERS_QUEUE[contact.phone]){
MEMBERS_QUEUE[contact.phone].Status = status;
}
if(utils.queueLength() >= TOTAL_MEMBERS) {
stop();
break;
}
//console.log(`%c Extracted [${utils.queueLength()} / ${TOTAL_MEMBERS}] Members `,`font-size:13px;color:white;background:green;border-radius:10px;`)
}
}
/**
* Scrapes phone no from html node
* @param {object} el - HTML node
* @returns {string} - phone number without special chars
*/
var scrapePhoneNum = function(el){
var phone, isUnsaved = false;
if (el.querySelector('img') && el.querySelector('img').src.match(/u=[0-9]*/)) {
phone = el.querySelector('img').src.match(/u=[0-9]*/)[0].substring(2).replace(/[+\s]/g, '');
} else {
var temp = el.querySelector('span[title]').getAttribute('title').match(/(.?)*[0-9]{3}$/);
if(temp){
phone = temp[0].replace(/\D/g,'');
isUnsaved = true;
}else{
phone = 'NIL';
}
}
return { 'phone': phone, 'isUnsaved': isUnsaved };
}
/**
* Scrapes name from HTML node
* @param {object} el - HTML node
* @returns {string} - returns name..if no name is present phone number is returned
*/
var scrapeName = function (el){
var expectedName;
expectedName = el.firstChild.firstChild.childNodes[1].childNodes[1].childNodes[1].querySelector('span').innerText;
if(expectedName == ""){
return el.querySelector('span[title]').getAttribute('title'); //phone number
}
return expectedName;
}
/**
* A utility function to download the result as CSV file
* @References
* [1] - https://stackoverflow.com/questions/4617935/is-there-a-way-to-include-commas-in-csv-columns-without-breaking-the-formatting
*
*/
var downloadAsCSV = function (header) {
var groupName = document.querySelectorAll("#main > header span")[1].title;
var fileName = groupName.replace(/[^\d\w\s]/g,'') ? groupName.replace(/[^\d\w\s]/g,'') : 'WAXP-group-members';
var name = `${fileName}.csv`, data = `${header.join(',')}\n`;
if(utils.queueLength() > 0){
for (key in MEMBERS_QUEUE) {
// Wrapping each variable around double quotes to prevent commas in the string from adding new cols in CSV
// replacing any double quotes within the text to single quotes
if(header.includes('Status'))
data += `"${MEMBERS_QUEUE[key]['Name']}","${key}","${MEMBERS_QUEUE[key]['Status'].replace(/\"/g,"'")}"\n`;
else
data += `"${MEMBERS_QUEUE[key]['Name']}","${key}"\n`;
}
utils.createDownloadLink(data,name);
}else{
alert("Couldn't find any contacts with the given options");
}
}
/**
* Scrape contacts instantly from the group header.
* Saved Contacts cannot be exchanged for numbers with this method.
*/
var quickExport = function(){
var members = document.querySelectorAll("#main > header span")[2].title.replace(/ /g,'').split(',');
var groupName = document.querySelectorAll("#main > header span")[1].title;
var fileName = groupName.replace(/[^\d\w\s]/g,'') ? groupName.replace(/[^\d\w\s]/g,'') : 'WAXP-group-members';
fileName = `${fileName}.csv`;
members.pop(); //removing 'YOU' from array
MEMBERS_QUEUE = {};
for (i = 0; i < members.length; ++i) {
if (members[i].match(/^[\+]?[(]?[0-9]{3}[)]?[-\s\.]?[0-9]{3}[-\s\.]?[0-9]{4,6}$/)) {
MEMBERS_QUEUE[members[i]] = {
'Name': NAME_PREFIX + members[i]
};
continue;
} else if (!UNKNOWN_CONTACTS_ONLY) {
MEMBERS_QUEUE[members[i]] = {
'Name': members[i]
};
}
}
downloadAsCSV(['Name','Phone']);
}
/**
* Helper functions
* @References [1] https://stackoverflow.com/questions/53158796/get-scroll-position-with-reactjs/53158893#53158893
*/
var utils = (function(){
return {
scrollEndReached: function(el){
if((el.scrollHeight - (el.clientHeight + el.scrollTop)) == 0)
return true;
return false;
},
queueLength: function() {
var size = 0, key;
for (key in MEMBERS_QUEUE) {
if (MEMBERS_QUEUE.hasOwnProperty(key)) size++;
}
return size;
},
createDownloadLink: function (data,fileName) {
var a = document.createElement('a');
a.style.display = "none";
var url = window.URL.createObjectURL(new Blob([data], {
type: "data:attachment/text"
}));
a.setAttribute("href", url);
a.setAttribute("download", fileName);
document.body.append(a);
a.click();
window.URL.revokeObjectURL(url);
a.remove();
}
}
})();
// Defines the WAXP interface following module pattern
return {
start: function(){
MEMBERS_QUEUE = {}; //reset
try {
start();
} catch (error) {
//TO overcome below error..but not sure of any sideeffects
//TypeError: Failed to execute 'observe' on 'MutationObserver': parameter 1 is not of type 'Node'.
console.log(error, '\nRETRYING in 1 second')
setTimeout(start, 1000);
}
},
stop: function(){
stop()
},
options: {
// works for now...but consider refactoring it provided better approach exist
set NAME_PREFIX(val){ NAME_PREFIX = val },
set SCROLL_INTERVAL(val){ SCROLL_INTERVAL = val },
set SCROLL_INCREMENT(val){ SCROLL_INCREMENT = val },
set AUTO_SCROLL(val){ AUTO_SCROLL = val },
set UNKNOWN_CONTACTS_ONLY(val){ UNKNOWN_CONTACTS_ONLY = val },
// getter
get NAME_PREFIX(){ return NAME_PREFIX },
get SCROLL_INTERVAL(){ return SCROLL_INTERVAL },
get SCROLL_INCREMENT(){ return SCROLL_INCREMENT },
get AUTO_SCROLL(){ return AUTO_SCROLL },
get UNKNOWN_CONTACTS_ONLY(){ return UNKNOWN_CONTACTS_ONLY },
},
quickExport: function(){
quickExport();
},
debug: function(){
return {
size: utils.queueLength(),
q: MEMBERS_QUEUE
}
}
}
})();
@lubianat
Copy link

I am getting the same error as sarcasticccccc here.

@singhrohit9525
Copy link

Getting this error while executing the code in mac. How to fix this??

TypeError: Cannot read properties of null (reading 'firstChild')
at start (:23:53)
at Object.start (:255:22)
at :1:6 '\nRETRYING in 1 second'

@jamesmurickan
Copy link

I am getting error while executing on Firefox on Ubuntu.

WAXP.start()
TypeError: membersList.parentElement.parentElement.querySelector(...).innerText.match(...) is null
start debugger eval code:38
start debugger eval code:255
debugger eval code:1

RETRYING in 1 second debugger eval code:259:30

@allskandal
Copy link

NOT WORK right now

@mzahidriaz
Copy link

WhatsApp Group Contacts Export
Check this one, I made this tool for my own convenience.

@imaginovationz
Copy link

not working

WAXP.quickExport();
VM179:186 Uncaught TypeError: Cannot read properties of undefined (reading 'title')
at quickExport (:186:74)
at Object.quickExport (:281:17)
at :1:6

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment