Last active
November 18, 2025 13:14
-
-
Save shmidtelson/ebd807e2cf64069ee96d36f7d4894f53 to your computer and use it in GitHub Desktop.
Linkedin invite sender
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
| /** | |
| @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