Camamba Users Search Library

fetches Users

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

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

  1. // ==UserScript==
  2. // @name Camamba Users Search Library
  3. // @namespace hoehleg.userscripts.private
  4. // @version 0.0.8
  5. // @description fetches Users
  6. // @author Gerrit Höhle
  7. // @license MIT
  8. //
  9. // @require https://greasyfork.org/scripts/405144-httprequest/code/HttpRequest.js?version=1063408
  10. //
  11. // @grant GM_xmlhttpRequest
  12. // ==/UserScript==
  13.  
  14. // https://greasyfork.org/scripts/446634-camamba-users-search-library/
  15.  
  16. /* jslint esversion: 11 */
  17.  
  18. /**
  19. * @typedef {object} UserParams
  20. * @property {string} name
  21. * @property {uid} [number]
  22. * @property {'male'|'female'|'couple'?} [gender]
  23. * @property {number} [age]
  24. * @property {number} [level]
  25. * @property {number} [longitude]
  26. * @property {number} [latitude]
  27. * @property {string} [location]
  28. * @property {number} [distanceKM]
  29. * @property {boolean} [isReal]
  30. * @property {boolean} [hasPremium]
  31. * @property {boolean} [hasSuper]
  32. * @property {boolean} [isPerma]
  33. * @property {boolean} [isOnline]
  34. * @property {string} [room]
  35. * @property {Date} [lastSeen]
  36. * @property {Date} [regDate]
  37. * @property {string[]} [ipList]
  38. * @property {Date} [scorePassword]
  39. * @property {Date} [scoreFinal]
  40. * @property {(date: Date) => string} [dateToHumanReadable]
  41. */
  42.  
  43. class GuessLogSearch extends HttpRequestHtml {
  44.  
  45. constructor(name) {
  46. /**
  47. * @param {string} labelText
  48. * @param {string} textContent
  49. * @returns {number}
  50. */
  51. const matchScore = (labelText, textContent) => {
  52. const regexLookBehind = new RegExp("(?<=" + labelText + ":\\s)");
  53. const regexFloat = /\d{1,2}\.?\d{0,20}/;
  54. const regexLookAhead = /(?=\spoints)/;
  55.  
  56. for (const regexesToJoin of [
  57. [regexLookBehind, regexFloat, regexLookAhead],
  58. [regexLookBehind, regexFloat]
  59. ]) {
  60. const regexAsString = regexesToJoin.map(re => re.source).join("");
  61. const matcher = new RegExp(regexAsString, "i").exec(textContent);
  62. if (matcher != null) {
  63. return Number.parseFloat(matcher[0]);
  64. }
  65. }
  66. };
  67.  
  68. /**
  69. * @param {RegExp} regex
  70. * @param {string} textContent
  71. * @returns {Array<String>}
  72. */
  73. const matchList = (regex, textContent) => {
  74. const results = [...textContent.matchAll(regex)].reduce((a, b) => [...a, ...b], []);
  75. if (results.length) {
  76. const resultsDistinct = [...new Set(results)];
  77. return resultsDistinct;
  78. }
  79. };
  80.  
  81. super({
  82. url: 'https://www.camamba.com/guesslog.php',
  83. params: { name },
  84. resultTransformer: (resp) => {
  85. const textContent = resp.html.body.textContent;
  86.  
  87. 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);
  88. const prints = matchList(/(?<=Print\d{0,2}\schecked\sis\s)[0-9a-f]+/g, textContent);
  89.  
  90. const scorePassword = matchScore("password check", textContent);
  91. const scoreFinal = matchScore("final score", textContent);
  92.  
  93. return { userName: name, ipList, prints, scorePassword, scoreFinal };
  94. }
  95. });
  96. }
  97.  
  98. /** @returns {Promise<GuessLog>} */
  99. async send() {
  100. return await super.send();
  101. }
  102.  
  103. /**
  104. * @param {string} name
  105. * @returns {Promise<GuessLog>}
  106. */
  107. static async send(name) {
  108. return await new GuessLogSearch(name).send();
  109. }
  110. }
  111.  
  112. /**
  113. * @typedef {Object} BanLog
  114. * @property {string} moderator - user or moderator who triggered the log
  115. * @property {string} user - user who is subject
  116. * @property {Date} date - date of this log
  117. * @property {string} reason - content
  118. */
  119.  
  120. class BannLogSearch extends HttpRequestHtml {
  121. /**
  122. * @param {number} uid
  123. */
  124. constructor(uid = null) {
  125. super({
  126. url: 'https://www.camamba.com/banlog.php',
  127. params: uid ? { admin: uid } : {},
  128. resultTransformer: (response, _request) => {
  129. const results = [];
  130. const xPathExpr = "//tr" + ['User', 'Moderator', 'Date', 'Reason'].map(hdrText => `[td[span[text()='${hdrText}']]]`).join("");
  131. let tr = (response.html.evaluate(xPathExpr, response.html.body, null, XPathResult.FIRST_ORDERED_NODE_TYPE, null).singleNodeValue || {}).nextElementSibling;
  132.  
  133. while (tr) {
  134. const tds = tr.querySelectorAll('td');
  135. const user = tds[0].querySelector("a") || tds[0].textContent;
  136. const moderator = tds[1].textContent;
  137.  
  138. let date;
  139. const dateMatch = /(\d{2}).(\d{2}).(\d{4})<br>(\d{1,2}):(\d{2}):(\d{2})/.exec(tds[2].innerHTML);
  140. if (dateMatch) {
  141. const day = dateMatch[1];
  142. const month = dateMatch[2];
  143. const year = dateMatch[3];
  144. const hour = dateMatch[4];
  145. const minute = dateMatch[5];
  146. const second = dateMatch[6];
  147. date = new Date(year, month - 1, day, hour, minute, second);
  148. }
  149.  
  150. const reason = tds[3].textContent;
  151. results.push({ user, moderator, date, reason });
  152.  
  153. tr = tr.nextElementSibling;
  154. }
  155.  
  156. return results;
  157. }
  158. });
  159. }
  160.  
  161. /**
  162. * @param {number} uid
  163. * @returns {Promise<BanLog[]>}
  164. */
  165. static async send(uid) {
  166. return await new BannLogSearch(uid).send();
  167. }
  168. }
  169.  
  170. class GalleryImage {
  171. constructor({ dataURI, href }) {
  172. /** @type {string} */
  173. this.dataURI = dataURI;
  174. /** @type {string} */
  175. this.href = href;
  176. }
  177. }
  178.  
  179.  
  180. class UserLevel {
  181. constructor({ level, uid = null, name = null, timeStamp = null }) {
  182.  
  183. /** @type {number} */
  184. this.level = level !== null ? Number.parseInt(level) : null;
  185.  
  186. /** @type {number} */
  187. this.uid = uid !== null ? Number.parseInt(uid) : null;
  188.  
  189. /** @type {string} */
  190. this.name = name;
  191.  
  192. /** @type {number} */
  193. this.timeStamp = timeStamp !== null ? Number.parseInt(timeStamp) : null;
  194. }
  195. }
  196.  
  197. const UserLevelSearch = (() => {
  198. const cache = {};
  199. const maxDaysInCache = 1;
  200.  
  201. return class UserLevelSearch extends HttpRequestHtml {
  202. constructor(uid) {
  203. super({
  204. url: 'https://www.camamba.com/user_level.php',
  205. params: { uid },
  206. resultTransformer: (response, request) => {
  207. const html = response.html;
  208.  
  209. let name = null, level = null;
  210.  
  211. const nameElement = html.querySelector('b');
  212. if (nameElement) {
  213. name = nameElement.textContent;
  214. }
  215.  
  216. const levelElement = html.querySelector('font.xxltext');
  217. if (levelElement) {
  218. const levelMatch = /\d{1,3}/.exec(levelElement.textContent);
  219. if (levelMatch) {
  220. level = Number.parseInt(levelMatch);
  221. }
  222. }
  223.  
  224. return new UserLevel({ uid: request.params.uid, name, level, timeStamp: new Date().getTime() });
  225. }
  226. });
  227. }
  228.  
  229. /**
  230. * @returns {Promise<UserLevel>}
  231. */
  232. async send() {
  233. const key = `uls_${this.params.uid}`;
  234. let result = cache[this.params.uid] || JSON.parse(await GM.getValue(key, "{}"));
  235. const timeStamp = result?.timeStamp;
  236.  
  237. if (!timeStamp || new Date().getTime() - timeStamp >= maxDaysInCache * 60 * 60 * 1000) {
  238. result = await super.send();
  239.  
  240. cache[this.params.uid] = result;
  241. GM.setValue(key, JSON.stringify(result));
  242. }
  243.  
  244. return result;
  245. }
  246.  
  247. /**
  248. * @param {number} uid
  249. * @returns {Promise<UserLevel>}
  250. */
  251. static async send(uid) {
  252. return await new UserLevelSearch(uid).send();
  253. }
  254. };
  255. })();
  256.  
  257. class User {
  258. /** @param {UserParams} param0 */
  259. constructor({
  260. name, uid = 0, gender = null, age = null,
  261. longitude = null, latitude = null, location = null, distanceKM = null,
  262. isReal = null, hasPremium = null, hasSuper = null, isPerma = null,
  263. isOnline = null, room = null, lastSeen = null, regDate = null,
  264. dateToHumanReadable = (date) => date ?
  265. date.toLocaleString('de-DE', { timeStyle: "medium", dateStyle: "short", timeZone: 'CET' }) : '',
  266. }) {
  267. /** @type {string} */
  268. this.name = String(name);
  269. /** @type {number?} */
  270. this.uid = uid;
  271. /** @type {'male'|'female'|'couple'?} */
  272. this.gender = gender;
  273. /** @type {number?} */
  274. this.age = age;
  275.  
  276. /** @type {number?} */
  277. this.longitude = longitude;
  278. /** @type {number?} */
  279. this.latitude = latitude;
  280. /** @type {string?} */
  281. this.location = location;
  282. /** @type {number?} */
  283. this.distanceKM = distanceKM;
  284.  
  285. /** @type {boolean?} */
  286. this.isReal = isReal;
  287. /** @type {boolean?} */
  288. this.hasPremium = hasPremium;
  289. /** @type {boolean?} */
  290. this.hasSuper = hasSuper;
  291. /** @type {boolean?} */
  292. this.isPerma = isPerma;
  293.  
  294. /** @type {boolean?} */
  295. this.isOnline = isOnline;
  296. /** @type {string?} */
  297. this.room = room;
  298. /** @type {Date?} */
  299. this.lastSeen = lastSeen;
  300. /** @type {Date?} */
  301. this.regDate = regDate;
  302.  
  303. /** @type {string[]} */
  304. this.prints = [];
  305. /** @type {string[]} */
  306. this.ipList = [];
  307. /** @type {number?} */
  308. this.scorePassword = null;
  309. /** @type {number?} */
  310. this.scoreFinal = null;
  311. /** @type {number} */
  312. this.guessLogTS = null;
  313.  
  314. /** @type {(date: Date) => string} */
  315. this.dateToHumanReadable = dateToHumanReadable;
  316.  
  317. /** @type {number?} */
  318. this.level = null;
  319. /** @type {number} */
  320. this.levelTS = null;
  321.  
  322. /** @type {string[]} */
  323. this.galleryData = [];
  324. /** @type {number} */
  325. this.galleryDataTS = null;
  326. }
  327.  
  328. /** @type {string} @readonly */
  329. get lastSeenHumanReadable() {
  330. return this.dateToHumanReadable(this.lastSeen);
  331. }
  332.  
  333. /** @type {string} @readonly */
  334. get regDateHumanReadable() {
  335. return this.dateToHumanReadable(this.regDate);
  336. }
  337.  
  338. get galleryAsImgElements() {
  339. if (!this.galleryData) {
  340. return [];
  341. }
  342.  
  343. return this.galleryData.map(data => Object.assign(document.createElement('img'), {
  344. src: data.dataURI
  345. }));
  346. }
  347.  
  348. async updateGalleryHref() {
  349. const pictureLinks = (await HttpRequestHtml.send({
  350. url: "https://www.camamba.com/profile_view.php",
  351. params: Object.assign(
  352. { m: 'gallery' },
  353. this.uid ? { uid: this.uid } : { user: this.name }
  354. ),
  355.  
  356. pageNr: 0,
  357. pagesMaxCount: 500,
  358.  
  359. resultTransformer: (response) => {
  360. const hrefList = [...response.html.querySelectorAll("img.picborder")].map(img => img.src);
  361. return hrefList.map(href => href.slice(0, 0 - ".s.jpg".length) + ".l.jpg");
  362. },
  363. hasNextPage: (_resp, _httpRequestHtml, lastResult) => {
  364. return lastResult.length >= 15;
  365. },
  366. paramsConfiguratorForPageNr: (params, pageNr) => ({ ...params, page: pageNr }),
  367. })).flat();
  368.  
  369. this.galleryData = pictureLinks.map(href => ({ href }));
  370. this.galleryDataTS = new Date().getTime();
  371. }
  372.  
  373. async updateGalleryData(includeUpdateOfHref = true) {
  374. if (includeUpdateOfHref) {
  375. await this.updateGalleryHref();
  376. }
  377.  
  378. const readGalleryData = this.galleryData.map(({ href }) => (async () => {
  379. const dataURI = await HttpRequestBlob.send({ url: href });
  380. return new GalleryImage({ dataURI, href });
  381. })());
  382.  
  383. this.galleryData = await Promise.all(readGalleryData);
  384. this.galleryDataTS = new Date().getTime();
  385. }
  386.  
  387. async updateLevel() {
  388. const { level, timeStamp, name } = await UserLevelSearch.send(this.uid);
  389. this.level = level;
  390. this.levelTS = timeStamp;
  391. this.name = name;
  392. }
  393.  
  394. async updateGuessLog() {
  395. /** @type {GuessLog} */
  396. const guessLog = await GuessLogSearch.send(this.name);
  397. this.guessLogTS = new Date().getTime();
  398.  
  399. this.ipList = guessLog.ipList;
  400. this.prints = guessLog.prints;
  401. this.scorePassword = guessLog.scorePassword;
  402. this.scoreFinal = guessLog.scoreFinal;
  403. }
  404.  
  405. async addNote(text) {
  406. if (!this.uid) {
  407. return await Promise.reject({
  408. status: 500,
  409. statusText: "missing uid"
  410. });
  411. }
  412.  
  413. return await new Promise((res, rej) => GM_xmlhttpRequest({
  414. url: 'https://www.camamba.com/profile_view.php',
  415. method: 'POST',
  416. data: `uid=${this.uid}&modnote=${encodeURIComponent(text)}&m=admin&nomen=1`,
  417. headers: {
  418. "Content-Type": "application/x-www-form-urlencoded"
  419. },
  420. onload: (xhr) => {
  421. res(xhr.responseText);
  422. },
  423. onerror: (xhr) => rej({
  424. status: xhr.status,
  425. statusText: xhr.statusText
  426. }),
  427. }));
  428. }
  429. }
  430.  
  431. class UserSearch extends HttpRequestHtml {
  432. /** @param {{
  433. * name: string?,
  434. * uid: number?,
  435. * gender: ('any' | 'male' | 'female' |'couple')?,
  436. * isOnline: boolean?, hasReal: boolean?, hasPremium: boolean?, hasSuper: boolean?, hasPicture: boolean?,
  437. * isSortByRegDate: boolean?,
  438. * isSortByDistance: boolean?,
  439. * isShowAll: boolean?,
  440. * pageNr: number?,
  441. * pagesMaxCount: number?,
  442. * keepInCacheTimoutMs: number?
  443. * }} param0 */
  444. constructor({
  445. name = null,
  446. uid = 0,
  447. gender = 'any',
  448. isOnline = null,
  449. hasReal = null,
  450. hasPremium = null,
  451. hasSuper = null,
  452. hasPicture = null,
  453. isSortByRegDate = null,
  454. isSortByDistance = null,
  455. isShowAll = null,
  456. pageNr = 1,
  457. pagesMaxCount = 1,
  458. keepInCacheTimoutMs
  459. } = {}) {
  460. let params = Object.assign(
  461. (name ? {
  462. nick: name
  463. } : {}),
  464. {
  465. gender: gender.toLowerCase(),
  466. },
  467. Object.fromEntries(Object.entries({
  468. online: isOnline,
  469. isreal: hasReal,
  470. isprem: hasPremium,
  471. issuper: hasSuper,
  472. picture: hasPicture,
  473. sortreg: isSortByRegDate,
  474. byDistance: isSortByDistance,
  475. showall: isShowAll,
  476. })
  477. .filter(([_k, v]) => typeof v !== 'undefined' && v !== null)
  478. .map(([k, v]) => ([[k], v ? 1 : 0])))
  479. );
  480.  
  481. params = Object.entries(params).map(([key, value]) => key + '=' + value).join('&');
  482.  
  483. if (params.length) {
  484. params += "&";
  485. }
  486. params += `page=${Math.max(pageNr - 1, 0)}`;
  487.  
  488. super({
  489. url: 'https://www.camamba.com/search.php',
  490. params,
  491. pageNr: Math.max(pageNr, 1),
  492. pagesMaxCount: Math.max(pagesMaxCount, 1),
  493. keepInCacheTimoutMs,
  494.  
  495. resultTransformer: (response) => {
  496. /** @type {Array<User>} */
  497. const users = [];
  498.  
  499. for (const trNode of response.html.querySelectorAll('.searchSuper tr, .searchNormal tr')) {
  500. const innerHTML = trNode.innerHTML;
  501.  
  502. const nameMatch = /<a\s+?href=["']javascript:openProfile\(["'](.+?)["']\)/.exec(innerHTML);
  503.  
  504. if (!nameMatch) {
  505. break;
  506. }
  507.  
  508. const user = new User({
  509. name: nameMatch[1],
  510. isReal: /<img src="\/gfx\/real.png"/.test(innerHTML),
  511. hasPremium: /<a href="\/premium.php">/.test(innerHTML),
  512. hasSuper: /<img src="\/gfx\/super_premium.png"/.test(innerHTML),
  513. isOnline: /Online\snow(\s\in|,\snot in chat)/.test(innerHTML),
  514. });
  515.  
  516. const uidMatch = /<a\s+?href=["']javascript:sendMail\(["'](\d{1,8})["']\)/.exec(innerHTML) || /<img\ssrc="\/userpics\/(\d{1,8})/.exec(innerHTML);
  517. if (uidMatch) {
  518. user.uid = Number.parseInt(uidMatch[1]);
  519. }
  520.  
  521. // Längengrad, Breitengrad, Ortsname
  522. const locationMatch = /<a\s+?href="javascript:openMap\((-?\d{1,3}\.\d{8}),(-?\d{1,3}\.\d{8})\);">(.+?)<\/a>/.exec(innerHTML);
  523. if (locationMatch) {
  524. user.longitude = Number.parseFloat(locationMatch[1]);
  525. user.latitude = Number.parseFloat(locationMatch[2]);
  526. user.location = locationMatch[3];
  527. }
  528.  
  529. // Entfernung in km
  530. const distanceMatch = /(\d{1,5})\skm\sfrom\syou/.exec(innerHTML);
  531. if (distanceMatch) {
  532. user.distanceKM = parseInt(distanceMatch[1]);
  533. }
  534.  
  535. // Geschlecht und Alter
  536. const genderAgeMatch = /(male|female|couple),\s(\d{1,4})(?:<br>){2}Online/.exec(innerHTML);
  537. if (genderAgeMatch) {
  538. user.gender = genderAgeMatch[1];
  539. user.age = genderAgeMatch[2];
  540. }
  541.  
  542. // zuletzt Online
  543. if (user.isOnline) {
  544. user.lastSeen = new Date();
  545. } else {
  546. const lastSeenMatch = /(\d{1,4})\s(minutes|hours|days)\sago/.exec(innerHTML);
  547. if (lastSeenMatch) {
  548. const value = parseInt(lastSeenMatch[1]);
  549.  
  550. const factorToMillis = {
  551. 'minutes': 1000 * 60,
  552. 'hours': 1000 * 60 * 60,
  553. 'days': 1000 * 60 * 60 * 24,
  554. }[lastSeenMatch[2]];
  555.  
  556. user.lastSeen = new Date(Date.now() - value * factorToMillis);
  557. }
  558. }
  559.  
  560. // Raumname
  561. const roomMatch = /(?:ago|now)\sin\s([\w\s]+?|p\d{1,8})<br>/.exec(innerHTML);
  562. if (roomMatch) {
  563. user.room = roomMatch[1];
  564. }
  565.  
  566. // regDate
  567. const regDateMatch = /(\d{2}).(\d{2}).(\d{4})\s(\d{1,2}):(\d{2}):(\d{2})/.exec(innerHTML);
  568. if (regDateMatch) {
  569. const regDateDay = regDateMatch[1];
  570. const regDateMonth = regDateMatch[2];
  571. const regDateYear = regDateMatch[3];
  572. const regDateHour = regDateMatch[4];
  573. const regDateMinute = regDateMatch[5];
  574. const regDateSecond = regDateMatch[6];
  575. user.regDate = new Date(regDateYear, regDateMonth - 1, regDateDay, regDateHour, regDateMinute, regDateSecond);
  576. }
  577.  
  578. users.push(user);
  579. }
  580.  
  581. return users;
  582. },
  583.  
  584. hasNextPage: (_resp, _httpRequestHtml, lastResult) => {
  585. return lastResult.length >= 50;
  586. },
  587.  
  588. paramsConfiguratorForPageNr: (params, pageNr) => {
  589. return params.replace(/page=\d+(?:$)/, `page=${pageNr - 1}`);
  590. },
  591. });
  592. this.uid = uid || null;
  593. }
  594.  
  595. /** @returns {Promise<User[]>} */
  596. async send() {
  597. if (this.uid) {
  598. const user = new User({ uid: this.uid });
  599. await user.updateLevel();
  600.  
  601. if (!user.name || user.level) {
  602. return [];
  603. }
  604. if (this.params.nick) {
  605. const unameURIencoded = encodeURIComponent(user.name.toLowerCase());
  606. const unameFromSearchParam = encodeURIComponent(this.params.nick.toLowerCase()).trim();
  607. if (unameURIencoded.includes(unameFromSearchParam)) {
  608. return [];
  609. }
  610. }
  611.  
  612. this.params.nick = user.name;
  613. const result = (await super.send()).flat().find(u => u.uid == this.uid);
  614. if (!result) {
  615. return [];
  616. }
  617.  
  618. return [Object.assign(user, result)];
  619. }
  620.  
  621. return (await super.send()).flat();
  622. }
  623.  
  624. /**
  625. * @param {{
  626. * name: string,
  627. * uid: number?,
  628. * gender: 'any' | 'male' | 'female' |'couple',
  629. * isOnline: boolean,hasReal: boolean, hasPremium: boolean, hasSuper: boolean, hasPicture: boolean,
  630. * isSortByRegDate: boolean,
  631. * isSortByDistance: boolean,
  632. * isShowAll: boolean,
  633. * pageNr: number,
  634. * pagesMaxCount: number,
  635. * keepInCacheTimoutMs: number
  636. * }} param0
  637. * @returns {Promise<User[]>}
  638. */
  639. static async send(param0) {
  640. return await new UserSearch(param0).send();
  641. }
  642. }