Advertisement
Guest User

Untitled

a guest
Sep 23rd, 2019
328
0
Never
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
text 24.98 KB | None | 0 0
  1. // ==UserScript==
  2. // @name backpack.tf Premium Recent Sales Finder
  3. // @namespace http://steamcommunity.com/profiles/76561198080179568/
  4. // @version 4.0.9
  5. // @description Adds coloring to history pages indicating recent sales and includes compare links for sales
  6. // @author Julia
  7. // @include /^https?:\/\/(.*\.)?backpack\.tf(:\d+)?\/item\/\d+/
  8. // @include /^https?:\/\/(.*\.)?backpack\.tf\/profiles\/\d{17}\/?$
  9. // @include /^https?:\/\/(.*\.)?backpack\.tf(:\d+)?\/premium\/search.*/
  10. // @updateURL https://github.com/juliarose/backpack.tf-premium-sales-finder/raw/master/backpacktf-premium-sales-finder.meta.js
  11. // @downloadURL https://github.com/juliarose/backpack.tf-premium-sales-finder/raw/master/backpacktf-premium-sales-finder.user.js
  12. // @run-at document-end
  13. // @grant GM_addStyle
  14. // @grant unsafeWindow
  15. // ==/UserScript==
  16.  
  17. (function() {
  18.  
  19. 'use strict';
  20.  
  21. function getHistory({$, omitEmpty, dayDifference}) {
  22. // jquery elements
  23. const PAGE = {
  24. $history: $('.history-sheet table.table'),
  25. $item: $('.item'),
  26. $panelExtras: $('.panel-extras'),
  27. $username: $('.username')
  28. };
  29.  
  30. // add contents to the page
  31. function addTableLinks({$history, $item, $username}) {
  32. /**
  33. * Get a value from a URL according to pattern.
  34. * @param {String} url - URL.
  35. * @param {Object} pattern - Pattern to match. The 1st match group should be the value we are trying to get.
  36. * @returns {(String|null)} Matched value, or null if the pattern does not match.
  37. */
  38. function getValueFromURL(url, pattern) {
  39. const match = (url || '').match(pattern);
  40.  
  41. return (
  42. match &&
  43. match[1]
  44. );
  45. }
  46.  
  47. /**
  48. * Add contents for a row.
  49. * @param {Object} options - Options.
  50. * @param {Object} options.table - Object containing details of table.
  51. * @param {Number} options.$item - JQuery element for item.
  52. * @param {Number} options.index - Index of row.
  53. * @param {Object} options.$row - JQuery object of row.
  54. * @param {String} [options.loggedInUserSteamId] - The steamid of the currently logged in user.
  55. * @param {String} [options.prevSteamId] - The steamid of the previous row.
  56. * @returns {undefined}
  57. */
  58. function addRowContents({table, $item, index, $row, loggedInUserSteamId, prevSteamId}) {
  59. // contains methods for adding links
  60. const addLink = (function () {
  61. /**
  62. * Creates a jQuery link.
  63. * @param {String} href - URL.
  64. * @param {String} contents - HTML contents of link.
  65. * @returns {Object} JQuery object of link.
  66. */
  67. function getLink({href, contents}) {
  68. const $link = $('<a/>').html(contents).attr({
  69. 'href': href,
  70. 'target': '_blank'
  71. });
  72.  
  73. return $link;
  74. }
  75.  
  76. return {
  77. inline({href, contents, $cell}) {
  78. const $link = getLink({href, contents});
  79. const $span = $('<span/>').css({
  80. 'float': 'right',
  81. 'margin-left': '0.6em'
  82. }).append($link);
  83.  
  84. $cell.append($span);
  85. },
  86. column({href, contents, excludeLink, $cell}) {
  87. // the first row does not include a link
  88. const html = excludeLink ? '--------' : getLink({href, contents});
  89.  
  90. $cell.html(html);
  91. }
  92. };
  93. }());
  94. // contains methods for getting urls
  95. const getURL = {
  96. /**
  97. * Get URL of your steam history at date.
  98. * @param {Object} $item - JQuery object of item.
  99. * @param {Object} date - Date of history point.
  100. * @returns {String} Inventory history URL.
  101. */
  102. inventoryHistory($item, date) {
  103. const itemname = $item.attr('data-name');
  104. // for adding a filter with history bastard -
  105. // https://naknak.net/tf2/historybastard/historybastard.user.js
  106. const filter = itemname ? '#filter-' + itemname : '';
  107.  
  108. return [
  109. 'http://steamcommunity.com/my/inventoryhistory/',
  110. // unix timestamp
  111. '?after_time=' + Math.round(date.getTime() / 1000),
  112. '&prev=1',
  113. filter
  114. ].join('');
  115. },
  116. /**
  117. * Get URL of compare link on backpack.tf.
  118. * @param {String} steamid - SteamID of user.
  119. * @param {Object} date - Date of history point.
  120. * @returns {String} Inventory comparison URL.
  121. */
  122. compare(steamid, date) {
  123. // set date to beginning of day
  124. date.setUTCHours(0);
  125. date.setUTCMinutes(0);
  126. date.setUTCSeconds(0);
  127. date.setUTCMilliseconds(0);
  128.  
  129. // unix timestamp
  130. const x = Math.round(date.getTime() / 1000);
  131.  
  132. return [
  133. 'https://backpack.tf/profiles/' +
  134. steamid,
  135. '#!',
  136. '/compare/',
  137. x,
  138. '/',
  139. x,
  140. // add "/nearest" so that we can treat this compare link in a special manner
  141. // 'getInventory' will be called when this link is loaded
  142. '/nearest'
  143. ].join('');
  144. }
  145. };
  146. // get an object of the columns for this row by each column name e.g. "User"
  147. const rowColumns = Object.entries(table).reduce((prev, [name, column]) => {
  148. // get nth cell in column cells
  149. prev[name] = column.$cells.eq(index);
  150.  
  151. return prev;
  152. }, {});
  153. // get href from last seen
  154. const href = rowColumns['Last seen'].find('a').attr('href');
  155. // to extract its timmestamp value
  156. const timestampValue = getValueFromURL(href, /time=(\d+)$/);
  157. // then convert that value into a date
  158. // it is the date when the item was last seen
  159. const lastSeenDate = new Date(parseInt(timestampValue) * 1000);
  160. // get the steamid of the user from the row
  161. const userSteamId = rowColumns['User'].find('.user-handle a').attr('data-id');
  162. // add links for row
  163. const itemname = $item.attr('data-name');
  164. // adds highlighting to row
  165. const days = dayDifference(lastSeenDate, new Date());
  166.  
  167. // add coloring depending on how long ago the hat was last sold
  168. if (days <= 60) {
  169. $row.addClass('success');
  170. } else if (days <= 90) {
  171. $row.addClass('warning');
  172. } else if (days <= 120) {
  173. $row.addClass('danger');
  174. }
  175.  
  176. // links to be added to the row
  177. const links = {
  178. column: [
  179. // compare link for seller->buyer
  180. {
  181. href: getURL.compare(userSteamId, lastSeenDate),
  182. contents: 'Compare',
  183. // do not include the link if the index is 0
  184. excludeLink: index === 0,
  185. // add the link to the buyer cell
  186. $cell: rowColumns.Seller
  187. },
  188. // compare link for buyer->seller
  189. {
  190. href: getURL.compare(prevSteamId, lastSeenDate),
  191. contents: 'Compare',
  192. // do not include the link if the index is 0
  193. excludeLink: index === 0,
  194. // add the link to the seller cell
  195. $cell: rowColumns.Buyer
  196. }
  197. ],
  198. inline: []
  199. };
  200.  
  201. const addSteamLink = Boolean(
  202. loggedInUserSteamId &&
  203. // do not show if current owner, unless there is no item name (moved elsewhere)
  204. // logged in user and steamid of row must match
  205. ((prevSteamId || !itemname) && (loggedInUserSteamId == userSteamId)) ||
  206. // if previous row steamid is the same as logged in user
  207. (loggedInUserSteamId == prevSteamId)
  208. );
  209.  
  210. // add steam link if all conditions are met
  211. if (addSteamLink) {
  212. links.inline.push({
  213. href: getURL.inventoryHistory($item, lastSeenDate),
  214. contents: '<i class="stm stm-steam"/>',
  215. // add the link to the user cell
  216. $cell: rowColumns.User
  217. });
  218. }
  219.  
  220. // add the links
  221. Object.entries(links).forEach(([name, links]) => {
  222. links.forEach(addLink[name]);
  223. });
  224.  
  225. // set prev steamid to current now that we are done with this row
  226. return userSteamId;
  227. }
  228.  
  229. const $rows = $history.find('tbody > tr');
  230. const columnDefinitions = [
  231. {
  232. columnName: 'Seller',
  233. after: 'User'
  234. },
  235. {
  236. columnName: 'Buyer',
  237. after: 'User'
  238. }
  239. ];
  240. // creates a new column (and adds the header after the previous column)
  241. const defineColumn = ($rows, columnName, prevColumn) => {
  242. // get the index and header from the previous column
  243. const {index, $header, $cells} = prevColumn;
  244. const $prevTds = $cells;
  245. // increment from previous
  246. const columnIndex = index + 1;
  247. const $th = $('<th/>').text(columnName);
  248. // a blank td
  249. const $td = $('<td/>').html(columnName);
  250.  
  251. // add the header
  252. $th.insertAfter($header);
  253. // add the td after each previous td
  254. $td.insertAfter($prevTds);
  255.  
  256. const $columnCells = $rows.find(`> td:nth-child(${columnIndex + 1})`);
  257.  
  258. return {
  259. index: columnIndex,
  260. $header: $th,
  261. $cells: $columnCells
  262. };
  263. };
  264. let columnsAdded = 0;
  265. // construct a table
  266. const table = $history
  267. // all table headers in table head
  268. .find('thead tr th')
  269. // get the data for each column
  270. .map((index, el) => {
  271. const $header = $(el);
  272. const name = $header.text().trim();
  273. const $cells = $rows.find(`> td:nth-child(${index + 1})`);
  274.  
  275. return {
  276. name,
  277. // add index so we know the order of each column
  278. index,
  279. $header,
  280. $cells
  281. };
  282. })
  283. // get raw array value from jQuery map
  284. .get()
  285. // then reduce into object where the key is the column's name
  286. .reduce((prev, column) => {
  287. const {name, index, $header, $cells} = column;
  288.  
  289. // assign column based on column heading text
  290. prev[name] = {
  291. index: index + columnsAdded,
  292. $header,
  293. $cells
  294. };
  295.  
  296. const columnsToAdd = columnDefinitions.filter(({after}) => {
  297. return after === name;
  298. });
  299. let prevColumn = prev[name];
  300.  
  301. columnsAdded += columnsToAdd.length;
  302. columnsToAdd.forEach(({columnName}) =>{
  303. prev[columnName] = defineColumn($rows, columnName, prevColumn);
  304. prevColumn = prev[columnName];
  305. });
  306.  
  307. return prev;
  308. }, {});
  309. // throw 'no';
  310. // get the href from the element containing details of the logged in user
  311. const loggedInUserHref = $username.find('a').attr('href');
  312. // current logged in user
  313. const loggedInUserSteamId = getValueFromURL(loggedInUserHref, /\/profiles\/(\d{17})$/);
  314. let prevSteamId;
  315.  
  316. // iterate to add links for each row
  317. $rows.each((index, el) => {
  318. const $row = $(el);
  319.  
  320. // function will return the steamid of the row
  321. // which can then be passed to the next iteration
  322. prevSteamId = addRowContents({
  323. table,
  324. $item,
  325. index,
  326. $row,
  327. loggedInUserSteamId,
  328. prevSteamId
  329. });
  330. });
  331. }
  332.  
  333. /**
  334. * Adds a button link to the page.
  335. * @param {Object} options - Options.
  336. * @param {String} options.name - Link text.
  337. * @param {String} options.url - URL of link.
  338. * @param {String} [options.icon='fa-search'] - The icon for the link.
  339. * @param {Object} $container - JQuery object for container.
  340. * @returns {undefined}
  341. */
  342. function addButton($container, {name, url, icon}) {
  343. let $pullRight = $container.find('.pull-right');
  344. const $btnGroup = $('<div class="btn-group"/>');
  345. const $link = $(`<a class="btn btn-panel" href="${url}"><i class="fa ${icon || 'fa-search'}"></i> ${name}</a>`);
  346.  
  347. if ($pullRight.length === 0) {
  348. // add a pull-right element if one does not already exist
  349. // so that we can left align this on the right of the panel
  350. $pullRight = $('<div class="pull-right"/>');
  351. $container.append($pullRight);
  352. }
  353.  
  354. $btnGroup.append($link);
  355. $pullRight.prepend($btnGroup);
  356. }
  357.  
  358. const urlGenerators = {
  359. // get details for bot.tf listing snapshots link to page
  360. botTF($item) {
  361. const data = $item.data();
  362. const params = omitEmpty({
  363. def: data.defindex,
  364. q: data.quality,
  365. ef: data.effect_name,
  366. craft: data.craftable ? 1 : 0,
  367. aus: data.australium ? 1 : 0,
  368. ks: data.ks_tier || 0
  369. });
  370. const queryString = Object.keys(params).map((key) => {
  371. return `${key}=${encodeURIComponent(params[key])}`;
  372. }).join('&');
  373. const url = 'https://bot.tf/stats/listings?' + queryString;
  374.  
  375. return url;
  376. },
  377. // add marketplace link to page
  378. marketplaceTF($item) {
  379. const data = $item.data();
  380. const $itemIcon = $item.find('.item-icon');
  381. // get the war paint id from the background image
  382. const backgroundImage = $itemIcon.css('background-image');
  383. // matches the url for a war paint image
  384. const reWarPaintPattern = /https:\/\/scrap\.tf\/img\/items\/warpaint\/(?:(?![×Þß÷þø_])[%\-'0-9a-zÀ-ÿA-z])+_(\d+)_(\d+)_(\d+)\.png/i;
  385. const warPaintMatch = backgroundImage.match(reWarPaintPattern);
  386. // will be in first group
  387. const warPaintId = warPaintMatch ? warPaintMatch[1] : null;
  388. // get the id of the wear using the name of the wear
  389. const wearId = {
  390. 'Factory New': 1,
  391. 'Minimal Wear': 2,
  392. 'Field-Tested': 3,
  393. 'Well-Worn': 4,
  394. 'Battle Scarred': 5
  395. }[data.wear_tier];
  396. const params = [
  397. data.defindex,
  398. data.quality,
  399. data.effect_id ? 'u' + data.effect_id : null,
  400. wearId ? 'w' + wearId : null,
  401. warPaintId ? 'pk' + warPaintId : null,
  402. data.ks_tier ? 'kt-' + data.ks_tier : null,
  403. data.australium ? 'australium' : null,
  404. !data.craftable ? 'uncraftable' : null,
  405. // is a strange version
  406. data.quality_elevated == '11' ? 'strange' : null
  407. ].filter(param => param !== null);
  408. const url = 'https://marketplace.tf/items/tf2/' + params.join(';');
  409.  
  410. return url;
  411. }
  412. };
  413.  
  414. addTableLinks(PAGE);
  415.  
  416. // only if an item exists on page
  417. if (PAGE.$item.length > 0) {
  418. const $item = PAGE.$item;
  419. const $container = PAGE.$panelExtras;
  420. const generators = {
  421. 'Bot.tf': urlGenerators.botTF,
  422. 'Marketplace.tf': urlGenerators.marketplaceTF
  423. };
  424.  
  425. Object.entries(generators).forEach(([name, generator]) => {
  426. // generate the button details using the generator
  427. const url = generator($item);
  428.  
  429. // add it to the given container
  430. addButton($container, {
  431. name,
  432. url
  433. });
  434. });
  435. }
  436. }
  437.  
  438. function getInventory({$}) {
  439. // jquery elements
  440. const PAGE = {
  441. $snapshots: $('#historicalview option')
  442. };
  443.  
  444. // update the location so that each timestamp is at the closest time according to recorded inventory snapshots
  445. function changeLocation({$snapshots}) {
  446. /**
  447. * Get closet snapshot time according to timestamp.
  448. * @param {Number[]} snapshots - Array of snapshot unix timestamps.
  449. * @param {Number} timestamp - Unix timestamp.
  450. * @param {Boolean} [before] - Whether the closest snapshot should appear before 'timestamp'.
  451. * @param {Number} [other] - Snapshot must not be the same as this value.
  452. * @returns {(Number|null)} Closest snapshot to date.
  453. */
  454. function getClosestSnapshot(snapshots, timestamp, before, other) {
  455. // sort ascending
  456. const asc = (a, b) => (b - a);
  457. // sort descending
  458. const desc = (a, b) => (a - b);
  459.  
  460. // loop until we find the first result that is at or before the timestamp if "before" is set to true
  461. // when "before" is set, array is sorted in descending order, or ascending if not set
  462. return snapshots.sort(before ? desc : asc).find((snapshot) => {
  463. let isBefore = timestamp <= snapshot;
  464. let isAfter = timestamp >= snapshot;
  465. let isOther = snapshot === other;
  466.  
  467. return (
  468. before ? isBefore : isAfter
  469. ) && !isOther; // snapshot must also not be the same as "other"
  470. }) || (before ? Math.min : Math.max)(...snapshots);
  471. // default value is first or last snapshot if one did not meet conditions
  472. // will probably only default to this if the time is closest to the first or last snapshot
  473. // or with one-snapshot inventories
  474. }
  475.  
  476. // generate page snapshots
  477. const snapshots = $snapshots.map((i, el) => {
  478. return parseInt(el.value);
  479. }).get().filter(Boolean);
  480. const pattern = /(\d{10})\/(\d{10})\/nearest$/;
  481. // should always match
  482. const timestamps = location.href.match(pattern).slice(1).map(a => parseInt(a));
  483. // must be at or before the first date
  484. const from = getClosestSnapshot(snapshots, timestamps[0], true);
  485. // must be at or before the second date, and not the same date as 'from'
  486. const to = getClosestSnapshot(snapshots, timestamps[1], false, from);
  487.  
  488. // finally update location.href using new timestamps
  489. location.href = location.href.replace(pattern, [from, to].join('/'));
  490. }
  491.  
  492. changeLocation(PAGE);
  493. }
  494.  
  495. function getPremium({$, dayDifference}) {
  496. const PAGE = {
  497. $results: $('.premium-search-results .result')
  498. };
  499.  
  500. function highlightResults($results) {
  501. function highlightOwner($result, days) {
  502. function prependClass($element, front) {
  503. const classes = $element.attr('class');
  504.  
  505. $element.attr('class', [front, classes].join(' '));
  506. }
  507.  
  508. const $buttons = $result.find('.buttons a');
  509.  
  510. // add coloring depending on how long ago the hat was last sold
  511. if (days <= 60) {
  512. // we add it to the beginning of the classlist
  513. // because the order of classes takes priority in styling (from first to last)
  514. prependClass($buttons, 'btn-success');
  515. $result.addClass('success');
  516. } else if (days <= 90) {
  517. prependClass($buttons, 'btn-warning');
  518. $result.addClass('warning');
  519. } else if (days <= 120) {
  520. prependClass($buttons, 'btn-danger');
  521. $result.addClass('danger');
  522. }
  523. }
  524.  
  525. $results.each((i, el) => {
  526. const $result = $(el);
  527. const $previousOwner = $result.find('.owners .owner').eq(1);
  528. const $time = $previousOwner.find('abbr');
  529.  
  530. if ($time.length > 0) {
  531. const date = new Date($time.attr('title'));
  532. const now = new Date();
  533. const days = dayDifference(now, date);
  534.  
  535. highlightOwner($result, days);
  536. }
  537. });
  538. }
  539.  
  540. highlightResults(PAGE.$results);
  541. }
  542.  
  543. // run the page scripts
  544. (function() {
  545. const DEPS = (function() {
  546. // our window object
  547. const WINDOW = unsafeWindow;
  548.  
  549. // get our global variables from the window object
  550. const {$} = WINDOW;
  551.  
  552. /**
  553. * Super basic omitEmpty function.
  554. * @param {Object} obj - Object to omit values from.
  555. * @returns {Object} Object with null, undefined, or empty string values omitted.
  556. */
  557. function omitEmpty(obj) {
  558. // create clone so we do not modify original object
  559. let result = Object.assign({}, obj);
  560.  
  561. for (let k in result) {
  562. if (result[k] === null || result[k] === undefined || result[k] === '') {
  563. delete result[k];
  564. }
  565. }
  566.  
  567. return result;
  568. }
  569.  
  570. /**
  571. * Get difference in days between two dates.
  572. * @param {Object} date1 - First date.
  573. * @param {Object} date2 - Second date.
  574. * @returns {Number} Difference.
  575. */
  576. function dayDifference(date1, date2) {
  577. const oneDay = 24 * 60 * 60 * 1000;
  578. const difference = Math.abs(date1.getTime() - date2.getTime());
  579.  
  580. return Math.round(difference / oneDay);
  581. }
  582.  
  583. return {
  584. WINDOW,
  585. $,
  586. omitEmpty,
  587. dayDifference
  588. };
  589. }());
  590. const scripts = [
  591. {
  592. pattern: /^https?:\/\/(.*\.)?backpack\.tf(:\d+)?\/item\/\d+/,
  593. method: getHistory
  594. },
  595. {
  596. pattern: /^https?:\/\/(.*\.)?backpack\.tf\/profiles\/\d{17}#!\/compare\/\d{10}\/\d{10}\/nearest/,
  597. method: getInventory
  598. },
  599. {
  600. pattern: /^https?:\/\/(.*\.)?backpack\.tf(:\d+)?\/premium\/search.*/,
  601. styles: `
  602. .premium-search-results .result.success {
  603. background-color: #dff0d8;
  604. }
  605.  
  606. .premium-search-results .result.warning {
  607. background-color: #faf2cc;
  608. }
  609.  
  610. .premium-search-results .result.danger {
  611. background-color: #f2dede;
  612. }
  613. `,
  614. method: getPremium
  615. }
  616. ];
  617. const script = scripts.find(({pattern}) => pattern.test(location.href));
  618.  
  619. if (script) {
  620. if (script.styles) {
  621. // add the styles
  622. GM_addStyle(script.styles);
  623. }
  624.  
  625. // run the script
  626. script.method(DEPS);
  627. }
  628. }());
  629.  
  630. }());
Advertisement
Add Comment
Please, Sign In to add comment
Advertisement