// ==UserScript==
// @name Camamba Users Search Library
// @namespace hoehleg.userscripts.private
// @version 0.0.1
// @description fetches Users
// @author Gerrit Höhle
// @license MIT
//
// @require https://greasyfork.org/scripts/405144-httprequest/code/HttpRequest.js?version=1063178
//
// @grant GM_xmlhttpRequest
// ==/UserScript==
// https://greasyfork.org/de/scripts/446634-camamba-users-search-library
/* jslint esversion: 9 */
class GuessLogSearch extends HttpRequestHtml {
constructor(name) {
/**
* @param {string} labelText
* @param {string} textContent
* @returns {number}
*/
const matchScore = (labelText, textContent) => {
const regexLookBehind = new RegExp("(?<=" + labelText + ":\\s)");
const regexFloat = /\d{1,2}\.?\d{0,20}/;
const regexLookAhead = /(?=\spoints)/;
for (const regexesToJoin of [
[regexLookBehind, regexFloat, regexLookAhead],
[regexLookBehind, regexFloat]
]) {
const regexAsString = regexesToJoin.map(re => re.source).join("");
const matcher = new RegExp(regexAsString, "i").exec(textContent);
if (matcher != null) {
return Number.parseFloat(matcher[0]);
}
}
};
/**
* @param {RegExp} regex
* @param {string} textContent
* @returns {Array<String>}
*/
const matchList = (regex, textContent) => {
const results = [...textContent.matchAll(regex)].reduce((a, b) => [...a, ...b], []);
if (results.length) {
const resultsDistinct = [...new Set(results)];
return resultsDistinct;
}
};
super({
url: 'https://www.camamba.com/guesslog.php',
params: { name },
resultTransformer: (resp) => {
const textContent = resp.html.body.textContent;
const ipList = matchList(/(?:(?:25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9][0-9]|[0-9])\.){3}(?:25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9][0-9]|[0-9])/g, textContent);
const prints = matchList(/(?<=Print\d{0,2}\schecked\sis\s)[0-9a-f]+/g, textContent);
const scorePassword = matchScore("password check", textContent);
const scoreFinal = matchScore("final score", textContent);
return { userName: name, ipList, prints, scorePassword, scoreFinal };
}
});
}
/** @returns {Promise<GuessLog>} */
async send() {
return await super.send();
}
/**
* @param {string} name
* @returns {Promise<GuessLog>}
*/
static async send(name) {
return await new GuessLogSearch(name).send();
}
}
class GalleryImage {
constructor({ dataURI, href }) {
/** @type {string} */
this.dataURI = dataURI;
/** @type {string} */
this.href = href;
}
}
class User {
/** @param {UserParams} param0 */
constructor({
name, uid = 0, gender = null, age = null,
longitude = null, latitude = null, location = null, distanceKM = null,
isReal = null, hasPremium = null, hasSuper = null, isPerma = null,
isOnline = null, room = null, lastSeen = null, regDate = null,
dateToHumanReadable = (date) => date ?
date.toLocaleString('de-DE', { timeStyle: "medium", dateStyle: "short", timeZone: 'CET' }) : '',
}) {
/** @type {string} */
this.name = String(name);
/** @type {number?} */
this.uid = uid;
/** @type {'male'|'female'|'couple'?} */
this.gender = gender;
/** @type {number?} */
this.age = age;
/** @type {number?} */
this.longitude = longitude;
/** @type {number?} */
this.latitude = latitude;
/** @type {string?} */
this.location = location;
/** @type {number?} */
this.distanceKM = distanceKM;
/** @type {boolean?} */
this.isReal = isReal;
/** @type {boolean?} */
this.hasPremium = hasPremium;
/** @type {boolean?} */
this.hasSuper = hasSuper;
/** @type {boolean?} */
this.isPerma = isPerma;
/** @type {boolean?} */
this.isOnline = isOnline;
/** @type {string?} */
this.room = room;
/** @type {Date?} */
this.lastSeen = lastSeen;
/** @type {Date?} */
this.regDate = regDate;
/** @type {string[]} */
this.prints = [];
/** @type {string[]} */
this.ipList = [];
/** @type {number?} */
this.scorePassword = null;
/** @type {number?} */
this.scoreFinal = null;
/** @type {number} */
this.guessLogTS = null;
/** @type {(date: Date) => string} */
this.dateToHumanReadable = dateToHumanReadable;
/** @type {number?} */
this.level = null;
/** @type {number} */
this.levelTS = null;
/** @type {string[]} */
this.galleryData = [];
/** @type {number} */
this.galleryDataTS = null;
}
/** @type {string} @readonly */
get lastSeenHumanReadable() {
return this.dateToHumanReadable(this.lastSeen);
}
/** @type {string} @readonly */
get regDateHumanReadable() {
return this.dateToHumanReadable(this.regDate);
}
get galleryAsImgElements() {
if (!this.galleryData) {
return [];
}
return this.galleryData.map(data => Object.assign(document.createElement('img'), {
src: data.dataURI
}));
}
async updateGalleryData() {
const pictureLinks = (await HttpRequestHtml.send({
url: "https://www.camamba.com/profile_view.php",
params: Object.assign(
{ m: 'gallery' },
this.uid ? { uid: this.uid } : { user: this.name }
),
pageNr: 1,
pagesMaxCount: 500,
resultTransformer: (response) => {
const hrefList = [...response.html.querySelectorAll("img.picborder")].map(img => img.src);
return hrefList.map(href => href.slice(0, 0 - ".s.jpg".length) + ".l.jpg");
},
hasNextPage: (_resp, _httpRequestHtml, lastResult) => lastResult.length >= 15,
paramsConfiguratorForPageNr: (params, pageNr) => ({ ...params, page: pageNr - 1 }),
})).flat();
const readGalleryData = pictureLinks.map(href => (async () => {
const dataURI = await HttpRequestBlob.send({ url: href });
return new GalleryImage({ dataURI, href });
})());
this.galleryData = await Promise.all(readGalleryData);
this.galleryDataTS = new Date().getTime();
}
async updateLevel() {
this.level = await HttpRequestHtml.send({
url: 'https://www.camamba.com/user_level.php',
params: { uid: this.uid },
resultTransformer: (response) => {
const levelElement = response.html.querySelector('font.xxltext');
if (levelElement) {
const levelMatch = /\d{1,3}/.exec(levelElement.textContent);
if (levelMatch) {
return parseInt(levelMatch);
}
}
return null;
}
});
this.levelTS = new Date().getTime();
}
async updateGuessLog() {
/** @type {GuessLog} */
const guessLog = await GuessLogSearch.send(this.name);
this.guessLogTS = new Date().getTime();
this.ipList = guessLog.ipList;
this.prints = guessLog.prints;
this.scorePassword = guessLog.scorePassword;
this.scoreFinal = guessLog.scoreFinal;
}
stringify() {
}
static parse() {
}
}
class UserSearch extends HttpRequestHtml {
/** @param {UserSearchParams} param0 */
constructor({
name = '',
gender = 'any',
isOnline = null,
hasReal = null,
hasPremium = null,
hasSuper = null,
hasPicture = null,
isSortByRegDate = null,
isSortByDistance = null,
isShowAll = null,
pageNr = 1,
pagesMaxCount = 1,
keepInCacheTimoutMs
} = {}) {
super({
url: 'https://www.camamba.com/search.php',
params: Object.assign(
{
nick: name,
gender: gender.toLowerCase(),
page: Math.max(pageNr - 1, 0),
},
Object.fromEntries(Object.entries({
online: isOnline,
isreal: hasReal,
isprem: hasPremium,
issuper: hasSuper,
picture: hasPicture,
sortreg: isSortByRegDate,
byDistance: isSortByDistance,
showall: isShowAll,
})
.filter(([_k, v]) => typeof v !== 'undefined' && v !== null)
.map(([k, v]) => ([[k], v ? 1 : 0])))
),
pageNr: Math.max(pageNr, 1),
pagesMaxCount: Math.max(pagesMaxCount, 1),
keepInCacheTimoutMs,
resultTransformer: (response) => {
/** @type {Array<User>} */
const users = [];
for (const tdNode of response.html.querySelectorAll('.searchSuper td:nth-child(2), .searchNormal td:nth-child(2)')) {
const innerHTML = tdNode.innerHTML;
const uidMatch = /<a\s+?href=["']javascript:sendMail\(["'](\d{1,8})["']\)/.exec(innerHTML);
const nameMatch = /<a\s+?href=["']javascript:openProfile\(["'](.+?)["']\)/.exec(innerHTML);
if (!uidMatch || !nameMatch) {
break;
}
const user = new User({
name: nameMatch[1],
uid: Number.parseInt(uidMatch[1]),
isReal: /<img src="\/gfx\/real.png"/.test(innerHTML),
hasPremium: /<a href="\/premium.php">/.test(innerHTML),
hasSuper: /<img src="\/gfx\/super_premium.png"/.test(innerHTML),
isOnline: /Online\snow(\s\in|,\snot in chat)/.test(innerHTML),
});
// Längengrad, Breitengrad, Ortsname
const locationMatch = /<a\s+?href="javascript:openMap\((-?\d{1,3}\.\d{8}),(-?\d{1,3}\.\d{8})\);">(.+?)<\/a>/.exec(innerHTML);
if (locationMatch) {
user.longitude = Number.parseFloat(locationMatch[1]);
user.latitude = Number.parseFloat(locationMatch[2]);
user.location = locationMatch[3];
}
// Entfernung in km
const distanceMatch = /(\d{1,5})\skm\sfrom\syou/.exec(innerHTML);
if (distanceMatch) {
user.distanceKM = parseInt(distanceMatch[1]);
}
// Geschlecht und Alter
const genderAgeMatch = /(male|female|couple),\s(\d{1,3})(?:<br>){2}Online/.exec(innerHTML);
if (genderAgeMatch) {
user.gender = genderAgeMatch[1];
user.age = genderAgeMatch[2];
}
// zuletzt Online
if (user.isOnline) {
user.lastSeen = new Date();
} else {
const lastSeenMatch = /(\d{1,4})\s(minutes|hours|days)\sago/.exec(innerHTML);
if (lastSeenMatch) {
const value = parseInt(lastSeenMatch[1]);
const factorToMillis = {
'minutes': 1000 * 60,
'hours': 1000 * 60 * 60,
'days': 1000 * 60 * 60 * 24,
}[lastSeenMatch[2]];
user.lastSeen = new Date(Date.now() - value * factorToMillis);
}
}
// Raumname
const roomMatch = /(?:ago|now)\sin\s([\w\s]+?|p\d{1,8})<br>/.exec(innerHTML);
if (roomMatch) {
user.room = roomMatch[1];
}
// regDate
const regDateMatch = /(\d{2}).(\d{2}).(\d{4})\s(\d{1,2}):(\d{2}):(\d{2})/.exec(innerHTML);
if (regDateMatch) {
const regDateDay = regDateMatch[1];
const regDateMonth = regDateMatch[2];
const regDateYear = regDateMatch[3];
const regDateHour = regDateMatch[4];
const regDateMinute = regDateMatch[5];
const regDateSecond = regDateMatch[6];
user.regDate = new Date(regDateYear, regDateMonth - 1, regDateDay, regDateHour, regDateMinute, regDateSecond);
}
users.push(user);
}
return users;
},
hasNextPage: (_resp, _httpRequestHtml, lastResult) => {
return lastResult.length >= 50;
},
paramsConfiguratorForPageNr: (params, pageNr) => ({ ...params, page: pageNr }),
});
}
/** @returns {Promise<User[]>} */
async send() {
return (await super.send()).flat();
}
/**
* @param {UserSearchParams} param0
* @returns {Promise<User[]>}
*/
static async send(param0) {
return await new UserSearch(param0).send();
}
}