Last active
May 8, 2019 21:07
-
-
Save sheerlox/63dd738b8e79e1c36dc553da8bf99248 to your computer and use it in GitHub Desktop.
Navigate to this page: https://www.linkedin.com/search/results/people/?facetNetwork=%5B"F"%5D, then paste this code in your console (Ctrl + Shift + I, Console Tab), then do not touch anything on your computer. Remember to adjust your filters before.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/* global fetch */ | |
// TODO: Brute API Method = Collect all IDs to remove, then rekt API limits | |
// Array of string that will lead to no | |
// deletion if found in a search result card | |
const filterTerms = [ | |
'Toulouse', | |
'Midi-Pyrénées', | |
'Bordeaux', | |
'Aquitaine', | |
'BRICOPRIVE', | |
'Premium member' | |
] | |
const removalArray = [] | |
let runRemoved = 0 | |
let deletionRunRemoved = 0 | |
let remaining = document.getElementsByClassName('search-results__total')[0].innerHTML.split(' ')[9].replace(',', '') | |
let versionTag = null | |
let clientPageInstanceId = null | |
let clientVersion = null | |
let csrfToken = null | |
const getCookie = cname => { | |
var name = cname + '=' | |
var decodedCookie = decodeURIComponent(document.cookie) | |
var ca = decodedCookie.split(';') | |
for (var i = 0; i < ca.length; i++) { | |
var c = ca[i] | |
while (c.charAt(0) === ' ') { | |
c = c.substring(1) | |
} | |
if (c.indexOf(name) === 0) { | |
return c.substring(name.length, c.length) | |
} | |
} | |
return '' | |
} | |
const findVersionTag = () => { | |
return new Promise(resolve => { | |
const codeElems = Array.prototype.slice.call(document.getElementsByTagName('code')) | |
codeElems.map(e => { | |
try { | |
const parsed = JSON.parse(e.innerHTML) | |
if (parsed.data && parsed.data.versionTag) { | |
console.log('Found versionTag =', parsed.data.versionTag) | |
resolve(parsed.data.versionTag) | |
} | |
} catch (e) { | |
// not valid json | |
} | |
}) | |
}) | |
} | |
const findClientPageInstanceId = () => { | |
return new Promise(resolve => { | |
const metaElems = Array.prototype.slice.call(document.getElementsByTagName('meta')) | |
metaElems.map(e => { | |
if (e.getAttribute('name') === 'clientPageInstanceId') resolve(e.getAttribute('content')) | |
}) | |
}) | |
} | |
const findClientVersion = () => { | |
return new Promise(resolve => { | |
const metaElems = Array.prototype.slice.call(document.getElementsByTagName('meta')) | |
metaElems.map(e => { | |
if (e.getAttribute('name') === 'applicationInstance') { | |
const content = JSON.parse(e.getAttribute('content')) | |
resolve(content.version) | |
} | |
}) | |
}) | |
} | |
const disconnectFrom = async accountId => { | |
const res = await fetch('https://www.linkedin.com/voyager/api/identity/profiles/' + encodeURI(accountId) + '/profileActions?versionTag=' + versionTag + '&action=disconnect', { | |
'credentials': 'include', | |
'headers': { | |
'accept': 'application/vnd.linkedin.normalized+json+2.1', | |
'accept-language': 'en-US,en;q=0.9,fr;q=0.8', | |
'csrf-token': csrfToken, | |
'x-li-lang': 'en_US', | |
'x-li-page-instance': 'urn:li:page:d_flagship3_profile_view_base;' + clientPageInstanceId, | |
'x-li-track': '{"clientVersion":"' + clientVersion + '","osName":"web","timezoneOffset":2,"deviceFormFactor":"DESKTOP","mpName":"voyager-web"}', | |
'x-restli-protocol-version': '2.0.0' | |
}, | |
'referrer': 'https://www.linkedin.com/in/' + accountId + '/', | |
'referrerPolicy': 'no-referrer-when-downgrade', | |
'body': null, | |
'method': 'POST', | |
'mode': 'cors' | |
}) | |
if (res.status === 200) { | |
deletionRunRemoved++ | |
} | |
} | |
const clickNextButton = () => { | |
return new Promise((resolve, reject) => { | |
if (document.getElementsByClassName('artdeco-pagination__button--next')[0]) { | |
document.getElementsByClassName('artdeco-pagination__button--next')[0].click() | |
setTimeout(() => { | |
resolve() | |
}, 1500) | |
} else { | |
reject(new Error('Next button not found, end of connections.')) | |
} | |
}) | |
} | |
const clickPage = (page) => { | |
return new Promise((resolve) => { | |
document.querySelectorAll("[aria-label='Page " + page + "']")[0].click() | |
setTimeout(() => { | |
resolve() | |
}, 1500) | |
}) | |
} | |
const wait1500ms = () => { | |
return new Promise((resolve) => { | |
setTimeout(() => { | |
resolve() | |
}, 1500) | |
}) | |
} | |
const scrollToMid = () => { | |
return new Promise((resolve) => { | |
window.scrollTo(0, document.body.scrollHeight / 3) | |
setTimeout(() => { | |
resolve() | |
}, 1500) | |
}) | |
} | |
const scrollToBottom = () => { | |
return new Promise((resolve) => { | |
window.scrollTo(0, document.body.scrollHeight - (document.body.scrollHeight / 2)) | |
setTimeout(() => { | |
resolve() | |
}, 1500) | |
}) | |
} | |
const processAccounts = async accounts => { | |
const throttleTime = 100 // in milliseconds | |
if (accounts.length > 0) { | |
const accountId = accounts.shift() | |
if (accountId !== 'results') { | |
runRemoved++ | |
setTimeout(processAccounts, throttleTime, accounts) | |
} else { | |
setTimeout(processAccounts, throttleTime, accounts) | |
} | |
} else { | |
remaining -= 10 | |
if (runRemoved > 0) { | |
console.log(runRemoved + '/10 selected for removal.', removalArray.length + '/50 selected for next removal process.', remaining, 'rem to filter.') | |
} | |
runRemoved = 0 | |
try { | |
await clickNextButton() | |
} catch (e) { | |
console.log('Error clickNext:', e) | |
} | |
if (remaining > 0) { | |
run() | |
} else { | |
console.log('Totally cleared this search filters.') | |
console.log('Starting removal process for', removalArray.length, 'accounts ...') | |
removeAccounts() | |
} | |
} | |
} | |
const removeAccounts = async () => { | |
const throttleTime = 500 // in milliseconds | |
if (removalArray.length > 0) { | |
const accountId = removalArray.shift() | |
if (accountId !== 'results') { | |
disconnectFrom(accountId) | |
.then(() => { | |
setTimeout(removeAccounts, throttleTime) | |
}) | |
} else { | |
setTimeout(removeAccounts, throttleTime) | |
} | |
} else { | |
console.log('Done removing', deletionRunRemoved, 'selected accounts !') | |
deletionRunRemoved = 0 | |
await scrollToBottom() | |
await clickPage(1) | |
await wait1500ms() | |
remaining = document.getElementsByClassName('search-results__total')[0].innerHTML.split(' ')[9].replace(',', '') | |
run() | |
} | |
} | |
// Scrolling to the bottom of the page is really important for this. | |
// Without it we will not be able to detect the searched terms, and we | |
// will run in a `Cannot read property 'getElementsByClassName' of null` | |
// error when trying to get the IDs. | |
const selectAccountIdsForDeletion = () => { | |
const searchResults = Array.prototype.slice.call(document.getElementsByClassName('search-result__occluded-item')) | |
const filteredSearchResults = searchResults.filter(sr => { | |
const keep = filterTerms.some(term => { | |
return sr.innerHTML.includes(term) | |
}) | |
// If we want to keep the connection, return false | |
// so it is removed from the filtered array | |
return !keep | |
}) | |
const accountsIds = filteredSearchResults.map(el => el.firstElementChild.getElementsByClassName('search-result__result-link')[0].href.split('/')[4]) | |
const uniqueAccountsId = [...new Set(accountsIds)] | |
return uniqueAccountsId | |
} | |
const run = async () => { | |
if (!versionTag) versionTag = await findVersionTag() | |
if (!clientPageInstanceId) clientPageInstanceId = await findClientPageInstanceId() | |
if (!clientVersion) clientVersion = await findClientVersion() | |
if (!csrfToken) csrfToken = getCookie('JSESSIONID').replace('"', '').replace('"', '') | |
// Wait for the top elements to load | |
await wait1500ms() | |
// Scroll down and wait for the bottom elements to load | |
await scrollToMid() | |
await scrollToBottom() | |
const uniqueAccountsId = selectAccountIdsForDeletion() | |
removalArray.push(...uniqueAccountsId) | |
if (removalArray.length > 250) { | |
console.log('Starting removal process for', removalArray.length, 'accounts ...') | |
removeAccounts() | |
} else { | |
processAccounts(uniqueAccountsId) | |
} | |
} | |
run() |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment