LinkedIn Auto-Apply

Strict separation of concerns, minimalist UI, modular automation

Você precisará instalar uma extensão como Tampermonkey, Greasemonkey ou Violentmonkey para instalar este script.

Você precisará instalar uma extensão como Tampermonkey para instalar este script.

Você precisará instalar uma extensão como Tampermonkey ou Violentmonkey para instalar este script.

Você precisará instalar uma extensão como Tampermonkey ou Userscripts para instalar este script.

Você precisará instalar uma extensão como o Tampermonkey para instalar este script.

Você precisará instalar um gerenciador de scripts de usuário para instalar este script.

(Eu já tenho um gerenciador de scripts de usuário, me deixe instalá-lo!)

Você precisará instalar uma extensão como o Stylus para instalar este estilo.

Você precisará instalar uma extensão como o Stylus para instalar este estilo.

Você precisará instalar uma extensão como o Stylus para instalar este estilo.

Você precisará instalar um gerenciador de estilos de usuário para instalar este estilo.

Você precisará instalar um gerenciador de estilos de usuário para instalar este estilo.

Você precisará instalar um gerenciador de estilos de usuário para instalar este estilo.

(Eu já possuo um gerenciador de estilos de usuário, me deixar fazer a instalação!)

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

})();