Skip to content

Instantly share code, notes, and snippets.

@sheerlox
Last active May 8, 2019 21:07
Show Gist options
  • Save sheerlox/63dd738b8e79e1c36dc553da8bf99248 to your computer and use it in GitHub Desktop.
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.
/* 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