Last active
February 14, 2025 22:26
-
-
Save s-nt-s/fd24c8c331ac4e6265b528550814a3c0 to your computer and use it in GitHub Desktop.
Filtrar chats de Idealista para que solo aparezcan personas con perfil relleno según nuestras necesidades
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// ==UserScript== | |
// @name Idealista chat | |
// @description Filtrar el chat de Idealista | |
// @version 1.4 | |
// @include https://www.idealista.com/conversations | |
// @run-at document-end | |
// @grant none | |
// ==/UserScript== | |
function my_hack_ajax() { | |
const CHAT_URL = "/conversations/list.ajax"; | |
class Cache { | |
static set(key, obj) { | |
sessionStorage.setItem(key, JSON.stringify(obj)); | |
console.log("Cache.SET "+key); | |
} | |
static get(key) { | |
const v = sessionStorage.getItem(key); | |
if (v==null) return null; | |
console.log("Cache.GET "+key); | |
return JSON.parse(v); | |
} | |
} | |
(function(XHR) { | |
const open = XHR.prototype.open; | |
XHR.prototype.open = function(method, url, async, user, pass) { | |
if (url.startsWith(CHAT_URL)) url = url.replace(/\bsize=\d+/, "size=200"); | |
open.call(this, method, url, async, user, pass); | |
}; | |
})(XMLHttpRequest); | |
const toVCard = (contacts) => { | |
return contacts.map(c => { | |
const uid = 'uid-' + c.phone.replace(/\D/g, ''); | |
return [ | |
`BEGIN:VCARD`, | |
`VERSION:3.0`, | |
`FN:${c.name}`, | |
c.phone==null?null:`TEL:${c.phone}`, | |
c.email.map(e=>`EMAIL:${e}`), | |
c.image==null?null:`PHOTO;TYPE=JPEG;VALUE=URI:${c.image}`, | |
`NOTE:${c.note.replace(/\n/g, '\n ')}`, | |
`UID:${uid}`, | |
`END:VCARD` | |
].flat().filter(x=>x!=null).join("\n")+"\n\n" | |
}).join("\n") | |
} | |
const waitForGlobal = function(key, callback) { | |
const gvar = window[key]; | |
if (gvar == null) | |
setTimeout(waitForGlobal, 100, arguments); | |
else | |
callback.apply(gvar, Array.from(arguments).slice(2)); | |
}; | |
const gJSON = function(obj) { | |
if (typeof obj === "string") { | |
obj = { | |
dataType: "json", | |
url: obj, | |
async: false | |
} | |
} | |
const r = $.ajax(obj) | |
if (r.responseJSON == null) { | |
console.error("gJSON() == null", obj); | |
return null; | |
} | |
return r.responseJSON; | |
} | |
const dwnLoad = (file, txt) => { | |
if (typeof txt != "string") txt = JSON.stringify(txt, null, 2); | |
const a = document.createElement("a"); | |
const b = new Blob([txt], { | |
'type': 'text/plain' | |
}); | |
a.href = URL.createObjectURL(b); | |
a.download = file; | |
a.click(); | |
} | |
const gChat = function(id, size) { | |
if (size==null) size = 100; | |
return gJSON({ | |
contentType: "application/json; charset=utf-8", | |
dataType: "json", | |
type: "POST", | |
url: "https://www.idealista.com/conversations/messages/summary.ajax", | |
async: false, | |
data: JSON.stringify({ | |
"conversationId": id, | |
"readSize": size, | |
"unreadSize": size, | |
}) | |
}).messages; | |
} | |
const addMethods = function(obj, flds) { | |
Object.entries(flds).forEach(([key, value]) => { | |
if (key.startsWith("_")) { | |
obj[key] = value.bind(obj); | |
return; | |
} | |
obj[key] = (function(cached, fnc) { | |
if (this[cached] == null) { | |
this[cached] = fnc.apply(this, Array.from(arguments).slice(2)); | |
} | |
return this[cached]; | |
}).bind(obj, "_cached_" + key, value); | |
}); | |
} | |
const partition = function(array, isValid) { | |
return array.reduce(([pass, fail], elem) => { | |
return isValid(elem) ? [ | |
[...pass, elem], fail | |
] : [pass, [...fail, elem]]; | |
}, [ | |
[], | |
[] | |
]); | |
} | |
const safeJsonParse = function(s, url) { | |
if (s==null) return null; | |
try { | |
return JSON.parse(s); | |
} catch (error) { | |
console.error(url+" is not a json", s); | |
return null; | |
} | |
} | |
const findAll = function () { | |
const txt = arguments[0]; | |
if (txt==null) return []; | |
const arr = []; | |
for (let i=1; i<arguments.length; i++) { | |
let mth = txt.match(arguments[i]); | |
(mth||[]).forEach(m=>{ | |
if (m.length>0 && !arr.includes(m)) arr.push(m); | |
}) | |
} | |
return arr; | |
} | |
class CHAT { | |
constructor(file) { | |
this._conversations = []; | |
this._file = file; | |
} | |
get chats() { | |
return this._conversations; | |
} | |
add(chats) { | |
if (chats.conversations) chats = chats.conversations; | |
const ids = chats.map(c => c.id); | |
this._conversations = this._conversations.filter(c => !ids.includes(c.id)).concat(chats); | |
} | |
getConversations(full) { | |
if (full) { | |
this._conversations.forEach(c => { | |
c.getMessages(); | |
c.hasMe(); | |
c.getProfile(); | |
}); | |
} | |
if (full!=false) return this._conversations; | |
const arr = this._conversations.map(c=>{ | |
const a = {}; | |
Object.entries(c).forEach(([k, v])=>{ | |
if (!k.startsWith('_cached_')) a[k]=v; | |
}) | |
return a; | |
}); | |
return arr; | |
} | |
save(full) { | |
dwnLoad(this._file+'.json', this.getConversations(full===true)); | |
} | |
getContacts() { | |
return this.getConversations().flatMap(c=>{ | |
if (c.role != "ADVERTISER") return []; | |
const ct = c._getContact(); | |
if (ct==null || ct.email==null || ct.email.length==0) return []; | |
return ct; | |
}) | |
} | |
vCard() { | |
const vCard = toVCard(this.getContacts()); | |
dwnLoad(this._file+'.vCard', vCard); | |
} | |
csv() { | |
let head = "First Name,Phone 1 - Value,Photo,Notes"; | |
let mls = 0; | |
const lines = this.getContacts().map(c=>{ | |
const note = (c.note||'').replace('"', "'").replace(/\n+/g, ". "); | |
let ln = `"${c.name}",${c.phone},${c.image},"${note}",${c.email[0]}`; | |
c.email.forEach(m=>ln+`,"${m}"`); | |
mls = Math.max(mls, c.email.length); | |
return ln.replace(/"(null|undefined)"/g, "").replace(/,(null|undefined),/g, ",,"); | |
}).join("\n"); | |
for (let i=1; i<=mls; i++) head = head + `,E-mail ${i} - Value`; | |
dwnLoad(this._file+'.csv', head+"\n"+lines+"\n"); | |
} | |
} | |
window.DEBUG_CONVERSATIONS = { | |
"ok": new CHAT("idealista-ok"), | |
"ko": new CHAT("idealista-ko") | |
} | |
waitForGlobal( | |
"jQuery", | |
(chat_filter) => { | |
jQuery.ajaxSetup({ | |
dataFilter: function(data, type) { | |
if (!this.url.startsWith(CHAT_URL)) return data; | |
const js = safeJsonParse(data, this.url); | |
if (js==null) return data; | |
if (!js.conversations) return data; | |
/* Añadir funcionalidad al objeto */ | |
js.conversations.forEach(c => { | |
addMethods(c, { | |
getMessages: function() { | |
const key = 'chat_'+this.id; | |
let chat = Cache.get(key); | |
if (chat!=null && this.lastMessage == chat.text && this.isFromMe == chat.fromMe) { | |
return chat; | |
} | |
chat = gChat(this.id); | |
Cache.set(key, chat); | |
return chat; | |
}, | |
hasMe: function() { | |
if (this.lastMessage && this.lastMessage.isFromMe) return true; | |
return this.getMessages().filter(m => m.fromMe).length > 0; | |
}, | |
getProfile: function () { | |
let prf = null; | |
const url = "/conversations/" + this.id + "/seeker-profile.ajax"; | |
if ((prf = Cache.get(url))==null) { | |
prf = gJSON(url).body; | |
Cache.set(url, prf); | |
} | |
return prf; | |
}, | |
getTxt: function() { | |
return this.getMessages().reverse().flatMap(m=>{ | |
if (m.fromMe) return []; | |
if (m.text==null) return []; | |
const t = m.text.trim(); | |
return t.length==0?[]:t; | |
}).join("\n\n").trim(); | |
}, | |
_getPlainTxt: function(prf) { | |
const coverLetter = (prf!=null && (typeof prf.coverLetter == "string"))?prf.coverLetter:''; | |
return (coverLetter+'\n\n'+this.getTxt()).toLowerCase() | |
.replace(/á/g, "a") | |
.replace(/é/g, "e") | |
.replace(/í/g, "i") | |
.replace(/ó/g, "o") | |
.replace(/ú/g, "i") | |
.trim(); | |
}, | |
_getIncome: function(prf) { | |
if (prf!=null && prf.income!=null) return prf.income; | |
if (c.hasMe()) return Infinity; | |
const txt = c._getPlainTxt(prf); | |
const num = findAll( | |
txt, | |
/[\d\.\,]+\s*€/g, | |
/[\d\.\,]+\s*euros/g, | |
/€[\d\.\,]+/g | |
).flat().flatMap(x=>{ | |
x = x.replace(/\D/, "").trim(); | |
if (x.length == 0) return []; | |
const n = parseInt(x); | |
if (isNaN(n)) return []; | |
if (n<1000) return []; | |
if (n>5000) return []; | |
return n; | |
}) | |
if (num.length==0) return Infinity; | |
return Math.min(...num); | |
}, | |
_getContact: function() { | |
const txt = this._getPlainTxt(); | |
const email = findAll( | |
txt, | |
/(([^<>()[\]\.,;:\s@\"]+(\.[^<>()[\]\.,;:\s@\"]+)*)|(\".+\"))@(([^<>()[\]\.,;:\s@\"]+\.)+[^<>()[\]\.,;:\s@\"]{2,})/g | |
).sort(); | |
const name = this.userName.toLowerCase().split(/\s+/).map(s=>s.length<2?s:s.charAt(0).toUpperCase() + s.slice(1)).join(" "); | |
const prf = this.getProfile(); | |
const income = this._getIncome(prf); | |
return { | |
name: name, | |
phone: prf!=null && prf.phone==null?null:prf.phone.number, | |
email: email, | |
image: this.userPhoto, | |
note: [ | |
prf==null?null:prf==null?null:prf.preview, | |
prf.coverLetter, | |
prf!=null && (prf.kids || prf.teenagers)?"con niños":null, | |
prf!=null && prf.ownsPet?"con mascota":null, | |
income!=null && income!=Infinity?income+" €/mes":null, | |
txt | |
].filter(x=>x!=null).join("\n"), | |
} | |
} | |
}); | |
}); | |
/* Filtrar */ | |
const [ok, ko] = partition(js.conversations, chat_filter); | |
if (ok.length != js.conversations.length) { | |
console.log("conversations", js.conversations.length, "->", ok.length); | |
js.conversations = ok; | |
data = JSON.stringify(js); | |
console.log(js); | |
} | |
window.DEBUG_CONVERSATIONS.ok.add(ok); | |
window.DEBUG_CONVERSATIONS.ko.add(ko); | |
return data; | |
} | |
}) | |
}, | |
(c) => { | |
/* Only filter my ads */ | |
if (c.role != "ADVERTISER") return true; | |
/* Only filter not starred chats */ | |
if (c.isStarred) return true; | |
const doBan = (...arr) => { | |
const d = new Date(c.date); | |
if ((d.getFullYear()+2)<(new Date()).getFullYear()) return false; | |
const s = d.toISOString().split('T')[0]; | |
console.log(c.id, s, ...arr); | |
return false; | |
} | |
/* NO fraud, agency, counteroffer or empty profile */ | |
if (c.isFraudWarning) return doBan("isFraudWarning"); | |
if (c.agencyName && c.agencyName.length) return doBan("isAgency"); | |
if (c.lastMessage && c.lastMessage.counterOffer != null) return doBan("isCounteroffer"); | |
if (!(c.userProfilePreview && c.seekerHasUserProfile)) return doBan("isNotProfile"); | |
/* NO pet */ | |
if (/con mascota/.test(c.userProfilePreview)) return doBan("isPet", c.userProfilePreview); | |
/* NO minors */ | |
if (/con menores/.test(c.userProfilePreview)) return doBan("hasMinors", c.userProfilePreview); | |
const people = (()=>{ | |
if (c.userProfilePreview==null) return Infinity; | |
const userProfilePreview = c.userProfilePreview.trim().toLowerCase(); | |
const w1 = userProfilePreview.replace(/[, ].*$/, ""); | |
if (w1 == "pareja") return 2; | |
const fam = userProfilePreview.match(/familia de (\d+)/); | |
if (fam!=null && fam.length>0) { | |
const f = parseInt(fam[fam.length-1], 10); | |
if (!isNaN(f)) return -f; | |
} | |
const n1 = parseInt(w1, 10); | |
if (!isNaN(n1)) return n1; | |
if (w1 == "familia") return 3; | |
return Infinity; | |
})(); | |
/* NO too many people */ | |
if (Math.abs(people) > 2) return doBan("tooManyPeople", people, c.userProfilePreview); | |
/* Family of 2 persons usually has minors */ | |
if (people<0) return doBan("isFamily", people, c.userProfilePreview); | |
const prf = c.getProfile(); | |
/* NO in Spain yet */ | |
if (prf.phone && prf.phone.prefix && prf.phone.prefix != "34") return doBan("isNotResident", prf.phone.prefix); | |
const banIfMath = (msg, ...re) => { | |
const mth = findAll(c._getPlainTxt(prf), ...re); | |
if (mth == null || mth.length == 0) return false; | |
doBan(msg, ...mth); | |
return true; | |
} | |
/* NO minors */ | |
if (banIfMath( | |
"hasMinors", | |
/con una? niñ[oa]/g, | |
/con una? bebe/g, | |
)) return false; | |
if (banIfMath( | |
"isFamily", | |
/para mi madre y yo/g, | |
)) return false; | |
if (banIfMath( | |
"needAval", | |
/\baval\b/g, | |
/\bavalistas?\b/g, | |
)) return false; | |
const income = c._getIncome(prf); | |
/* NO low income */ | |
const rent = parseInt(c.ads[0].priceInfo.split()[0], 10); | |
if (income!=null) { | |
if (rent > (income * 0.40)) return doBan("isLowIncome", income); | |
if (people>1) { | |
if (income == Infinity) return doBan("dontHaveIncomeInfo", people); | |
if ((rent/people) > ((income/people) * 0.35)) return doBan("isLowIncomeForPeogle", people, income); | |
} | |
} | |
/* | |
if (banIfMath( | |
"notSingleMatch", | |
/para mi y mi pareja/g, | |
/para mi y mi marido/g, | |
/para mi y mi mujer/g, | |
/para mi y mi esposo/g, | |
/para mi y mi esposa/g, | |
/somos un matrimonio/g, | |
)) return false; | |
*/ | |
return true; | |
} | |
); | |
} | |
const script = document.createElement("script"); | |
script.textContent = "(" + my_hack_ajax.toString() + ")();"; | |
document.head.appendChild(script); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment