pinter.pro

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

// ==UserScript==
// @name         pinter.pro
// @namespace    http://tampermonkey.net/
// @version      6.7
// @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 = "patmIxxKmoSuJmA3f.18fe741333ad27c49416a18a7e9c0a45ae1c7e1b5bfee4e191d550decd751c7b";
  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:-2,emoji:"🧊",type:"neg"},
    {name:"Pas impliquée",score:-1,emoji:"🤷‍♀️",type:"neg"},
    {name:"Chrono",score:-1,emoji:"⏱️",type:"neg"},
    {name:"Pas trés propre",score:-1,emoji:"🚿⚠️",type:"neg"},
    {name:"Pas du tout propre",score:-2,emoji:"🚿🚫",type:"neg"},
    {name:"Prix +",score:-1,emoji:"💸🔺",type:"neg"},
    {name:"A fuir urgence",score:-3,emoji:"🤮",type:"neg"},
    {name:"Change-personne",score:-2,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 {
    // Étape 1 : Récupérer tous les anciens tags actifs pour ce profil dans Airtable
    const res = await fetch(`https://api.airtable.com/v0/${AIRTABLE_BASE}/${TABLE_TAGS}?filterByFormula=AND(ProfilID="${profilID}",Active=TRUE)`, {
      headers: {
        Authorization: `Bearer ${AIRTABLE_TOKEN}`
      }
    });
    const data = await res.json();

    // Étape 2 : Désactiver les anciens en les patchant (Active: false)
    if (data.records) {
      for (const record of data.records) {
        await fetch(`https://api.airtable.com/v0/${AIRTABLE_BASE}/${TABLE_TAGS}/${record.id}`, {
          method: 'PATCH',
          headers: {
            Authorization: `Bearer ${AIRTABLE_TOKEN}`,
            'Content-Type': 'application/json'
          },
          body: JSON.stringify({
            fields: { Active: false }
          })
        });
      }
    }

    // Étape 3 : Ajouter les nouveaux tags sélectionnés
    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 CSS =====
  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);

  // ===== Ban Confirmation Panel =====
  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();
    };
  }

  // ===== Affichage et gestion de la banlist =====
  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);
    });
  }
  // ===== UI Tag modal =====
  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);
  }

  // Mise à jour score tag affiché
  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}`;
    });
  }

  // Ajout des boutons dans les listings
  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 || !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();
  }
  // Ajoute les boutons sur une fiche profil
  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();
    }
  }

  // Bouton global pour afficher la banlist
  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);
  }

  // Observateur DOM pour injecter automatiquement dans les nouveaux listings
  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});
  }

  // Initialisation du script
  (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或关注我们的公众号极客氢云获取最新地址