Easy Property Rent Extension

Easily extend the rental property based on the previous rental agreement from your activity log

  1. // ==UserScript==
  2. // @name Easy Property Rent Extension
  3. // @namespace easy.property.rent.extension
  4. // @version v2.3.0
  5. // @description Easily extend the rental property based on the previous rental agreement from your activity log
  6. // @author IceBlueFire [776]
  7. // @license MIT
  8. // @match https://www.torn.com/properties.php*
  9. // @icon https://www.google.com/s2/favicons?sz=64&domain=torn.com
  10. // @grant GM_xmlhttpRequest
  11. // @grant GM_addStyle
  12. // @require https://code.jquery.com/jquery-1.8.2.min.js
  13. // ==/UserScript==
  14.  
  15. /******************** CONFIG SETTINGS ********************/
  16. const apikey = "#######"; // Full access API key required to pull historical activity log data.
  17. const days_remaining = 3; // Number of days remaining or less to be included in reminders.
  18. const default_days = 7; // Default number of days to extend the lease if data can't be found.
  19. const default_cost = 5600000; // Default cost of lease extension if data can't be found.
  20. const properties = [13]; // Array of property types allowed for renting. Ex: [12, 13] for Castles and Private Islands. Reference property IDs below as necessary.
  21. const hex_color = "#8ABEEF"; // Hexcode to apply to the box.
  22. const debug = 0; // Leave alone unless you want console logs.
  23. /****************** END CONFIG SETTINGS *******************/
  24.  
  25. /* Property IDs
  26. * 1 = Trailer;
  27. * 2 = Apartment;
  28. * 3 = Semi-Detached House;
  29. * 4 = Detached House;
  30. * 5 = Beach House;
  31. * 6 = Chalet;
  32. * 7 = Villa;
  33. * 8 = Penthouse;
  34. * 9 = Mansion;
  35. * 10 = Ranch;
  36. * 11 = Palace;
  37. * 12 = Castle;
  38. * 13 = Private Island;
  39. */
  40.  
  41.  
  42.  
  43. /******************** DO NOT TOUCH ANYTHING BELOW THIS ********************/
  44. $(document).ready(function() {
  45. log("Ready");
  46. let debounceTimeout;
  47. let rentalHistoryCache = null;
  48. let propertyListCache = null;
  49. let current_page = null;
  50. let property_details = null;
  51. let hex_darker = "#53728f";
  52. let player_id = $('#sidebarroot a[href*="profiles.php?XID="]').attr('href')?.match(/XID=(\d+)/)?.[1];
  53.  
  54. function drawNavigation() {
  55. let valid_properties = [];
  56. let total_properties = 0;
  57.  
  58. const navigation_div = `
  59. <div id="icey-container" style="display:none;">
  60. <div id="icey-property-rentals" class="m-top10">
  61. <div class="icey-head">
  62. <span class="icey-title">Easy Rental Extensions</span>
  63. </div>
  64. <div class="icey-content">
  65. <a id="previous-button" class="torn-btn">Previous</a>
  66. <div id="info-div" class="center-info"><p><strong>Rental Property Information</strong></p>
  67. <p><span class="rentals-count">0</span> / <span class="total-count">0</span> properties requiring attention.</p>
  68. <p class="viewing-rental"><span class="current-index">0</span> / <span class="rentals-count"></p>
  69. </div>
  70. <a id="next-button" class="torn-btn">Next</a>
  71.  
  72. </div>
  73. <hr class="page-head-delimiter m-top10 m-bottom10">
  74. </div>
  75. </div>
  76. `;
  77. if ($('#icey-property-rentals').length === 0) {
  78. log('Insert navigation div');
  79. $('#properties-page-wrap .content-title.m-bottom10').after(navigation_div);
  80. const propertyList = propertyListCache ? Promise.resolve(propertyListCache) : getPropertyList();
  81. propertyList.then(function(result) {
  82. if (!propertyListCache) {
  83. propertyListCache = result;
  84. }
  85. $.each(result.properties, function(key, value) {
  86. if (value.owner_id == player_id && properties.includes(value.property_type)) {
  87. total_properties++;
  88. if((key != result.property_id && !value.rented) || (value.rented && value.rented.days_left <= days_remaining)) {
  89. valid_properties.push(key);
  90. }
  91. }
  92. });
  93.  
  94. // Get the current property ID from the URL
  95. const current_property_id = getParam('ID');
  96.  
  97. if(current_property_id) {
  98. property_details = result.properties[current_property_id];
  99. injectOptionIntoEmptySlot('property-option-info', 'Happy: '+property_details.happy);
  100. }
  101.  
  102. let current_index = valid_properties.indexOf(current_property_id);
  103. let tab = 'offerExtension';
  104.  
  105. if (current_index > 0) {
  106. const previous_property_id = valid_properties[current_index - 1];
  107. if(result.properties[previous_property_id].rented) {
  108. tab = 'offerExtension';
  109. } else {
  110. tab = 'lease';
  111. }
  112. $('#previous-button').attr('href', `#/p=options&ID=${previous_property_id}&tab=${tab}`);
  113. } else {
  114. $('#previous-button').attr('href', '#').addClass('disabled'); // Disable if no previous
  115. }
  116.  
  117. // Handle the "Next" button
  118. if (current_index < valid_properties.length - 1) {
  119. const next_property_id = valid_properties[current_index === -1 ? 0 : current_index + 1];
  120. if(result.properties[next_property_id].rented) {
  121. tab = 'offerExtension';
  122. } else {
  123. tab = 'lease';
  124. }
  125. $('#next-button').attr('href', `#/p=options&ID=${next_property_id}&tab=${tab}`);
  126. } else {
  127. $('#next-button').attr('href', '#').addClass('disabled'); // Disable if no next
  128. }
  129. $('#icey-container .total-count').html(total_properties);
  130. $('#icey-container .rentals-count').html(valid_properties.length);
  131. if(current_index === -1 || current_page == 'properties') {
  132. $('#icey-container .viewing-rental').hide();
  133. } else {
  134. $('#icey-container .viewing-rental').show();
  135. $('#icey-container .current-index').html(current_index + 1);
  136. }
  137. setDynamicGradient();
  138. $('.icey-head').css('background', `linear-gradient(180deg, ${hex_color}, ${hex_darker})`);
  139. $('#icey-container').show();
  140. });
  141. }
  142. }
  143.  
  144.  
  145.  
  146. function checkTabAndRunScript() {
  147. // Do the stuff!
  148. var page = getParam('tab');
  149. if (page === 'offerExtension') {
  150. drawNavigation();
  151. let current_renter = null;
  152. let link = $('.offerExtension-form').find('a.h.t-blue');
  153. if (link.length > 0) {
  154. current_renter = link.attr('href')?.match(/XID=(\d+)/)?.[1] || null;
  155. log("Renter: " + current_renter);
  156. getPreviousValues(current_renter);
  157. } else {
  158. log("No renter found.");
  159. }
  160. } else if(page === 'lease') {
  161. drawNavigation();
  162. log("Add to rental market");
  163. setDefaultLeaseFields();
  164. } else if(page == null) {
  165. drawNavigation();
  166. } else {
  167. log("Wrong properties tab.");
  168. }
  169. }
  170.  
  171. function setDefaultLeaseFields()
  172. {
  173. const checkVisibility = setInterval(function() {
  174. const marketDiv = $('#market');
  175. if (marketDiv.is(':visible')) {
  176. // Access the input field and set its value
  177. $('#market').find('input.lease.input-money[data-name="money"]').val(default_cost);
  178. $('#market').find('input.lease.input-money[data-name="days"]').val(default_days);
  179.  
  180. clearInterval(checkVisibility); // Stop the interval once the value is set
  181. }
  182. }, 100); // Check every 100 milliseconds
  183.  
  184. $('#market input[type="submit"]').prop('disabled', false);
  185. }
  186.  
  187. function getPreviousValues(current_renter) {
  188. // Look for previous rental agreements and auto-fill input boxes
  189. var duration = default_days || 0;
  190. var cost = default_cost || 0;
  191. const property_id = getParam('ID');
  192. const activity = rentalHistoryCache ? Promise.resolve(rentalHistoryCache) : getRentalHistory();
  193. activity.then(function(result) {
  194. // Cache the result if not already cached
  195. if (!rentalHistoryCache) {
  196. rentalHistoryCache = result;
  197. }
  198.  
  199. $.each(result.log, function(key, value) {
  200. if(value.data.property_id == property_id && value.data.renter == current_renter) {
  201. duration = value.data.days;
  202. cost = value.data.rent;
  203. return false;
  204. }
  205. });
  206. // Target the input fields
  207. let costInput = $('.offerExtension.input-money[data-name="offercost"]');
  208. let daysInput = $('.offerExtension.input-money[data-name="days"]');
  209.  
  210. // Set the values
  211. costInput.val(cost);
  212. daysInput.val(duration);
  213. // costInput.tornInputMoney({buttonElement: null, skipBlurCheck: true});
  214.  
  215. // Trigger events to mimic manual input
  216. // costInput.trigger('input').trigger('change').trigger('keyup'); // Attempt to simulate input event for Torn
  217. // daysInput.trigger('input').trigger('change').trigger('keyup'); // Attempt to simulate input event for Torn
  218.  
  219. $('.offerExtension-form input[type="submit"]').prop('disabled', false);
  220. });
  221. }
  222.  
  223. async function getRentalHistory() {
  224. // Get the activity log for both rental extensions and new rental agreements
  225. return new Promise(resolve => {
  226. const request_url = `https://api.torn.com/user/?selections=log&key=`+apikey+`&log=5943,5937&comment=EasyRentalExtensions`;
  227. GM_xmlhttpRequest ({
  228. method: "GET",
  229. url: request_url,
  230. headers: {
  231. "Content-Type": "application/json"
  232. },
  233. onload: response => {
  234. try {
  235. const data = JSON.parse(response.responseText);
  236. if(!data) {
  237. log('No response from Torn API');
  238. } else {
  239. log('Log data fetched.');
  240. return resolve(data);
  241. }
  242. }
  243. catch (e) {
  244. console.error(e);
  245. }
  246.  
  247. },
  248. onerror: (e) => {
  249. console.error(e);
  250. }
  251. })
  252. });
  253. }
  254.  
  255. async function getPropertyList() {
  256. // Get the activity log for both rental extensions and new rental agreements
  257. return new Promise(resolve => {
  258. const request_url = `https://api.torn.com/user/?selections=profile,properties&key=`+apikey+`&comment=EasyRentalExtensions`;
  259. GM_xmlhttpRequest ({
  260. method: "GET",
  261. url: request_url,
  262. headers: {
  263. "Content-Type": "application/json"
  264. },
  265. onload: response => {
  266. try {
  267. const data = JSON.parse(response.responseText);
  268. if(!data) {
  269. log('No response from Torn API');
  270. } else {
  271. log('Property data fetched.');
  272. return resolve(data);
  273. }
  274. }
  275. catch (e) {
  276. console.error(e);
  277. }
  278.  
  279. },
  280. onerror: (e) => {
  281. console.error(e);
  282. }
  283. })
  284. });
  285. }
  286.  
  287. function injectOptionIntoEmptySlot(iconClass, text) {
  288. log("Adding details to property", text);
  289. const emptySlot = $('ul.options-list.left li.empty').first();
  290. if (emptySlot.length) {
  291. emptySlot.removeClass('empty').addClass('custom-extension');
  292. emptySlot.find('.p-icon').html(`<i class="${iconClass}"></i>`);
  293. emptySlot.find('.desc').text(text);
  294. }
  295. }
  296.  
  297. function getParam(name) {
  298.  
  299. // Extract the part of the URL after the #
  300. const fragment = window.location.href.split('#')[1];
  301. if (!fragment) return null; // No fragment present
  302.  
  303. // Treat the fragment like a query string
  304. const results = new RegExp(name + '=([^&#]*)').exec(fragment);
  305. return results ? decodeURIComponent(results[1]) : null; // Decode and return the value, or null if not found
  306. }
  307.  
  308. function log(message) {
  309. if(debug){
  310. console.log("[RentExtension] "+message);
  311. }
  312. }
  313.  
  314. function setDynamicGradient() {
  315. // Convert base hex to RGB
  316. const baseRgb = hexToRgb(hex_color);
  317.  
  318. // Create a darker shade by reducing brightness
  319. const darkerRgb = darkenRgb(baseRgb, 0.7); // 0.7 is the factor for darkening
  320.  
  321. // Convert the darker RGB back to hex
  322. hex_darker = rgbToHex(darkerRgb.r, darkerRgb.g, darkerRgb.b);
  323. }
  324.  
  325. // Helper: Convert hex to RGB
  326. function hexToRgb(hex) {
  327. const bigint = parseInt(hex.replace('#', ''), 16);
  328. return {
  329. r: (bigint >> 16) & 255,
  330. g: (bigint >> 8) & 255,
  331. b: bigint & 255,
  332. };
  333. }
  334.  
  335. // Helper: Darken an RGB color
  336. function darkenRgb(rgb, factor) {
  337. return {
  338. r: Math.max(0, Math.min(255, Math.floor(rgb.r * factor))),
  339. g: Math.max(0, Math.min(255, Math.floor(rgb.g * factor))),
  340. b: Math.max(0, Math.min(255, Math.floor(rgb.b * factor))),
  341. };
  342. }
  343.  
  344. // Helper: Convert RGB to hex
  345. function rgbToHex(r, g, b) {
  346. return `#${((1 << 24) + (r << 16) + (g << 8) + b).toString(16).slice(1).toUpperCase()}`;
  347. }
  348.  
  349. // Create an observer for the properties page to watch for page changes
  350. // Select the target node
  351. const targetNode = document.getElementById('properties-page-wrap');
  352.  
  353. if (targetNode) {
  354. // Create a MutationObserver to watch for changes
  355. const observer = new MutationObserver((mutationsList) => {
  356. clearTimeout(debounceTimeout); // Reset the debounce timeout on every change
  357. debounceTimeout = setTimeout(() => {
  358. log("Content changed in #properties-page-wrap");
  359. current_page = getParam('p');
  360. checkTabAndRunScript(); // Run your script when content settles
  361. }, 500); // Debounce for 500ms
  362. });
  363.  
  364. // Start observing the target node for configured mutations
  365. observer.observe(targetNode, {
  366. childList: true, // Watch for added/removed child nodes
  367. subtree: true, // Watch the entire subtree of the target node
  368. });
  369.  
  370. //console.log("MutationObserver is set up with debouncing.");
  371. } else {
  372. console.error("Target node #properties-page-wrap not found.");
  373. }
  374.  
  375. // Run the script initially in case the page is already on the correct tab
  376. checkTabAndRunScript();
  377.  
  378.  
  379. });
  380. GM_addStyle(`
  381. #icey-container {
  382. width: 100%;
  383. }
  384. .icey-head {
  385. border-radius: 5px 5px 0 0;
  386. height: 30px;
  387. line-height: 30px;
  388. width: 100%;
  389. background: linear-gradient(180deg, #8ABEEF, #53728f);
  390. color: white;
  391. }
  392. .icey-title {
  393. width: 100%;
  394. font-size: 13px;
  395. letter-spacing: 1px;
  396. font-weight: 700;
  397. line-height: 16px;
  398. flex-grow: 2;
  399. padding: 5px;
  400. margin: 5px;
  401. text-transform: capitalize;
  402. text-shadow: rgba(0, 0, 0, 0.65) 1px 1px 2px;
  403. }
  404. body.dark-mode .icey-content {
  405. background: #333333;
  406. }
  407. body.dark-mode .icey-content td {
  408. color: #c0c0c0;
  409. }
  410. .icey-content {
  411. display: flex;
  412. justify-content: space-between;
  413. align-items: center;
  414. padding: 5px;
  415. background: #f2f2f2;
  416. border-radius: 0 0 5px 5px;
  417. box-shadow: 0 1px 3px #06060680;
  418. }
  419. .icey-content .torn-btn {
  420. flex: 0 0 auto;
  421. }
  422.  
  423. .center-info {
  424. flex: 1 1 auto;
  425. text-align: center;
  426. }
  427.  
  428. `);