Camamba User

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

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

  1. // ==UserScript==
  2. // @name Camamba User
  3. // @namespace dannysaurus.camamba
  4. //
  5. // @include http://www.camamba.com/*
  6. // @include https://www.camamba.com/*
  7. // @include http://www.de.camamba.com/*
  8. // @include https://www.de.camamba.com/*
  9. //
  10. // @connect camamba.com
  11. // @grant GM_xmlhttpRequest
  12. //
  13. // @require https://greasyfork.org/scripts/405144-httprequest/code/HttpRequest.js
  14. // @require https://greasyfork.org/scripts/391854-enum/code/Enum.js
  15. //
  16. // @require https://ajax.googleapis.com/ajax/libs/jquery/3.5.1/jquery.min.js
  17. //
  18. // @version 0.1
  19. // @license MIT License
  20. // @description User represents a camamba User, UserSearch represents a search-request for one or many users
  21. // @author Gerrit Höhle
  22. // ==/UserScript==
  23.  
  24. /* jslint esnext: true */
  25. /* globals HttpRequest, HttpRequestHtml, Enum, $, jQuery */
  26. const User = (() => {
  27. 'use strict';
  28. const GENDERS = class GENDER extends Enum {}.init({ ANY: 'any' , FEMALE: 'female' , MALE: 'male', COUPLE: 'couple' });
  29.  
  30. return class User {
  31. constructor({ uid, nick, age, gender, location, distanceKm, latitudeDec, longitudeDec, isOnline, lastSeen, room, roomId, isReal, isPremium, isSuper, userLevel, timeStamp = new Date() } = {}) {
  32. Object.assign(this, { uid, nick, age, _gender: gender, location, distanceKm, latitudeDec, longitudeDec, isOnline, lastSeen, room, roomId, isReal, isPremium, isSuper, userLevel, timeStamp });
  33. }
  34.  
  35. get gender() {
  36. return this._gender instanceof GENDERS ? this._gender : GENDERS.ANY;
  37. }
  38.  
  39. set gender(value) {
  40. if (value instanceof GENDERS) {
  41. this._gender = value;
  42. } else {
  43. this._gender = [ ...GENDERS ].find(gender => gender.toString() === String(value)) || GENDERS.ANY;
  44. }
  45. }
  46.  
  47. static get GENDERS() {
  48. return GENDERS;
  49. }
  50. };
  51. })();
  52.  
  53. const UserSearch = (() => {
  54. const CONST = {
  55. URL_SEARCH: 'https://www.camamba.com/search.php',
  56. URL_USER_LEVEL: 'https://www.camamba.com/user_level.php'
  57. };
  58.  
  59. const UserControllerInternal = (() => {
  60. const GENDERS = User.GENDERS;
  61.  
  62. const getTextNodeValues = node => {
  63. if (node.nodeType === Node.TEXT_NODE) {
  64. const nodeValue = node.nodeValue.replace(/"/,'');
  65. return (/\S/.test(nodeValue)) ? [ nodeValue ] : [];
  66. }
  67. return Array.from(node.childNodes).flatMap(node => getTextNodeValues(node));
  68. };
  69.  
  70. const fetchJsLinks = (html, user) => {
  71. const jsLinks = Object.assign( {}, ...[ 'openProfile', 'sendMail', 'openChat', 'openMap' ].map(fncName => {
  72. const linkElement = $(html).find(`a[href*="javascript:${fncName}("]`);
  73. if (linkElement.length > 0) {
  74. const href = linkElement.attr('href') || '';
  75. const fncParam = ((href.match(/\((.+)\)/) || [])[1] || '').replace(/['"]/g,'');
  76. const innerHtml = (linkElement.html() || '').trim();
  77.  
  78. return { [fncName]: { href, fncParam, html: innerHtml } };
  79. }
  80. }));
  81.  
  82. if (typeof jsLinks.sendMail !== 'undefined') {
  83. user.uid = Number.parseInt(jsLinks.sendMail.fncParam);
  84. }
  85.  
  86. if (typeof jsLinks.openProfile !== 'undefined') {
  87. user.nick = jsLinks.openProfile.fncParam;
  88. }
  89.  
  90. if (typeof jsLinks.openChat !== 'undefined') {
  91. user.roomId = jsLinks.openChat.fncParam;
  92. user.room = jsLinks.openChat.html;
  93. }
  94.  
  95. if (typeof jsLinks.openMap !== 'undefined') {
  96. const matchCoords = jsLinks.openMap.fncParam.match(/(-?\d{1,3}\.\d{8}),(-?\d{1,2}\.\d{8})/);
  97. if (matchCoords !== null) {
  98. user.longitudeDec = Number.parseFloat(matchCoords[1]);
  99. user.latitudeDec = Number.parseFloat(matchCoords[2]);
  100. }
  101. user.location = jsLinks.openMap.html;
  102. }
  103. };
  104.  
  105. const fetchTextNodes = (html, user) => {
  106. const textNodes = getTextNodeValues(html);
  107.  
  108. for (let gender of [ GENDERS.FEMALE, GENDERS.MALE, GENDERS.COUPLE ].map(enm => enm.text)) {
  109. const match = textNodes[1].match(new RegExp(`^\\W*\(${gender}\),\\s\(\\d{1,3}\)`));
  110. if (match !== null) {
  111.  
  112. user.age = Number.parseInt(match[2]);
  113. user.gender = match[1];
  114. break;
  115. }
  116. }
  117.  
  118. const matches = textNodes[2].match(/Online\s((\d{1,4})\s(minutes|hours|days)\s)?(ago|now|never)/);
  119. if (matches !== null) {
  120. const term = matches[4];
  121. if (term === 'never') {
  122. user.lastSeen = new Date(0);
  123. user.isOnline = false;
  124. } else {
  125. const timeStampShort = new Date(user.timeStamp.getFullYear(), user.timeStamp.getMonth(), user.timeStamp.getDate(), user.timeStamp.getHours(), user.timeStamp.getMinutes());
  126.  
  127. if (term === 'ago') {
  128. user.lastSeen = timeStampShort;
  129. user.isOnline = false;
  130.  
  131. const value = Number.parseInt(matches[2]);
  132. const unit = matches[3];
  133. switch (unit) {
  134. case 'minutes':
  135. user.lastSeen.setMinutes(user.lastSeen.getMinutes() - value);
  136. break;
  137. case 'hours':
  138. user.lastSeen.setMinutes(0);
  139. user.lastSeen.setHours(user.lastSeen.getHours() - value);
  140. break;
  141. case 'days':
  142. user.lastSeen.setMinutes(0);
  143. user.lastSeen.setHours(0);
  144. user.lastSeen.setDate(user.lastSeen.getDate() - value);
  145. break;
  146. }
  147. } else if (term === 'now') {
  148. user.lastSeen = timeStampShort;
  149. user.isOnline = true;
  150. }
  151. }
  152. }
  153.  
  154. for (let txt of textNodes.slice(2)) {
  155. const distanceMatch = txt.match(/(\d{0,2},?\d{1,3})\skm\sfrom\syou/);
  156. if (distanceMatch !== null) {
  157.  
  158. const distanceKm = Number.parseInt(distanceMatch[1].replace(/\D/g, ''));
  159. if (Number.isInteger(distanceKm)) {
  160.  
  161. user.distanceKm = distanceKm;
  162. }
  163. }
  164. }
  165. };
  166.  
  167. const fetchImages = (html, user) => {
  168. for (let [property, imageName] of Object.entries({ isPremium: 'premium.png', isSuper: 'super_premium.png', isReal: 'real.png' })) {
  169. user[property] = $(html).find(`img[src$='/gfx/${imageName}']`).length > 0;
  170. }
  171. };
  172.  
  173. const fetchCustom = (html, user) => {
  174. const embedRankSrc = $(html).find("embed[src^='/levelRank.swf?displayRank']").attr('src') || '';
  175. const match = embedRankSrc.match(/displayRank=(\d{1,3})/);
  176. if (match !== null) {
  177. user.userLevel = Number.parseInt(match[1]);
  178. }
  179. };
  180.  
  181. return class UserControllerInternal {
  182. constructor(user = new User()) {
  183. this.user = user;
  184. }
  185.  
  186. parseHtml({ html = '', url = '' } = {}) {
  187. if (url.includes(CONST.URL_SEARCH)) {
  188. fetchJsLinks(html, this.user);
  189. fetchTextNodes(html, this.user);
  190. fetchImages(html, this.user);
  191.  
  192. } else if (url.includes(CONST.URL_USER_LEVEL)) {
  193. const matchUid = url.match(/uid=(\d{1,7})/);
  194. if (matchUid !== null) {
  195. this.user.uid = Number.parseInt(matchUid[1]);
  196. }
  197.  
  198. const nick = $(html).find('div.popupcontent').find('div.bbottom').text();
  199. if (nick) {
  200. this.user.nick = nick;
  201. }
  202.  
  203. fetchCustom(html, this.user);
  204. }
  205.  
  206. return this;
  207. }
  208.  
  209. static parseHtml({ html, url, user = new User() } = {}) {
  210. return new UserControllerInternal(user).parseHtml({ html, url });
  211. }
  212. };
  213. })();
  214.  
  215. const createUrlParamsFromUserSearch = userSearch => Object.fromEntries(
  216. Object.entries({
  217. nick: 'nick',
  218. gender: 'gender',
  219. page: 'page',
  220. isOnline: 'online',
  221. hasPicture: 'picture',
  222. isPremium: 'isprem',
  223. isReal: 'isreal',
  224. isSuper: 'issuper'
  225. })
  226. .filter(([ propName, paramName ]) => userSearch[propName])
  227. .map(([ propName, paramName ]) => {
  228. let paramValue = userSearch[propName];
  229. paramValue = (typeof paramValue === 'boolean') ? (paramValue ? 1 : 0) : String(paramValue).replace(/\s/g, '+');
  230. return [ paramName, paramValue ];
  231. })
  232. );
  233.  
  234. const executeUserSearch = (userSearch) => {
  235. return HttpRequestHtml.send({
  236. url: CONST.URL_SEARCH, params: createUrlParamsFromUserSearch(userSearch)
  237. }).then((response) => {
  238. const userNodes = $(response.html).find('.searchNormal, .searchSuper').get();
  239. const users = userNodes.map(node => UserControllerInternal.parseHtml({ html: node, url: response.finalUrl }).user);
  240. return users;
  241. });
  242. };
  243.  
  244. return class UserSearch {
  245. constructor({ nick = '', gender = User.GENDERS.ANY, isOnline = false, hasPicture = false, isPremium = false, isReal = false, isSuper = false, page = 0 } = {}) {
  246. Object.assign(this, { nick, gender, isOnline, hasPicture, isPremium, isReal, isSuper, page });
  247. }
  248.  
  249. execute() {
  250. return executeUserSearch(this).then(users => Promise.all(
  251. users.map(user => HttpRequestHtml.send({
  252. url: CONST.URL_USER_LEVEL, params: { uid: user.uid }
  253. }).then(response => UserControllerInternal.parseHtml({ html: response.html, user: user, url: response.finalUrl }).user))
  254. ));
  255. }
  256.  
  257. static execute(...args) {
  258. return new UserSearch(...args).execute();
  259. }
  260.  
  261. static byUid(uid) {
  262. return HttpRequestHtml.send({
  263. url: CONST.URL_USER_LEVEL, params: { uid: uid }
  264. }).then(response => UserControllerInternal.parseHtml({ html: response.html, url: response.finalUrl }).user).then(user => {
  265. if (!(user && user.nick)) {
  266. return Promise.reject(new Error(`No result for User with uid '${uid}'.`));
  267. }
  268. return executeUserSearch(new UserSearch({ nick: user.nick }));
  269. }).then(users => users.find(user => user.uid === uid ));
  270. }
  271.  
  272. static get GENDERS() {
  273. return User.GENDERS;
  274. }
  275. };
  276. })();