DKK Torn Utilities DEVELOPMENT

Commonly used functions in my Torn scripts.

当前为 2019-11-19 提交的版本,查看 最新版本

此脚本不应直接安装。它是供其他脚本使用的外部库,要使用该库请加入元指令 // @require https://update.cn-greasyfork.org/scripts/392610/751105/DKK%20Torn%20Utilities%20DEVELOPMENT.js

  1. // ==UserScript==
  2. // @name DKK Torn Utilities DEVELOPMENT
  3. // @description Commonly used functions in my Torn scripts.
  4. // @version 2.0.5
  5. // @exclude *
  6. // @namespace https://openuserjs.org/users/DeKleineKobini
  7. // @homepageURL https://www.torn.com/forums.php#/p=threads&t=16110079
  8. // ==/UserScript==
  9.  
  10. /* Script Setup */
  11. loadCSS();
  12. addFunctions();
  13.  
  14. function loadCSS() {
  15. if ($("#dkkutilscss").length) return;
  16. $("head").append("<style id='dkkutilscss'>"
  17. + ".dkk-button, .dkk-button-green { -webkit-appearance: button; -moz-appearance: button; appearance: button; text-decoration: none; ext-shadow: rgba(0, 0, 0, 0.05) 1px 1px 2px; cursor: pointer; font-weight: 400; text-transform: none; position: relative; text-align: center; line-height: 1.2; box-shadow: rgba(255, 255, 255, 0.5) 0px 1px 1px 0px inset, rgba(0, 0, 0, 0.25) 0px 1px 1px 1px; border-width: initial; border-style: none; border-color: initial; border-image: initial; padding: 2px 10px; border-radius: 4px; } "
  18. + ".dkk-button-green { background-color: rgba(255, 255, 255, 0.15); color: rgb(255, 255, 255); }"
  19. + ".dkk-widget { margin-top: 10px; }"
  20. + ".dkk-widget_red, .dkk-widget_green { background-image: linear-gradient(90deg, transparent 50%, rgba(0, 0, 0, 0.07) 0px); background-size: 4px; display: flex; align-items: center; color: rgb(255, 255, 255); font-size: 13px; letter-spacing: 1px; text-shadow: rgba(0, 0, 0, 0.65) 1px 1px 2px; padding: 6px 10px; border-radius: 5px 5px 0 0; } "
  21. + ".dkk-widget_green { background-color: rgb(144, 176, 46); }"
  22. + ".dkk-widget_red { background-color: rgb(251, 0, 25); }"
  23. + ".dkk-widget_title { flex-grow: 1; box-sizing: border-box; }"
  24. + ".dkk-widget_body { display: flex; padding: 0px; line-height: 1.4; background-color: rgb(242, 242, 242); }"
  25. + ".dkk-round-bottom { border-radius: 0 0 10px 10px; }"
  26. + ".dkk-round { border-radius: 5px; }"
  27. + ".dkk-panel-left, .dkk-panel-right, .dkk-panel-middle { flex: 1 0 0px; max-height: 120px; overflow: auto; min-height: 60px; }"
  28. + ".dkk-panel-left { border-left: 1px solid transparent; }"
  29. + ".dkk-panel-middle { display: flex-direction: column; }"
  30. + ".dkk-panel-right { display: flex-direction: column; border-radius: 0 0 5px 5px; }"
  31. + ".dkk-data-table { width: 100%; height: 100%; border-collapse: separate; text-align: left; }"
  32. + ".dkk-data-table > tbody > tr > th { height: 16px; white-space: nowrap; text-overflow: ellipsis; font-weight: 700; padding: 2px 10px; border-top: 1px solid rgb(255, 255, 255); border-bottom: 1px solid rgb(204, 204, 204); background: linear-gradient(rgb(255, 255, 255), rgb(215, 205, 220)); }"
  33. + ".dkk-data-table > tbody > tr > td { padding: 2px 10px; border-top: 1px solid rgb(255, 255, 255); border-right: 1px solid rgb(204, 204, 204); border-bottom: 1px solid rgb(204, 204, 204); }"
  34. + "</style>"
  35. );
  36. }
  37.  
  38. function addFunctions() {
  39. // formating for numbers
  40. Number.prototype.format = function(n, x) {
  41. var re = '\\d(?=(\\d{' + (x || 3) + '})+' + (n > 0 ? '\\.' : '$') + ')';
  42. return this.toFixed(Math.max(0, ~~n)).replace(new RegExp(re, 'g'), '$&,');
  43. };
  44. // string functions
  45. String.prototype.replaceAll = function(text, replace) {
  46. let str = this.toString();
  47. while (str.includes(text)) { str = str.replace(text, replace); }
  48. return str;
  49. };
  50. }
  51.  
  52. function initScript(options) {
  53. // some easy name conversion
  54. if (options.name) {
  55. if (!options.id) options.id = options.name.replaceAll(" ", "").toLowerCase();
  56. if (!options.abbr) options.abbr = "";
  57. }
  58. if (!options.logging) {
  59. options.logging = "WARN";
  60. }
  61. // actually init
  62. if (options.api){
  63. if (options.apikey) _setAPI(options.apikey);
  64. requireAPI();
  65. }
  66. if (!unsafeWindow.scripts) unsafeWindow.scripts = { ids: [] };
  67. unsafeWindow.scripts.ids.push(options.id);
  68. unsafeWindow.scripts[options.id] = {
  69. name: options.name,
  70. abbr: options.abbr,
  71. api: options.api ? true : false
  72. };
  73. dkklog.level = options.logging;
  74. dkklog.prefix = "[" + options.abbr + "]";
  75. }
  76.  
  77. /* Messaging */
  78. class DKKLog {
  79. constructor(prefix, level) {
  80. this.levels = {
  81. FATAL: 0,
  82. ERROR: 1,
  83. WARN: 2,
  84. INFO: 3,
  85. DEBUG: 4,
  86. TRACE: 5,
  87. ALL: 6,
  88. OFF: 7
  89. },
  90. this.prefix = prefix;
  91. this._level = level || 2;
  92. }
  93. get level() {
  94. return this._level;
  95. }
  96. set level(val) {
  97. this._level = this.levels[val];
  98. }
  99. logging(level, message, objs) {
  100. if (this._level< this.levels[level]) return;
  101.  
  102. let msg = `${this.prefix}[${level}] ${message}`;
  103.  
  104. if (objs && objs.length) console.log(msg, ...objs);
  105. else console.log(msg);
  106. }
  107. fatal(message, ...objs) { this.logging("FATAL", message, objs); }
  108. error(message, ...objs) { this.logging("ERROR", message, objs); }
  109. warn(message, ...objs) { this.logging("WARN", message, objs); }
  110. info(message, ...objs) { this.logging("INFO", message, objs); }
  111. debug(message, ...objs) { this.logging("DEBUG", message, objs); }
  112. trace(message, ...objs) { this.logging("TRACE", message, objs); }
  113. all(message, ...objs) { this.logging("ALL", message, objs); }
  114. }
  115.  
  116. var dkklog = new DKKLog("[DKK]", 3);
  117.  
  118. /* Torn API */
  119. class TornAPI {
  120. constructor(callback, local) {
  121. this.key = local || localStorage.getItem("dkkutils_apikey");
  122. if (!this.isValid()) {
  123. dkklog.trace("Asking for the api key.");
  124. let createPrompt = () => {
  125. if (!$("#dkkapi").length) {
  126. let selector;
  127. switch (location.pathname) {
  128. case "/christmas_town.php":
  129. selector = ".content-wrapper div[id*='root'] > div > div:eq(0)";
  130. break;
  131. default:
  132. selector = ".content-title";
  133. break;
  134. }
  135. $(selector).after(
  136. "<div><article class='dkk-widget' id='dkkapi'><header class='dkk-widget_green dkk-round'><span class='dkk-widget_title'>API Promt</span>"
  137. + "<input type='text' id='dkkapi-promt' style='margin-right: 8px;'><a id='dkkapi-save' class='dkk-button-green' href='#'>Save</a>"
  138. + "</header></article></div><div class='clear'></div>"
  139. );
  140. }
  141. $("#dkkapi-save").click(event => {
  142. event.preventDefault();
  143. this.key = $("#dkkapi-promt").val();
  144. if (this.isValid()) {
  145. $("#dkkapi").remove();
  146. localStorage.setItem("dkkutils_apikey", this.key);
  147. if (callback) callback();
  148. } else {
  149. $("#dkkapi-promt").val("");
  150. }
  151. });
  152. }
  153. if ($(".content-wrapper").length) createPrompt();
  154. else observeMutations(document, ".content-wrapper", true, createPromt, { childList: true, subtree: true });
  155. } else {
  156. if (callback) callback();
  157. }
  158. }
  159. isValid() {
  160. if (!this.key || this.key === undefined || this.key == "undefined" || this.key === null || this.key == "null" || this.key === "") return false;
  161. if (this.key.length != 16) return false;
  162. return true;
  163. }
  164. // TODO - test and finish
  165. sendRequest(part, id, selections) {
  166. dkklog.debug(`Sending API request to ${part}/${selections} for id ${id} with key ${this.key}`);
  167. return new Promise((resolve, reject) => {
  168. GM_xmlhttpRequest({
  169. method: "GET",
  170. url: `https://api.torn.com/${part}/${id}?selections=${selections}&key=${this.key}`,
  171. onreadystatechange: function(res) {
  172. if (res.readyState > 3 && res.status === 200) {
  173. dkklog.debug("API response received.")
  174. if (!isJsonString(res.responseText)) {
  175. reject("JSON Error", res.responseText);
  176. return;
  177. }
  178.  
  179. var json = JSON.parse(res.responseText);
  180. resolve(json);
  181.  
  182. if (json.error) {
  183. var code = json.error.code;
  184. dkklog.debug("API Error: " + code)
  185. if (code == 2) setAPI(null);
  186. }
  187. }
  188. },
  189. onerror: function(err) {
  190. console.log(err);
  191. reject('XHR error.');
  192. }
  193. })
  194. }).catch(e => {
  195. console.log(e);
  196. });
  197. }
  198. }
  199.  
  200. unsafeWindow.TornAPI = TornAPI;
  201.  
  202. var apiKey;
  203.  
  204. const API_LENGTH = 16;
  205. const STORAGE_LOCATION = "dkkutils_apikey";
  206.  
  207. function requireAPI() {
  208. dkklog.debug("Require API")
  209. apiKey = getAPI();
  210. if (!_isValidAPIKey(apiKey)) {
  211. dkklog.debug("Require API - no valid api found")
  212. let response = prompt("Please enter your API key: ");
  213. if (_isValidAPIKey(response)) {
  214. setAPI(response);
  215. } else {
  216. dkklog.debug("Require API - no valid api given")
  217. alert("Given API key was not valid, script might not work.\nRefresh to try again.")
  218. }
  219. }
  220. }
  221.  
  222. function _isValidAPIKey(key) {
  223. if (!key) key = apiKey;
  224.  
  225. if (!key || key === undefined || key == "undefined" || key === null || key == "null" || key === "") return false;
  226. if (key.length != API_LENGTH) return false;
  227.  
  228. return true;
  229. }
  230.  
  231. function sendAPIRequest(part, id, selections, key) {
  232. dkklog.debug(`Sending API request to ${part}/${selections} on id ${id}`);
  233. if (!key) key = apiKey;
  234.  
  235. return new Promise((resolve, reject) => {
  236. GM_xmlhttpRequest({
  237. method: "GET",
  238. url: `https://api.torn.com/${part}/${id}?selections=${selections}&key=${key}`,
  239. onreadystatechange: function(res) {
  240. if (res.readyState > 3 && res.status === 200) {
  241. dkklog.debug("API response received.")
  242. if (!isJsonString(res.responseText)) {
  243. reject("JSON Error", res.responseText);
  244. return;
  245. }
  246.  
  247. var json = JSON.parse(res.responseText);
  248. resolve(json);
  249.  
  250. if (json.error) {
  251. var code = json.error.code;
  252. dkklog.debug("API Error: " + code)
  253. if (code == 2) setAPI(null);
  254. }
  255. }
  256. },
  257. onerror: function(err) {
  258. console.log(err);
  259. reject('XHR error.');
  260. }
  261. })
  262. }).catch(e => {
  263. console.log(e);
  264. });
  265. }
  266.  
  267. function setAPI(key) {
  268. if (!_isValidAPIKey(key)) {
  269. localStorage.removeItem(STORAGE_LOCATION);
  270. dkklog.debug("Removed key from storage.")
  271. } else {
  272. localStorage.setItem(STORAGE_LOCATION, key);
  273. dkklog.debug(`Added key '${key}' to storage. '${localStorage.getItem(STORAGE_LOCATION)}'`)
  274. }
  275.  
  276. apiKey = key;
  277. }
  278.  
  279. function _setAPI(key) {
  280. if (!_isValidAPIKey(key)) localStorage.removeItem(STORAGE_LOCATION);
  281. else localStorage.setItem(STORAGE_LOCATION, key);
  282.  
  283. apiKey = key;
  284. }
  285.  
  286. function getAPI() {
  287. let utilsKey = localStorage.getItem(STORAGE_LOCATION);
  288. if (_isValidAPIKey(utilsKey)) {
  289. dkklog.debug("getAPI - from own storage '" + utilsKey.length + "'");
  290. return utilsKey;
  291. } else {
  292. dkklog.debug("getAPI - from own storage is invalid '" + utilsKey + "'");
  293. }
  294.  
  295. let key = localStorage.getItem("_apiKey") || localStorage.getItem("x_apikey") || localStorage.getItem("jebster.torn") || localStorage.getItem("TornApiKey");
  296.  
  297. if (_isValidAPIKey(key)) {
  298. dkklog.debug("getAPI - from other storage");
  299. setAPI(key);
  300. } else if (localStorage.getItem("_apiKey")) {
  301. dkklog.debug("getAPI - removed 1");
  302. localStorage.removeItem("_apiKey");
  303. } else if (localStorage.getItem("x_apikey")) {
  304. dkklog.debug("getAPI - removed 2");
  305. localStorage.removeItem("x_apikey");
  306. } else if (localStorage.getItem("jebster.torn")) {
  307. dkklog.debug("getAPI - removed 3");
  308. localStorage.removeItem("jebster.torn");
  309. } else if (localStorage.getItem("_apiKey")) {
  310. dkklog.debug("getAPI - removed 4");
  311. localStorage.removeItem("TornApiKey");
  312. }
  313.  
  314. return utilsKey;
  315. }
  316.  
  317. /* Torn General*/
  318. function getScriptUser() {
  319. let body = $("body")
  320. let contentWrapper = $("#mainContainer > .content-wrapper");
  321.  
  322. return {
  323. isJailed: body.hasClass("jail"),
  324. isHospitalized: body.hasClass("hospital"),
  325. isTravelling: contentWrapper.hasClass("travelling")
  326. }
  327. }
  328.  
  329. /* Networking */
  330.  
  331. function xhrIntercept(callback) {
  332. let oldXHROpen = window.XMLHttpRequest.prototype.open;
  333. window.XMLHttpRequest.prototype.open = function() {
  334. this.addEventListener('readystatechange', function() {
  335. if (this.readyState > 3 && this.status == 200) {
  336. var page = this.responseURL.substring(this.responseURL.indexOf("torn.com/") + "torn.com/".length, this.responseURL.indexOf(".php"));
  337.  
  338. var json, uri;
  339. if (isJsonString(this.response)) json = JSON.parse(this.response);
  340. else uri = getUrlParams(this.responseURL);
  341.  
  342. callback(page, json, uri, this);
  343. }
  344. });
  345.  
  346. return oldXHROpen.apply(this, arguments);
  347. }
  348. }
  349.  
  350. function ajax(callback) {
  351. $(document).ajaxComplete((event, xhr, settings) => {
  352. if (xhr.readyState > 3 && xhr.status == 200) {
  353. if (settings.url.indexOf("torn.com/") < 0) settings.url = "torn.com" + (settings.url.startsWith("/") ? "" : "/") + settings.url;
  354. var page = settings.url.substring(settings.url.indexOf("torn.com/") + "torn.com/".length, settings.url.indexOf(".php"));
  355.  
  356. var json, uri;
  357. if (isJsonString(xhr.responseText)) json = JSON.parse(xhr.responseText);
  358. else uri = getUrlParams(settings.url);
  359.  
  360. callback(page, json, uri, xhr, settings);
  361. }
  362. });
  363. }
  364.  
  365. function interceptFetch(url, callback) {
  366. unsafeWindow.fetch = async (input, options) => {
  367. const response = await fetch(input, options)
  368.  
  369. if (response.url.startsWith("https://www.torn.com/" + url)) {
  370. let res = response.clone();
  371.  
  372. Promise.resolve(res.json().then((json) => callback(json, res.url)));
  373. }
  374.  
  375. return response;
  376. }
  377. }
  378.  
  379. /* DOM */
  380. function observeMutationsFull(root, callback, options) {
  381. if (!options) options = {
  382. childList: true
  383. };
  384.  
  385. new MutationObserver(callback).observe(root, options);
  386. }
  387.  
  388. function observeMutations(root, selector, runOnce, callback, options, callbackRemoval) {
  389. var ran = false;
  390. observeMutationsFull(root, function(mutations, me) {
  391. var check = $(selector);
  392.  
  393. if (check.length) {
  394. if (runOnce) me.disconnect();
  395.  
  396. ran = true;
  397. callback(mutations, me);
  398. } else if (ran) {
  399. ran = false;
  400. if (callbackRemoval) callbackRemoval(mutations, me);
  401. }
  402. }, options);
  403. }
  404.  
  405. /* Caching */
  406. function setCache(key, value, time, sub) {
  407. var end = time == -1 ? -1 : Date.now() + time;
  408.  
  409. var obj = sub ? value : {
  410. value: value,
  411. end: Date.now() + time
  412. };
  413.  
  414. GM_setValue(key, JSON.stringify(obj));
  415. }
  416.  
  417. async function getCache(key, subbed) {
  418. let _obj = await GM_getValue(key, subbed ? "{}" : "{\"end\":0}");
  419. let obj = JSON.parse(_obj);
  420.  
  421. var end = obj.end;
  422. if (!end || end == -1 || end > Date.now())
  423. return subbed ? obj : obj.value;
  424.  
  425. return undefined;
  426. }
  427.  
  428. function getSubCache(cache, id) {
  429. if (cache[id]) {
  430. var end = cache[id].end;
  431. if (end == -1 || end > Date.now())
  432. return cache[id].value;
  433. }
  434.  
  435. return undefined;
  436. }
  437.  
  438. /* General Utilities */
  439.  
  440. function isJsonString(str) {
  441. if (!str || str == "") return false;
  442. try {
  443. JSON.parse(str);
  444. } catch (e) {
  445. return false;
  446. }
  447. return true;
  448. }
  449.  
  450. /**
  451. * JavaScript Get URL Parameter (https://www.kevinleary.net/javascript-get-url-parameters/)
  452. *
  453. * @param String prop The specific URL parameter you want to retreive the value for
  454. * @return String|Object If prop is provided a string value is returned, otherwise an object of all properties is returned
  455. */
  456. function getUrlParams(url, prop) {
  457. var params = {};
  458. var search = decodeURIComponent(((url) ? url : window.location.href).slice(window.location.href.indexOf('?') + 1));
  459. var definitions = search.split('&');
  460.  
  461. definitions.forEach(function(val, key) {
  462. var parts = val.split('=', 2);
  463. params[parts[0]] = parts[1];
  464. });
  465.  
  466. return (prop && prop in params) ? params[prop] : params;
  467. }
  468.  
  469. function getSpecialSearch() {
  470. let hash = window.location.hash;
  471.  
  472. hash = hash.replace("#/", "?");
  473. hash = hash.replace("#!", "?");
  474.  
  475. return hash;
  476. }
  477.  
  478. function stripHtml(html) {
  479. var tmp = document.createElement("DIV");
  480. tmp.innerHTML = html;
  481. var stripped = tmp.textContent || tmp.innerText || "";
  482.  
  483. stripped = stripped.replaceAll("\n", "");
  484. stripped = stripped.replaceAll("\t", "");
  485. stripped = stripped.replaceAll(" ", " ");
  486.  
  487. return stripped;
  488. }
  489.  
  490. function getNewDay() {
  491. let now = new Date();
  492. let newDay = new Date(Date.UTC(now.getFullYear(), now.getMonth(), now.getDate() - 1, 0, 0, 0));
  493.  
  494. if (Date.now() >= newDay.getTime()) newDay.setUTCDate(newDay.getUTCDate() + 1);
  495. if (Date.now() >= newDay.getTime()) newDay.setUTCDate(newDay.getUTCDate() + 1);
  496.  
  497. return newDay;
  498. }
  499.  
  500. function getMillisUntilNewDay() {
  501. return getNewDay().getTime() - Date.now();
  502. }
  503.  
  504. function _isMobile() {
  505. return navigator.userAgent.match(/Android/i) ||
  506. navigator.userAgent.match(/webOS/i) ||
  507. navigator.userAgent.match(/iPhone/i) ||
  508. navigator.userAgent.match(/iPad/i) ||
  509. navigator.userAgent.match(/iPod/i) ||
  510. navigator.userAgent.match(/BlackBerry/i) ||
  511. navigator.userAgent.match(/Windows Phone/i);
  512. }
  513.  
  514. function runOnEvent(funct, event, runBefore) {
  515. if (runBefore) funct();
  516.  
  517. $(window).bind(event, function() {
  518. funct();
  519. });
  520. }
  521.  
  522. function timeSince(timeStamp) {
  523. let now = new Date();
  524. let secondsPast = (now.getTime() - timeStamp.getTime()) / 1000;
  525.  
  526. if (secondsPast < 60) return parseInt(secondsPast) + 's';
  527. else if (secondsPast < 3600) return parseInt(secondsPast / 60) + 'm';
  528. else if (secondsPast <= 86400) return parseInt(secondsPast / 3600) + 'h';
  529. else if (secondsPast > 86400) {
  530. let day = timeStamp.getDate();
  531. let month = timeStamp.toDateString().match(/ [a-zA-Z]*/)[0].replace(" ", "");
  532. let year = timeStamp.getFullYear() == now.getFullYear() ? "" : " " + timeStamp.getFullYear();
  533. return day + " " + month + year;
  534. }
  535. }