LinkedIn Auto-Apply

Strict separation of concerns, minimalist UI, modular automation

Tendrás que instalar una extensión para tu navegador como Tampermonkey, Greasemonkey o Violentmonkey si quieres utilizar este script.

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

Tendrás que instalar una extensión como Tampermonkey o Violentmonkey para instalar este script.

Necesitarás instalar una extensión como Tampermonkey o Userscripts para instalar este script.

Tendrás que instalar una extensión como Tampermonkey antes de poder instalar este script.

Necesitarás instalar una extensión para administrar scripts de usuario si quieres instalar este script.

(Ya tengo un administrador de scripts de usuario, déjame instalarlo)

Tendrás que instalar una extensión como Stylus antes de poder instalar este script.

Tendrás que instalar una extensión como Stylus antes de poder instalar este script.

Tendrás que instalar una extensión como Stylus antes de poder instalar este script.

Para poder instalar esto tendrás que instalar primero una extensión de estilos de usuario.

Para poder instalar esto tendrás que instalar primero una extensión de estilos de usuario.

Para poder instalar esto tendrás que instalar primero una extensión de estilos de usuario.

(Ya tengo un administrador de estilos de usuario, déjame instalarlo)

// ==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();
    }

})();