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