Skip to content

Instantly share code, notes, and snippets.

@thealphadollar
Last active June 25, 2024 13:41
Show Gist options
  • 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);
@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.

@ViniciusNyp
Copy link

ViniciusNyp commented Jun 25, 2024

My script wasn't working when not set to send a note, so I fixed it to catch the "Send without a note" button too, here's it:

It was modified upon @hargun0360 version already:

// 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" || el.textContent.trim() === "Send without a note";
    });
    // 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);

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