TORN Friend Extender

A script that lets you add, store, and delete friends on Torn with custom descriptions.

  1. // ==UserScript==
  2. // @name TORN Friend Extender
  3. // @namespace http://tampermonkey.net/
  4. // @version 1.1.1
  5. // @author Clyoth
  6. // @description A script that lets you add, store, and delete friends on Torn with custom descriptions.
  7. // @match https://www.torn.com/friendlist.php
  8. // @icon https://play-lh.googleusercontent.com/BkaIDbibtUpGcziVQsgCya-eC7oxTUHL5G8m8v3XW3S11_-GZEItaxzeXxhKmoAiX8x6
  9. // @license MIT
  10. // @grant none
  11. // ==/UserScript==
  12.  
  13. (function () {
  14. const apiKey = 'ADD_API_KEY_HERE';
  15. const FRIENDS_STORAGE_KEY = 'tornFriendList';
  16.  
  17. let addButtonAdded = false;
  18. let addedFriends = new Set(); // Track added friends by ID
  19. let friendsLoaded = false; // Flag to track if friends have been loaded already
  20.  
  21. const style = document.createElement('style');
  22. style.textContent = `
  23. .add-button {
  24. padding: 12px 20px;
  25. background-color: #3b3b3b;
  26. color: white;
  27. border: none;
  28. border-radius: 5px;
  29. cursor: pointer;
  30. font-size: 14px;
  31. transition: background-color 0.3s ease, transform 0.2s ease;
  32. margin-top: 4px;
  33. width: auto;
  34. text-align: center;
  35. }
  36. .add-button:hover {
  37. background-color: #5c5c5c;
  38. transform: scale(1.05);
  39. }
  40. #friendForm {
  41. padding: 20px;
  42. border-radius: 8px;
  43. margin-top: 10px;
  44. color: white;
  45. box-shadow: 0 4px 6px rgba(0, 0, 0, 0.2);
  46. background-color: #2c2f33;
  47. }
  48. #friendForm h3 {
  49. margin-top:4px;
  50. }
  51. #friendDataForm input[type="text"] {
  52. width: 30%;
  53. padding: 8px;
  54. margin-top: 8px;
  55. border: 2px solid #555;
  56. border-radius: 5px;
  57. background-color: #444;
  58. color: white;
  59. font-size: 12px;
  60. }
  61. #friendDataForm label {
  62. font-weight: bold;
  63. display: block;
  64. margin-bottom: 4px;
  65. color: #d3d3d3;
  66. }
  67. #friendDataForm button {
  68. padding: 8px 16px;
  69. background-color: #3b3b3b;
  70. color: white;
  71. border: none;
  72. border-radius: 5px;
  73. cursor: pointer;
  74. font-size: 12px;
  75. margin-top: 10px;
  76. transition: background-color 0.3s ease;
  77. }
  78. #friendDataForm button:hover {
  79. background-color: #5c5c5c;
  80. }
  81. #friendDataForm #cancelForm {
  82. background-color: #8b0000;
  83. margin-left: 10px;
  84. }
  85. #friendDataForm #cancelForm:hover {
  86. background-color: #b22222;
  87. }
  88. `;
  89. document.head.appendChild(style);
  90.  
  91. function loadFriends() {
  92. const storedFriends = localStorage.getItem(FRIENDS_STORAGE_KEY);
  93. return storedFriends ? JSON.parse(storedFriends) : [];
  94. }
  95.  
  96. function saveFriends(friends) {
  97. localStorage.setItem(FRIENDS_STORAGE_KEY, JSON.stringify(friends));
  98. }
  99.  
  100. function createForm() {
  101. if (document.getElementById('friendForm')) return;
  102.  
  103. const formHTML = `
  104. <div id="friendForm" style="padding: 10px; background-color: #323233; border-radius: 5px; margin-top: 10px;">
  105. <h3>Add Friend</h3>
  106. <form id="friendDataForm">
  107. <label for="friendId">ID:</label>
  108. <input type="text" id="friendId" name="id" required /><br><br>
  109. <label for="friendDescription">Description:</label>
  110. <input type="text" id="friendDescription" name="description" maxlength="40"/><br><br>
  111. <button type="submit">Fetch and Add Friend</button>
  112. <button type="button" id="cancelForm">Cancel</button>
  113. </form>
  114. </div>
  115. `;
  116.  
  117. const formContainer = document.createElement('div');
  118. formContainer.innerHTML = formHTML;
  119.  
  120. const paginationWrapper = document.querySelector('.pagination-wrapper');
  121. if (paginationWrapper) {
  122. paginationWrapper.appendChild(formContainer);
  123. } else {
  124. console.error('pagination-wrapper not found.');
  125. }
  126.  
  127. document.getElementById('cancelForm').addEventListener('click', () => {
  128. formContainer.remove();
  129. });
  130.  
  131. document.getElementById('friendDataForm').addEventListener('submit', async (event) => {
  132. event.preventDefault();
  133. const friendId = document.getElementById('friendId').value;
  134. const description = document.getElementById('friendDescription').value || 'None';
  135.  
  136. // Only store the ID and description
  137. const friends = loadFriends();
  138. friends.push({ id: friendId, description });
  139. saveFriends(friends);
  140. addFriendToList({ id: friendId, description });
  141.  
  142. formContainer.remove();
  143. });
  144. }
  145.  
  146. async function fetchFriendData(id) {
  147. try {
  148. const response = await fetch(`https://api.torn.com/user/${id}?selections=profile&key=${apiKey}`);
  149. const data = await response.json();
  150.  
  151. if (data.error) {
  152. alert(`Error: ${data.error.code} - ${data.error.error}`);
  153. return null;
  154. }
  155.  
  156. return {
  157. id: id,
  158. name: data.name,
  159. level: data.level,
  160. status: data.status.description,
  161. profileUrl: `/profiles.php?XID=${id}`,
  162. description: "None" // The description will be stored separately
  163. };
  164. } catch (error) {
  165. console.error("Failed to fetch friend data:", error);
  166. alert("Failed to fetch friend data. Please check the console for details.");
  167. return null;
  168. }
  169. }
  170.  
  171. function addFriendButton() {
  172. // Check if the button already exists
  173. if (document.querySelector('.add-button')) return;
  174.  
  175. const addButton = document.createElement('button');
  176. addButton.textContent = 'Add Friend by ID';
  177. addButton.addEventListener('click', createForm);
  178. addButton.classList.add('add-button');
  179.  
  180. const paginationWrapper = document.querySelector('.pagination-wrapper');
  181. if (paginationWrapper && !paginationWrapper.contains(addButton)) {
  182. paginationWrapper.appendChild(addButton);
  183. }
  184. }
  185.  
  186. async function addFriendToList(friendData) {
  187. const targetUL = document.querySelector(".user-info-blacklist-wrap");
  188.  
  189. if (targetUL) {
  190. if (addedFriends.has(friendData.id)) return; // Prevent adding duplicate friends
  191.  
  192. // Fetch the latest data every time a friend is added
  193. const latestData = await fetchFriendData(friendData.id);
  194. if (!latestData) return;
  195.  
  196. // Extract the first word from the status
  197. const firstWordStatus = latestData.status.split(' ')[0];
  198.  
  199. const newLI = document.createElement("li");
  200. newLI.setAttribute('data-id', friendData.id);
  201.  
  202. newLI.innerHTML = `
  203. <div class="delete">
  204. <i class="delete-user"></i>
  205. </div>
  206.  
  207. <div class="acc-wrapper">
  208. <div class="expander left">
  209. <span class="honor-text-wrap blue big" style="display: block; text-align: center; width: 100%;">
  210. <a href="${latestData.profileUrl}" title="${latestData.name} [${latestData.id}]" style="text-decoration: none; color:white;">
  211. ${latestData.name}
  212. </a>
  213. </span>
  214. <div class="d-hide expand right">
  215. <i class="collapse-arrow"></i>
  216. </div>
  217. </div>
  218.  
  219. <div class="acc-body">
  220. <div class="level left">
  221. <span class="d-hide bold">Level:</span> ${latestData.level}
  222. </div>
  223.  
  224. <div class="status left">
  225. <span class="d-hide bold">Status:</span>
  226. <div class="status-description" style="text-overflow:elipsis;">${firstWordStatus}</div>
  227. </div>
  228.  
  229. <div class="description">
  230. <div class="left"></div>
  231. <div class="text left t-overflow">${friendData.description}</div>
  232. </div>
  233. </div>
  234. </div>
  235. `;
  236.  
  237. targetUL.appendChild(newLI);
  238.  
  239. addedFriends.add(friendData.id); // Mark as added to prevent duplicates
  240.  
  241. const deleteButton = newLI.querySelector('.delete-user');
  242. deleteButton.addEventListener('click', () => deleteFriend(friendData.id, newLI));
  243. }
  244. }
  245.  
  246.  
  247. function deleteFriend(id, listItem) {
  248. listItem.remove();
  249. addedFriends.delete(id); // Remove from the added friends set
  250. const friends = loadFriends().filter(friend => friend.id !== id);
  251. saveFriends(friends);
  252. }
  253.  
  254. function loadAndDisplayFriends() {
  255. if (friendsLoaded) return; // Prevent loading friends again
  256.  
  257. const friends = loadFriends();
  258. friends.forEach(friendData => {
  259. // Avoid re-adding the same friend
  260. if (!addedFriends.has(friendData.id)) {
  261. addFriendToList(friendData);
  262. }
  263. });
  264.  
  265. friendsLoaded = true; // Set the flag to prevent reloading
  266. }
  267.  
  268. const observer = new MutationObserver(() => {
  269. if (document.querySelector('.pagination-wrapper')) {
  270. addFriendButton();
  271. }
  272. if (document.querySelector('.user-info-blacklist-wrap')) {
  273. loadAndDisplayFriends();
  274. }
  275. });
  276.  
  277. observer.observe(document.body, { childList: true, subtree: true });
  278. })();
  279.