Camamba Users Search Library

fetches Users

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

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