Skip to content

Instantly share code, notes, and snippets.

@thealphadollar
Last active April 30, 2024 19:20
Show Gist options
  • Star 94 You must be signed in to star a gist
  • Fork 41 You must be signed in to fork a gist
  • Save thealphadollar/7c0ee76664cbd28aecc1bd235f0202fd to your computer and use it in GitHub Desktop.
Save thealphadollar/7c0ee76664cbd28aecc1bd235f0202fd to your computer and use it in GitHub Desktop.
JS script to send connection requests to your LinkedIn search results with customisation options, accept all received connection requests, and withdraw pending sent connection requests.
// If the script does not work, you may need to allow same site scripting https://stackoverflow.com/a/50902950
Linkedin = {
config: {
scrollDelay: 3000,
actionDelay: 5000,
nextPageDelay: 5000,
// set to -1 for no limit
maxRequests: -1,
totalRequestsSent: 0,
// set to false to skip adding note in invites
addNote: true,
note: "Hey {{name}}, I'm looking forward to connecting with you!",
},
init: function (data, config) {
console.info("INFO: script initialized on the page...");
console.debug(
"DEBUG: scrolling to bottom in " + config.scrollDelay + " ms"
);
setTimeout(() => this.scrollBottom(data, config), config.actionDelay);
},
scrollBottom: function (data, config) {
window.scrollTo({ top: document.body.scrollHeight, behavior: "smooth" });
console.debug("DEBUG: scrolling to top in " + config.scrollDelay + " ms");
setTimeout(() => this.scrollTop(data, config), config.scrollDelay);
},
scrollTop: function (data, config) {
window.scrollTo({ top: 0, behavior: "smooth" });
console.debug(
"DEBUG: inspecting elements in " + config.scrollDelay + " ms"
);
setTimeout(() => this.inspect(data, config), config.scrollDelay);
},
inspect: function (data, config) {
var totalRows = this.totalRows();
console.debug("DEBUG: total search results found on page are " + totalRows);
if (totalRows >= 0) {
this.compile(data, config);
} else {
console.warn("WARN: end of search results!");
this.complete(config);
}
},
compile: function (data, config) {
var elements = document.querySelectorAll("button");
data.pageButtons = [...elements].filter(function (element) {
return element.textContent.trim() === "Connect";
});
if (!data.pageButtons || data.pageButtons.length === 0) {
console.warn("ERROR: no connect buttons found on page!");
console.info("INFO: moving to next page...");
setTimeout(() => {
this.nextPage(config);
}, config.nextPageDelay);
} else {
data.pageButtonTotal = data.pageButtons.length;
console.info("INFO: " + data.pageButtonTotal + " connect buttons found");
data.pageButtonIndex = 0;
var names = document.getElementsByClassName("entity-result__title-text");
names = [...names].filter(function (element) {
return element.parentNode.parentNode.parentNode.parentNode.parentNode.parentNode.parentNode.parentNode.textContent.includes(
"Connect\n"
);
});
data.connectNames = [...names].map(function (element) {
return element.innerText.split(" ")[0];
});
console.debug(
"DEBUG: starting to send invites in " + config.actionDelay + " ms"
);
setTimeout(() => {
this.sendInvites(data, config);
}, config.actionDelay);
}
},
sendInvites: function (data, config) {
console.debug("remaining requests " + config.maxRequests);
if (config.maxRequests == 0) {
console.info("INFO: max requests reached for the script run!");
this.complete(config);
} else {
console.debug(
"DEBUG: sending invite to " +
(data.pageButtonIndex + 1) +
" out of " +
data.pageButtonTotal
);
var button = data.pageButtons[data.pageButtonIndex];
button.click();
if (config.addNote && config.note) {
console.debug(
"DEBUG: clicking Add a note in popup, if present, in " +
config.actionDelay +
" ms"
);
setTimeout(() => this.clickAddNote(data, config), config.actionDelay);
} else {
console.debug(
"DEBUG: clicking done in popup, if present, in " +
config.actionDelay +
" ms"
);
setTimeout(() => this.clickDone(data, config), config.actionDelay);
}
}
},
clickAddNote: function (data, config) {
var buttons = document.querySelectorAll("button");
var addNoteButton = Array.prototype.filter.call(buttons, function (el) {
return el.textContent.trim() === "Add a note";
});
// adding note if required
if (addNoteButton && addNoteButton[0]) {
console.debug("DEBUG: clicking add a note button to paste note");
addNoteButton[0].click();
console.debug("DEBUG: pasting note in " + config.actionDelay);
setTimeout(() => this.pasteNote(data, config), config.actionDelay);
} else {
console.debug(
"DEBUG: add note button not found, clicking send on the popup in " +
config.actionDelay
);
setTimeout(() => this.clickDone(data, config), config.actionDelay);
}
},
pasteNote: function (data, config) {
noteTextBox = document.getElementById("custom-message");
noteTextBox.value = config.note.replace(
"{{name}}",
data.connectNames[data.pageButtonIndex]
);
noteTextBox.dispatchEvent(
new Event("input", {
bubbles: true,
})
);
console.debug(
"DEBUG: clicking send in popup, if present, in " +
config.actionDelay +
" ms"
);
setTimeout(() => this.clickDone(data, config), config.actionDelay);
},
clickDone: function (data, config) {
var buttons = document.querySelectorAll("button");
var doneButton = Array.prototype.filter.call(buttons, function (el) {
return el.textContent.trim() === "Send";
});
// Click the first send button
if (doneButton && doneButton[0]) {
console.debug("DEBUG: clicking send button to close popup");
doneButton[0].click();
} else {
console.debug(
"DEBUG: send button not found, clicking close on the popup in " +
config.actionDelay
);
}
setTimeout(() => this.clickClose(data, config), config.actionDelay);
},
clickClose: function (data, config) {
var closeButton = document.getElementsByClassName(
"artdeco-modal__dismiss artdeco-button artdeco-button--circle artdeco-button--muted artdeco-button--2 artdeco-button--tertiary ember-view"
);
if (closeButton && closeButton[0]) {
closeButton[0].click();
}
console.info(
"INFO: invite sent to " +
(data.pageButtonIndex + 1) +
" out of " +
data.pageButtonTotal
);
config.maxRequests--;
config.totalRequestsSent++;
if (data.pageButtonIndex === data.pageButtonTotal - 1) {
console.debug(
"DEBUG: all connections for the page done, going to next page in " +
config.actionDelay +
" ms"
);
setTimeout(() => this.nextPage(config), config.actionDelay);
} else {
data.pageButtonIndex++;
console.debug(
"DEBUG: sending next invite in " + config.actionDelay + " ms"
);
setTimeout(() => this.sendInvites(data, config), config.actionDelay);
}
},
nextPage: function (config) {
var pagerButton = document.getElementsByClassName(
"artdeco-pagination__button--next"
);
if (
!pagerButton ||
pagerButton.length === 0 ||
pagerButton[0].hasAttribute("disabled")
) {
console.info("INFO: no next page button found!");
return this.complete(config);
}
console.info("INFO: Going to next page...");
pagerButton[0].click();
setTimeout(() => this.init({}, config), config.nextPageDelay);
},
complete: function (config) {
console.info(
"INFO: script completed after sending " +
config.totalRequestsSent +
" connection requests"
);
},
totalRows: function () {
var search_results = document.getElementsByClassName("search-result");
if (search_results && search_results.length != 0) {
return search_results.length;
} else {
return 0;
}
},
};
Linkedin.init({}, Linkedin.config);
@Aliss20
Copy link

Aliss20 commented Apr 11, 2022

Otherwise , it can be the problem of Content Security Policy
After checking the first posssibility , visit this website
https://stackoverflow.com/questions/37298608/content-security-policy-the-pages-settings-blocked-the-loading-of-a-resource

@Crewxx
Copy link

Crewxx commented May 24, 2022

Greetings @thealphadollar, have you noticed the new linkedin features in connecting? Linkedin added a pop up that you have to choose how you know each other then you have to select option before the connect buttons shows up thus making the script not able to connect. Please kindly fix. See image below
Thanks
lnkd

@thealphadollar
Copy link
Author

@Aliss20 @Crewxx Thank you for bringing forward the issues. Unfortunately, I'm not actively maintaining the project and prioritising my other works. Hence, I would wait for someone to solve the problem - and update the doc then.

@valerioviale
Copy link

valerioviale commented Nov 24, 2022

inkedin memb
About the fact that sometimes does not find the next page button:
I got the same problem and I solved increasing slightly the delays in lines 5, 6 ,7:
scrollDelay: 3000,
actionDelay: 5000,
nextPageDelay: 7000,

it gives more time to render the page and see the button "next" page, it is slower but breaks less

@demoaccountdfe545
Copy link

Thank You so much it help me so much........ thanks again .

@ucalyptus2
Copy link

@thealphadollar how would u use this when there are "Follow" buttons and not "Connect" buttons. You can still connect with these people but it needs you to go inside the profile and manual work.

@thealphadollar
Copy link
Author

Hey @forkbabu , it'll require adding the logic to click the dropdown and then sending a connection request. That case can be automated, however, it hasn't been done at the moment.

@valerioviale
Copy link

what if we add a loop also for the Message?

sendMessages: function(data, config) {
// Get the elements for the "Message" buttons on the page
var messageButtons = [...data.pageButtons].filter(function(element) {
return element.textContent.trim() === "Message";
});

// Loop through the message buttons and send a message to each person
[...messageButtons].forEach(function(button, index) {
// Click the button to open the message form
button.click();

// Wait a moment for the form to open
setTimeout(function() {
  // Get the message form and the input field where you enter the message
  var form = document.querySelector("form[role='form']");
  var input = form.querySelector("textarea");

  // Enter the message into the input field
  input.value = "Hi there! I saw your profile on LinkedIn and thought I would reach out to see if you're interested in connecting. Let me know if you have any questions.";

  // Submit the form to send the message
  form.submit();

  // Wait a moment for the form to submit
  setTimeout(function() {
    // Close the form by clicking the "Done" button
    var doneButton = form.querySelector("button[type='submit']");
    doneButton.click();
  }, config.actionDelay);
}, config.actionDelay);

});
},

@Aliss20
Copy link

Aliss20 commented Dec 12, 2022

I had thr same problem ,but found out that the solution to make the script working is by checking those points 💯
-First of all check your Linkedin Language and set it to english
-Let page maximize without minimizing it , or you can drag the chrome tab to the bottom
If that works for you , like the comment

AliSs2018

@eyadevv
Copy link

eyadevv commented Feb 24, 2023

Good Job ,This is really good.

@Crewxx
Copy link

Crewxx commented May 20, 2023

Hi @thealphadollar hope you are having a great day. Please any fix to the script yet to work with the new linkedin process that I informed you about?

@thealphadollar
Copy link
Author

Hey @Crewxx , I'm unable to find time to work on the script. Any PRs or changes are welcome, and I'll be happy to include.

@barryis50ish
Copy link

I tried doing this with MS Power Automate, I believe it used to work a few years ago. Linkedin rely heavily of subscription, so they change the connect format and vary URLs to stop software doing this automatically easily. If you search Linkedin too much without membership it limits your results. Here’s what I am doing. I actually think it is a good idea to capture URLs based on string searches from a google search, once list results can be copied and pasted into an excel table, so you may track your invites and subsequent messages. I have a couple of scripts written to 1. request invites, 2. Thank you for connecting and send some information, 3 follow up for feedback, 4. Send some more info, 5. Request a meeting. In the Chrome store there is an app call Save Search Results(https://chrome.google.com/webstore/detail/save-search-results/fppgodjcdekgbhbipabkkdhbmnahbhnj), it is simple to use and will conveniently save your URL search results ready for copying and pasting into excel into column A, I then add each person’s name to column B, first and second. Then a search google with "UK. Linkedin" My+Key+Words followed by -jobs, by add -jobs your results will not show job adverts posted by Linkedin. Next with this list I used mail merge to create a letter to each first name recipient this opens many pages of the same 1st message. A certain amount of data manipulation is required but this allows me to list the employer and check each prospect in turn quickly whilst pasting the text from my word letter into Linkedin. By having the URL's listed in excel it is easy to see which ones I have invited as they turn into hyperlinks after one click.

@Crewxx
Copy link

Crewxx commented Jul 26, 2023

I tried doing this with MS Power Automate, I believe it used to work a few years ago. Linkedin rely heavily of subscription, so they change the connect format and vary URLs to stop software doing this automatically easily. If you search Linkedin too much without membership it limits your results. Here’s what I am doing. I actually think it is a good idea to capture URLs based on string searches from a google search, once list results can be copied and pasted into an excel table, so you may track your invites and subsequent messages. I have a couple of scripts written to 1. request invites, 2. Thank you for connecting and send some information, 3 follow up for feedback, 4. Send some more info, 5. Request a meeting. In the Chrome store there is an app call Save Search Results(https://chrome.google.com/webstore/detail/save-search-results/fppgodjcdekgbhbipabkkdhbmnahbhnj), it is simple to use and will conveniently save your URL search results ready for copying and pasting into excel into column A, I then add each person’s name to column B, first and second. Then a search google with "UK. Linkedin" My+Key+Words followed by -jobs, by add -jobs your results will not show job adverts posted by Linkedin. Next with this list I used mail merge to create a letter to each first name recipient this opens many pages of the same 1st message. A certain amount of data manipulation is required but this allows me to list the employer and check each prospect in turn quickly whilst pasting the text from my word letter into Linkedin. By having the URL's listed in excel it is easy to see which ones I have invited as they turn into hyperlinks after one click.

Please do you mind sharing your script. Would be helpful. Thanks.

@thealphadollar I hope you are okay and your current endeavor is going great, all the best.

@thealphadollar
Copy link
Author

@thealphadollar I hope you are okay and your current endeavor is going great, all the best.

Hey thanks for asking, I'm good.

@shivamvora
Copy link

Do you have any experience with react, I want to add this to a google extension, having problems with type script..

yes I have

@Vladyslav2003
Copy link

image

@thealphadollar
Copy link
Author

Thank you @Vladyslav2003 for reporting the issue. I've fixed the issue in the latest version and pushed it. Please try now 🙇

A heartfelt gratitude to everyone else for using the script and sharing their findings here 🙏 I'll try to maintain the script and support it more religiously.

@thealphadollar
Copy link
Author

Reposting the Guide for visibility. From here on, I will put updates in new comments, if any.

What is this?

So, have you ever wanted to connect to people on LinkedIn who are recruiters at X company or study at Y university or work at Z and more. What we usually do is search for "recruiter X", then choose People and start sending connect request to them. This is a manual task, and the conversion rate is very low; so after sending 100 requests to people, 10 accept your request in the best case. What can you do to increase your chances and acceptance? Obviously, you won't sit and send it to 1000 people, right? That's where the script comes in.

The script, once run (instructions below), works in the background on the page and keeps on sending connection requests to the results on the page as long as either it meets maximum requests specified by you or there are no more results. This allows you to just search, run the script, keep the tab open and forget about it. The script has an adequate amount of delay placed between actions so you are not banned, and your connections to the particular company have higher chances of increasing 😄

How to use the above script?

  • Open Linkedin, search for terms such as "Amazon recruiter", choose People, choose 2nd and 3rd+ connection level.
  • Open the developer console of your browser following the instructions here for your browser.
  • Copy the entire above script and paste it into the console and hit enter.
  • Do not close or refresh the browser tab unless you want the script to stop abruptly.

You'll see that connection requests are being sent now after delays set in the config of the above file. If you want to limit the number of connections to be sent, please update maxRequests above to the desired number of requests.

When does the script stop?

  • When there is no next button.
  • When there are no search results on the current page.
  • When the number of max requests (if specified) is reached.

What to configure?

If it is taking too long or you are on a very fast internet connection, you can lower the delays between each action since elements will be loading faster for you. Below is an explanation of what each of the configuration item does:

  • scrollDelay: specifies the wait in milliseconds after scrolling page to bottom and top to load results fully into the page.
  • actionDelay: specifies the delay in milliseconds between each action such as pressing the connect button, then the done button, and so on.
  • nextPageDelay: specifies the wait in milliseconds for the next page to load after pressing the next page button.
  • maxRequests: set to a positive number to limit the number of connection requests to be sent to that number; a negative number leads to infinite connection requests (not really).
  • totalRequestsSent: stores the number of total requests sent (displayed when the script stops gracefully) and is not to be changed, and should remain 0.
  • addNote: set to true if you wish to add a note in your invites, otherwise `false.
  • note: the text of the note to be sent; if it is an empty string, no note is added. You can also add the first name of the person by using the placeholder "{{name}}" (without quotes, with curly brackets).

Acknowledgement

Special thanks to @phpenterprise for the initial work at the script.

Reposting other small scripts for LinkedIn.

A small code to accept all connection requests at once to be used on the all connections page.

var x = document.getElementsByClassName('invitation-card__action-btn artdeco-button artdeco-button--2 artdeco-button--secondary ember-view'); for (var i=0 ; i<x.length; i++) x[i].click();

UPDATE:

A small code snippet to withdraw all unaccepted connection requests (to be run in console on page - linkedin.com/mynetwork/invitation-manager/sent).

var Allbuttons = document.querySelectorAll('button');
var withdrawButtons = Array.prototype.filter.call(Allbuttons, function (el) {
    return el.textContent.trim() === 'Withdraw';
});

var withdrawRecursively = (index) => {
    if (index === withdrawButtons.length) {
        alert("All connections withdrawn on the page!");
        checkNextPage();
    } else {
        withdrawButtons[index].click();
        setTimeout(() => clickNewWithdraw(index), 1000);
    }
}

var clickNewWithdraw = (index) => {
    var AllButtons = document.querySelectorAll('button');
    var newWithdrawButtons = Array.prototype.filter.call(AllButtons, function (el) {
        return el.textContent.trim() === 'Withdraw' && !withdrawButtons.includes(el);
    });
    newWithdrawButtons[0].click();
    setTimeout(() => withdrawRecursively(index+1), 1000);
}
withdrawRecursively(0);

@ucalyptus2
Copy link

@thealphadollar great script. But some people have "Message" button instead of "Connect". How to modify your script to work with that. I presume that the only way is the script has to open these N profiles in N new tabs separately. and then when I'm done with the "Connect" ones, I can run a different script catering to these N profile main pages and close that tab. I see that this has to be run a total of N+1 times.

Please tell me if you have a workaround thought of it.

@thealphadollar
Copy link
Author

Thank you for asking @forkbabu In my experience, I've observed that opening N tabs simultaneously is never a good idea.

I'd suggest modifying the script such that for every potential connection, it opens their profile page, sends a connection request, sends the message, and goes back to the search results page.

This will be a significant modification - let me know if you need more information.

@ucalyptus2
Copy link

ucalyptus2 commented Dec 4, 2023 via email

@valerioviale
Copy link

I am in the process of allowing the script to find the urls of profiles which have "Message" or "Follow" but when it opens a new page the script reset (connecting logic to send a message from the profile page is still missing. Any idea on how to solve this?

`Linkedin = {
config: {
scrollDelay: 3000,
actionDelay: 5000,
nextPageDelay: 5000,
maxRequests: -1,
totalRequestsSent: 0,
addNote: true,
note: "Hey {{name}}, I'm looking forward to connecting with you!",
},

init: function (data, config) {
    console.info("INFO: script initialized on the page...");
    setTimeout(() => this.scrollBottom(data, config), config.actionDelay);
},

scrollBottom: function (data, config) {
    window.scrollTo({ top: document.body.scrollHeight, behavior: 'smooth' });
    setTimeout(() => this.scrollTop(data, config), config.scrollDelay);
},

scrollTop: function (data, config){
    window.scrollTo({ top: 0, behavior: 'smooth' });
    setTimeout(() => this.inspect(data, config), config.scrollDelay);
},

inspect: function (data, config) {
    var totalRows = this.totalRows();
    if (totalRows >= 0) {
        this.compile(data, config);
    } else {
        this.complete(config);
    }
},

compile: function (data, config) {
    var buttons = document.querySelectorAll("button");
    var contents = document.querySelectorAll(".entity-result__content");
   
      // Logic for buttons
    data.pageButtons = [...buttons].filter(function (element) {
         var buttonText = element.textContent.trim();
        return buttonText === "Connect" || buttonText === "Follow" || buttonText === "Message";
 });

// Logic for contents
    data.pageContents = [...contents].filter(function (element) {
    // Add logic to filter contents based on your criteria
    // For example, here I'm assuming checking if it contains "Follow" or "Message"
        var contentText = element.textContent.trim();
        return contentText.includes("Follow") || contentText.includes("Message");
        
});
  
    if (!data.pageButtons || data.pageButtons.length === 0) {
        console.warn("ERROR: no connect, follow, or message buttons found on page!");
        console.info("INFO: moving to next page...");
        setTimeout(() => {
            this.nextPage(config);
        }, config.nextPageDelay);
    } else {
        data.pageButtonTotal = data.pageButtons.length;
        console.info("INFO: " + data.pageButtonTotal + " connect, follow, or message buttons found");
        data.pageButtonIndex = 0;
    
        var names = document.getElementsByClassName("entity-result__title-text");
        var namesArray = [...names].filter(function (element) {
            var parentText = element.parentNode.parentNode.parentNode.parentNode.parentNode.parentNode.parentNode.parentNode.textContent;
            return parentText.includes("Connect\n") || parentText.includes("Follow\n") || parentText.includes("Message\n");
        });
        data.connectNames = [...namesArray].map(function (element) {
            return element.innerText.split(" ")[0];
        });
    
        var results = document.querySelectorAll('.entity-result__title-text');
        data.profileInfo = [];
    
        results.forEach(function (element) {
            var parent = element.closest('.entity-result__content');
            if (parent) {
                var profileLinkElement = parent.querySelector('.app-aware-link');
                if (profileLinkElement) {
                    var profileInfo = {
                        name: element.innerText.split(" ")[0],
                        profileURL: profileLinkElement.href
                    };
                    data.profileInfo.push(profileInfo);
                }
            }
        });
          // New logic to fetch URLs from elements with class '.entity-result__content'
        const contentElements = document.querySelectorAll('.entity-result__content');
        contentElements.forEach(element => {
        const profileLinkElement = element.querySelector('.app-aware-link');
        if (profileLinkElement) {
            const profileLink = profileLinkElement.href;
            console.log(profileLink);
   } 
});
    
        setTimeout(() => {
            this.performAction(data, config);
        }, config.actionDelay);
    }
    
},

performAction: function (data, config) {
    if (config.maxRequests === 0) {
        this.complete(config);
    } else {
        var button = data.pageButtons[data.pageButtonIndex];
        var buttonText = button.textContent.trim();

        if (buttonText === "Connect") {
            // ... (existing logic remains unchanged)
        } else if (buttonText === "Follow" || buttonText === "Message") {
            var entityContentElements = document.querySelectorAll('.entity-result__content');

            entityContentElements.forEach(element => {
                const profileLinkElement = element.querySelector('.app-aware-link');
                if (profileLinkElement) {
                    const profileLink = profileLinkElement.href;
                    console.log(profileLink);
                    // You can add additional actions here if needed

                    // Open the profile link in the same tab and navigate back
                    window.open(profileLink, '_self');
                    setTimeout(() => {
                        window.history.back();
                    }, config.actionDelay * 2); // Adjust the delay as needed
                }
            });

            // Update the index for the next button action
            if (data.pageButtonIndex === (data.pageButtonTotal - 1)) {
                console.debug("DEBUG: all connections for the page done, going to next page in " + config.actionDelay + " ms");
                setTimeout(() => this.nextPage(config), config.actionDelay);
            } else {
                data.pageButtonIndex++;
                console.debug("DEBUG: sending next invite in " + config.actionDelay + " ms");
                setTimeout(() => this.performAction(data, config), config.actionDelay);
            }
        }

        // Additional logic and actions can be added here
        // ...

        // Update the index for the next button action if needed
        // ...

        // Call nextPage function when required
        // ...
    }
},





clickAddNote: function (data, config) {
    var buttons = document.querySelectorAll('button');
    var addNoteButton = Array.prototype.filter.call(buttons, function (el) {
        return el.textContent.trim() === 'Add a note';
    });

    if (addNoteButton && addNoteButton[0]) {
        addNoteButton[0].click();
        setTimeout(() => this.pasteNote(data, config), config.actionDelay);
    } else {
        setTimeout(() => this.clickDone(data, config), config.actionDelay);
    }
},

pasteNote: function (data, config) {
    noteTextBox = document.getElementById("custom-message");
    noteTextBox.value = config.note.replace("{{name}}", data.connectNames[data.pageButtonIndex]);
    noteTextBox.dispatchEvent(new Event('input', {
        bubbles: true
    }));
    console.debug("DEBUG: clicking send in popup, if present, in " + config.actionDelay + " ms");
    setTimeout(() => this.clickDone(data, config), config.actionDelay);
},

clickDone: function (data, config) {
    var buttons = document.querySelectorAll('button');
    var doneButton = Array.prototype.filter.call(buttons, function (el) {
        return el.textContent.trim() === 'Send';
    });

    if (doneButton && doneButton[0]) {
        console.debug("DEBUG: clicking send button to close popup");
        doneButton[0].click();
    } else {
        console.debug("DEBUG: send button not found, clicking close on the popup in " + config.actionDelay);
    }
    setTimeout(() => this.clickClose(data, config), config.actionDelay);
},

clickClose: function (data, config) {
    var closeButton = document.getElementsByClassName('artdeco-modal__dismiss artdeco-button artdeco-button--circle artdeco-button--muted artdeco-button--2 artdeco-button--tertiary ember-view');
    if (closeButton && closeButton[0]) {
        closeButton[0].click();
    }
    console.info('INFO: invite sent to ' + (data.pageButtonIndex + 1) + ' out of ' + data.pageButtonTotal);
    config.maxRequests--;
    config.totalRequestsSent++;

    if (data.pageButtonIndex === (data.pageButtonTotal - 1)) {
        console.debug("DEBUG: all connections for the page done, going to next page in " + config.actionDelay + " ms");
        setTimeout(() => this.nextPage(config), config.actionDelay);
    } else {
        data.pageButtonIndex++;
        console.debug("DEBUG: sending next invite in " + config.actionDelay + " ms");
        setTimeout(() => this.performAction(data, config), config.actionDelay);
    }
},

nextPage: function (config) {
    var pagerButton = document.getElementsByClassName('artdeco-pagination__button--next');
    if (!pagerButton || pagerButton.length === 0 || pagerButton[0].hasAttribute('disabled')) {
        console.info("INFO: no next page button found!");
        return this.complete(config);
    }
    console.info("INFO: Going to next page...");
    pagerButton[0].click();
    setTimeout(() => this.init({}, config), config.nextPageDelay);
},

complete: function (config) {
    console.info('INFO: script completed after sending ' + config.totalRequestsSent + ' connection requests');
},

totalRows: function () {
    var search_results = document.getElementsByClassName('search-result');
    if (search_results && search_results.length != 0) {
        return search_results.length;
    } else {
        return 0;
    }
}

}

Linkedin.init({}, Linkedin.config);
`

@thealphadollar
Copy link
Author

I think we can break the script into two independent logical flows -> main script that handles sending connection requests (current) + conditional script that runs in a new tab for users with "Message" or "Follow".

For the second script, I propose writing it with minimal context passing from the last page. As is shared in the StackOverflow answer, this new conditional script will be executed in the newly opened tab and send a message (context passed from config of the main script) or Follow.

If it's not feasible for you, I'll try to write the conditional script over the weekend 🙇🏻

@valerioviale @forkbabu

@ucalyptus3
Copy link

Go ahead @thealphadollar and @valerioviale . the community counts on yall 🙇🏻

@valerioviale
Copy link

I got to a point where I can open the new profile page but here I have a logic problem. The JS can launch a script that will start in the new page that include also the instruction to go back but it will not have the instruction to start back the process again for each profile.
Should we try with Python/Selenium? I know it's a pain for several reason, since you have to log in and you are identified as a robot by lnkdn but it looks quite complex to switch from page to page with a same page script.

Another problem, when I am already inside the profile page I have to click the button More (or ...) in the profile page. I attempted with the class="artdeco-dropdown" and filtering and max request 1. But I get at least 2 different artdeco-dropdown__trigger, maybe filtering out the navbar tag? Any suggestion on that?

@sabrina5zuno
Copy link

Thanks

@hargun0360
Copy link

hargun0360 commented Apr 30, 2024

@thealphadollar
Implemented logic to invite current employees, excluding past employees.

// If the script does not work, you may need to allow same site scripting https://stackoverflow.com/a/50902950
Linkedin = {
config: {
scrollDelay: 3000,
actionDelay: 5000,
nextPageDelay: 5000,
// set to -1 for no limit
maxRequests: -1,
totalRequestsSent: 0,
// set to false to skip adding note in invites
addNote: true,
note: "Hey {{name}}, I'm looking forward to connecting with you!",
},
init: function (data, config) {
console.info("INFO: script initialized on the page...");
console.debug(
"DEBUG: scrolling to bottom in " + config.scrollDelay + " ms"
);
setTimeout(() => this.scrollBottom(data, config), config.actionDelay);
},
scrollBottom: function (data, config) {
window.scrollTo({ top: document.body.scrollHeight, behavior: "smooth" });
console.debug("DEBUG: scrolling to top in " + config.scrollDelay + " ms");
setTimeout(() => this.scrollTop(data, config), config.scrollDelay);
},
scrollTop: function (data, config) {
window.scrollTo({ top: 0, behavior: "smooth" });
console.debug(
"DEBUG: inspecting elements in " + config.scrollDelay + " ms"
);
setTimeout(() => this.inspect(data, config), config.scrollDelay);
},
inspect: function (data, config) {
var totalRows = this.totalRows();
console.debug("DEBUG: total search results found on page are " + totalRows);
if (totalRows >= 0) {
this.compile(data, config);
} else {
console.warn("WARN: end of search results!");
this.complete(config);
}
},
compile: function (data, config) {
 var elements = document.querySelectorAll(".entity-result__actions button");
var isCurrent = [];
var paragraphs = document.getElementsByClassName(
"entity-result__summary--2-lines"
);
Array.from(paragraphs).forEach((element) => {
if (element.textContent.includes("Current:")) {
isCurrent.push(true);
} else {
isCurrent.push(false);
}
});
data.pageButtons = [...elements].filter((element, index) => {
 if (element.textContent.trim() === "Connect" && isCurrent[index]) {
        return element.textContent.trim() === "Connect";
      }
});
if (!data.pageButtons || data.pageButtons.length === 0) {
console.warn("ERROR: no connect buttons found on page!");
console.info("INFO: moving to next page...");
setTimeout(() => {
this.nextPage(config);
}, config.nextPageDelay);
} else {
data.pageButtonTotal = data.pageButtons.length;
console.info("INFO: " + data.pageButtonTotal + " connect buttons found");
data.pageButtonIndex = 0;
var names = document.getElementsByClassName("entity-result__title-text");
names = [...names].filter(function (element, index) {
if (isCurrent[index]) {
return element.parentNode.parentNode.parentNode.parentNode.parentNode.parentNode.parentNode.parentNode.textContent.includes(
"Connect\n"
);
}
});
data.connectNames = [...names].map(function (element) {
return element.innerText.split(" ")[0];
});
console.debug(
"DEBUG: starting to send invites in " + config.actionDelay + " ms"
);
setTimeout(() => {
this.sendInvites(data, config);
}, config.actionDelay);
}
},
sendInvites: function (data, config) {
console.debug("remaining requests " + config.maxRequests);
if (config.maxRequests == 0) {
console.info("INFO: max requests reached for the script run!");
this.complete(config);
} else {
console.debug(
"DEBUG: sending invite to " +
(data.pageButtonIndex + 1) +
" out of " +
data.pageButtonTotal
);
var button = data.pageButtons[data.pageButtonIndex];
button.click();
if (config.addNote && config.note) {
console.debug(
"DEBUG: clicking Add a note in popup, if present, in " +
config.actionDelay +
" ms"
);
setTimeout(() => this.clickAddNote(data, config), config.actionDelay);
} else {
console.debug(
"DEBUG: clicking done in popup, if present, in " +
config.actionDelay +
" ms"
);
setTimeout(() => this.clickDone(data, config), config.actionDelay);
}
}
},
clickAddNote: function (data, config) {
var buttons = document.querySelectorAll("button");
var addNoteButton = Array.prototype.filter.call(buttons, function (el) {
return el.textContent.trim() === "Add a note";
});
// adding note if required
if (addNoteButton && addNoteButton[0]) {
console.debug("DEBUG: clicking add a note button to paste note");
addNoteButton[0].click();
console.debug("DEBUG: pasting note in " + config.actionDelay);
setTimeout(() => this.pasteNote(data, config), config.actionDelay);
} else {
console.debug(
"DEBUG: add note button not found, clicking send on the popup in " +
config.actionDelay
);
setTimeout(() => this.clickDone(data, config), config.actionDelay);
}
},
pasteNote: function (data, config) {
noteTextBox = document.getElementById("custom-message");
noteTextBox.value = config.note.replace(
"{{name}}",
data.connectNames[data.pageButtonIndex]
);
noteTextBox.dispatchEvent(
new Event("input", {
bubbles: true,
})
);
console.debug(
"DEBUG: clicking send in popup, if present, in " +
config.actionDelay +
" ms"
);
setTimeout(() => this.clickDone(data, config), config.actionDelay);
},
clickDone: function (data, config) {
var buttons = document.querySelectorAll("button");
var doneButton = Array.prototype.filter.call(buttons, function (el) {
return el.textContent.trim() === "Send";
});
// Click the first send button
if (doneButton && doneButton[0]) {
console.debug("DEBUG: clicking send button to close popup");
doneButton[0].click();
} else {
console.debug(
"DEBUG: send button not found, clicking close on the popup in " +
config.actionDelay
);
}
setTimeout(() => this.clickClose(data, config), config.actionDelay);
},
clickClose: function (data, config) {
var closeButton = document.getElementsByClassName(
"artdeco-modal__dismiss artdeco-button artdeco-button--circle artdeco-button--muted artdeco-button--2 artdeco-button--tertiary ember-view"
);
if (closeButton && closeButton[0]) {
closeButton[0].click();
}
console.info(
"INFO: invite sent to " +
(data.pageButtonIndex + 1) +
" out of " +
data.pageButtonTotal
);
config.maxRequests--;
config.totalRequestsSent++;
if (data.pageButtonIndex === data.pageButtonTotal - 1) {
console.debug(
"DEBUG: all connections for the page done, going to next page in " +
config.actionDelay +
" ms"
);
setTimeout(() => this.nextPage(config), config.actionDelay);
} else {
data.pageButtonIndex++;
console.debug(
"DEBUG: sending next invite in " + config.actionDelay + " ms"
);
setTimeout(() => this.sendInvites(data, config), config.actionDelay);
}
},
nextPage: function (config) {
var pagerButton = document.getElementsByClassName(
"artdeco-pagination__button--next"
);
if (
!pagerButton ||
pagerButton.length === 0 ||
pagerButton[0].hasAttribute("disabled")
) {
console.info("INFO: no next page button found!");
return this.complete(config);
}
console.info("INFO: Going to next page...");
pagerButton[0].click();
setTimeout(() => this.init({}, config), config.nextPageDelay);
},
complete: function (config) {
console.info(
"INFO: script completed after sending " +
config.totalRequestsSent +
" connection requests"
);
},
totalRows: function () {
var search_results = document.getElementsByClassName("search-result");
if (search_results && search_results.length != 0) {
return search_results.length;
} else {
return 0;
}
},
};

Linkedin.init({}, Linkedin.config);

@ucalyptus2
Copy link

ucalyptus2 commented Apr 30, 2024

@hargun0360 were you able to only filter out people with "Connect" buttons? People with "Message" buttons or some other kind of button won't be able to take personalized invites..

@hargun0360
Copy link

@ucalyptus2 Yup, works with "Connect" buttons only. That way we can send those personalized invites! I also filtered for current company employees only.

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