Skip to content

Instantly share code, notes, and snippets.

@shmidtelson
Last active November 18, 2025 13:14
Show Gist options
  • Select an option

  • Save shmidtelson/ebd807e2cf64069ee96d36f7d4894f53 to your computer and use it in GitHub Desktop.

Select an option

Save shmidtelson/ebd807e2cf64069ee96d36f7d4894f53 to your computer and use it in GitHub Desktop.
Linkedin invite sender
/**
@description Auto-adding connections in linkedin
@instruction Open url https://www.linkedin.com/search/results/people/ and type your query.
When you see the result, you should paste the code (below) into the console of your browser and press enter.
This bot can add connections automatically, and it also uses page pagination.
When you reach the weekly limit, the bot will stop.
*/
// Configuration constants
const CONFIG = {
DELAY_SHORT: 500,
DELAY_MEDIUM: 1000,
DELAY_LONG: 2000,
DELAY_SEND: 3000,
MAX_RETRIES: 5,
PAGINATION_TIMEOUT: 300,
CONTENT_TIMEOUT: 120,
};
const currentLanguage = document.querySelector('html')?.lang || 'en';
const languageMap = {
'invite': {
'ru': 'Пригласить',
'en': 'Invite',
},
'howknow': {
'ru': 'Откуда вы знаете',
'en': 'How do you know',
},
'dontknow': {
'ru': 'Мы не знакомы',
'en': 'We don\'t know each other',
},
'connect': {
'ru': 'Добавить', // это не точно
'en': 'Connect',
},
'sendNow': {
'ru': 'Отправить', // исправлено
'en': 'Send now',
},
'sendWithoutNote': {
'ru': 'Отправить без заметки',
'en': 'Send without a note',
},
};
function sleep(ms) {
return new Promise(resolve => setTimeout(resolve, ms));
}
function getButtonByAriaLabel(ariaLabel) {
if (!ariaLabel) {
// Try both button and anchor tag formats
return document.querySelector(`button[aria-label$="connect"], a[aria-label$="connect"]`);
}
// Try both button and anchor tag formats
return document.querySelector(`button[aria-label="${ariaLabel}"], a[aria-label="${ariaLabel}"]`);
}
async function waitForContentAfterPaginationClick() {
let tries = 1;
while (tries <= CONFIG.MAX_RETRIES) {
let elapsed = 0;
while (elapsed < CONFIG.CONTENT_TIMEOUT) {
// Check for new Connect button format (anchor tag with aria-label containing "Invite" and "connect")
if (document.querySelectorAll('a[aria-label^="Invite"][aria-label$="connect"], a[href*="/preload/search-custom-invite/"]').length) {
return true;
}
await sleep(CONFIG.DELAY_MEDIUM);
elapsed++;
}
console.log('Page waiting timeout, retry:', tries);
tries++;
}
throw new Error('Page waiting error - content did not load after maximum retries');
}
function getInteropShadowRoot() {
const interopOutlet = document.querySelector('#interop-outlet, [data-testid="interop-shadowdom"]');
if (interopOutlet && interopOutlet.shadowRoot) {
return interopOutlet.shadowRoot;
}
return null;
}
async function waitForModal() {
const timeout = 10000; // 10 seconds timeout
const startTime = Date.now();
while (Date.now() - startTime < timeout) {
// Prioritize shadow DOM since LinkedIn uses it for modals
const shadowRoot = getInteropShadowRoot();
if (shadowRoot) {
let modalContainer = shadowRoot.querySelector('[data-test-modal-id="send-invite-modal"]');
let modalElement = shadowRoot.querySelector('#send-invite-modal');
if (modalContainer && modalContainer.getAttribute('aria-hidden') !== 'true') {
console.log('Modal container appeared in shadow DOM');
return { shadowRoot, element: modalContainer };
}
if (modalElement) {
const parentModal = modalElement.closest('[data-test-modal-id="send-invite-modal"]');
if (parentModal && parentModal.getAttribute('aria-hidden') !== 'true') {
console.log('Modal element appeared in shadow DOM');
return { shadowRoot, element: parentModal };
}
}
}
// Fallback to regular DOM
let modalContainer = document.querySelector('[data-test-modal-id="send-invite-modal"]');
let modalElement = document.querySelector('#send-invite-modal');
if (modalContainer && modalContainer.getAttribute('aria-hidden') !== 'true') {
console.log('Modal container appeared in regular DOM');
return modalContainer;
}
if (modalElement) {
const parentModal = modalElement.closest('[data-test-modal-id="send-invite-modal"]');
if (parentModal && parentModal.getAttribute('aria-hidden') !== 'true') {
console.log('Modal element appeared in regular DOM');
return parentModal;
}
}
await sleep(CONFIG.DELAY_SHORT);
}
console.warn('Modal did not appear within timeout');
return null;
}
function findElementInShadowDOM(root, selector) {
// Try to find element in regular DOM first
let element = root.querySelector(selector);
if (element) return element;
// If not found, check shadow DOM
const allElements = root.querySelectorAll('*');
for (const el of allElements) {
if (el.shadowRoot) {
element = el.shadowRoot.querySelector(selector);
if (element) return element;
// Recursively check nested shadow roots
const nested = findElementInShadowDOM(el.shadowRoot, selector);
if (nested) return nested;
}
}
return null;
}
async function waitForSendButtonInModal(modalContainer) {
const timeout = 10000; // 10 seconds timeout
const startTime = Date.now();
while (Date.now() - startTime < timeout) {
let btnSend = null;
// Check if modalContainer is an object with shadowRoot (from shadow DOM)
if (modalContainer && typeof modalContainer === 'object' && modalContainer.shadowRoot) {
const shadowRoot = modalContainer.shadowRoot;
const modalElement = modalContainer.element;
// Search within shadow DOM modal
const searchRoot = modalElement || shadowRoot;
btnSend = searchRoot.querySelector(`button[aria-label="${languageMap.sendWithoutNote[currentLanguage]}"]`) ||
searchRoot.querySelector('button[aria-label="Send without a note"]') ||
searchRoot.querySelector('button[aria-label*="Send without"]') ||
searchRoot.querySelector('.artdeco-modal__actionbar .artdeco-button.artdeco-button--primary') ||
searchRoot.querySelector('#artdeco-modal-outlet button[aria-label*="Send without"]') ||
searchRoot.querySelector('button[aria-label*="Send"], button[aria-label*="Отправить"]');
} else {
// Regular DOM search
const searchRoot = modalContainer || document;
// Try multiple selectors for send button inside modal
// First try within artdeco-modal-outlet
const modalOutlet = document.querySelector('#artdeco-modal-outlet');
const searchInOutlet = modalOutlet || searchRoot;
btnSend = searchInOutlet.querySelector(`button[aria-label="${languageMap.sendWithoutNote[currentLanguage]}"]`) ||
searchInOutlet.querySelector('button[aria-label="Send without a note"]') ||
searchInOutlet.querySelector('button[aria-label*="Send without"]') ||
searchInOutlet.querySelector('.artdeco-modal__actionbar .artdeco-button.artdeco-button--primary') ||
searchInOutlet.querySelector('button[aria-label*="Send"], button[aria-label*="Отправить"]');
// If not found, try searching in shadow DOM
if (!btnSend) {
const shadowRoot = getInteropShadowRoot();
if (shadowRoot) {
btnSend = shadowRoot.querySelector(`button[aria-label="${languageMap.sendWithoutNote[currentLanguage]}"]`) ||
shadowRoot.querySelector('button[aria-label="Send without a note"]') ||
shadowRoot.querySelector('button[aria-label*="Send without"]') ||
shadowRoot.querySelector('.artdeco-modal__actionbar .artdeco-button.artdeco-button--primary') ||
shadowRoot.querySelector('#artdeco-modal-outlet button[aria-label*="Send without"]');
}
}
}
if (btnSend) {
// Check if button is visible and not disabled
const isVisible = btnSend.offsetParent !== null;
const isDisabled = btnSend.disabled || btnSend.getAttribute('aria-disabled') === 'true';
if (isVisible && !isDisabled) {
console.log('Send button found in modal and is clickable', btnSend);
return btnSend;
}
}
await sleep(CONFIG.DELAY_SHORT);
}
console.warn('Send button did not appear in modal within timeout');
return null;
}
async function sendPollResponseIfActiveModal() {
const pollModal = document.querySelector('#send-invite-modal, [data-test-modal-id="send-invite-modal"]');
if (!pollModal) return;
console.log('pollModal detected', pollModal);
// Check if this is the "How do you know" modal (old format) or "Add a note" modal (new format)
const modalText = pollModal.textContent || pollModal.innerText || '';
const howKnowText = languageMap.howknow[currentLanguage];
const isHowKnowModal = howKnowText && modalText.includes(howKnowText);
// If it's the "How do you know" modal, handle it
if (isHowKnowModal) {
try {
const btnDontKnowEachOther = document.querySelector(`button[aria-label="${languageMap.dontknow[currentLanguage]}"]`);
if (btnDontKnowEachOther) {
btnDontKnowEachOther.click();
await sleep(CONFIG.DELAY_SHORT);
}
const btnConnect = getButtonByAriaLabel();
if (btnConnect) {
btnConnect.click();
await sleep(CONFIG.DELAY_SHORT);
}
const getNewButtonConnect = getButtonByAriaLabel(languageMap.connect[currentLanguage]);
if (getNewButtonConnect) {
getNewButtonConnect.click();
await sleep(CONFIG.DELAY_SHORT);
}
const getSendNow = getButtonByAriaLabel(languageMap.sendNow[currentLanguage]);
if (getSendNow) {
getSendNow.click();
await sleep(CONFIG.DELAY_SHORT);
}
} catch (error) {
console.error('Error handling poll modal:', error);
}
}
// If it's the "Add a note" modal, the send button is already handled in clicks() function
}
async function sendGotItIfGrowingNetworkModal() {
await sleep(CONFIG.DELAY_MEDIUM);
const growingNetworkModal = document.querySelector('[data-test-modal-id="fuse-limit-alert"]');
if (!growingNetworkModal) return;
console.log('Growing network modal detected');
const btnGotIt = document.querySelector('[aria-label="Got it"]');
if (btnGotIt) {
btnGotIt.click();
await sleep(CONFIG.DELAY_SHORT);
}
}
async function waitForNextButtonAndThenClickIt() {
let elapsed = 0;
while (elapsed < CONFIG.PAGINATION_TIMEOUT) {
// Try new LinkedIn pagination selector first
let btn = document.querySelector('button[data-testid="pagination-controls-next-button-visible"]');
// Fallback to old selector for backward compatibility
if (!btn) {
btn = document.querySelector('.artdeco-pagination__button.artdeco-pagination__button--next');
}
// Also check shadow DOM if needed
if (!btn) {
const shadowRoot = getInteropShadowRoot();
if (shadowRoot) {
btn = shadowRoot.querySelector('button[data-testid="pagination-controls-next-button-visible"]');
}
}
if (btn) {
console.log('Clicking next page button');
btn.scrollIntoView({ behavior: 'smooth', block: 'center' });
await sleep(CONFIG.DELAY_SHORT);
btn.click();
return true;
}
await sleep(CONFIG.DELAY_MEDIUM);
elapsed++;
}
throw new Error('Pagination timeout - next button not found');
}
async function clicks() {
// Updated selector for new LinkedIn markup: Connect buttons are now anchor tags
// Match buttons with aria-label starting with "Invite" and ending with "connect"
// Also match by href pattern as fallback
const btns = document.querySelectorAll(`a[aria-label^="Invite"][aria-label$="connect"]:not([aria-disabled="true"]), a[href*="/preload/search-custom-invite/"]:not([aria-disabled="true"])`);
for (const btn of btns) {
// Check if we hit the weekly limit
if (document.querySelector('.ip-fuse-limit-alert__warning')) {
throw new Error('LIMIT');
}
await sleep(CONFIG.DELAY_MEDIUM);
try {
btn.scrollIntoView({ behavior: 'smooth', block: 'center' });
btn.click();
console.log('Clicked connect button for:', btn);
// Wait for modal to appear before trying to click send button
const modalContainer = await waitForModal();
if (modalContainer) {
// Wait for the send button to appear inside the modal
const btnSend = await waitForSendButtonInModal(modalContainer);
if (btnSend) {
// Scroll button into view and add small delay before clicking
btnSend.scrollIntoView({ behavior: 'smooth', block: 'center' });
await sleep(CONFIG.DELAY_SHORT);
// Try multiple click methods
try {
btnSend.click();
console.log('Clicked send button inside modal');
} catch (e) {
// Fallback: dispatch click event
console.log('Using fallback click method');
const clickEvent = new MouseEvent('click', {
bubbles: true,
cancelable: true,
view: window
});
btnSend.dispatchEvent(clickEvent);
}
await sleep(CONFIG.DELAY_SHORT);
await sendPollResponseIfActiveModal();
await sendGotItIfGrowingNetworkModal();
} else {
console.warn('Send button not found in modal');
}
} else {
console.warn('Modal did not appear, skipping send button click');
}
} catch (error) {
console.error('Error processing button:', error);
// Continue with next button instead of breaking
}
}
console.log('Done processing all buttons on page');
}
async function scrollToBottom() {
window.scrollTo({
top: document.body.scrollHeight,
behavior: 'smooth'
});
// Wait for scroll to complete
await sleep(CONFIG.DELAY_MEDIUM);
}
async function navigation() {
try {
while (true) {
await clicks();
console.log('PAGE COMPLETE');
await sleep(CONFIG.DELAY_LONG);
await scrollToBottom();
console.log('SCROLLED TO BOTTOM');
await sleep(CONFIG.DELAY_LONG);
await waitForNextButtonAndThenClickIt();
await waitForContentAfterPaginationClick();
}
} catch (error) {
if (error.message === 'LIMIT') {
console.log('⛔ WEEKLY LIMIT REACHED - Stopping automation');
alert('Weekly limit reached. The automation has stopped.');
} else {
console.error('❌ Error in navigation:', error);
throw error;
}
}
}
// Start the automation
console.log('🤖 Starting LinkedIn connection automation...');
navigation();
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment