// ==UserScript==
// @name Universal Captcha Learner — FULL (reCAPTCHA + hCaptcha)
// @namespace https://example.com
// @version 1.3
// @description Learns image-based reCAPTCHA & hCaptcha selections (auto-select known, highlight unknown, GUI to manage DB). Stores data via GM_setValue / GM_getValue.
// @author You
// @match *://*/*
// @grant GM_setValue
// @grant GM_getValue
// @grant GM_listValues
// @grant GM_deleteValue
// @require https://ajax.googleapis.com/ajax/libs/jquery/3.6.0/jquery.min.js
// @run-at document-end
// ==/UserScript==
(function(){
'use strict';
const POLL_INTERVAL = 1500; // ms - how often to scan page for captchas
const DB_PREFIX = 'captcha_learner_v1_';
const MAX_IMG_DIM = 160; // scale images for hashing
const LOG = true; // set false to reduce console logs
// ---------- Utilities ----------
function log(...args){ if(LOG) console.log('[CaptchaLearner]', ...args); }
function gmGet(key, def){ try { const v = GM_getValue(key); return typeof v === 'undefined' ? def : v; } catch(e){ return def; } }
function gmSet(key, val){ try { GM_setValue(key, val); } catch(e){ console.error(e); } }
function gmList(){ try { return GM_listValues(); } catch(e){ return []; } }
function gmDelete(key){ try { GM_deleteValue(key); } catch(e){ console.error(e); } }
// ---------- UI: info box + control panel ----------
const INFO_ID = 'captcha_learner_info_box';
const PANEL_ID = 'captcha_learner_control_panel';
function ensureStyles(){
if(document.getElementById('captcha_learner_css')) return;
const css = `
#${INFO_ID}{ position:fixed; left:14px; top:14px; z-index:2147483647; background:#222; color:#fff; padding:12px 14px; border-radius:10px; font-family:Arial, sans-serif; font-size:13px; box-shadow:0 6px 18px rgba(0,0,0,0.4); max-width:320px; }
#${INFO_ID} .title{ font-weight:700; margin-bottom:6px; color:#fff; }
#${INFO_ID} .msg{ color:#ddd; font-size:12px; line-height:1.35; }
#${PANEL_ID}{ position:fixed; right:14px; bottom:14px; z-index:2147483647; background:#fff; color:#111; padding:12px; border-radius:10px; width:420px; font-family:Arial, sans-serif; font-size:13px; box-shadow:0 6px 18px rgba(0,0,0,0.15); }
#${PANEL_ID} .row{ margin-bottom:8px; }
#${PANEL_ID} textarea{ width:100%; height:120px; font-family:monospace; font-size:12px; }
#captcha_learner_db_list{ max-height:200px; overflow:auto; border:1px solid #eee; padding:6px; border-radius:6px; background:#fafafa; }
.cl-entry{ padding:6px; border-bottom:1px dashed #eee; display:flex; justify-content:space-between; align-items:center; }
.cl-entry .k{ font-weight:600; color:#333; }
.cl-entry button{ margin-left:8px; }
.cl-img-thumb{ max-width:60px; max-height:50px; border:1px solid #ccc; margin-right:6px; }
.cl-highlight{ outline:3px solid #ff6b6b !important; }
`;
const style = document.createElement('style');
style.id = 'captcha_learner_css';
style.innerText = css;
document.head.appendChild(style);
}
function ensureInfoBox(){
if(document.getElementById(INFO_ID)) return;
ensureStyles();
const div = document.createElement('div');
div.id = INFO_ID;
div.innerHTML = '<div class="title">Captcha Learner</div><div class="msg">Scanning page...</div>';
document.body.appendChild(div);
}
function updateInfo(msg){
ensureInfoBox();
const el = document.getElementById(INFO_ID);
el.querySelector('.msg').innerText = msg;
}
function ensureControlPanel(){
if(document.getElementById(PANEL_ID)) return;
ensureStyles();
const panel = document.createElement('div');
panel.id = PANEL_ID;
panel.innerHTML = `
<div style="font-weight:700;margin-bottom:8px">Captcha Learner — Settings & DB</div>
<div class="row">
<label>Auto-scan interval (ms): <input id="cl_interval" type="number" value="${POLL_INTERVAL}" style="width:100px"></label>
<label style="margin-left:10px"><input id="cl_autostart" type="checkbox" checked> Auto</label>
</div>
<div class="row">
<button id="cl_refresh_btn">Scan now</button>
<button id="cl_export_btn">Export DB</button>
<button id="cl_import_btn">Import DB</button>
<button id="cl_clear_btn" style="float:right;background:#d9534f;color:#fff;border:none;padding:6px 10px;border-radius:6px">Clear DB</button>
</div>
<div class="row"><div id="captcha_learner_db_list"></div></div>
<div class="row">
<div style="font-size:12px;color:#666">Tip: click an unknown image on captcha to teach the label. Stored locally via GM_setValue.</div>
</div>
<div style="margin-top:6px"><textarea id="cl_export_area" placeholder="Paste JSON here to import"></textarea></div>
`;
document.body.appendChild(panel);
document.getElementById('cl_refresh_btn').addEventListener('click', ()=> scanOnce(true));
document.getElementById('cl_export_btn').addEventListener('click', exportDB);
document.getElementById('cl_import_btn').addEventListener('click', importDBFromText);
document.getElementById('cl_clear_btn').addEventListener('click', clearDB);
}
// ---------- Image hashing / base64 ----------
function downscaleImgToCanvas(img){
// returns dataURL resized
try {
const canvas = document.createElement('canvas');
const ctx = canvas.getContext('2d');
// scale preserving aspect ratio
const w = img.naturalWidth || img.width;
const h = img.naturalHeight || img.height;
const scale = Math.min(1, MAX_IMG_DIM / Math.max(w,h));
const nw = Math.max(20, Math.round(w * scale));
const nh = Math.max(20, Math.round(h * scale));
canvas.width = nw;
canvas.height = nh;
ctx.drawImage(img, 0, 0, nw, nh);
// use jpeg small quality
return canvas.toDataURL('image/jpeg', 0.45);
} catch(e){
log('downscale error', e);
return null;
}
}
// ---------- DB helpers ----------
function dbKeyForLabel(label){
return DB_PREFIX + label.replace(/\s+/g, '_').toLowerCase();
}
function dbGet(label){ return gmGet(dbKeyForLabel(label), []); }
function dbAdd(label, dataUrl){
const key = dbKeyForLabel(label);
let arr = gmGetValueSafe(key, []);
if(!Array.isArray(arr)) arr = [];
if(!arr.includes(dataUrl)){
arr.push(dataUrl);
gmSet(key, arr);
log('DB add', label);
}
}
function dbRemove(label){ gmDelete(dbKeyForLabel(label)); }
function dbList(){
const keys = gmList();
const out = [];
for(const k of keys){
if(k.startsWith(DB_PREFIX)){
out.push({key:k, label:k.slice(DB_PREFIX.length), count: (gmGetValueSafe(k,[])).length});
}
}
return out;
}
function gmGetValueSafe(k, def){
try{ const v = GM_getValue(k); return typeof v === 'undefined' ? def : v; } catch(e){ return def; }
}
// ---------- Captcha detection & processing ----------
function findReCaptchaWidget(){
// reCAPTCHA image challenge tiles have class rc-image-tile-wrapper containing <img>
const tiles = document.querySelectorAll('.rc-imageselect-tile-wrapper img, .rc-image-tile-wrapper img');
const instruct = document.querySelector('.rc-imageselect-instructions, .rc-imageselect-instructions div, .rc-imageselect-desc-no-canonical');
if(tiles && tiles.length && instruct){
return {type:'recaptcha', tiles:Array.from(tiles), label: instruct.innerText.trim()};
}
return null;
}
function findHcaptchaWidget(){
// hCaptcha images are inside .task-image or .image-grid
const tiles = document.querySelectorAll('.task-image img, .image-grid img, .challenge-image img');
const instruct = document.querySelector('.prompt-text, .caption-text');
if(tiles && tiles.length && instruct){
return {type:'hcaptcha', tiles:Array.from(tiles), label: instruct.innerText.trim()};
}
return null;
}
function highlightNode(node){
try{
node.classList.add('cl-highlight');
}catch(e){}
}
function attachLearnClick(imgEl, label){
// on click, add to DB and give feedback
const handler = (ev)=>{
ev.stopPropagation(); ev.preventDefault();
const data = downscaleImgToCanvas(imgEl);
if(!data) return;
dbAdd(label, data);
updateInfo('Learned image for "'+label+'" — saved.');
// remove highlight
imgEl.classList.remove('cl-highlight');
// remove handler to prevent duplicates
imgEl.removeEventListener('click', handler);
};
imgEl.addEventListener('click', handler);
}
function tryAutoSelect(tileImg, label){
const data = downscaleImgToCanvas(tileImg);
if(!data) return false;
const arr = dbGet(label) || [];
if(arr.includes(data)){
// attempt to click the tile container
const clickable = tileImg.closest('button, td, div, a, span') || tileImg;
try { clickable.click(); } catch(e){ tileImg.click(); }
return true;
}
return false;
}
function processWidget(widget){
if(!widget) return;
updateInfo(widget.type + ' — "'+widget.label+'" — '+widget.tiles.length+' images');
let matched = 0;
for(const img of widget.tiles){
try{
const ok = tryAutoSelect(img, widget.label);
if(ok) matched++;
else {
highlightNode(img);
attachLearnClick(img, widget.label);
}
}catch(e){ log('process tile error', e); }
}
if(matched) updateInfo('Auto-selected '+matched+' images for "'+widget.label+'"');
}
// ---------- Scanning loop ----------
let POLLER = null;
function scanOnce(force){
try{
const rec = findReCaptchaWidget();
if(rec){ processWidget(rec); return; }
const h = findHcaptchaWidget();
if(h){ processWidget(h); return; }
if(force) updateInfo('No known captcha widget found.');
}catch(e){ log('scanOnce err', e); }
}
function startPoller(interval){
if(POLLER) clearInterval(POLLER);
POLLER = setInterval(()=> scanOnce(false), interval || POLL_INTERVAL);
}
// ---------- Control panel actions ----------
function renderDBList(){
const container = document.getElementById('captcha_learner_db_list');
if(!container) return;
container.innerHTML = '';
const items = dbList();
if(items.length === 0) { container.innerHTML = '<div style="color:#888;padding:8px">DB empty</div>'; return; }
for(const it of items){
const row = document.createElement('div');
row.className = 'cl-entry';
const left = document.createElement('div');
left.style.display = 'flex'; left.style.alignItems='center';
// show small thumb from first image
const arr = gmGetValueSafe(it.key, []);
const thumb = document.createElement('img');
thumb.className = 'cl-img-thumb';
thumb.src = arr && arr[0] ? arr[0] : '';
left.appendChild(thumb);
const title = document.createElement('div');
title.innerHTML = '<div class="k">'+it.label+'</div><div style="font-size:11px;color:#666">'+it.count+' images</div>';
left.appendChild(title);
row.appendChild(left);
const controls = document.createElement('div');
const del = document.createElement('button'); del.textContent='Delete'; del.style.padding='6px 8px';
del.addEventListener('click', ()=>{ if(confirm('Delete "'+it.label+'"?')){ dbRemove(it.label); renderDBList(); }});
const view = document.createElement('button'); view.textContent='View'; view.style.marginLeft='8px'; view.style.padding='6px 8px';
view.addEventListener('click', ()=>{ const arr = gmGetValueSafe(it.key,[]); document.getElementById('cl_export_area').value = JSON.stringify({label:it.label, images:arr}, null, 2); });
controls.appendChild(view); controls.appendChild(del);
row.appendChild(controls);
container.appendChild(row);
}
}
function exportDB(){
// build JSON of all DB items
const items = dbList();
const out = {};
for(const it of items){
out[it.label] = gmGetValueSafe(DB_PREFIX + it.label, []);
}
const txt = JSON.stringify(out, null, 2);
document.getElementById('cl_export_area').value = txt;
updateInfo('DB exported to textarea. You can copy and save it.');
}
function importDBFromText(){
const txt = document.getElementById('cl_export_area').value.trim();
if(!txt) return alert('Paste JSON into textarea first');
try{
const parsed = JSON.parse(txt);
for(const k in parsed){
const key = DB_PREFIX + k;
gmSet(key, parsed[k]);
}
updateInfo('Imported DB entries: '+Object.keys(parsed).length);
renderDBList();
}catch(e){ alert('Invalid JSON: '+e.message); }
}
function clearDB(){
if(!confirm('Clear all learned DB entries?')) return;
const keys = gmList();
for(const k of keys){
if(k.startsWith(DB_PREFIX)) gmDelete(k);
}
renderDBList();
updateInfo('DB cleared');
}
// ---------- Init ----------
function init(){
ensureInfoBox();
ensureControlPanel();
renderDBList();
// start poller
const intervalInput = document.getElementById('cl_interval');
intervalInput.addEventListener('change', ()=> {
const v = parseInt(intervalInput.value) || POLL_INTERVAL;
startPoller(v);
});
document.getElementById('cl_autostart').checked = true;
startPoller(POLL_INTERVAL);
// refresh db list periodically
setInterval(renderDBList, 4000);
// one initial scan
setTimeout(()=> scanOnce(true), 1500);
}
// start
init();
})();