Proxy Collector & Checker V20

🟢 Сбор бесплатных HTTPS-прокси + API-проверка. Автообновление, подсветка самых быстрых, фильтр, сортировка, многопоточность, отображение скорости (ms), логотип "Зроблено в Україні".

// ==UserScript==
// @name         Proxy Collector & Checker V20
// @namespace    https://example.com/
// @version      20.0
// @description  🟢 Сбор бесплатных HTTPS-прокси + API-проверка. Автообновление, подсветка самых быстрых, фильтр, сортировка, многопоточность, отображение скорости (ms), логотип "Зроблено в Україні".
// @author       ChatGPT
// @match        *://*/*
// @grant        GM_xmlhttpRequest
// @grant        GM_setValue
// @grant        GM_getValue
// @connect      *
// ==/UserScript==

(function () {
    'use strict';

    // ================== CONFIG ==================
    const SOURCES = [
        'https://raw.githubusercontent.com/TheSpeedX/PROXY-List/master/http.txt',
        'https://raw.githubusercontent.com/roosterkid/openproxylist/main/HTTPS_RAW.txt',
        'https://raw.githubusercontent.com/mertguvencli/http-proxy-list/main/proxy-list/data.txt',
        'https://raw.githubusercontent.com/monosans/proxy-list/main/proxies/http.txt',
        'https://raw.githubusercontent.com/almroot/proxylist/master/list.txt'
    ];

    const API_TEMPLATE = 'https://api.proxy-checker.net/api/check?proxy={proxy}';
    const CONCURRENCY = 10;
    const REQUEST_TIMEOUT_MS = 15000;
    const STORAGE_PREFIX = 'proxyWidgetV20_';
    const AUTO_UPDATE_INTERVAL_MS = 5*60*1000; // 5 минут

    let allProxies = [];
    let checkResults = {};
    let checking = false;
    let showOnlyWorking = GM_getValue(STORAGE_PREFIX + 'filter', 'false') === 'true';
    let sortMode = GM_getValue(STORAGE_PREFIX + 'sortMode', 'working');

    // ================== UI ==================
    function createWidget() {
        if (document.getElementById('proxy-widget-v20')) return;

        const savedPos = JSON.parse(GM_getValue(STORAGE_PREFIX + 'pos', null) || 'null') || { top: 50, left: 50 };
        const collapsed = GM_getValue(STORAGE_PREFIX + 'collapsed', 'false') === 'true';

        const w = document.createElement('div');
        w.id = 'proxy-widget-v20';
        w.style.cssText = `
            position: fixed;
            top: ${savedPos.top}px;
            left: ${savedPos.left}px;
            width: 500px;
            max-height: 75vh;
            overflow: auto;
            background: #111;
            color: #eee;
            font-family: Arial, sans-serif;
            font-size: 13px;
            border-radius: 10px;
            box-shadow: 0 8px 30px rgba(0,0,0,0.6);
            border: 1px solid #222;
            z-index: 2147483647;
            padding: 10px;
        `;

        w.innerHTML = `
            <div id="phead" style="display:flex;justify-content:space-between;align-items:center;margin-bottom:8px;cursor:grab;">
                <div style="font-weight:700;color:#4caf50;display:flex;align-items:center;gap:6px">
                    Proxy V20
                    <span style="font-size:10px;color:#feda4a;font-weight:700;">Зроблено в Україні</span>
                </div>
                <div style="display:flex;gap:6px;align-items:center">
                    <button id="btnCollapse" title="Свернуть" style="background:#333;color:#fff;border:none;padding:4px 8px;border-radius:6px;cursor:pointer">${collapsed ? '+' : '−'}</button>
                    <button id="btnClose" title="Закрыть" style="background:#b33;color:#fff;border:none;padding:4px 8px;border-radius:6px;cursor:pointer">×</button>
                </div>
            </div>
            <div id="pcontent" style="${collapsed ? 'display:none' : ''}">
                <div style="display:flex;flex-wrap:wrap;gap:6px;margin-bottom:8px">
                    <button id="btnLoad" style="padding:6px 8px;border-radius:6px;border:none;cursor:pointer;background:#2b6cff;color:#fff">Обновить списки</button>
                    <button id="btnCheck" style="padding:6px 8px;border-radius:6px;border:none;cursor:pointer;background:#27ae60;color:#fff">Проверить (API)</button>
                    <button id="btnCopy" style="padding:6px 8px;border-radius:6px;border:none;cursor:pointer;background:#555;color:#fff">Копировать</button>
                    <button id="btnDownload" style="padding:6px 8px;border-radius:6px;border:none;cursor:pointer;background:#555;color:#fff">Скачать</button>
                    <button id="btnFilter" style="padding:6px 8px;border-radius:6px;border:none;cursor:pointer;background:#777;color:#fff">Фильтр: ${showOnlyWorking ? '✅' : 'Все'}</button>
                    <button id="btnSort" style="padding:6px 8px;border-radius:6px;border:none;cursor:pointer;background:#999;color:#fff">Сортировать: ${sortMode}</button>
                </div>
                <div id="pstatus" style="margin-bottom:8px;color:#bbb">Готов</div>
                <div id="plist" style="background:#0f0f10;border:1px solid #222;padding:8px;border-radius:6px;max-height:50vh;overflow:auto;white-space:pre-wrap;word-break:break-all"></div>
            </div>
        `;
        document.body.appendChild(w);

        // ===== Drag =====
        let dragging = false;
        let offset = { x: 0, y: 0 };
        const header = document.getElementById('phead');
        header.addEventListener('mousedown', (e) => { if (e.target.tagName === 'BUTTON') return; dragging = true; offset.x = e.clientX - w.offsetLeft; offset.y = e.clientY - w.offsetTop; header.style.cursor = 'grabbing'; e.preventDefault(); });
        document.addEventListener('mousemove', (e) => { if (!dragging) return; w.style.left = (e.clientX - offset.x) + 'px'; w.style.top = (e.clientY - offset.y) + 'px'; });
        document.addEventListener('mouseup', () => { if(dragging){ dragging=false; header.style.cursor='grab'; GM_setValue(STORAGE_PREFIX+'pos',JSON.stringify({top:parseInt(w.style.top,10)||50,left:parseInt(w.style.left,10)||50})); }});

        // ===== Buttons =====
        document.getElementById('btnClose').addEventListener('click',()=>w.remove());
        document.getElementById('btnCollapse').addEventListener('click',()=>{
            const content=document.getElementById('pcontent'); const collapsedNow=content.style.display==='none';
            content.style.display=collapsedNow?'block':'none'; document.getElementById('btnCollapse').textContent=collapsedNow?'−':'+';
            GM_setValue(STORAGE_PREFIX+'collapsed',(!collapsedNow).toString());
        });
        document.getElementById('btnLoad').addEventListener('click',loadSources);
        document.getElementById('btnCopy').addEventListener('click',()=>{navigator.clipboard.writeText(formatRenderText()); setStatus('Скопировано в буфер');});
        document.getElementById('btnDownload').addEventListener('click',()=>{const blob=new Blob([formatRenderText()],{type:'text/plain'});const a=document.createElement('a');a.href=URL.createObjectURL(blob);a.download='proxies_v20.txt';document.body.appendChild(a);a.click();a.remove();});
        document.getElementById('btnFilter').addEventListener('click',()=>{showOnlyWorking=!showOnlyWorking;GM_setValue(STORAGE_PREFIX+'filter',showOnlyWorking.toString());document.getElementById('btnFilter').textContent=showOnlyWorking?'Фильтр: ✅':'Фильтр: Все'; renderList();});
        document.getElementById('btnSort').addEventListener('click',()=>{ const modes=['working','country','speed']; let idx=modes.indexOf(sortMode); idx=(idx+1)%modes.length; sortMode=modes[idx]; GM_setValue(STORAGE_PREFIX+'sortMode',sortMode); document.getElementById('btnSort').textContent=`Сортировать: ${sortMode}`; renderList();});
        document.getElementById('btnCheck').addEventListener('click',()=>{if(!allProxies.length){setStatus('Сначала обнови списки');return;} runChecks();});

        // Load saved proxies
        const saved = GM_getValue(STORAGE_PREFIX+'lastProxies', null);
        if(saved){ try{ const arr=JSON.parse(saved); if(Array.isArray(arr)) allProxies=arr; renderList();}catch(e){}}

        // ===== Auto-update interval =====
        setInterval(()=>{loadSources();}, AUTO_UPDATE_INTERVAL_MS);
    }

    function setStatus(text){ const el=document.getElementById('pstatus'); if(el) el.textContent=text; }
    function formatRenderText(){ const box=document.getElementById('plist'); return box?box.textContent:''; }

    // ================== Source Loading ==================
    function loadSources(){
        setStatus('Загрузка источников...');
        allProxies=[]; checkResults={}; renderList();
        let remaining=SOURCES.length;
        SOURCES.forEach(url=>{
            try{ GM_xmlhttpRequest({ method:'GET', url, timeout:REQUEST_TIMEOUT_MS, onload: res=>{ if(res.status>=200&&res.status<300&&res.responseText) parseSourceText(res.responseText); if(--remaining===0){ finishLoading(); runChecks(); } }, onerror: ()=>{if(--remaining===0){ finishLoading(); runChecks();}}, ontimeout: ()=>{if(--remaining===0){ finishLoading(); runChecks();} } }); } catch(e){ if(--remaining===0){ finishLoading(); runChecks(); } }
        });
    }

    function parseSourceText(text){ if(!text) return; const matches=text.match(/\d{1,3}(?:\.\d{1,3}){3}:\d{1,5}/g); if(matches){ for(const m of matches) allProxies.push(m.trim()); } }
    function finishLoading(){ allProxies=Array.from(new Set(allProxies)).filter(l=>{const m=l.match(/^(\d{1,3}(?:\.\d{1,3}){3}):(\d{1,5})$/); return m&&Number(m[2])>0&&Number(m[2])<65536;}); GM_setValue(STORAGE_PREFIX+'lastProxies',JSON.stringify(allProxies)); setStatus(`Готово. Прокси: ${allProxies.length}`); renderList(); }

    // ================== API Check ==================
    function callApiCheck(proxy){ return new Promise(resolve=>{ const t0=Date.now(); let done=false; const timer=setTimeout(()=>{if(done) return; done=true; checkResults[proxy]={working:false,https:false,country:null,info:'timeout',time:null}; resolve();},REQUEST_TIMEOUT_MS);
        try{ GM_xmlhttpRequest({ method:'GET', url:API_TEMPLATE.replace('{proxy}',encodeURIComponent(proxy)), timeout:REQUEST_TIMEOUT_MS,
            onload:res=>{if(done)return; done=true; clearTimeout(timer); let json=null; try{json=JSON.parse(res.responseText);}catch(e){} let working=false,isHttps=false,country=null,info=''; if(json){if(json.status&&typeof json.status==='string'){if(/ok|working|success|alive|open/i.test(json.status)) working=true;} if(typeof json.working==='boolean') working=json.working; const typeField=json.type||json.protocol||json.protocols||json.proxy_type||json.methods;if(typeField){ const sf=JSON.stringify(typeField).toLowerCase(); if(sf.includes('https')) isHttps=true;} else{ if('https' in json && (json.https===true||json.https==='true')) isHttps=true; if('ssl' in json && (json.ssl===true||json.ssl==='true')) isHttps=true;} country=json.country||json.country_code||json.countryCode||null; info=(json.msg||json.message||json.info||JSON.stringify(json)).toString().slice(0,200);} else { const txt=(res.responseText||'').toLowerCase(); if(txt.includes('ok')||txt.includes('working')||txt.includes('alive')||txt.includes('success')) working=true; if(txt.includes('https')||txt.includes('ssl')) isHttps=true; info=res.responseText.slice(0,200);} checkResults[proxy]={working:working&&isHttps,https:isHttps,country:country,info:info,time:Date.now()-t0}; resolve(); },
            onerror:()=>{if(done)return; done=true; clearTimeout(timer); checkResults[proxy]={working:false,https:false,country:null,info:'network error',time:null}; resolve();},
            ontimeout:()=>{if(done)return; done=true; clearTimeout(timer); checkResults[proxy]={working:false,https:false,country:null,info:'timeout',time:null}; resolve();}
        }); }catch(e){if(!done){done=true;clearTimeout(timer); checkResults[proxy]={working:false,https:false,country:null,info:'exception',time:null}; resolve();}}});}

    async function runChecks(){
        if(checking) return; checking=true; checkResults={}; setStatus('Запуск проверок через API...');
        const queue=allProxies.slice(); let active=0,completed=0,total=queue.length;
        async function worker(){ while(true){ const proxy=queue.shift(); if(!proxy) break; active++; setStatus(`Проверяю ${completed}/${total} (active ${active}) — ${proxy}`); await callApiCheck(proxy); completed++; active--; renderList();}}
        const workers=[]; for(let i=0;i<CONCURRENCY;i++) workers.push(worker()); await Promise.all(workers);
        GM_setValue(STORAGE_PREFIX+'lastProxies',JSON.stringify(allProxies));
        setStatus(`Проверено: ${Object.keys(checkResults).length}. Рабочих HTTPS: ${Object.values(checkResults).filter(r=>r.working&&r.https).length}`);
        renderList(); checking=false;
    }

    function renderList(){
        const box=document.getElementById('plist'); if(!box) return;
        let items=[];
        if(Object.keys(checkResults).length){ items=allProxies.map(p=>{const r=checkResults[p]; return{proxy:p,working:r?r.working:null,https:r?!!r.https:null,country:r?r.country:null,time:r?r.time:null};});}
        else{ items=allProxies.map(p=>({proxy:p,working:null,https:null,country:null,time:null}));}
        if(showOnlyWorking) items=items.filter(i=>i.working===true && i.https===true);

        // Sorting
        if(sortMode==='working'){ items.sort((a,b)=>{const ka=(a.working&&a.https)?0:1; const kb=(b.working&&b.https)?0:1; if(ka!==kb) return ka-kb; return (a.country||'').localeCompare(b.country||'');});}
        else if(sortMode==='country'){ items.sort((a,b)=>(a.country||'').localeCompare(b.country||''));}
        else if(sortMode==='speed'){ items.sort((a,b)=>(a.time||Infinity)-(b.time||Infinity));}

        // Determine fastest time for highlight
        const fastestTime = Math.min(...items.filter(i=>i.time!=null).map(i=>i.time), Infinity);

        const lines=items.map(i=>{
            const speed=i.time!=null?` | ${i.time}ms`:'';
            const fastHighlight=(i.time!=null && i.time===fastestTime)?' 🟢':'';
            if(i.working===null) return i.proxy;
            if(i.working&&i.https) return `✅ ${i.proxy} | ${i.country||'?'} | HTTPS${speed}${fastHighlight}`;
            if(i.working&&!i.https) return `⚠️ ${i.proxy} | ${i.country||'?'} | not-HTTPS${speed}${fastHighlight}`;
            return `❌ ${i.proxy}${speed}`;
        });
        box.textContent=lines.join('\n');
    }

    createWidget();
})();