Camamba User

User represents a camamba User, UserSearch represents a search-request for one or many users

目前為 2021-05-24 提交的版本,檢視 最新版本

此腳本不應該直接安裝,它是一個供其他腳本使用的函式庫。欲使用本函式庫,請在腳本 metadata 寫上: // @require https://update.cn-greasyfork.org/scripts/405699/934111/Camamba%20User.js

您需要先安裝使用者腳本管理器擴展,如 TampermonkeyGreasemonkeyViolentmonkey 之後才能安裝該腳本。

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

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyViolentmonkey 後才能安裝該腳本。

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyUserscripts 後才能安裝該腳本。

你需要先安裝一款使用者腳本管理器擴展,比如 Tampermonkey,才能安裝此腳本

您需要先安裝使用者腳本管理器擴充功能後才能安裝該腳本。

(我已經安裝了使用者腳本管理器,讓我安裝!)

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

(我已經安裝了使用者樣式管理器,讓我安裝!)

// ==UserScript==
// @name         Camamba User
// @namespace    dannysaurus.camamba
//
// @include      http://www.camamba.com/*
// @include      https://www.camamba.com/*
// @include      http://www.de.camamba.com/*
// @include      https://www.de.camamba.com/*
//
// @connect      camamba.com
// @grant        GM_xmlhttpRequest
//
// @require      https://greasyfork.org/scripts/405144-httprequest/code/HttpRequest.js
// @require      https://greasyfork.org/scripts/391854-enum/code/Enum.js
//
// @require      https://ajax.googleapis.com/ajax/libs/jquery/3.5.1/jquery.min.js
//
// @version      0.1
// @license      MIT License
// @description  User represents a camamba User, UserSearch represents a search-request for one or many users
// @author       Gerrit Höhle
// ==/UserScript==

/* jslint esnext: true */
/* globals HttpRequest, HttpRequestHtml, Enum, $, jQuery */
const User = (() => {
    'use strict';
    const GENDERS = class GENDER extends Enum {}.init({ ANY: 'any' , FEMALE: 'female' , MALE: 'male', COUPLE: 'couple' });

    return class User {
        constructor({ uid, nick, age, gender, location, distanceKm, latitudeDec, longitudeDec, isOnline, lastSeen, room, roomId, isReal, isPremium, isSuper, userLevel, timeStamp = new Date() } = {}) {
            Object.assign(this, { uid, nick, age, _gender: gender, location, distanceKm, latitudeDec, longitudeDec, isOnline, lastSeen, room, roomId, isReal, isPremium, isSuper, userLevel, timeStamp });
        }

        get gender() {
            return this._gender instanceof GENDERS ? this._gender : GENDERS.ANY;
        }

        set gender(value) {
            if (value instanceof GENDERS) {
                this._gender = value;
            } else {
                this._gender = [ ...GENDERS ].find(gender => gender.toString() === String(value)) || GENDERS.ANY;
            }
        }

        static get GENDERS() {
            return GENDERS;
        }
    };
})();

const UserSearch = (() => {
    const CONST = {
        URL_SEARCH: 'https://www.camamba.com/search.php',
        URL_USER_LEVEL: 'https://www.camamba.com/user_level.php'
    };

    const UserControllerInternal = (() => {
        const GENDERS = User.GENDERS;

        const getTextNodeValues = node => {
            if (node.nodeType === Node.TEXT_NODE) {
                const nodeValue = node.nodeValue.replace(/"/,'');
                return (/\S/.test(nodeValue)) ? [ nodeValue ] : [];
            }
            return Array.from(node.childNodes).flatMap(node => getTextNodeValues(node));
        };

        const fetchJsLinks = (html, user) => {
            const jsLinks = Object.assign( {}, ...[ 'openProfile', 'sendMail', 'openChat', 'openMap' ].map(fncName => {
                const linkElement = $(html).find(`a[href*="javascript:${fncName}("]`);
                if (linkElement.length > 0) {
                    const href = linkElement.attr('href') || '';
                    const fncParam = ((href.match(/\((.+)\)/) || [])[1] || '').replace(/['"]/g,'');
                    const innerHtml = (linkElement.html() || '').trim();

                    return { [fncName]: { href, fncParam, html: innerHtml } };
                }
            }));

            if (typeof jsLinks.sendMail !== 'undefined') {
                user.uid = Number.parseInt(jsLinks.sendMail.fncParam);
            }

            if (typeof jsLinks.openProfile !== 'undefined') {
                user.nick = jsLinks.openProfile.fncParam;
            }

            if (typeof jsLinks.openChat !== 'undefined') {
                user.roomId = jsLinks.openChat.fncParam;
                user.room = jsLinks.openChat.html;
            }

            if (typeof jsLinks.openMap !== 'undefined') {
                const matchCoords = jsLinks.openMap.fncParam.match(/(-?\d{1,3}\.\d{8}),(-?\d{1,2}\.\d{8})/);
                if (matchCoords !== null) {
                    user.longitudeDec = Number.parseFloat(matchCoords[1]);
                    user.latitudeDec = Number.parseFloat(matchCoords[2]);
                }
                user.location = jsLinks.openMap.html;
            }
        };

        const fetchTextNodes = (html, user) => {
            const textNodes = getTextNodeValues(html);

            for (let gender of [ GENDERS.FEMALE, GENDERS.MALE, GENDERS.COUPLE ].map(enm => enm.text)) {
                const match = textNodes[1].match(new RegExp(`^\\W*\(${gender}\),\\s\(\\d{1,3}\)`));
                if (match !== null) {

                    user.age = Number.parseInt(match[2]);
                    user.gender = match[1];
                    break;
                }
            }

            const matches = textNodes[2].match(/Online\s((\d{1,4})\s(minutes|hours|days)\s)?(ago|now|never)/);
            if (matches !== null) {
                const term = matches[4];
                if (term === 'never') {
                    user.lastSeen = new Date(0);
                    user.isOnline = false;
                } else {
                    const timeStampShort = new Date(user.timeStamp.getFullYear(), user.timeStamp.getMonth(), user.timeStamp.getDate(), user.timeStamp.getHours(), user.timeStamp.getMinutes());

                    if (term === 'ago') {
                        user.lastSeen = timeStampShort;
                        user.isOnline = false;

                        const value = Number.parseInt(matches[2]);
                        const unit = matches[3];
                        switch (unit) {
                            case 'minutes':
                                user.lastSeen.setMinutes(user.lastSeen.getMinutes() - value);
                                break;
                            case 'hours':
                                user.lastSeen.setMinutes(0);
                                user.lastSeen.setHours(user.lastSeen.getHours() - value);
                                break;
                            case 'days':
                                user.lastSeen.setMinutes(0);
                                user.lastSeen.setHours(0);
                                user.lastSeen.setDate(user.lastSeen.getDate() - value);
                                break;
                        }
                    } else if (term === 'now') {
                        user.lastSeen = timeStampShort;
                        user.isOnline = true;
                    }
                }
            }

            for (let txt of textNodes.slice(2)) {
                const distanceMatch = txt.match(/(\d{0,2},?\d{1,3})\skm\sfrom\syou/);
                if (distanceMatch !== null) {

                    const distanceKm = Number.parseInt(distanceMatch[1].replace(/\D/g, ''));
                    if (Number.isInteger(distanceKm)) {

                        user.distanceKm = distanceKm;
                    }
                }
            }
        };

        const fetchImages = (html, user) => {
            for (let [property, imageName] of Object.entries({ isPremium: 'premium.png', isSuper: 'super_premium.png', isReal: 'real.png' })) {
                user[property] = $(html).find(`img[src$='/gfx/${imageName}']`).length > 0;
            }
        };

        const fetchCustom = (html, user) => {
            const embedRankSrc = $(html).find("embed[src^='/levelRank.swf?displayRank']").attr('src') || '';
            const match = embedRankSrc.match(/displayRank=(\d{1,3})/);
            if (match !== null) {
                user.userLevel = Number.parseInt(match[1]);
            }
        };

        return class UserControllerInternal {
            constructor(user = new User()) {
                this.user = user;
            }

            parseHtml({ html = '', url = '' } = {}) {
                if (url.includes(CONST.URL_SEARCH)) {
                    fetchJsLinks(html, this.user);
                    fetchTextNodes(html, this.user);
                    fetchImages(html, this.user);

                } else if (url.includes(CONST.URL_USER_LEVEL)) {
                    const matchUid = url.match(/uid=(\d{1,7})/);
                    if (matchUid !== null) {
                        this.user.uid = Number.parseInt(matchUid[1]);
                    }

                    const nick = $(html).find('div.popupcontent').find('div.bbottom').text();
                    if (nick) {
                        this.user.nick = nick;
                    }

                    fetchCustom(html, this.user);
                }

                return this;
            }

            static parseHtml({ html, url, user = new User() } = {}) {
                return new UserControllerInternal(user).parseHtml({ html, url });
            }
        };
    })();

    const createUrlParamsFromUserSearch = userSearch => Object.fromEntries(
        Object.entries({
            nick: 'nick',
            gender: 'gender',
            page: 'page',
            isOnline: 'online',
            hasPicture: 'picture',
            isPremium: 'isprem',
            isReal: 'isreal',
            isSuper: 'issuper'
        })
        .filter(([ propName, paramName ]) => userSearch[propName])
        .map(([ propName, paramName ]) => {
            let paramValue = userSearch[propName];
            paramValue = (typeof paramValue === 'boolean') ? (paramValue ? 1 : 0) : String(paramValue).replace(/\s/g, '+');
            return [ paramName, paramValue ];
        })
    );

    const executeUserSearch = (userSearch) => {
        return HttpRequestHtml.send({
            url: CONST.URL_SEARCH, params: createUrlParamsFromUserSearch(userSearch)
        }).then((response) => {
            const userNodes = $(response.html).find('.searchNormal, .searchSuper').get();
            const users = userNodes.map(node => UserControllerInternal.parseHtml({ html: node, url: response.finalUrl }).user);
            return users;
        });
    };

    return class UserSearch {
        constructor({ nick = '', gender = User.GENDERS.ANY, isOnline = false, hasPicture = false, isPremium = false, isReal = false, isSuper = false, page = 0 } = {}) {
            Object.assign(this, { nick, gender, isOnline, hasPicture, isPremium, isReal, isSuper, page });
        }

        execute() {
            return executeUserSearch(this).then(users => Promise.all(
                users.map(user => HttpRequestHtml.send({
                    url: CONST.URL_USER_LEVEL, params: { uid: user.uid }
                }).then(response => UserControllerInternal.parseHtml({ html: response.html, user: user, url: response.finalUrl }).user))
            ));
        }

        static execute(...args) {
            return new UserSearch(...args).execute();
        }

        static byUid(uid) {
            return HttpRequestHtml.send({
                url: CONST.URL_USER_LEVEL, params: { uid: uid }
            }).then(response => UserControllerInternal.parseHtml({ html: response.html, url: response.finalUrl }).user).then(user => {
                if (!(user && user.nick)) {
                    return Promise.reject(new Error(`No result for User with uid '${uid}'.`));
                }
                return executeUserSearch(new UserSearch({ nick: user.nick }));
            }).then(users => users.find(user => user.uid === uid ));
        }

        static get GENDERS() {
            return User.GENDERS;
        }
    };
})();