pinter.pro

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

You will need to install an extension such as Tampermonkey, Greasemonkey or Violentmonkey to install this script.

You will need to install an extension such as Tampermonkey to install this script.

You will need to install an extension such as Tampermonkey or Violentmonkey to install this script.

You will need to install an extension such as Tampermonkey or Userscripts to install this script.

You will need to install an extension such as Tampermonkey to install this script.

You will need to install a user script manager extension to install this script.

(I already have a user script manager, let me install it!)

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

(I already have a user style manager, let me install it!)

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