LinkedIn Auto-Apply

Strict separation of concerns, minimalist UI, modular automation

You will need to install an extension such as Tampermonkey, Greasemonkey or Violentmonkey to install this script.

You will need to install an extension such as Tampermonkey to install this script.

You will need to install an extension such as Tampermonkey or Violentmonkey to install this script.

You will need to install an extension such as Tampermonkey or Userscripts to install this script.

You will need to install an extension such as Tampermonkey to install this script.

You will need to install a user script manager extension to install this script.

(У мене вже є менеджер скриптів, дайте мені встановити його!)

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

(I already have a user style manager, let me install it!)

// ==UserScript==
// @name         LinkedIn Auto-Apply
// @namespace    http://tampermonkey.net/
// @version      21.0
// @license      MIT
// @description  Strict separation of concerns, minimalist UI, modular automation
// @author       Roshan Kumar
// @match        *://*.linkedin.com/*
// @grant        GM_setValue
// @grant        GM_getValue
// ==/UserScript==

(function() {
    'use strict';

    const State = {
        url: GM_getValue('url', 'https://www.linkedin.com/jobs/search/?f_AL=true&keywords=Full%20Stack'),
        workplaceType: GM_getValue('workplaceType', '2'),
        email: GM_getValue('email', ''),
        phone: GM_getValue('phone', ''),
        country: GM_getValue('country', ''),
        exp: GM_getValue('exp', '3'),
        currentCtc: GM_getValue('currentCtc', '12.0'),
        expectedCtc: GM_getValue('expectedCtc', '18.0'),
        onsite: GM_getValue('onsite', 'Yes'),
        immediateStart: GM_getValue('immediateStart', 'Yes'),
        bachelors: GM_getValue('bachelors', 'Yes'),
        academicProject: GM_getValue('academicProject', 'No'),
        skillRating: GM_getValue('skillRating', '8'),
        include: GM_getValue('include', ''),
        exclude: GM_getValue('exclude', ''),
        running: GM_getValue('running', false)
    };

    const Storage = {
        save: (data) => {
            Object.keys(data).forEach(k => GM_setValue(k, data[k]));
            Object.assign(State, data);
        },
        setRunning: (val) => {
            GM_setValue('running', val);
            State.running = val;
        }
    };

    const UI = {
        container: null,
        panel: null,
        tabHeader: null,
        tabBody: null,
        toggleBtn: null,
        startBtn: null,
        stopBtn: null,
        inputs: {},

        init: () => {
            UI.container = document.createElement('div');
            UI.container.style.cssText = 'position:fixed;bottom:20px;right:20px;z-index:2147483647;font-family:monospace;font-size:12px;display:flex;flex-direction:column;align-items:flex-end;gap:10px;pointer-events:none;';
            document.documentElement.appendChild(UI.container);

            UI.panel = document.createElement('div');
            UI.panel.style.cssText = 'background:#111;color:#eee;padding:15px;width:400px;display:none;flex-direction:column;gap:10px;pointer-events:auto;box-shadow:0 4px 12px rgba(0,0,0,0.5);max-height:85vh;overflow:hidden;border:1px solid #333;';

            const style = document.createElement('style');
            style.textContent = `
                .rk-scroll::-webkit-scrollbar { width: 4px; }
                .rk-scroll::-webkit-scrollbar-track { background: transparent; }
                .rk-scroll::-webkit-scrollbar-thumb { background: #444; }
                .rk-tab-btn { background: transparent; color: #666; border: none; padding: 8px 12px; cursor: pointer; font-family: monospace; font-size: 11px; font-weight: bold; text-transform: uppercase; border-bottom: 2px solid transparent; transition: all 0.2s; white-space: nowrap; }
                .rk-tab-btn.active { color: #00ff00; border-bottom: 2px solid #00ff00; }
                .rk-tab-btn:hover:not(.active) { color: #aaa; }
                .rk-tab-content { display: none; flex-direction: column; gap: 12px; padding: 10px 5px; }
                .rk-tab-content.active { display: flex; }
                .rk-field-wrapper { display: flex; flex-direction: column; gap: 4px; }
                .rk-input { background: #1a1a1a !important; border: 1px solid #333 !important; color: #eee !important; padding: 8px !important; outline: none !important; font-family: monospace !important; width: 100% !important; transition: border 0.2s !important; box-sizing: border-box !important; appearance: auto !important; }
                .rk-input:focus { border-color: #00ff00 !important; }
                .rk-label { color: #888; font-size: 10px; text-transform: uppercase; letter-spacing: 1px; }
                option { background: #111; color: #eee; }
            `;
            document.head.appendChild(style);
            UI.container.appendChild(UI.panel);

            UI.tabHeader = document.createElement('div');
            UI.tabHeader.style.cssText = 'display:flex;gap:5px;border-bottom:1px solid #333;overflow-x:auto;padding-bottom:5px;scrollbar-width: none;';
            UI.tabHeader.className = 'rk-scroll';
            UI.panel.appendChild(UI.tabHeader);

            UI.tabBody = document.createElement('div');
            UI.tabBody.style.cssText = 'overflow-y:auto;flex:1;min-height: 300px;';
            UI.tabBody.className = 'rk-scroll';
            UI.panel.appendChild(UI.tabBody);

            const tabSearch = UI.createTab('search', 'Engine', true);
            UI.createField('url', 'Target Search URL', State.url, tabSearch);
            UI.createSelect('workplaceType', 'Workplace Type', [{label:'Remote',value:'2'},{label:'Onsite',value:'1'},{label:'Hybrid',value:'3'},{label:'Any',value:''}], State.workplaceType, tabSearch);
            UI.createField('include', 'Must Include Keywords', State.include, tabSearch);
            UI.createField('exclude', 'Strict Exclude Keywords', State.exclude, tabSearch);

            const tabProfile = UI.createTab('profile', 'Identity', false);
            UI.createField('email', 'Email Address', State.email, tabProfile);
            UI.createField('phone', 'Mobile (+Code)', State.phone, tabProfile);
            UI.createField('country', 'Country Name', State.country, tabProfile);
            UI.createField('exp', 'General Experience (Yrs)', State.exp, tabProfile);

            const tabJob = UI.createTab('job', 'Salary', false);
            UI.createField('currentCtc', 'Current CTC', State.currentCtc, tabJob);
            UI.createField('expectedCtc', 'Expected CTC', State.expectedCtc, tabJob);
            UI.createField('skillRating', 'Skill Rating (Scale 1-10)', State.skillRating, tabJob);

            const tabQs = UI.createTab('questions', 'Logic', false);
            UI.createField('onsite', 'Comfortable Onsite?', State.onsite, tabQs);
            UI.createField('immediateStart', 'Immediate Start?', State.immediateStart, tabQs);
            UI.createField('bachelors', 'Degree Completed?', State.bachelors, tabQs);
            UI.createField('academicProject', 'Internship/College Req?', State.academicProject, tabQs);

            const btnWrapper = document.createElement('div');
            btnWrapper.style.cssText = 'display:flex;gap:10px;margin-top:10px;border-top:1px solid #333;padding-top:10px;';
            UI.panel.appendChild(btnWrapper);

            UI.startBtn = UI.createButton('INITIATE', '#eee', '#111');
            UI.stopBtn = UI.createButton('TERMINATE', '#ff4444', '#111');
            UI.stopBtn.style.display = 'none';
            btnWrapper.appendChild(UI.startBtn);
            btnWrapper.appendChild(UI.stopBtn);

            UI.toggleBtn = document.createElement('button');
            UI.toggleBtn.innerText = 'CMD';
            UI.toggleBtn.style.cssText = 'background:#111;color:#eee;border:1px solid #333;padding:12px 24px;cursor:pointer;pointer-events:auto;font-family:monospace;font-weight:bold;letter-spacing:2px;';
            UI.container.appendChild(UI.toggleBtn);

            UI.bindEvents();

            if (State.running) {
                UI.setRunningState(true);
                Engine.start(false);
            }
        },

        createTab: (id, title, isActive) => {
            const btn = document.createElement('button');
            btn.innerText = title;
            btn.className = `rk-tab-btn ${isActive ? 'active' : ''}`;

            const content = document.createElement('div');
            content.className = `rk-tab-content ${isActive ? 'active' : ''}`;

            btn.addEventListener('click', () => {
                UI.tabHeader.querySelectorAll('.rk-tab-btn').forEach(b => b.classList.remove('active'));
                UI.tabBody.querySelectorAll('.rk-tab-content').forEach(c => c.classList.remove('active'));
                btn.classList.add('active');
                content.classList.add('active');
            });

            UI.tabHeader.appendChild(btn);
            UI.tabBody.appendChild(content);
            return content;
        },

        createField: (id, labelText, val, parent) => {
            const wrapper = document.createElement('div');
            wrapper.className = 'rk-field-wrapper';
            const label = document.createElement('label');
            label.innerText = labelText;
            label.className = 'rk-label';
            const input = document.createElement('input');
            input.value = val;
            input.className = 'rk-input';
            UI.inputs[id] = input;
            wrapper.appendChild(label);
            wrapper.appendChild(input);
            parent.appendChild(wrapper);
        },

        createSelect: (id, labelText, options, selectedValue, parent) => {
            const wrapper = document.createElement('div');
            wrapper.className = 'rk-field-wrapper';
            const label = document.createElement('label');
            label.innerText = labelText;
            label.className = 'rk-label';
            const select = document.createElement('select');
            select.className = 'rk-input';

            options.forEach(opt => {
                const optionEl = document.createElement('option');
                optionEl.value = opt.value;
                optionEl.innerText = opt.label;
                if (opt.value === selectedValue) optionEl.selected = true;
                select.appendChild(optionEl);
            });

            UI.inputs[id] = select;
            wrapper.appendChild(label);
            wrapper.appendChild(select);
            parent.appendChild(wrapper);
        },

        createButton: (text, bg, color) => {
            const btn = document.createElement('button');
            btn.innerText = text;
            btn.style.cssText = `background:${bg};color:${color};border:none;padding:12px;cursor:pointer;flex:1;font-weight:bold;font-family:monospace;letter-spacing:1px;`;
            return btn;
        },

        bindEvents: () => {
            UI.toggleBtn.addEventListener('click', () => {
                UI.panel.style.display = UI.panel.style.display === 'none' ? 'flex' : 'none';
            });

            UI.startBtn.addEventListener('click', () => {
                const data = {};
                for (const key in UI.inputs) {
                    data[key] = UI.inputs[key].value;
                }
                Storage.save(data);
                Storage.setRunning(true);
                UI.setRunningState(true);
                Engine.start(true);
            });

            UI.stopBtn.addEventListener('click', () => {
                Storage.setRunning(false);
                UI.setRunningState(false);
                Engine.stop();
            });
        },

        setRunningState: (isRunning) => {
            UI.startBtn.style.display = isRunning ? 'none' : 'block';
            UI.stopBtn.style.display = isRunning ? 'block' : 'none';
            UI.toggleBtn.style.borderColor = isRunning ? '#00ff00' : '#333';
            UI.toggleBtn.style.color = isRunning ? '#00ff00' : '#eee';
            UI.toggleBtn.innerText = isRunning ? 'RUN' : 'CMD';
        }
    };

    const Engine = {
        running: false,

        sleep: (ms) => new Promise(r => setTimeout(r, ms)),

        start: async (isManualClick) => {
            if (isManualClick) {
                let finalUrl = State.url;
                try {
                    const urlObj = new URL(finalUrl);
                    if (State.workplaceType) urlObj.searchParams.set('f_WT', State.workplaceType);
                    else urlObj.searchParams.delete('f_WT');
                    urlObj.searchParams.set('f_TPR', 'r604800');
                    finalUrl = urlObj.toString();
                } catch(e) {}

                window.location.href = finalUrl;
                return;
            }

            Engine.running = true;
            await Engine.sleep(2000);
            await Engine.loop();
        },

        stop: () => {
            Engine.running = false;
        },

        loop: async () => {
            let processedCount = 0;
            let stuckCounter = 0;

            while (Engine.running) {
                let cards = Helpers.getJobCards();

                if (!cards || cards.length === processedCount) {
                    const leftPane = document.querySelector('.jobs-search-results-list');
                    if (leftPane) {
                        leftPane.scrollTo(0, leftPane.scrollHeight);
                        await Engine.sleep(2000);
                        cards = Helpers.getJobCards();
                    }
                }

                if (!cards || cards.length === processedCount) {
                    stuckCounter++;
                    if (stuckCounter > 3) {
                        const nextBtn = Helpers.getPaginationNext();
                        if (nextBtn) {
                            nextBtn.click();
                            processedCount = 0;
                            stuckCounter = 0;
                            await Engine.sleep(4000);
                            continue;
                        } else {
                            UI.stopBtn.click();
                            break;
                        }
                    }
                } else {
                    stuckCounter = 0;
                }

                for (let i = processedCount; i < cards.length; i++) {
                    if (!Engine.running) break;

                    const card = cards[i];
                    const title = Helpers.getJobTitle(card);

                    processedCount++;

                    if (!Helpers.passesFilters(title, State.include, State.exclude)) continue;

                    Helpers.clickJobCard(card);
                    await Engine.sleep(2500);

                    const applyBtn = Helpers.getEasyApplyButton();
                    if (applyBtn) {
                        applyBtn.click();
                        await Engine.processModal();
                    }
                }
            }
        },

        processModal: async () => {
            let isModalActive = true;

            while (isModalActive && Engine.running) {
                await Engine.sleep(1500);
                Helpers.fillFormFields(State);

                const actions = Helpers.getModalActions();

                if (actions.submit) {
                    actions.submit.click();
                    await Engine.sleep(3000);
                    Helpers.closePostSubmitPopup();
                    isModalActive = false;
                } else if (actions.review) {
                    actions.review.click();
                } else if (actions.next) {
                    actions.next.click();
                } else {
                    if (!Helpers.isModalPresent()) {
                        isModalActive = false;
                    }
                }
            }
        }
    };

    const Helpers = {
        passesFilters: (title, includeStr, excludeStr) => {
            if (!title) return true;
            const t = title.toLowerCase();
            const inc = includeStr.split(',').map(w => w.trim().toLowerCase()).filter(Boolean);
            const exc = excludeStr.split(',').map(w => w.trim().toLowerCase()).filter(Boolean);

            for (let word of exc) {
                if (t.includes(word)) return false;
            }
            if (inc.length === 0) return true;
            for (let word of inc) {
                if (t.includes(word)) return true;
            }
            return false;
        },

        getJobCards: () => {
            return document.querySelectorAll('div[data-job-id], .job-card-container, .scaffold-layout__list-item');
        },

        getJobTitle: (cardElement) => {
            const titleEl = cardElement.querySelector('.job-card-list__title, .artdeco-entity-lockup__title, strong, a');
            return titleEl ? titleEl.innerText : '';
        },

        clickJobCard: (cardElement) => {
            cardElement.scrollIntoView({ behavior: 'smooth', block: 'center' });
            const clickable = cardElement.querySelector('a, button, [role="button"]') || cardElement;
            clickable.click();
        },

        getEasyApplyButton: () => {
            return document.querySelector('.jobs-apply-button--top-card button, button[aria-label*="Easy Apply"], button[aria-label*="Apply to this job"]');
        },

        setSelect: (el, text) => {
            if (!text) return;
            Array.from(el.options).forEach((opt, i) => {
                if (opt.innerText.toLowerCase().includes(text.toLowerCase()) || opt.value.toLowerCase().includes(text.toLowerCase())) {
                    el.selectedIndex = i;
                    el.dispatchEvent(new Event('change', { bubbles: true }));
                }
            });
        },

        fillFormFields: (stateObj) => {
            const groups = document.querySelectorAll('.fb-dash-form-element, .jobs-easy-apply-form-element, .artdeco-text-input--container, fieldset');

            groups.forEach(g => {
                const labelEl = g.querySelector('label, legend');
                const label = labelEl ? labelEl.innerText.toLowerCase() : '';

                const input = g.querySelector('input[type="text"], input[type="tel"], input[type="number"]');
                if (input) {
                    if ((label.includes('phone') || label.includes('mobile')) && !input.value) {
                        input.value = stateObj.phone;
                        input.dispatchEvent(new Event('input', { bubbles: true }));
                    } else if (label.includes('current ctc') && !input.value) {
                        input.value = stateObj.currentCtc;
                        input.dispatchEvent(new Event('input', { bubbles: true }));
                    } else if (label.includes('expected ctc') && !input.value) {
                        input.value = stateObj.expectedCtc;
                        input.dispatchEvent(new Event('input', { bubbles: true }));
                    } else if ((label.includes('experience') || label.includes('years')) && !input.value) {
                        input.value = stateObj.exp;
                        input.dispatchEvent(new Event('input', { bubbles: true }));
                    } else if (label.includes('rate') || label.includes('scale')) {
                        if (!input.value) {
                            input.value = stateObj.skillRating;
                            input.dispatchEvent(new Event('input', { bubbles: true }));
                        }
                    }
                }

                const select = g.querySelector('select');
                if (select) {
                    if (label.includes('email')) Helpers.setSelect(select, stateObj.email);
                    if (label.includes('country code') || label.includes('country')) Helpers.setSelect(select, stateObj.country);
                    if (label.includes('internship') || label.includes('academic') || label.includes('college')) Helpers.setSelect(select, stateObj.academicProject);
                }

                if (label.includes('sponsorship') || label.includes('authorized') || label.includes('citizenship') || label.includes('clearance') || label.includes('onsite') || label.includes('immediately') || label.includes('urgently') || label.includes('bachelor') || label.includes('degree')) {
                    let target = 'Yes';

                    if (label.includes('require') || label.includes('sponsor')) target = 'No';
                    if (label.includes('onsite') && stateObj.onsite.toLowerCase() === 'no') target = 'No';
                    if ((label.includes('immediately') || label.includes('urgently')) && stateObj.immediateStart.toLowerCase() === 'no') target = 'No';
                    if ((label.includes('bachelor') || label.includes('degree')) && stateObj.bachelors.toLowerCase() === 'no') target = 'No';

                    const rad = g.querySelector(`input[value="${target}"], input[id*="${target}"], input[data-test-text-selectable-option__input="${target}"]`);
                    if (rad && !rad.checked) {
                        rad.click();
                        rad.dispatchEvent(new Event('change', { bubbles: true }));
                    }
                }
            });
        },

        getModalActions: () => {
            return {
                next: document.querySelector('button[data-easy-apply-next-button], button[aria-label="Continue to next step"]'),
                review: document.querySelector('button[aria-label="Review your application"]'),
                submit: document.querySelector('button[aria-label="Submit application"]')
            };
        },

        closePostSubmitPopup: () => {
            const closeBtn = document.querySelector('button[aria-label="Dismiss"], button[data-test-modal-close-btn]');
            if (closeBtn) closeBtn.click();
        },

        isModalPresent: () => {
            return !!document.querySelector('.jobs-easy-apply-modal, .artdeco-modal');
        },

        getPaginationNext: () => {
            return document.querySelector('button[aria-label="View next page"], button.artdeco-pagination__button--next, button[data-testid="pagination-controls-next-button-visible"]');
        }
    };

    if (document.readyState === 'loading') {
        document.addEventListener('DOMContentLoaded', UI.init);
    } else {
        UI.init();
    }

})();