您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
fetches Users
当前为
此脚本不应直接安装。它是供其他脚本使用的外部库,要使用该库请加入元指令 // @require https://update.greasyfork.org/scripts/446634/1127012/Camamba%20Users%20Search%20Library.js
// ==UserScript== // @name Camamba Users Search Library // @namespace hoehleg.userscripts.private // @version 0.0.8 // @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: 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(); } }