RedBlack / Interceptador JSON Profissional v5.1

// ==UserScript==
// @name         Interceptador JSON Profissional v5.1
// @namespace    http://tampermonkey.net/
// @version      5.1
// @description  Intercepta requisições, edita payloads, aplica regras e envia automaticamente. UI moderna com painel e botões de teste/cópia/envio.
// @author       Você
// @match        *://*/*
// @grant        none
// @license     0BSD
// ==/UserScript==

(function () {
    'use strict';

    /************** Utils **************/
    const host = location.host;
    const LS_KEYS = {
        IGNORED: 'ri:ignoredSites',
        RULES: (h) => `ri:rules:${h}`,
        AUTO_ORIG: (h) => `ri:autoOriginal:${h}`,
        AUTO_EDIT: (h) => `ri:autoEdited:${h}`,
        PANEL: 'ri:panelState'
    };

    const getJSON = (k, df) => { try { const v = localStorage.getItem(k); return v ? JSON.parse(v) : df; } catch { return df; } };
    const setJSON = (k, v) => localStorage.setItem(k, JSON.stringify(v));

    let ignoredSites = getJSON(LS_KEYS.IGNORED, []);
    let rules = getJSON(LS_KEYS.RULES(host), []);
    let autoOriginal = !!getJSON(LS_KEYS.AUTO_ORIG(host), false);
    let autoEdited = !!getJSON(LS_KEYS.AUTO_EDIT(host), false);
    let panelState = getJSON(LS_KEYS.PANEL, { minimized: false });

    const queue = [];
    let active = null;

    function enqueue(item) { queue.push(item); updateBadge(); if (!active) nextItem(); }
    function nextItem() {
        active = queue.shift() || null;
        updateBadge();
        if (!active) return;
        if (isIgnored()) { active.cancel(); nextItem(); return; }
        if (autoEdited) { const processed = applyRules(active.rawBody); active.apply(processed); nextItem(); return; }
        if (autoOriginal) { active.cancel(); nextItem(); return; }
        showEditor(active);
    }
    function isIgnored() { return ignoredSites.includes(host); }

    function parseMaybeRegex(pattern, flags) {
        const m = pattern.match(/^\/(.+)\/([a-z]*)$/i);
        if (m) { try { return new RegExp(m[1], m[2] || flags || 'g'); } catch {} }
        return new RegExp(pattern.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'), flags || 'g');
    }
    function applyRules(str) {
        let out = String(str);
        for (const r of rules) {
            try {
                const re = r.isRegex ? new RegExp(r.find, r.flags || 'g') : parseMaybeRegex(r.find, r.flags || 'g');
                out = out.replace(re, r.replace);
            } catch {}
        }
        return out;
    }

    function detectBody(body) {
        if (body == null) return { type: 'none', raw: '' };
        if (typeof body === 'string') return { type: isLikelyJSON(body) ? 'json-string' : 'text', raw: body };
        if (body instanceof FormData) { const obj = {}; body.forEach((v, k) => obj[k] = v); return { type: 'formdata', raw: JSON.stringify(obj, null, 2), original: body }; }
        if (body instanceof URLSearchParams) return { type: 'urlencoded', raw: body.toString(), original: body };
        try { return { type: 'json-object', raw: JSON.stringify(body, null, 2), original: body }; } catch { return { type: 'text', raw: String(body) }; }
    }
    function isLikelyJSON(s) { const t = s.trim(); return (t.startsWith('{') && t.endsWith('}')) || (t.startsWith('[') && t.endsWith(']')); }
    function buildBodyFromRaw(type, raw, original) {
        switch(type) {
            case 'json-string':
            case 'text': return raw;
            case 'json-object': try { return JSON.parse(raw); } catch { return original ?? raw; }
            case 'formdata': try { const obj = JSON.parse(raw); const fd = new FormData(); Object.entries(obj||{}).forEach(([k,v])=>fd.append(k,v)); return fd; } catch { return original; }
            case 'urlencoded': try { return new URLSearchParams(raw); } catch { return original ?? raw; }
            default: return raw;
        }
    }

    function formatJSON(s) { try { return JSON.stringify(JSON.parse(s), null, 2); } catch { return s; } }

    /************** Painel **************/
    const panel = document.createElement('div');
    panel.style.cssText = `
        position:fixed; right:12px; bottom:12px; width:420px; height:360px;
        background:#111827; color:#e5e7eb; border:1px solid #1f2937; border-radius:10px;
        box-shadow:0 12px 30px rgba(0,0,0,.45); font:13px/1.4 Inter,Segoe UI,Roboto,Arial;
        z-index:999999; display:flex; flex-direction:column; overflow:hidden;
    `;
    document.documentElement.appendChild(panel);

    const header = document.createElement('div');
    header.style.cssText = `background:linear-gradient(90deg,#1f2937,#111827); padding:8px 12px; display:flex; align-items:center; gap:8px; color:#f9fafb; font-weight:600;`;
    const title = document.createElement('div'); title.textContent='Interceptador v5.1';
    const badge = document.createElement('span'); badge.style.cssText='margin-left:auto; font-size:12px; color:#9ca3af;';
    const minimizeBtn = document.createElement('button'); minimizeBtn.textContent = panelState.minimized?'▣':'▢'; minimizeBtn.style.cssText='background:none;border:none;color:#9ca3af;font-size:16px;cursor:pointer;';
    header.append(title, badge, minimizeBtn);
    panel.appendChild(header);

    const toolbar = document.createElement('div'); toolbar.style.cssText='padding:6px 12px; display:flex; gap:10px; border-bottom:1px solid #1f2937; background:#0f131b;';
    const ignoreCb = mkCheckbox('Ignorar site', isIgnored());
    const autoOrigCb = mkCheckbox('Auto original', autoOriginal);
    const autoEditCb = mkCheckbox('Auto editado', autoEdited);
    toolbar.append(ignoreCb.label, autoOrigCb.label, autoEditCb.label);
    panel.appendChild(toolbar);

    const infoLine = document.createElement('div'); infoLine.style.cssText='padding:6px 12px; font-size:12px; color:#9ca3af; border-bottom:1px solid #1f2937;'; infoLine.textContent='Aguardando requisição…';
    panel.appendChild(infoLine);

    const main = document.createElement('div'); main.style.cssText='display:flex; flex-direction:column; gap:6px; padding:8px 12px; flex:1; overflow:hidden;';
    const payloadBox = document.createElement('textarea'); payloadBox.placeholder='Payload aparecerá aqui...'; payloadBox.style.cssText='flex:1; width:100%; background:#0c0f16; color:#d1fae5; border:1px solid #1f2937; border-radius:8px; padding:8px; font-family:monospace; font-size:12px; resize:none; outline:none;';
    main.appendChild(payloadBox);

    const findReplaceWrap = document.createElement('div'); findReplaceWrap.style.cssText='display:grid; grid-template-columns: 1fr 1fr auto auto; gap:6px;';
    const findInput = mkInput('Procurar'); const replaceInput = mkInput('Substituir');
    const regexToggle = document.createElement('input'); regexToggle.type='checkbox';
    const regexLabel = document.createElement('label'); regexLabel.append(regexToggle,' Regex'); regexLabel.style.color='#cbd5e1';
    const addRuleBtn = mkBtn('➕','#2563eb');
    findReplaceWrap.append(findInput, replaceInput, regexLabel, addRuleBtn);
    main.appendChild(findReplaceWrap);

    const rulesList = document.createElement('div'); rulesList.style.cssText='flex:0 0 70px; overflow:auto; border:1px dashed #1f2937; border-radius:8px; padding:4px;';
    main.appendChild(rulesList);

    const footer = document.createElement('div'); footer.style.cssText='padding:6px 12px; display:flex; gap:6px; border-top:1px solid #1f2937; background:#0f131b;';
    const btnApplyRulesPreview = mkBtn('🧪 Regras','#7c3aed'); const btnCopy = mkBtn('📋 Copiar','#0369a1'); const btnSend = mkBtn('✅ Enviar editado','#16a34a'); const btnSendOrig = mkBtn('↩️ Original','#6b7280');
    footer.append(btnApplyRulesPreview, btnCopy, btnSend, btnSendOrig);
    panel.appendChild(main); panel.appendChild(footer);

    // --- Funções UI ---
    function applyMinimized(){if(panelState.minimized){panel.style.height='40px'; main.style.display='none'; toolbar.style.display='none'; infoLine.style.display='none'; footer.style.display='none'; minimizeBtn.textContent='▣';}else{panel.style.height='360px'; main.style.display=''; toolbar.style.display=''; infoLine.style.display=''; footer.style.display=''; minimizeBtn.textContent='▢';}}
    applyMinimized(); minimizeBtn.onclick=()=>{panelState.minimized=!panelState.minimized; setJSON(LS_KEYS.PANEL,panelState); applyMinimized();};
    ignoreCb.input.onchange=()=>{if(ignoreCb.input.checked&&!ignoredSites.includes(host))ignoredSites.push(host);else ignoredSites=ignoredSites.filter(h=>h!==host); setJSON(LS_KEYS.IGNORED,ignoredSites);};
    autoOrigCb.input.onchange=()=>{autoOriginal=autoOrigCb.input.checked; setJSON(LS_KEYS.AUTO_ORIG(host),autoOriginal);};
    autoEditCb.input.onchange=()=>{autoEdited=autoEditCb.input.checked; setJSON(LS_KEYS.AUTO_EDIT(host),autoEdited);};

    function renderRules(){rulesList.innerHTML=''; if(!rules.length){rulesList.innerHTML='<div style="color:#94a3b8;font-size:12px;">Sem regras</div>';return;} rules.forEach((r,i)=>{const row=document.createElement('div'); row.style.cssText='display:flex;justify-content:space-between;font-size:12px;'; row.textContent=`${r.isRegex?`/${r.find}/`:`"${r.find}"`} → "${r.replace}"`; const del=mkBtn('🗑️','#ef4444'); del.style.padding='2px 6px'; del.onclick=()=>{rules.splice(i,1);setJSON(LS_KEYS.RULES(host),rules);renderRules();}; row.appendChild(del); rulesList.appendChild(row);}); }
    renderRules();

    addRuleBtn.onclick=()=>{if(!findInput.value)return alert('Informe o que procurar.'); rules.push({find:findInput.value,replace:replaceInput.value||'',flags:'g',isRegex:regexToggle.checked}); setJSON(LS_KEYS.RULES(host),rules); findInput.value=''; replaceInput.value=''; regexToggle.checked=false; renderRules();};
    btnApplyRulesPreview.onclick=()=>{payloadBox.value=applyRules(payloadBox.value);};
    btnCopy.onclick=()=>{navigator.clipboard.writeText(payloadBox.value).then(()=>alert('Copiado!'));};
    btnSend.onclick=()=>{if(active){active.apply(payloadBox.value); clearEditor(); nextItem();}};
    btnSendOrig.onclick=()=>{if(active){active.cancel(); clearEditor(); nextItem();}};
    function clearEditor(){payloadBox.value=''; infoLine.textContent='Aguardando requisição…';}
    function updateBadge(){const n=queue.length+(active?1:0); badge.textContent=n?n+' pend.':'';}
    function showEditor(item){panelState.minimized=false; setJSON(LS_KEYS.PANEL,panelState); applyMinimized(); infoLine.textContent=`[${item.method}] ${item.url}`; payloadBox.value=formatJSON(item.rawBody);}

    function mkBtn(label,color){const b=document.createElement('button'); b.textContent=label; b.style.cssText=`background:${color};color:#fff;border:none;padding:6px 8px;border-radius:6px;cursor:pointer;font-size:12px;`; return b; }
    function mkInput(ph){const i=document.createElement('input'); i.placeholder=ph; i.style.cssText='background:#0c0f16;color:#e5e7eb;border:1px solid #1f2937;border-radius:6px;padding:6px;font-size:12px;'; return i; }
    function mkCheckbox(label,checked){const input=document.createElement('input'); input.type='checkbox'; input.checked=checked; const lbl=document.createElement('label'); lbl.append(input, ` ${label}`); lbl.style.cssText='color:#cbd5e1;font-size:12px;display:flex;gap:4px;align-items:center;'; return {input,label:lbl};}

    /************** Interceptores **************/
    const originalFetch = window.fetch;
    window.fetch = function(...args){
        try{
            const url = typeof args[0]==='string'?args[0]:(args[0]?.url||'');
            const method=(args[1]?.method||'GET').toUpperCase();
            const hasBody=!!args[1]?.body;
            if(!hasBody)return originalFetch.apply(this,args);
            const det=detectBody(args[1].body);
            enqueue({
                kind:'fetch', url, method, rawBody:det.raw,
                apply:(newRaw)=>{const processed=applyRules(newRaw); args[1].body=buildBodyFromRaw(det.type,processed,det.original); return originalFetch.apply(this,args);},
                cancel:()=>originalFetch.apply(this,args)
            });
            return new Promise((resolve,reject)=>{
                const interval=setInterval(()=>{
                    if(!active)return;
                    if(active.url===url&&active.method===method&&active.rawBody===det.raw){
                        const oldApply=active.apply, oldCancel=active.cancel;
                        active.apply=(newRaw)=>{try{args[1].body=buildBodyFromRaw(det.type,applyRules(newRaw),det.original); originalFetch.apply(this,args).then(resolve).catch(reject);}catch(e){reject(e);}};
                        active.cancel=()=>{try{originalFetch.apply(this,args).then(resolve).catch(reject);}catch(e){reject(e);}};
                        clearInterval(interval);
                    }
                },10);
            });
        }catch{return originalFetch.apply(this,args);}
    };

    const originalXHROpen = XMLHttpRequest.prototype.open;
    const originalXHRSend = XMLHttpRequest.prototype.send;
    XMLHttpRequest.prototype.open=function(method,url,...rest){this.__ri=this.__ri||{}; this.__ri.method=(method||'GET').toUpperCase(); this.__ri.url=url||''; return originalXHROpen.apply(this,[method,url,...rest]);};
    XMLHttpRequest.prototype.send=function(body){
        try{
            const method=this.__ri?.method||'POST'; const url=this.__ri?.url||location.href;
            if(body==null)return originalXHRSend.call(this,body);
            const det=detectBody(body);
            enqueue({
                kind:'xhr', url, method, rawBody:det.raw,
                apply:(newRaw)=>{const processed=applyRules(newRaw); originalXHRSend.call(this,buildBodyFromRaw(det.type,processed,det.original));},
                cancel:()=>originalXHRSend.call(this,body)
            });
            return;
        }catch{return originalXHRSend.call(this,body);}
    };

    updateBadge();
})();