pinter.pro

Gestion collaborative des profils (ban & tags) avec interface améliorée

// ==UserScript==
// @name         pinter.pro
// @namespace    http://tampermonkey.net/
// @version      6.5
// @description  Gestion collaborative des profils (ban & tags) avec interface améliorée
// @match        *://sexemodel.com/*
// @match        *://www.sexemodel.com/*
// @match        *://m.sexemodel.com/*
// @grant        none
// ==/UserScript==

(function() {
  'use strict';

  // ===== Airtable config =====
  const AIRTABLE_TOKEN = "patpbe8APun8JckNM.bd007388d8f9807a82733510209428202ea2a43af6d5b805b667933cd45a64bf";
  const AIRTABLE_BASE = "appUf8jMiwrXzpsP7";
  const TABLE_BAN = "BanList";
  const TABLE_TAGS = "Taglist";

  const CACHE_TTL = 15 * 60 * 1000;
  let bannedList = [];
  let tagMap = {};

  // ===== Tags =====
  const TAGS = [
    {name:"Photos pas ok",score:-1,emoji:"❌📸",type:"neg"},
    {name:"Beaucoup plus vieille",score:-2,emoji:"⏳👵",type:"neg"},
    {name:"Glaçon",score:-1,emoji:"🧊",type:"neg"},
    {name:"Chrono",score:-1,emoji:"⏱️",type:"neg"},
    {name:"Pas propre 1",score:-1,emoji:"🚿⚠️",type:"neg"},
    {name:"Pas propre 2",score:-2,emoji:"🚿🚫",type:"neg"},
    {name:"Pas propre 3",score:-3,emoji:"🚿❌",type:"neg"},
    {name:"Prix +",score:-1,emoji:"💸🔺",type:"neg"},
    {name:"Pas impliquée",score:-1,emoji:"🤷‍♀️",type:"neg"},
    {name:"Beaucoup refaite",score:-2,emoji:"🤡",type:"neg"},
    {name:"A fuir urgence",score:-3,emoji:"🤮",type:"neg"},
    {name:"Photos ok",score:+1,emoji:"✅📸",type:"pos"},
    {name:"Top service",score:+3,emoji:"💯",type:"pos"},
    {name:"Propre++",score:+2,emoji:"✨",type:"pos"},
    {name:"Respect temps",score:+1,emoji:"🕰️✅",type:"pos"}
  ];

  // ===== Utils =====
  function isElementVisible(el){
    if(!el||!(el instanceof HTMLElement))return false;
    const s=getComputedStyle(el);
    if(s.display==='none'||s.visibility==='hidden'||s.opacity==='0')return false;
    if(!document.body.contains(el))return false;
    return true;
  }

  function loadBanlistFromCache(){
    const raw=localStorage.getItem('pinterpro_banlist');
    if(!raw)return false;
    const parsed=JSON.parse(raw);
    if(Date.now()-parsed.timestamp<CACHE_TTL){bannedList=parsed.data;return true;}
    return false;
  }
  function saveBanlistToCache(){
    localStorage.setItem('pinterpro_banlist',JSON.stringify({timestamp:Date.now(),data:bannedList}));
  }
  function loadTaglistFromCache(){
    const raw=localStorage.getItem('pinterpro_taglist');
    if(!raw)return false;
    const parsed=JSON.parse(raw);
    if(Date.now()-parsed.timestamp<CACHE_TTL){tagMap=parsed.data;return true;}
    return false;
  }
  function saveTaglistToCache(){
    localStorage.setItem('pinterpro_taglist',JSON.stringify({timestamp:Date.now(),data:tagMap}));
  }

  function showQuickNotif(msg){
    const n=document.createElement('div');
    n.textContent=msg;
    Object.assign(n.style,{position:'fixed',top:'20px',right:'20px',background:'#4caf50',color:'#fff',padding:'8px 12px',borderRadius:'5px',zIndex:'999999',fontWeight:'bold'});
    document.body.appendChild(n);
    setTimeout(()=>n.remove(),1000);
  }

  function showSyncNotif(){
    if(document.getElementById('pinter-sync'))return;
    const sync=document.createElement('div');
    sync.id='pinter-sync';
    sync.textContent='🔄 Sync…';
    Object.assign(sync.style,{position:'fixed',bottom:'20px',right:'20px',background:'#333',color:'#fff',padding:'6px 10px',borderRadius:'5px',zIndex:999999});
    document.body.appendChild(sync);
  }
  function hideSyncNotif(){
    const el=document.getElementById('pinter-sync');
    if(el)el.remove();
  }

  // ===== Airtable calls =====
  async function fetchBanListFromAirtable(){
    const url=`https://api.airtable.com/v0/${AIRTABLE_BASE}/${TABLE_BAN}?pageSize=100`;
    const res=await fetch(url,{headers:{Authorization:`Bearer ${AIRTABLE_TOKEN}`}});
    const data=await res.json();
    return data.records.filter(r=>r.fields.Active===true).map(r=>({
      airtableId:r.id,id:r.fields.ProfilID,name:r.fields.ProfilName||"",date:r.createdTime
    }));
  }

  async function fetchTaglistFromAirtable(){
    const url=`https://api.airtable.com/v0/${AIRTABLE_BASE}/${TABLE_TAGS}?pageSize=1000`;
    const res=await fetch(url,{headers:{Authorization:`Bearer ${AIRTABLE_TOKEN}`}});
    const data=await res.json();
    const map={};
    data.records.forEach(r=>{
      if(!map[r.fields.ProfilID])map[r.fields.ProfilID]=[];
      map[r.fields.ProfilID].push({name:r.fields.Tag,score:r.fields.Score});
    });
    return map;
  }

  async function pushBanToAirtable(profilID,profilName){
    showSyncNotif();
    try{
      const res=await fetch(`https://api.airtable.com/v0/${AIRTABLE_BASE}/${TABLE_BAN}`,{
        method:'POST',headers:{Authorization:`Bearer ${AIRTABLE_TOKEN}`,'Content-Type':'application/json'},
        body:JSON.stringify({records:[{fields:{ProfilID:profilID,ProfilName:profilName,Active:true}}]})
      });
      hideSyncNotif();
      const json=await res.json();
      if(json.error){alert("❌ Erreur Airtable : "+json.error.message);return;}
      bannedList.push({airtableId:json.records[0].id,id:profilID,name:profilName,date:json.records[0].createdTime});
      saveBanlistToCache();
      showQuickNotif(`✅ Profil banni : ${profilName}`);
    }catch(e){hideSyncNotif();alert("❌ Erreur connexion Airtable");}
  }

  async function deactivateBan(airtableRecordId){
    showSyncNotif();
    try{
      await fetch(`https://api.airtable.com/v0/${AIRTABLE_BASE}/${TABLE_BAN}/${airtableRecordId}`,{
        method:'PATCH',headers:{Authorization:`Bearer ${AIRTABLE_TOKEN}`,'Content-Type':'application/json'},
        body:JSON.stringify({fields:{Active:false}})
      });
      hideSyncNotif();
      showQuickNotif('🔄 Profil débanni');
    }catch(e){hideSyncNotif();alert("❌ Erreur connexion Airtable");}
  }

  async function setTags(profilID,tagsSelected){
    showSyncNotif();
    try{
      for(const t of tagsSelected){
        await fetch(`https://api.airtable.com/v0/${AIRTABLE_BASE}/${TABLE_TAGS}`,{
          method:'POST',headers:{Authorization:`Bearer ${AIRTABLE_TOKEN}`,'Content-Type':'application/json'},
          body:JSON.stringify({records:[{fields:{ProfilID:profilID,Tag:t.name,Score:t.score,Active:true}}]})
        });
      }
      hideSyncNotif();
    }catch(e){hideSyncNotif();alert("❌ Erreur sync tags");}
  }

  // ===== UI =====
  const style=document.createElement('style');
  style.textContent=`
    .pinter-btn-container{position:absolute;top:5px;right:5px;display:flex;gap:6px;z-index:9999;}
    .pinter-btn-container button{padding:6px 8px;border-radius:6px;font-weight:bold;cursor:pointer;border:none;}
    .pinter-btn-ban{background:#e00;color:#fff;}
    .pinter-btn-tags{background:#007bff;color:#fff;}
    @media (max-width:768px){
      .tagModal, #banManager .content{width:100%!important;height:100%!important;border-radius:0!important;max-width:none!important;max-height:none!important;}
    }
  `;
  document.head.appendChild(style);

  function showConfirmPanel(profileID,profileName){
    const old=document.getElementById('banConfirmPanel');if(old)old.remove();
    const panel=document.createElement('div');panel.id='banConfirmPanel';
    panel.innerHTML=`<div class="inner" style="background:#fff;padding:20px;border-radius:8px;text-align:center;position:relative;">
      <div style="position:absolute;top:5px;right:10px;cursor:pointer;font-size:18px;font-weight:bold;" id="closeBanX">×</div>
      <p style="font-weight:bold;color:#d00;">⚠️ Ce bouton est réservé aux profils arnaqueurs ou dangereux</p>
      <p style="font-size:13px;margin-bottom:15px;">(ex. demande de tickets Transcash, plan suspect).</p>
      <p>Confirmer le ban du profil :</p>
      <p><b>${profileName}</b></p>
      <button id="confirmBan">✅ Confirmer</button>
      <button id="cancelBan">❌ Annuler</button>
    </div>`;
    Object.assign(panel.style,{position:'fixed',top:0,left:0,right:0,bottom:0,background:'rgba(0,0,0,0.6)',display:'flex',alignItems:'center',justifyContent:'center',zIndex:'10001'});
    document.body.appendChild(panel);
    panel.onclick=e=>{if(e.target===panel)panel.remove();};
    panel.querySelector('#closeBanX').onclick=()=>panel.remove();
    panel.querySelector('#cancelBan').onclick=()=>panel.remove();
    panel.querySelector('#confirmBan').onclick=async()=>{
      bannedList.push({id:profileID,name:profileName,date:new Date().toISOString()});
      saveBanlistToCache();
      document.querySelectorAll(`a[href*="${profileID}"]`).forEach(a=>{const card=a.closest('div');if(card)card.style.display='none';});
      await pushBanToAirtable(profileID,profileName);
      panel.remove();
    };
  }

  function showBanManager(){
    const ex=document.getElementById('banManager');if(ex){ex.remove();return;}
    const panel=document.createElement('div');panel.id='banManager';
    panel.innerHTML=`<div class="content" style="background:#fff;padding:15px;border-radius:8px;max-width:500px;width:90%;max-height:80%;overflow:auto;position:relative;">
      <div class="banCloseX" style="position:absolute;top:8px;right:10px;font-size:20px;font-weight:bold;cursor:pointer;">×</div>
      <h3>Banlist</h3><ul id="banListItems"></ul>
      <div style="text-align:center;margin-top:10px;"><button id="closeBanManager">Fermer</button></div>
    </div>`;
    Object.assign(panel.style,{position:'fixed',top:0,left:0,right:0,bottom:0,background:'rgba(0,0,0,0.6)',display:'flex',alignItems:'center',justifyContent:'center',zIndex:'10002'});
    document.body.appendChild(panel);
    panel.onclick=e=>{if(e.target===panel)panel.remove();};
    panel.querySelector('.banCloseX').onclick=()=>panel.remove();
    panel.querySelector('#closeBanManager').onclick=()=>panel.remove();
    const list=panel.querySelector('#banListItems');
    if(bannedList.length===0){list.innerHTML="<li>Aucun profil banni.</li>";return;}
    bannedList.sort((a,b)=>new Date(b.date)-new Date(a.date)).forEach(item=>{
      const li=document.createElement('li');
      li.style.display='flex';li.style.justifyContent='space-between';li.style.marginBottom='6px';
      li.innerHTML=`<span>${item.name||'(sans nom)'} [${item.id}]<br><small>${new Date(item.date).toLocaleString()}</small></span>`;
      const btn=document.createElement('button');btn.textContent='Déban';
      btn.onclick=async()=>{await deactivateBan(item.airtableId);li.remove();};
      li.appendChild(btn);list.appendChild(li);
    });
  }

  function openTagModal(profilID,profilName){
    const current=tagMap[profilID]||[];
    const overlay=document.createElement('div');overlay.className='tagModalOverlay';
    Object.assign(overlay.style,{position:'fixed',top:0,left:0,right:0,bottom:0,background:'rgba(0,0,0,0.6)',display:'flex',alignItems:'center',justifyContent:'center',zIndex:'10001'});
    const modal=document.createElement('div');modal.className='tagModal';
    Object.assign(modal.style,{background:'#fff',padding:'20px',borderRadius:'8px',width:'90%',maxWidth:'500px',maxHeight:'80%',overflow:'auto',position:'relative'});
    const closeX=document.createElement('div');closeX.textContent='×';
    Object.assign(closeX.style,{position:'absolute',top:'8px',right:'10px',cursor:'pointer',fontSize:'20px',fontWeight:'bold'});
    closeX.onclick=()=>overlay.remove();
    modal.appendChild(closeX);
    const title=document.createElement('h3');title.textContent=`Tags pour ${profilName}`;title.style.marginBottom='10px';modal.appendChild(title);

    const negTitle=document.createElement('div');negTitle.textContent='🔴 Négatifs';Object.assign(negTitle.style,{fontWeight:'bold',marginTop:'10px',marginBottom:'5px'});modal.appendChild(negTitle);
    const negGrid=document.createElement('div');Object.assign(negGrid.style,{display:'flex',flexWrap:'wrap',gap:'6px',marginBottom:'15px'});modal.appendChild(negGrid);

    const posTitle=document.createElement('div');posTitle.textContent='🟢 Positifs';Object.assign(posTitle.style,{fontWeight:'bold',marginTop:'10px',marginBottom:'5px'});modal.appendChild(posTitle);
    const posGrid=document.createElement('div');Object.assign(posGrid.style,{display:'flex',flexWrap:'wrap',gap:'6px',marginBottom:'15px'});modal.appendChild(posGrid);

    const selected=new Set(current.map(t=>t.name));

    TAGS.forEach(t=>{
      const chip=document.createElement('div');chip.textContent=`${t.emoji} ${t.name} (${t.score>0?`+${t.score}`:t.score})`;
      chip.style.border='1px solid #ccc';chip.style.padding='4px 6px';chip.style.borderRadius='4px';chip.style.cursor='pointer';
      if(selected.has(t.name)){chip.style.background='#007aff';chip.style.color='#fff';}
      chip.onclick=()=>{if(selected.has(t.name)){selected.delete(t.name);chip.style.background='';chip.style.color='';}else{selected.add(t.name);chip.style.background='#007aff';chip.style.color='#fff';}};
      (t.type==='neg'?negGrid:posGrid).appendChild(chip);
    });

    const footer=document.createElement('div');footer.style.textAlign='right';footer.style.marginTop='10px';
    const cancel=document.createElement('button');cancel.textContent='Annuler';cancel.onclick=()=>overlay.remove();
    const valid=document.createElement('button');valid.textContent='Valider';
    valid.onclick=async()=>{
      const selectedTags=TAGS.filter(t=>selected.has(t.name));
      tagMap[profilID]=selectedTags;saveTaglistToCache();updateAllTagButtons();
      await setTags(profilID,selectedTags);
      overlay.remove();
    };
    footer.appendChild(cancel);footer.appendChild(valid);modal.appendChild(footer);

    overlay.appendChild(modal);
    overlay.onclick=e=>{if(e.target===overlay)overlay.remove();};
    document.body.appendChild(overlay);
  }

  function updateAllTagButtons(){
    document.querySelectorAll('[data-profilid]').forEach(btn=>{
      const id=btn.getAttribute('data-profilid');
      const tags=tagMap[id]||[];
      const score=tags.reduce((acc,t)=>acc+t.score,0);
      btn.textContent=(tags.length===0)?'#':`#${score>0?'+':''}${score}`;
    });
  }

  function addButtonsToListing(){
    document.querySelectorAll('a[href*="/escort/"]').forEach(link=>{
      const m=link.href.match(/\/escort\/([^\/]+)-(\d+)/);if(!m)return;
      const id=m[2], name=decodeURIComponent(m[1]).replace(/-/g,' ');
      const card=link.closest('div');if(!card)return;
      if(!isElementVisible(card))return;
      if(card.querySelector('.pinter-btn-container'))return;
      if(bannedList.some(b=>b.id===id)){card.style.display='none';return;}
      if(getComputedStyle(card).position==='static')card.style.position='relative';
      const containerDiv=document.createElement('div');containerDiv.className='pinter-btn-container';
      const ban=document.createElement('button');ban.textContent='🚫';ban.className='pinter-btn-ban';ban.onclick=e=>{e.preventDefault();e.stopPropagation();showConfirmPanel(id,name);};
      const tag=document.createElement('button');tag.textContent='#';tag.className='pinter-btn-tags';tag.setAttribute('data-profilid',id);tag.onclick=e=>{e.preventDefault();e.stopPropagation();openTagModal(id,name);};
      containerDiv.appendChild(ban);containerDiv.appendChild(tag);
      card.appendChild(containerDiv);
    });
    updateAllTagButtons();
  }

  function addButtonsToProfile(id){
    const bc=document.querySelector('.breadcrumbs a.element.active')||document.querySelector('h1');
    const name=bc?bc.textContent.trim():"(inconnu)";
    const container=document.querySelector('.main-photo')||document.querySelector('h1');
    if(container){
      container.style.position='relative';
      const containerDiv=document.createElement('div');containerDiv.className='pinter-btn-container';
      const ban=document.createElement('button');ban.textContent='🚫';ban.className='pinter-btn-ban';ban.onclick=()=>showConfirmPanel(id,name);
      const tag=document.createElement('button');tag.textContent='#';tag.className='pinter-btn-tags';tag.setAttribute('data-profilid',id);tag.onclick=()=>openTagModal(id,name);
      containerDiv.appendChild(ban);containerDiv.appendChild(tag);
      container.appendChild(containerDiv);
      updateAllTagButtons();
    }
  }

  function addGlobalUI(){
    const ui=document.createElement('button');ui.textContent='📋 Banlist';
    Object.assign(ui.style,{position:'fixed',top:'50px',right:'10px',background:'#fff',padding:'6px',border:'1px solid #ccc',borderRadius:'4px',zIndex:10000,opacity:0.7});
    ui.onmouseenter=()=>ui.style.opacity=1;ui.onmouseleave=()=>ui.style.opacity=0.7;
    ui.onclick=showBanManager;
    document.body.appendChild(ui);
  }

  // ===== Observer sécurisé =====
  let domObserver;
  function observeDOMChanges(){
    const container=document.querySelector('.content')||document.querySelector('#main')||document.body;
    const callback=(mutationsList)=>{
      domObserver.disconnect();
      addButtonsToListing();
      domObserver.observe(container,{childList:true,subtree:true});
    };
    domObserver=new MutationObserver(callback);
    domObserver.observe(container,{childList:true,subtree:true});
  }

  // ===== Init =====
  (async()=>{
    loadBanlistFromCache();
    loadTaglistFromCache();

    const m=window.location.pathname.match(/\/escort\/([^\/]+)-(\d+)/);
    if(m){
      const id=m[2];
      if(bannedList.some(b=>b.id===id)){
        document.body.innerHTML='<div style="padding:2em;background:#fee;">Ce profil est marqué comme fake.</div>';return;
      }
      addButtonsToProfile(id);
    }else{
      addButtonsToListing();
      observeDOMChanges();
    }
    addGlobalUI();

    const freshBan=await fetchBanListFromAirtable();bannedList=freshBan;saveBanlistToCache();
    const freshTags=await fetchTaglistFromAirtable();tagMap=freshTags;saveTaglistToCache();updateAllTagButtons();
  })();

})();

QingJ © 2025

镜像随时可能失效,请加Q群300939539或关注我们的公众号极客氢云获取最新地址