Created
November 13, 2025 17:39
-
-
Save benjaminjackson/60713b37dd9eb4eea00d567f4f98a43a to your computer and use it in GitHub Desktop.
Unfollow Everyone on LinkedIn (Manual Confirmation)
This file contains hidden or 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
| // LinkedIn Semi-Automated Unfollow Script | |
| // Opens unfollow modals one at a time and waits for manual confirmation | |
| // You stay in control - manually click "Unfollow" or "Cancel" for each person | |
| (() => { | |
| let count = 0; | |
| const processedPeople = new Set(); // Track who we've successfully unfollowed | |
| const skippedPeople = new Set(); // Track who we've cancelled on | |
| function getAllFollowingButtons() { | |
| return Array.from(document.querySelectorAll('button.artdeco-button--muted.artdeco-button--secondary')) | |
| .filter(btn => { | |
| const buttonText = btn.querySelector('.artdeco-button__text')?.innerText; | |
| const ariaLabel = btn.getAttribute('aria-label'); | |
| // Only include buttons we haven't processed or skipped yet | |
| return buttonText === "Following" && !processedPeople.has(ariaLabel) && !skippedPeople.has(ariaLabel); | |
| }); | |
| } | |
| function waitForModalToClose() { | |
| return new Promise((resolve) => { | |
| const checkInterval = setInterval(() => { | |
| const modal = document.querySelector('.artdeco-modal'); | |
| if (!modal) { | |
| clearInterval(checkInterval); | |
| resolve(); | |
| } | |
| }, 500); | |
| }); | |
| } | |
| async function unfollowNext() { | |
| const buttons = getAllFollowingButtons(); | |
| console.log(`Found ${buttons.length} unprocessed "Following" buttons`); | |
| if (buttons.length === 0) { | |
| // Try multiple times to load more content | |
| let loadAttempts = 0; | |
| const maxAttempts = 3; | |
| while (loadAttempts < maxAttempts) { | |
| loadAttempts++; | |
| console.log(`Load attempt ${loadAttempts}/${maxAttempts}...`); | |
| // Click "Show more results" button if it exists | |
| const showMoreButton = document.querySelector('.scaffold-finite-scroll__load-button'); | |
| if (showMoreButton && !showMoreButton.disabled) { | |
| console.log("Clicking 'Show more results' button..."); | |
| showMoreButton.click(); | |
| await new Promise((resolve) => setTimeout(resolve, 5000)); | |
| } else { | |
| // Otherwise try scrolling | |
| console.log("Scrolling to bottom..."); | |
| window.scrollTo(0, document.body.scrollHeight); | |
| await new Promise((resolve) => setTimeout(resolve, 5000)); | |
| } | |
| // Check for new buttons | |
| const newButtons = getAllFollowingButtons(); | |
| console.log(`After attempt ${loadAttempts}: found ${newButtons.length} new buttons`); | |
| if (newButtons.length > 0) { | |
| console.log("Found new buttons, continuing..."); | |
| return unfollowNext(); | |
| } | |
| if (loadAttempts < maxAttempts) { | |
| console.log("No new buttons yet, waiting before next attempt..."); | |
| await new Promise((resolve) => setTimeout(resolve, 2000)); | |
| } | |
| } | |
| // After all attempts, we're done | |
| console.log("Could not find more content after multiple attempts - reached the end"); | |
| console.log(`All done! Processed ${count} Following buttons total.`); | |
| console.log(`Successfully unfollowed: ${processedPeople.size}`); | |
| console.log(`Skipped/Cancelled: ${skippedPeople.size}`); | |
| if (skippedPeople.size > 0) { | |
| console.log(`Skipped people:`, Array.from(skippedPeople)); | |
| } | |
| return; | |
| } | |
| // Get the first unprocessed button | |
| const button = buttons[0]; | |
| const ariaLabel = button.getAttribute("aria-label"); | |
| // Extract person's name from aria-label (format: "Click to stop following [Name]") | |
| const personName = ariaLabel.replace('Click to stop following ', ''); | |
| count++; | |
| console.log(`${count}. Opening modal: ${ariaLabel}`); | |
| // Scroll button into view more reliably | |
| button.scrollIntoView({ behavior: 'smooth', block: 'center' }); | |
| await new Promise((resolve) => setTimeout(resolve, 500)); | |
| // Click to open modal | |
| button.click(); | |
| // Wait for you to manually confirm/cancel the modal | |
| await waitForModalToClose(); | |
| // Delay to let the button update (LinkedIn needs time to re-render) | |
| await new Promise((resolve) => setTimeout(resolve, 1000)); | |
| // Re-query for buttons with this person's name (aria-label changes after unfollowing) | |
| const updatedButton = Array.from(document.querySelectorAll('button.artdeco-button--secondary')) | |
| .find(btn => { | |
| const label = btn.getAttribute('aria-label'); | |
| return label && ( | |
| label === ariaLabel || // Still has old label (cancelled) | |
| label === `Click to follow ${personName}` // Changed to new label (unfollowed) | |
| ); | |
| }); | |
| if (!updatedButton) { | |
| // Button disappeared completely, count it as success | |
| processedPeople.add(ariaLabel); | |
| console.log(`✓ Unfollowed (button removed from page)`); | |
| } else { | |
| // Check if the button text changed | |
| const buttonText = updatedButton.querySelector('.artdeco-button__text')?.innerText; | |
| const isMuted = updatedButton.classList.contains('artdeco-button--muted'); | |
| if (buttonText === "Follow" || !isMuted) { | |
| // Successfully unfollowed (button changed to "Follow" or lost muted class) | |
| processedPeople.add(ariaLabel); | |
| console.log(`✓ Unfollowed`); | |
| } else if (buttonText === "Following" && isMuted) { | |
| // User cancelled, skip this person for now | |
| skippedPeople.add(ariaLabel); | |
| console.log(`✗ Cancelled - skipping for now`); | |
| } | |
| } | |
| // Continue to next | |
| unfollowNext(); | |
| } | |
| console.log("Starting semi-automated unfollow. Click 'Unfollow' in each modal to proceed to the next person."); | |
| unfollowNext(); | |
| })(); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment