DKK Torn Utilities DEVELOPMENT

Commonly used functions in my Torn scripts.

目前為 2019-11-19 提交的版本,檢視 最新版本

此腳本不應該直接安裝,它是一個供其他腳本使用的函式庫。欲使用本函式庫,請在腳本 metadata 寫上: // @require https://update.cn-greasyfork.org/scripts/392610/751119/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. /* Classes */
  11. class DKKLog {
  12. constructor(prefix, level) {
  13. this.levels = {
  14. FATAL: 0,
  15. ERROR: 1,
  16. WARN: 2,
  17. INFO: 3,
  18. DEBUG: 4,
  19. TRACE: 5,
  20. ALL: 6,
  21. OFF: 7
  22. },
  23. this.prefix = prefix;
  24. this._level = level || 2;
  25. }
  26. get level() {
  27. return this._level;
  28. }
  29. set level(val) {
  30. this._level = this.levels[val];
  31. }
  32. logging(level, message, objs) {
  33. if (this._level< this.levels[level]) return;
  34.  
  35. let msg = `${this.prefix}[${level}] ${message}`;
  36.  
  37. if (objs && objs.length) console.log(msg, ...objs);
  38. else console.log(msg);
  39. }
  40. fatal(message, ...objs) { this.logging("FATAL", message, objs); }
  41. error(message, ...objs) { this.logging("ERROR", message, objs); }
  42. warn(message, ...objs) { this.logging("WARN", message, objs); }
  43. info(message, ...objs) { this.logging("INFO", message, objs); }
  44. debug(message, ...objs) { this.logging("DEBUG", message, objs); }
  45. trace(message, ...objs) { this.logging("TRACE", message, objs); }
  46. all(message, ...objs) { this.logging("ALL", message, objs); }
  47. }
  48.  
  49. class TornAPI {
  50. constructor(callback, local) {
  51. this.key = local || localStorage.getItem("dkkutils_apikey");
  52. if (!this.isValid()) {
  53. dkklog.trace("Asking for the api key.");
  54. let createPrompt = () => {
  55. if (!$("#dkkapi").length) {
  56. let selector;
  57. switch (location.pathname) {
  58. case "/christmas_town.php":
  59. selector = ".content-wrapper div[id*='root'] > div > div:eq(0)";
  60. break;
  61. default:
  62. selector = ".content-title";
  63. break;
  64. }
  65. $(selector).after(
  66. "<div><article class='dkk-widget' id='dkkapi'><header class='dkk-widget_green dkk-round'><span class='dkk-widget_title'>API Promt</span>"
  67. + "<input type='text' id='dkkapi-promt' style='margin-right: 8px;'><a id='dkkapi-save' class='dkk-button-green' href='#'>Save</a>"
  68. + "</header></article></div><div class='clear'></div>"
  69. );
  70. }
  71. $("#dkkapi-save").click(event => {
  72. event.preventDefault();
  73. this.key = $("#dkkapi-promt").val();
  74. if (this.isValid()) {
  75. $("#dkkapi").remove();
  76. localStorage.setItem("dkkutils_apikey", this.key);
  77. if (callback) callback(this);
  78. } else {
  79. $("#dkkapi-promt").val("");
  80. }
  81. });
  82. }
  83. if ($(".content-wrapper").length) createPrompt();
  84. else observeMutations(document, ".content-wrapper", true, createPromt, { childList: true, subtree: true });
  85. } else {
  86. if (callback) callback(this);
  87. }
  88. }
  89. isValid() {
  90. if (!this.key || this.key === undefined || this.key == "undefined" || this.key === null || this.key == "null" || this.key === "") return false;
  91. if (this.key.length != 16) return false;
  92. return true;
  93. }
  94. sendRequest(part, id, selections) {
  95. dkklog.debug(`Sending API request to ${part}/${selections} for id ${id} with key ${this.key}`);
  96. return new Promise((resolve, reject) => {
  97. GM_xmlhttpRequest({
  98. method: "GET",
  99. url: `https://api.torn.com/${part}/${id}?selections=${selections}&key=${this.key}`,
  100. onreadystatechange: (res) => {
  101. if (res.readyState > 3 && res.status === 200) {
  102. dkklog.trace("API response received.", res)
  103. if (!isJsonString(res.responseText)) {
  104. reject("JSON Error", res.responseText);
  105. return;
  106. }
  107.  
  108. if (json.error) {
  109. var code = json.error.code;
  110. if (code == 2) {
  111. this.key = null;
  112. localStorage.removeItem("dkkutils_apikey");
  113. }
  114. dkklog.warn("A TornAPI error occured.", json.error);
  115. reject("API Error: " + code);
  116. } else {
  117. resolve(json, JSON.parse(res.responseText));
  118. }
  119. }
  120. },
  121. onerror: function(err) {
  122. dkklog.error("An XHR error occured.", err)
  123. reject('XHR error.');
  124. }
  125. })
  126. });
  127. }
  128. }
  129.  
  130. /* Script Setup */
  131. var dkklog = new DKKLog("[DKK]", 2);;
  132.  
  133. loadCSS();
  134. addFunctions();
  135.  
  136. if (!unsafeWindow.scripts) unsafeWindow.scripts = { ids: [] };
  137.  
  138. function loadCSS() {
  139. if ($("#dkkutilscss").length) return;
  140. $("head").append("<style id='dkkutilscss'>"
  141. + ".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; } "
  142. + ".dkk-button-green { background-color: rgba(255, 255, 255, 0.15); color: rgb(255, 255, 255); }"
  143. + ".dkk-widget { margin-top: 10px; }"
  144. + ".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; } "
  145. + ".dkk-widget_green { background-color: rgb(144, 176, 46); }"
  146. + ".dkk-widget_red { background-color: rgb(251, 0, 25); }"
  147. + ".dkk-widget_title { flex-grow: 1; box-sizing: border-box; }"
  148. + ".dkk-widget_body { display: flex; padding: 0px; line-height: 1.4; background-color: rgb(242, 242, 242); }"
  149. + ".dkk-round-bottom { border-radius: 0 0 10px 10px; }"
  150. + ".dkk-round { border-radius: 5px; }"
  151. + ".dkk-panel-left, .dkk-panel-right, .dkk-panel-middle { flex: 1 0 0px; max-height: 120px; overflow: auto; min-height: 60px; }"
  152. + ".dkk-panel-left { border-left: 1px solid transparent; }"
  153. + ".dkk-panel-middle { display: flex-direction: column; }"
  154. + ".dkk-panel-right { display: flex-direction: column; border-radius: 0 0 5px 5px; }"
  155. + ".dkk-data-table { width: 100%; height: 100%; border-collapse: separate; text-align: left; }"
  156. + ".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)); }"
  157. + ".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); }"
  158. + "</style>"
  159. );
  160. }
  161.  
  162. function addFunctions() {
  163. // formating for numbers
  164. Number.prototype.format = function(n, x) {
  165. var re = '\\d(?=(\\d{' + (x || 3) + '})+' + (n > 0 ? '\\.' : '$') + ')';
  166. return this.toFixed(Math.max(0, ~~n)).replace(new RegExp(re, 'g'), '$&,');
  167. };
  168. // string functions
  169. String.prototype.replaceAll = function(text, replace) {
  170. let str = this.toString();
  171. while (str.includes(text)) { str = str.replace(text, replace); }
  172. return str;
  173. };
  174. }
  175.  
  176. function initScript(options) {
  177. // some easy name conversion
  178. if (options.name) {
  179. if (!options.id) options.id = options.name.replaceAll(" ", "").toLowerCase();
  180. if (!options.abbr) options.abbr = "";
  181. }
  182. if (!options.logging) {
  183. options.logging = "WARN";
  184. }
  185. unsafeWindow.scripts.ids.push(options.id);
  186. unsafeWindow.scripts[options.id] = {
  187. name: options.name,
  188. abbr: options.abbr,
  189. api: options.api ? true : false
  190. };
  191. dkklog.prefix = "[" + options.abbr + "]";
  192. dkklog.level = options.logging;
  193. }
  194.  
  195. /* Torn General*/
  196. function getScriptUser() {
  197. let body = $("body")
  198. let contentWrapper = $("#mainContainer > .content-wrapper");
  199.  
  200. return {
  201. isJailed: body.hasClass("jail"),
  202. isHospitalized: body.hasClass("hospital"),
  203. isTravelling: contentWrapper.hasClass("travelling")
  204. }
  205. }
  206.  
  207. /* Networking */
  208.  
  209. function xhrIntercept(callback) {
  210. let oldXHROpen = window.XMLHttpRequest.prototype.open;
  211. window.XMLHttpRequest.prototype.open = function() {
  212. this.addEventListener('readystatechange', function() {
  213. if (this.readyState > 3 && this.status == 200) {
  214. var page = this.responseURL.substring(this.responseURL.indexOf("torn.com/") + "torn.com/".length, this.responseURL.indexOf(".php"));
  215.  
  216. var json, uri;
  217. if (isJsonString(this.response)) json = JSON.parse(this.response);
  218. else uri = getUrlParams(this.responseURL);
  219.  
  220. callback(page, json, uri, this);
  221. }
  222. });
  223.  
  224. return oldXHROpen.apply(this, arguments);
  225. }
  226. }
  227.  
  228. function ajax(callback) {
  229. $(document).ajaxComplete((event, xhr, settings) => {
  230. if (xhr.readyState > 3 && xhr.status == 200) {
  231. if (settings.url.indexOf("torn.com/") < 0) settings.url = "torn.com" + (settings.url.startsWith("/") ? "" : "/") + settings.url;
  232. var page = settings.url.substring(settings.url.indexOf("torn.com/") + "torn.com/".length, settings.url.indexOf(".php"));
  233.  
  234. var json, uri;
  235. if (isJsonString(xhr.responseText)) json = JSON.parse(xhr.responseText);
  236. else uri = getUrlParams(settings.url);
  237.  
  238. callback(page, json, uri, xhr, settings);
  239. }
  240. });
  241. }
  242.  
  243. function interceptFetch(url, callback) {
  244. unsafeWindow.fetch = async (input, options) => {
  245. const response = await fetch(input, options)
  246.  
  247. if (response.url.startsWith("https://www.torn.com/" + url)) {
  248. let res = response.clone();
  249.  
  250. Promise.resolve(res.json().then((json) => callback(json, res.url)));
  251. }
  252.  
  253. return response;
  254. }
  255. }
  256.  
  257. /* DOM */
  258. function observeMutationsFull(root, callback, options) {
  259. if (!options) options = {
  260. childList: true
  261. };
  262.  
  263. new MutationObserver(callback).observe(root, options);
  264. }
  265.  
  266. function observeMutations(root, selector, runOnce, callback, options, callbackRemoval) {
  267. var ran = false;
  268. observeMutationsFull(root, function(mutations, me) {
  269. var check = $(selector);
  270.  
  271. if (check.length) {
  272. if (runOnce) me.disconnect();
  273.  
  274. ran = true;
  275. callback(mutations, me);
  276. } else if (ran) {
  277. ran = false;
  278. if (callbackRemoval) callbackRemoval(mutations, me);
  279. }
  280. }, options);
  281. }
  282.  
  283. /* Caching */
  284. function setCache(key, value, time, sub) {
  285. var end = time == -1 ? -1 : Date.now() + time;
  286.  
  287. var obj = sub ? value : {
  288. value: value,
  289. end: Date.now() + time
  290. };
  291.  
  292. GM_setValue(key, JSON.stringify(obj));
  293. }
  294.  
  295. async function getCache(key, subbed) {
  296. let _obj = await GM_getValue(key, subbed ? "{}" : "{\"end\":0}");
  297. let obj = JSON.parse(_obj);
  298.  
  299. var end = obj.end;
  300. if (!end || end == -1 || end > Date.now())
  301. return subbed ? obj : obj.value;
  302.  
  303. return undefined;
  304. }
  305.  
  306. function getSubCache(cache, id) {
  307. if (cache[id]) {
  308. var end = cache[id].end;
  309. if (end == -1 || end > Date.now())
  310. return cache[id].value;
  311. }
  312.  
  313. return undefined;
  314. }
  315.  
  316. /* General Utilities */
  317.  
  318. function isJsonString(str) {
  319. if (!str || str == "") return false;
  320. try {
  321. JSON.parse(str);
  322. } catch (e) {
  323. return false;
  324. }
  325. return true;
  326. }
  327.  
  328. /**
  329. * JavaScript Get URL Parameter (https://www.kevinleary.net/javascript-get-url-parameters/)
  330. *
  331. * @param String prop The specific URL parameter you want to retreive the value for
  332. * @return String|Object If prop is provided a string value is returned, otherwise an object of all properties is returned
  333. */
  334. function getUrlParams(url, prop) {
  335. var params = {};
  336. var search = decodeURIComponent(((url) ? url : window.location.href).slice(window.location.href.indexOf('?') + 1));
  337. var definitions = search.split('&');
  338.  
  339. definitions.forEach(function(val, key) {
  340. var parts = val.split('=', 2);
  341. params[parts[0]] = parts[1];
  342. });
  343.  
  344. return (prop && prop in params) ? params[prop] : params;
  345. }
  346.  
  347. function getSpecialSearch() {
  348. let hash = window.location.hash;
  349.  
  350. hash = hash.replace("#/", "?");
  351. hash = hash.replace("#!", "?");
  352.  
  353. return hash;
  354. }
  355.  
  356. function stripHtml(html) {
  357. var tmp = document.createElement("DIV");
  358. tmp.innerHTML = html;
  359. var stripped = tmp.textContent || tmp.innerText || "";
  360.  
  361. stripped = stripped.replaceAll("\n", "");
  362. stripped = stripped.replaceAll("\t", "");
  363. stripped = stripped.replaceAll(" ", " ");
  364.  
  365. return stripped;
  366. }
  367.  
  368. function getNewDay() {
  369. let now = new Date();
  370. let newDay = new Date(Date.UTC(now.getFullYear(), now.getMonth(), now.getDate() - 1, 0, 0, 0));
  371.  
  372. if (Date.now() >= newDay.getTime()) newDay.setUTCDate(newDay.getUTCDate() + 1);
  373. if (Date.now() >= newDay.getTime()) newDay.setUTCDate(newDay.getUTCDate() + 1);
  374.  
  375. return newDay;
  376. }
  377.  
  378. function getMillisUntilNewDay() {
  379. return getNewDay().getTime() - Date.now();
  380. }
  381.  
  382. function _isMobile() {
  383. return navigator.userAgent.match(/Android/i) ||
  384. navigator.userAgent.match(/webOS/i) ||
  385. navigator.userAgent.match(/iPhone/i) ||
  386. navigator.userAgent.match(/iPad/i) ||
  387. navigator.userAgent.match(/iPod/i) ||
  388. navigator.userAgent.match(/BlackBerry/i) ||
  389. navigator.userAgent.match(/Windows Phone/i);
  390. }
  391.  
  392. function runOnEvent(funct, event, runBefore) {
  393. if (runBefore) funct();
  394.  
  395. $(window).bind(event, function() {
  396. funct();
  397. });
  398. }
  399.  
  400. function timeSince(timeStamp) {
  401. let now = new Date();
  402. let secondsPast = (now.getTime() - timeStamp.getTime()) / 1000;
  403.  
  404. if (secondsPast < 60) return parseInt(secondsPast) + 's';
  405. else if (secondsPast < 3600) return parseInt(secondsPast / 60) + 'm';
  406. else if (secondsPast <= 86400) return parseInt(secondsPast / 3600) + 'h';
  407. else if (secondsPast > 86400) {
  408. let day = timeStamp.getDate();
  409. let month = timeStamp.toDateString().match(/ [a-zA-Z]*/)[0].replace(" ", "");
  410. let year = timeStamp.getFullYear() == now.getFullYear() ? "" : " " + timeStamp.getFullYear();
  411. return day + " " + month + year;
  412. }
  413. }