Greasy Fork

Greasy Fork is available in English.

Camamba Users Search Library

fetches Users

当前为 2022-12-10 提交的版本,查看 最新版本

此脚本不应直接安装。它是供其他脚本使用的外部库,要使用该库请加入元指令 // @require https://update.greasyfork.icu/scripts/446634/1127008/Camamba%20Users%20Search%20Library.js

您需要先安装一个扩展,例如 篡改猴Greasemonkey暴力猴,之后才能安装此脚本。

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

您需要先安装一个扩展,例如 篡改猴暴力猴,之后才能安装此脚本。

您需要先安装一个扩展,例如 篡改猴Userscripts ,之后才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。

您需要先安装用户脚本管理器扩展后才能安装此脚本。

(我已经安装了用户脚本管理器,让我安装!)

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

(我已经安装了用户样式管理器,让我安装!)

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

// http://greasyfork.icu/scripts/446634-camamba-users-search-library/

/* jslint esversion: 11 */

/**
 * @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;
    }
}

const UserLevelSearch = (() => {
    const cache = {};
    const maxDaysInCache = 1;

    return 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() {
            const key = `uls_${this.uid}`;
            let cachedSearch = cache[this.uid] || JSON.parse(await GM.getValue(key, "{}"));
            const timeStamp = cachedSearch[this.uid]?.timeStamp;

            if (!timeStamp || new Date().getTime() - timeStamp >= maxDaysInCache * 60 * 60 * 1000) {
                cachedSearch = await super.send();
                cache[this.uid] = cachedSearch;
                GM.setValue(key, JSON.stringify(cachedSearch));
            }

            return cachedSearch;
        }

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