Camamba Users Search Library

fetches Users

Per 02-09-2022. Zie de nieuwste versie.

Dit script moet niet direct worden geïnstalleerd - het is een bibliotheek voor andere scripts om op te nemen met de meta-richtlijn // @require https://update.greasyfork.org/scripts/446634/1088943/Camamba%20Users%20Search%20Library.js

Voor het installeren van scripts heb je een extensie nodig, zoals Tampermonkey, Greasemonkey of Violentmonkey.

Voor het installeren van scripts heb je een extensie nodig, zoals Tampermonkey of Violentmonkey.

Voor het installeren van scripts heb je een extensie nodig, zoals Tampermonkey of Violentmonkey.

Voor het installeren van scripts heb je een extensie nodig, zoals Tampermonkey of Userscripts.

Voor het installeren van scripts heb je een extensie nodig, zoals {tampermonkey_link:Tampermonkey}.

Voor het installeren van scripts heb je een gebruikersscriptbeheerder nodig.

(Ik heb al een user script manager, laat me het downloaden!)

Voor het installeren van gebruikersstijlen heb je een extensie nodig, zoals {stylus_link:Stylus}.

Voor het installeren van gebruikersstijlen heb je een extensie nodig, zoals {stylus_link:Stylus}.

Voor het installeren van gebruikersstijlen heb je een extensie nodig, zoals {stylus_link:Stylus}.

Voor het installeren van gebruikersstijlen heb je een gebruikersstijlbeheerder nodig.

Voor het installeren van gebruikersstijlen heb je een gebruikersstijlbeheerder nodig.

Voor het installeren van gebruikersstijlen heb je een gebruikersstijlbeheerder nodig.

(Ik heb al een beheerder - laat me doorgaan met de installatie!)

// ==UserScript==
// @name         Camamba Users Search Library
// @namespace    hoehleg.userscripts.private
// @version      0.0.6
// @description  fetches Users
// @author       Gerrit Höhle
// @license MIT
//
// @require      https://greasyfork.org/scripts/405144-httprequest/code/HttpRequest.js?version=1063408
//  
// @grant        GM_xmlhttpRequest
// ==/UserScript==

// https://greasyfork.org/scripts/446634-camamba-users-search-library/

/* jslint esversion: 9 */

/**
 * @typedef {object} UserParams
 * @property {string} name
 * @property {uid} [number]
 * @property {'male'|'female'|'couple'?} [gender]
 * @property {number} [age]
 * @property {number} [level]
 * @property {number} [longitude]
 * @property {number} [latitude]
 * @property {string} [location]
 * @property {number} [distanceKM]
 * @property {boolean} [isReal]
 * @property {boolean} [hasPremium]
 * @property {boolean} [hasSuper]
 * @property {boolean} [isPerma]
 * @property {boolean} [isOnline]
 * @property {string} [room]
 * @property {Date} [lastSeen]
 * @property {Date} [regDate]
 * @property {string[]} [ipList]
 * @property {Date} [scorePassword]
 * @property {Date} [scoreFinal]
 * @property {(date: Date) => string} [dateToHumanReadable]
 */

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

/**
 * @typedef {Object} BanLog
 * @property {string} moderator - user or moderator who triggered the log
 * @property {string} user - user who is subject
 * @property {Date} date - date of this log
 * @property {string} reason - content
 */

class BannLogSearch extends HttpRequestHtml {
    /**
     * @param {number} uid 
     */
    constructor(uid = null) {
        super({
            url: 'https://www.camamba.com/banlog.php',
            params: uid ? { admin: uid } : {},
            resultTransformer: (response, _request) => {
                const results = [];
                const xPathExpr = "//tr" + ['User', 'Moderator', 'Date', 'Reason'].map(hdrText => `[td[span[text()='${hdrText}']]]`).join("");
                let tr = (response.html.evaluate(xPathExpr, response.html.body, null, XPathResult.FIRST_ORDERED_NODE_TYPE, null).singleNodeValue || {}).nextElementSibling;

                while (tr) {
                    const tds = tr.querySelectorAll('td');
                    const user = tds[0].querySelector("a") || tds[0].textContent;
                    const moderator = tds[1].textContent;

                    let date;
                    const dateMatch = /(\d{2}).(\d{2}).(\d{4})<br>(\d{1,2}):(\d{2}):(\d{2})/.exec(tds[2].innerHTML);
                    if (dateMatch) {
                        const day = dateMatch[1];
                        const month = dateMatch[2];
                        const year = dateMatch[3];
                        const hour = dateMatch[4];
                        const minute = dateMatch[5];
                        const second = dateMatch[6];
                        date = new Date(year, month - 1, day, hour, minute, second);
                    }

                    const reason = tds[3].textContent;
                    results.push({ user, moderator, date, reason });

                    tr = tr.nextElementSibling;
                }

                return results;
            }
        });
    }

    /**
     * @param {number} uid
     * @returns {Promise<BanLog[]>}
     */
    static async send(uid) {
        return await new BannLogSearch(uid).send();
    }
}

class GalleryImage {
    constructor({ dataURI, href }) {
        /** @type {string} */
        this.dataURI = dataURI;
        /** @type {string} */
        this.href = href;
    }
}


class UserLevel {
    constructor({ level, uid = null, name = null, timeStamp = null }) {

        /** @type {number} */
        this.level = level !== null ? Number.parseInt(level) : null;

        /** @type {number} */
        this.uid = uid !== null ? Number.parseInt(uid) : null;

        /** @type {string} */
        this.name = name;

        /** @type {number} */
        this.timeStamp = timeStamp !== null ? Number.parseInt(timeStamp) : null;
    }
}

class UserLevelSearch extends HttpRequestHtml {
    constructor(uid) {
        super({
            url: 'https://www.camamba.com/user_level.php',
            params: { uid },
            resultTransformer: (response, request) => {
                const html = response.html;

                let name = null, level = null;

                const nameElement = html.querySelector('b');
                if (nameElement) {
                    name = nameElement.textContent;
                }

                const levelElement = html.querySelector('font.xxltext');
                if (levelElement) {
                    const levelMatch = /\d{1,3}/.exec(levelElement.textContent);
                    if (levelMatch) {
                        level = Number.parseInt(levelMatch);
                    }
                }

                return new UserLevel({ uid: request.params.uid, name, level, timeStamp: new Date().getTime() });
            }
        });
    }

    /**
     * @returns {Promise<UserLevel>}
     */
    async send() {
        return await super.send();
    }

    /**
     * @param {number} uid 
     * @returns {Promise<UserLevel>}
     */
    static async send(uid) {
        return await new UserLevelSearch(uid).send();
    }
}

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 updateGalleryHref() {
        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: 0,
            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) => {
                return lastResult.length >= 15;
            },
            paramsConfiguratorForPageNr: (params, pageNr) => ({ ...params, page: pageNr }),
        })).flat();

        this.galleryData = pictureLinks.map(href => ({ href }));
        this.galleryDataTS = new Date().getTime();
    }

    async updateGalleryData(includeUpdateOfHref = true) {
        if (includeUpdateOfHref) {
            await this.updateGalleryHref();
        }

        const readGalleryData = this.galleryData.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() {
        const { level, timeStamp, name } = await UserLevelSearch.send(this.uid);
        this.level = level;
        this.levelTS = timeStamp;
        this.name = name;
    }

    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;
    }

    async addNote(text) {
        return new Promise((res, rej) => {
            if (this.uid) {
                GM_xmlhttpRequest({
                    url: 'https://www.camamba.com/profile_view.php',
                    method: 'POST',
                    data: `uid=${this.uid}&modnote=${encodeURIComponent(text)}&m=admin&nomen=1`,
                    headers: {
                        "Content-Type": "application/x-www-form-urlencoded"
                    },
                    onload: (xhr) => {
                        res(xhr.responseText);
                    },
                    onerror: (xhr) => rej({
                        status: xhr.status,
                        statusText: xhr.statusText
                    }),
                });
            } else {
                rej({
                    status: 500,
                    statusText: "missing uid"
                });
            }
        });
    }
}

class UserSearch extends HttpRequestHtml {
    /** @param {{ 
     * name: string?, 
     * uid: number?,
     * gender: ('any' | 'male' | 'female' |'couple')?, 
     * isOnline: boolean?, hasReal: boolean?, hasPremium: boolean?, hasSuper: boolean?, hasPicture: boolean?,
     * isSortByRegDate: boolean?,
     * isSortByDistance: boolean?,
     * isShowAll: boolean?,
     * pageNr: number?,
     * pagesMaxCount: number?,
     * keepInCacheTimoutMs: number?
     * }} param0 */
    constructor({
        name = null,
        uid = 0,
        gender = 'any',
        isOnline = null,
        hasReal = null,
        hasPremium = null,
        hasSuper = null,
        hasPicture = null,
        isSortByRegDate = null,
        isSortByDistance = null,
        isShowAll = null,
        pageNr = 1,
        pagesMaxCount = 1,
        keepInCacheTimoutMs
    } = {}) {
        let params = Object.assign(
            (name ? {
                nick: name
            } : {}),
            {
                gender: gender.toLowerCase(),
            },
            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])))
        );

        params = Object.entries(params).map(([key, value]) => key + '=' + value).join('&');

        if (params.length) {
            params += "&";
        }
        params += `page=${Math.max(pageNr - 1, 0)}`;

        super({
            url: 'https://www.camamba.com/search.php',
            params,
            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,4})(?:<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) => {
                return params.replace(/page=\d+(?:$)/, `page=${pageNr - 1}`);
            },
        });
        this.uid = uid || null;
    }

    /** @returns {Promise<User[]>} */
    async send() {
        if (this.uid) {
            const user = new User({ uid: this.uid });
            await user.updateLevel();

            if (!user.name || user.level) {
                return [];
            }
            if (this.params.nick) {
                const unameURIencoded = encodeURIComponent(user.name.toLowerCase());
                const unameFromSearchParam = encodeURIComponent(this.params.nick.toLowerCase()).trim();
                if (unameURIencoded.includes(unameFromSearchParam)) {
                    return [];
                }
            }

            this.params.nick = user.name;
            const result = (await super.send()).flat().find(u => u.uid == this.uid);
            if (!result) {
                return [];
            }

            return [Object.assign(user, result)];
        }

        return (await super.send()).flat();
    }

    /**
     * @param {{ 
     * name: string, 
     * uid: number?,
     * gender: 'any' | 'male' | 'female' |'couple', 
     * isOnline: boolean,hasReal: boolean, hasPremium: boolean, hasSuper: boolean, hasPicture: boolean,
     * isSortByRegDate: boolean,
     * isSortByDistance: boolean,
     * isShowAll: boolean,
     * pageNr: number,
     * pagesMaxCount: number,
     * keepInCacheTimoutMs: number
     * }} param0 
     * @returns {Promise<User[]>} 
     */
    static async send(param0) {
        return await new UserSearch(param0).send();
    }
}