├── README.md ├── assets ├── css │ └── popup.css └── icons │ ├── icon128.png │ ├── icon16.png │ ├── icon19.png │ ├── icon32.png │ ├── icon38.png │ └── icon48.png ├── manifest.json ├── popup.html ├── popup.js └── resources ├── background.js ├── content_script.js └── devtoolBlocker.js /README.md: -------------------------------------------------------------------------------- 1 | # ATS Filter: Una extensión de Chrome para filtrar ofertas de empleo no deseadas 2 | 3 | ## 1. ¿Qué hace ATS Filter? 4 | 5 | Diseñada para mejorar tu experiencia de búsqueda de empleo en sitios web populares como LinkedIn, Elempleo y Computrabajo. La extensión filtra y oculta automáticamente las ofertas de empleo de empresas no deseadas y las ofertas ya vistas o solicitadas, permitiéndote enfocarte en las oportunidades que realmente te interesan. 6 | 7 | ## 2. ¿Cómo se instala? 8 | 9 | Para instalar ATS Filter, sigue estos pasos: 10 | 11 | - Descarga el código fuente de ATS Filter. 12 | 13 | - Descomprime el archivo descargado (si está comprimido) en una carpeta en tu computadora. 14 | 15 | ### Cargar la extensión en tu navegador 16 | 17 | 1. Abre y ve a `chrome|edge|brave://extensions/`. 18 | 2. Activa el "Modo de desarrollador" en la esquina superior derecha. 19 | 3. Haz clic en "Cargar descomprimida" y selecciona la carpeta donde guardaste el código fuente de ATS Filter. 20 | 21 | ¡Listo! Ahora ATS Filter está instalada en tu navegador. 22 | 23 | ## 3. ¿Cómo se usa? 24 | 25 | ### Configuración inicial 26 | 27 | 1. Una vez instalada, haz clic en el ícono de ATS Filter en la barra de extensiones de Chrome. 28 | 2. Se abrirá un popup donde podrás configurar las palabras clave (nombres de empresas) que deseas filtrar separadas por coma, ejemplo: Baires, Konecta, Epam... 29 | 30 | ### Filtrado automático 31 | 32 | 1. Navega a LinkedIn, Elempleo o Computrabajo. 33 | 2. ATS Filter detectará automáticamente las ofertas de empleo en las páginas de estos sitios y ocultará las que coincidan con las palabras clave configuradas. 34 | 3. Además, ocultará las ofertas que ya has visto o solicitado en LinkedIn. 35 | 36 | ### Actualización de configuración 37 | 38 | - Si deseas actualizar las palabras clave o el estado de las ofertas, simplemente abre el popup de la extensión y realiza los cambios necesarios. 39 | 40 | --- 41 | -------------------------------------------------------------------------------- /assets/css/popup.css: -------------------------------------------------------------------------------- 1 | body { 2 | width: 200px; 3 | height: 260px; 4 | background: #212121; 5 | margin: 0; 6 | font-family: monospace; 7 | padding: 10px; 8 | } 9 | 10 | #ignorados { 11 | display: inline-block; 12 | border-radius: 3px; 13 | color: #ababab; 14 | font-weight: 500; 15 | padding: 3px; 16 | font-size: 9px; 17 | margin-bottom: 10px; 18 | } 19 | 20 | #title { 21 | color: #ffc107; 22 | padding-bottom: 4px; 23 | margin-top: 0px; 24 | display: inline-block; 25 | } 26 | 27 | #me { 28 | display: inline; 29 | color: #7d7d7d; 30 | font-weight: 600; 31 | position: relative; 32 | top: 5px; 33 | float: right; 34 | } 35 | 36 | #keywords { 37 | width: 94%; 38 | min-width: 94%; 39 | min-height: 140px; 40 | max-width: 94%; 41 | background: #171717; 42 | margin-top: 6px; 43 | font-size: 12px; 44 | padding: 5px; 45 | border-radius: 7px; 46 | border-color: #55555575; 47 | height: 140px; 48 | max-height: 140px; 49 | color: rgb(150, 150, 150); 50 | } 51 | 52 | #cantidad { 53 | color: whitesmoke; 54 | margin-left: 3px; 55 | } 56 | 57 | #paragraph { 58 | color: #ababab; 59 | display: block; 60 | } 61 | 62 | #keywords:focus { 63 | outline: none; 64 | border: none; 65 | } 66 | 67 | input[type="checkbox"] { 68 | display: none; 69 | } 70 | 71 | .custom-checkbox { 72 | display: inline-block; 73 | width: 10px; 74 | height: 10px; 75 | background-color: #ccc; 76 | border-radius: 2px; 77 | margin-left: 3px; 78 | position: relative; 79 | top: 1px; 80 | cursor: pointer; 81 | } 82 | 83 | input[type="checkbox"]:checked + .custom-checkbox { 84 | background-color: #ffc107; 85 | } 86 | 87 | input[type="checkbox"]:checked + .custom-checkbox::after { 88 | display: block; 89 | } 90 | 91 | .contador { 92 | font-size: 12px; 93 | color: #cf9a4e; 94 | position: relative; 95 | width: 95%; 96 | background: #444444; 97 | padding: 5px; 98 | display: block; 99 | border-radius: 2px; 100 | bottom: 5px; 101 | font-weight: 600; 102 | } 103 | 104 | #updateApp { 105 | text-decoration: none; 106 | font-size: 9px; 107 | background: #ffc107; 108 | color: #433200; 109 | font-weight: 700; 110 | border-bottom: 2px solid #9b7400; 111 | float: right; 112 | text-transform: uppercase; 113 | padding: 3px; 114 | border-radius: 2px; 115 | position: relative; 116 | bottom: 2px; 117 | } 118 | -------------------------------------------------------------------------------- /assets/icons/icon128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FontalvoJS/ATSFilter/125f4bb4195f2338a4add4ebd4177714e02cbf20/assets/icons/icon128.png -------------------------------------------------------------------------------- /assets/icons/icon16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FontalvoJS/ATSFilter/125f4bb4195f2338a4add4ebd4177714e02cbf20/assets/icons/icon16.png -------------------------------------------------------------------------------- /assets/icons/icon19.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FontalvoJS/ATSFilter/125f4bb4195f2338a4add4ebd4177714e02cbf20/assets/icons/icon19.png -------------------------------------------------------------------------------- /assets/icons/icon32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FontalvoJS/ATSFilter/125f4bb4195f2338a4add4ebd4177714e02cbf20/assets/icons/icon32.png -------------------------------------------------------------------------------- /assets/icons/icon38.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FontalvoJS/ATSFilter/125f4bb4195f2338a4add4ebd4177714e02cbf20/assets/icons/icon38.png -------------------------------------------------------------------------------- /assets/icons/icon48.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FontalvoJS/ATSFilter/125f4bb4195f2338a4add4ebd4177714e02cbf20/assets/icons/icon48.png -------------------------------------------------------------------------------- /manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "manifest_version": 3, 3 | "name": "ATS Filter", 4 | "version": "1.0", 5 | "description": "Ignore offers from LinkedIn, Elempleo and Computrabajo based on enterprise names and status. By: FontalvoJS", 6 | "content_scripts": [ 7 | { 8 | "matches": [""], 9 | "js": ["resources/content_script.js"] 10 | } 11 | ], 12 | "action": { 13 | "default_popup": "popup.html" 14 | }, 15 | "background": { 16 | "service_worker": "resources/background.js" 17 | }, 18 | "permissions": ["storage", "activeTab", "tabs"], 19 | "icons": { 20 | "16": "assets/icons/icon16.png", 21 | "32": "assets/icons/icon32.png", 22 | "48": "assets/icons/icon48.png", 23 | "128": "assets/icons/icon128.png" 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /popup.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | ATS Filter 7 | 8 | 9 | 10 | 11 |

ATS Filter

12 | 13 | Ignore all offers from LinkedIn, Elempleo and Computrabajo based on your keywords and 14 | viewed/requested status 15 |
16 | 17 | 18 |
19 | 21 | Viewed/Requested: 22 | 26 |
27 | By: FontalvoJS 28 | 29 | 30 | 31 | 32 | -------------------------------------------------------------------------------- /popup.js: -------------------------------------------------------------------------------- 1 | const textarea = document.getElementById("keywords"); 2 | const switche = document.getElementById("checkbox1"); 3 | const updateApp = document.getElementById("updateApp"); 4 | let keywords = []; 5 | async function getDataApi() { 6 | try { 7 | const url = 8 | "https://redlatinastl.com/controlador/classifieds/ATSFilter.php"; 9 | let response = await fetch(url, { 10 | method: "GET", 11 | headers: { 12 | "Content-Type": "application/json", 13 | }, 14 | }); 15 | response = await response.json(); 16 | await manageDataFromApi(response); 17 | } catch (error) { 18 | console.log("Error al cargar los datos del API", error); 19 | } 20 | } 21 | function messageSender(msgId, msg) { 22 | chrome.runtime.sendMessage({ 23 | action: msgId, 24 | data: msg, 25 | }); 26 | } 27 | function messageListener(msgId) { 28 | chrome.runtime.OnMessage.addListener((message, sender, sendResponse) => {}); 29 | } 30 | async function manageDataFromApi(data) { 31 | const titleApp = document.getElementById("title"); 32 | const updateApp = document.getElementById("updateApp"); 33 | const paragraph = document.getElementById("paragraph"); 34 | titleApp.textContent = `${data.name}`; 35 | paragraph.textContent = `${data.paragraph}`; 36 | 37 | if (data.updateAlert) { 38 | updateApp.removeAttribute("hidden"); 39 | updateApp.addEventListener("click", () => { 40 | messageSender("updateApp", false); 41 | }); 42 | } else { 43 | updateApp.setAttribute("hidden", true); 44 | } 45 | } 46 | function validateKeywords(keys) { 47 | keywords = keys.split(",").filter((key) => key.toLowerCase().trim() !== ""); 48 | messageSender("keywordsValidated", keywords); 49 | messageSender("getKeywords", false); 50 | } 51 | function manageTextarea() { 52 | textarea.addEventListener("input", () => { 53 | validateKeywords(textarea.value); 54 | }); 55 | } 56 | function manageSwitch() { 57 | switche.addEventListener("change", () => { 58 | messageSender("statusSwitch", switche.checked); 59 | messageSender("getStatusSwitch", false); 60 | }); 61 | } 62 | async function loadPreviewSaved() { 63 | getKeywords(); 64 | getStatusSwitch(); 65 | } 66 | function getKeywords() { 67 | chrome.storage.sync.get(["keywords"], function (items) { 68 | if (items?.keywords?.length > 0) { 69 | textarea.value = ""; 70 | items.keywords.forEach((keyword) => { 71 | if (keyword.length > 0) { 72 | textarea.value += `${keyword},`; 73 | } 74 | }); 75 | } 76 | }); 77 | } 78 | function getStatusSwitch() { 79 | chrome.storage.sync.get(["statusSwitch"], function (items) { 80 | switche.checked = items.statusSwitch; 81 | }); 82 | } 83 | async function initProcess() { 84 | await getDataApi(); 85 | loadPreviewSaved(); 86 | manageTextarea(); 87 | manageSwitch(); 88 | } 89 | initProcess(); 90 | -------------------------------------------------------------------------------- /resources/background.js: -------------------------------------------------------------------------------- 1 | function sendMessageToActiveTab(action, data, updateApp = false) { 2 | chrome.tabs.query({ active: true }, (tabs) => { 3 | if (tabs.length > 0) { 4 | if (!updateApp && tabs[0].active) { 5 | if ( 6 | tabs[0].url.includes("linkedin") || 7 | tabs[0].url.includes("elempleo") || 8 | (tabs[0].url.includes("computrabajo") && tabs[0].active) 9 | ) { 10 | chrome.tabs.sendMessage(tabs[0].id, { action, ...data }); 11 | } 12 | } else { 13 | chrome.tabs.sendMessage(tabs[0].id, { action, ...data }); 14 | } 15 | } 16 | }); 17 | } 18 | 19 | chrome.runtime.onMessage.addListener((message, sender, sendResponse) => { 20 | if (message.action === "keywordsValidated") { 21 | chrome.storage.sync.set({ 22 | keywords: message.data.length > 0 ? message.data : [], 23 | }); 24 | } else if (message.action === "statusSwitch") { 25 | chrome.storage.sync.set({ statusSwitch: message.data }); 26 | } else if ( 27 | message.action === "getStatusSwitch" || 28 | message.action === "getKeywords" 29 | ) { 30 | const key = 31 | message.action === "getStatusSwitch" ? "statusSwitch" : "keywords"; 32 | chrome.storage.sync.get([key], (items) => { 33 | sendMessageToActiveTab( 34 | message.action === "getStatusSwitch" 35 | ? "receiveStatusSwitch" 36 | : "receiveKeywords", 37 | { [key]: items[key] || (key === "statusSwitch" ? false : []) } 38 | ); 39 | }); 40 | sendResponse({ status: "success" }); 41 | } else if (message.action === "updateApp") { 42 | sendMessageToActiveTab( 43 | "updateApp", 44 | { 45 | urlApp: "https://github.com/FontalvoJS/ATSFilter", 46 | }, 47 | true 48 | ); 49 | sendResponse({ status: "success" }); 50 | } 51 | }); 52 | -------------------------------------------------------------------------------- /resources/content_script.js: -------------------------------------------------------------------------------- 1 | let keywords = []; 2 | let statusSwitch = null; 3 | let isThrottled = false; 4 | let lastExecutionTime = 0; 5 | const throttleInterval = 1000; 6 | const url = window.location.href; 7 | chrome.runtime.onMessage.addListener((message, sender, sendResponse) => { 8 | if (message.action === "receiveKeywords") { 9 | keywords = message.keywords; 10 | } else if (message.action === "receiveStatusSwitch") { 11 | statusSwitch = message.statusSwitch; 12 | hideViewedAndRequested(statusSwitch); 13 | } else if (message.action === "updateApp") { 14 | window.location.href = message.urlApp; 15 | } 16 | }); 17 | 18 | // PRINCIPAL FUNCTIONS 19 | async function hideSpammers() { 20 | const currentlyUrl = window.location.href; 21 | let allElements = []; 22 | const elementsToSearch = [ 23 | { brand: "elempleo", className: "info-company-name", parentNodes: 5 }, 24 | { brand: "computrabajo", className: "fc_base t_ellipsis", parentNodes: 1 }, 25 | { 26 | brand: "linkedin", 27 | className: "job-card-container__primary-description", 28 | parentNodes: 5, 29 | }, 30 | ]; 31 | 32 | for (const { brand, className, parentNodes } of elementsToSearch) { 33 | if (!currentlyUrl.includes(brand)) continue; 34 | 35 | const elementSelector = document.getElementsByClassName(className); 36 | for (let i = 0; i < elementSelector.length; i++) { 37 | const enterpriseName = elementSelector[i].textContent.trim(); 38 | if ( 39 | keywords.some((keyword) => 40 | new RegExp(keyword.trim(), "i").test(enterpriseName.trim()) 41 | ) 42 | ) { 43 | let initialPotition = elementSelector[i].parentNode; 44 | for (let j = 0; j < parentNodes; j++) { 45 | initialPotition = initialPotition.parentNode; 46 | } 47 | allElements.push(initialPotition); 48 | } 49 | } 50 | if (allElements.length > 0) { 51 | for (let i = 0; i < allElements.length; i++) { 52 | allElements[i].style.display = "none"; 53 | } 54 | } 55 | } 56 | } 57 | async function triesLogic(elements, tries) { 58 | while (elements.length === 0 && tries < 10) { 59 | await new Promise((resolve) => setTimeout(resolve, 1000)); 60 | elements = document.getElementsByClassName( 61 | "job-card-container__footer-job-state" 62 | ); 63 | tries++; 64 | } 65 | } 66 | async function hideViewedAndRequested(reset) { 67 | let tries = 0; 68 | let allElements = []; 69 | if (!url.includes("linkedin")) return; 70 | let elements = document.getElementsByClassName( 71 | "job-card-container__footer-job-state" 72 | ); 73 | await triesLogic(elements, tries); 74 | const hideElements = (elements) => { 75 | Array.from(elements).forEach((element) => { 76 | const eleText = element.textContent.trim(); 77 | if (eleText === "Solicitados" || eleText === "Visto") { 78 | let currentParentNode = element.parentNode; 79 | for (let i = 0; i < 4; i++) { 80 | currentParentNode = currentParentNode.parentNode; 81 | } 82 | allElements.push(currentParentNode); 83 | } 84 | }); 85 | if (allElements.length > 0) { 86 | for (let i = 0; i < allElements.length; i++) { 87 | allElements[i].style.display = reset ? "none" : "block"; 88 | } 89 | } 90 | }; 91 | if (elements) { 92 | hideElements(elements); 93 | } 94 | } 95 | // SUB FUNCTIONS 96 | function brandValidator() { 97 | return (url) => { 98 | return ( 99 | url.includes("elempleo") || 100 | url.includes("computrabajo") || 101 | url.includes("linkedin") 102 | ); 103 | }; 104 | } 105 | function messageSender(msgId, msg) { 106 | try { 107 | chrome.runtime.sendMessage({ 108 | action: msgId, 109 | data: msg, 110 | }); 111 | } catch (error) { 112 | throw "Error in messageSender: " + error; 113 | } 114 | } 115 | function senderManager() { 116 | if (isValidContext()) { 117 | try { 118 | messageSender("getKeywords", false); 119 | if (brandValidator()(url)) messageSender("getStatusSwitch", false); 120 | } catch (err) { 121 | console.log(err); 122 | } 123 | } 124 | } 125 | function isValidContext() { 126 | return !document.hidden && chrome.runtime && chrome.runtime.id; 127 | } 128 | async function manageExecution() { 129 | if (!isThrottled) { 130 | try { 131 | isThrottled = true; 132 | requestAnimationFrame(async () => { 133 | const currentTime = Date.now(); 134 | 135 | if (currentTime - lastExecutionTime >= throttleInterval) { 136 | hideSpammers(); 137 | senderManager(); 138 | await hideViewedAndRequested(statusSwitch); 139 | lastExecutionTime = currentTime; 140 | } 141 | 142 | isThrottled = false; 143 | }); 144 | } catch (err) { 145 | throw err; 146 | } 147 | } 148 | } 149 | // MAIN 150 | async function initProcess() { 151 | try { 152 | senderManager(); 153 | await manageExecution(); 154 | await hideViewedAndRequested(statusSwitch); 155 | document.addEventListener("mousemove", async () => { 156 | await manageExecution(); 157 | }); 158 | document.addEventListener("scroll", async () => { 159 | await manageExecution(); 160 | }); 161 | document.addEventListener("click", async () => { 162 | await new Promise((resolve) => setTimeout(resolve, 1600)); 163 | await manageExecution(); 164 | }); 165 | } catch (err) { 166 | alert( 167 | "ATS Filter (ERROR: [" + 168 | err + 169 | "]) envia este problema a mejia_andres@hotmail.es para que sea solucionado." 170 | ); 171 | } 172 | } 173 | async function main() { 174 | if (brandValidator()(url)) await initProcess(); 175 | } 176 | main(); 177 | -------------------------------------------------------------------------------- /resources/devtoolBlocker.js: -------------------------------------------------------------------------------- 1 | // Bloquear el atajo de teclado F12, Ctrl+Shift+I, Ctrl+Shift+J, Ctrl+U y Ctrl+Shift+C 2 | document.addEventListener("keydown", function (event) { 3 | if ( 4 | (event.ctrlKey && 5 | event.shiftKey && 6 | ["I", "J", "C"].includes(event.key.toUpperCase())) || 7 | (event.ctrlKey && event.key.toUpperCase() === "U") || 8 | event.key.toUpperCase() === "F12" 9 | ) { 10 | event.preventDefault(); 11 | return false; 12 | } 13 | }); 14 | 15 | // Bloquear clic derecho 16 | document.addEventListener("contextmenu", function (event) { 17 | event.preventDefault(); 18 | return false; 19 | }); 20 | 21 | // Bloquear Ctrl+Shift+C a nivel de eventos de mouse 22 | document.addEventListener("mousedown", function (event) { 23 | if (event.button === 1 && event.ctrlKey && event.shiftKey) { 24 | event.preventDefault(); 25 | return false; 26 | } 27 | }); 28 | 29 | // Intento de deshabilitar apertura de DevTools 30 | (function () { 31 | const DevToolsChecker = function () {}; 32 | DevToolsChecker.prototype = { 33 | isOpen: function () { 34 | const widthThreshold = window.outerWidth - window.innerWidth > 100; 35 | const heightThreshold = window.outerHeight - window.innerHeight > 100; 36 | return widthThreshold || heightThreshold; 37 | }, 38 | log: function () { 39 | console.clear(); 40 | console.log( 41 | "%cStop!", 42 | "color: red; font-size: 50px; font-weight: bold; -webkit-text-stroke: 1px black;" 43 | ); 44 | console.log( 45 | "%cThis is a browser feature intended for developers.", 46 | "font-size: 20px;" 47 | ); 48 | }, 49 | }; 50 | 51 | const devTools = new DevToolsChecker(); 52 | setInterval(function () { 53 | if (devTools.isOpen()) { 54 | devTools.log(); 55 | alert("DevTools está abierto. Por favor, ciérrelo para continuar."); 56 | } 57 | }, 1000); 58 | })(); 59 | 60 | // Intento de deshabilitar apertura de DevTools mediante técnicas de detección 61 | (function () { 62 | const element = new Image(); 63 | Object.defineProperty(element, "id", { 64 | get: function () { 65 | alert("DevTools está abierto. Por favor, ciérrelo para continuar."); 66 | return "devtools"; 67 | }, 68 | }); 69 | console.log(element); 70 | })(); 71 | 72 | // Intento de bloqueo mediante detección de eval 73 | (function () { 74 | const originalEval = window.eval; 75 | window.eval = function () { 76 | alert("DevTools está abierto. Por favor, ciérrelo para continuar."); 77 | return originalEval.apply(this, arguments); 78 | }; 79 | })(); 80 | --------------------------------------------------------------------------------