Skip to content

Instantly share code, notes, and snippets.

@phistuck
Last active April 23, 2023 23:10
Show Gist options
  • Save phistuck/97adcd14809980c54d468e925b8089be to your computer and use it in GitHub Desktop.
Save phistuck/97adcd14809980c54d468e925b8089be to your computer and use it in GitHub Desktop.
Automation of contact deletion or addition via the Google Contacts page
// Public domain, or MIT if not legal.
// Run using the console on contacts.google.com
// Approve notifications
/** @type {(xPath: string) => HTMLElement[]} */
var $x = $x;
/** @type {(cssSelector: string) => HTMLElement} */
var $ = $;
localStorage.contactsToProcess = JSON.stringify([
"email@example.com"
]);
var c = /** @type {(string | string[])[]} */ JSON.parse(localStorage.contactsToProcess)
var kActions = {
remove: 'Delete',
add: "Add to contacts"
};
var stopProcessing = false;
runActionOnBatch(c, kActions.remove);
/**
* @param {(string | string[])[]} list
* @param {string} action
*/
async function runActionOnBatch(list, action)
{
while (list.length)
{
console.log(list.length);
if (stopProcessing)
{
throw new Error('Stopped everything');
}
await wait(5000);
var email = list.pop();
try
{
await runAction(/** @type {string | string[]} */(email), action);
} catch (e)
{
new Notification('Contact process failed', {
requireInteraction: true
});
throw e;
}
localStorage.contactsToProcess = JSON.stringify(list);
}
new Notification('Finished contact processes', { requireInteraction: true });
}
function resetFocus()
{
document.body.click();
const a = $('.Ax4B8.ZAGvjd');
a.click();
}
/**
* @param {string | string[]} email
*/
function typeSearchQuery(email)
{
const a = /** @type {HTMLInputElement} */ ($('.Ax4B8.ZAGvjd'));
a.focus();
a.value = Array.isArray(email) ? email[0] : email;
var b = document.createEvent("HTMLEvents");
b.initEvent("input", !0, !0);
a.dispatchEvent(b);
}
function runSearch()
{
$('.gb_5e.gb_6e').click();
}
/**
* @param {string | string[]} email
*/
function search(email)
{
resetFocus();
typeSearchQuery(email);
runSearch();
}
/**
* @param {number} time
*/
function wait(time)
{
return new Promise(resolve => setTimeout(resolve, time));
}
/**
* @template {string | string[]} P
* @param {number} timeout
* @param {(...parameters: P[]) => Promise<boolean>} act
* @param {...P} parameters
*/
async function retry(timeout, act, ...parameters)
{
const startTime = Date.now();
while (!(await act(...parameters)))
{
if (timeout < Date.now() - startTime)
{
throw new Error('Timeout!');
}
if (stopProcessing)
{
throw new Error('Stopped everything');
}
await wait(200);
}
}
/**
* @param {string | string[]} email
*/
async function selectContactRow(email)
{
const relevantRows = getContactRow(email);
if (!relevantRows.length)
{
return false;
}
relevantRows[0].click();
return true;
}
/**
* @param {string | string[]} email
*/
function getContactRow(email)
{
const createEMailQuery = (oneEmail) => `text() = "${oneEmail}"`;
const emails = Array.isArray(email) ? email : [email];
const emailQueries = emails.map(createEMailQuery).join(' or ');
return $x(`//*[contains(@style, "visibility: visible")]//*[@jscontroller = "PMaUNb" and .//*[${emailQueries}]]//*[@role="checkbox"]`);
}
/**
* @param {string | string[]} email
*/
async function ensureNoContactRow(email)
{
return !getContactRow(email).length;
}
async function runAddToContacts()
{
await wait(200);
$('[style*="visibility: visible;"] [aria-label="Add to contacts"]').click();
}
async function runMoreActions()
{
await wait(200);
$('[style*="visibility: visible;"] [aria-label="More actions"]').click();
}
async function runDelete()
{
await wait(400);
const deleteButton = $('[style*="visibility: visible;"] [aria-label="Delete"]');
deleteButton.dispatchEvent(new MouseEvent('mousedown', {
bubbles: true,
cancelable: true
}));
deleteButton.dispatchEvent(new MouseEvent('mouseup', {
bubbles: true,
cancelable: true
}));
deleteButton.click();
}
async function confirmDeletion()
{
await wait(500);
$x('//*[@class = "VfPpkd-vQzf8d" and text() = "Delete"]')[0].click();
}
/**
* @param {string | string[]} email
* @param {string} action
*/
async function runAction(email, action)
{
search(email);
await retry(5000, selectContactRow, email);
if (action === 'Add to contacts')
{
await runAddToContacts();
}
else if (action === 'Delete')
{
await runMoreActions();
await runDelete();
await confirmDeletion();
await retry(5000, ensureNoContactRow, email);
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment