A script that lets you add, store, and delete friends on Torn with custom descriptions.
// ==UserScript==
// @name TORN Friend Extender
// @namespace http://tampermonkey.net/
// @version 1.1.1
// @author Clyoth
// @description A script that lets you add, store, and delete friends on Torn with custom descriptions.
// @match https://www.torn.com/friendlist.php
// @icon https://play-lh.googleusercontent.com/BkaIDbibtUpGcziVQsgCya-eC7oxTUHL5G8m8v3XW3S11_-GZEItaxzeXxhKmoAiX8x6
// @license MIT
// @grant none
// ==/UserScript==
(function () {
const apiKey = 'ADD_API_KEY_HERE';
const FRIENDS_STORAGE_KEY = 'tornFriendList';
let addButtonAdded = false;
let addedFriends = new Set(); // Track added friends by ID
let friendsLoaded = false; // Flag to track if friends have been loaded already
const style = document.createElement('style');
style.textContent = `
.add-button {
padding: 12px 20px;
background-color: #3b3b3b;
color: white;
border: none;
border-radius: 5px;
cursor: pointer;
font-size: 14px;
transition: background-color 0.3s ease, transform 0.2s ease;
margin-top: 4px;
width: auto;
text-align: center;
}
.add-button:hover {
background-color: #5c5c5c;
transform: scale(1.05);
}
#friendForm {
padding: 20px;
border-radius: 8px;
margin-top: 10px;
color: white;
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.2);
background-color: #2c2f33;
}
#friendForm h3 {
margin-top:4px;
}
#friendDataForm input[type="text"] {
width: 30%;
padding: 8px;
margin-top: 8px;
border: 2px solid #555;
border-radius: 5px;
background-color: #444;
color: white;
font-size: 12px;
}
#friendDataForm label {
font-weight: bold;
display: block;
margin-bottom: 4px;
color: #d3d3d3;
}
#friendDataForm button {
padding: 8px 16px;
background-color: #3b3b3b;
color: white;
border: none;
border-radius: 5px;
cursor: pointer;
font-size: 12px;
margin-top: 10px;
transition: background-color 0.3s ease;
}
#friendDataForm button:hover {
background-color: #5c5c5c;
}
#friendDataForm #cancelForm {
background-color: #8b0000;
margin-left: 10px;
}
#friendDataForm #cancelForm:hover {
background-color: #b22222;
}
`;
document.head.appendChild(style);
function loadFriends() {
const storedFriends = localStorage.getItem(FRIENDS_STORAGE_KEY);
return storedFriends ? JSON.parse(storedFriends) : [];
}
function saveFriends(friends) {
localStorage.setItem(FRIENDS_STORAGE_KEY, JSON.stringify(friends));
}
function createForm() {
if (document.getElementById('friendForm')) return;
const formHTML = `
<div id="friendForm" style="padding: 10px; background-color: #323233; border-radius: 5px; margin-top: 10px;">
<h3>Add Friend</h3>
<form id="friendDataForm">
<label for="friendId">ID:</label>
<input type="text" id="friendId" name="id" required /><br><br>
<label for="friendDescription">Description:</label>
<input type="text" id="friendDescription" name="description" maxlength="40"/><br><br>
<button type="submit">Fetch and Add Friend</button>
<button type="button" id="cancelForm">Cancel</button>
</form>
</div>
`;
const formContainer = document.createElement('div');
formContainer.innerHTML = formHTML;
const paginationWrapper = document.querySelector('.pagination-wrapper');
if (paginationWrapper) {
paginationWrapper.appendChild(formContainer);
} else {
console.error('pagination-wrapper not found.');
}
document.getElementById('cancelForm').addEventListener('click', () => {
formContainer.remove();
});
document.getElementById('friendDataForm').addEventListener('submit', async (event) => {
event.preventDefault();
const friendId = document.getElementById('friendId').value;
const description = document.getElementById('friendDescription').value || 'None';
// Only store the ID and description
const friends = loadFriends();
friends.push({ id: friendId, description });
saveFriends(friends);
addFriendToList({ id: friendId, description });
formContainer.remove();
});
}
async function fetchFriendData(id) {
try {
const response = await fetch(`https://api.torn.com/user/${id}?selections=profile&key=${apiKey}`);
const data = await response.json();
if (data.error) {
alert(`Error: ${data.error.code} - ${data.error.error}`);
return null;
}
return {
id: id,
name: data.name,
level: data.level,
status: data.status.description,
profileUrl: `/profiles.php?XID=${id}`,
description: "None" // The description will be stored separately
};
} catch (error) {
console.error("Failed to fetch friend data:", error);
alert("Failed to fetch friend data. Please check the console for details.");
return null;
}
}
function addFriendButton() {
// Check if the button already exists
if (document.querySelector('.add-button')) return;
const addButton = document.createElement('button');
addButton.textContent = 'Add Friend by ID';
addButton.addEventListener('click', createForm);
addButton.classList.add('add-button');
const paginationWrapper = document.querySelector('.pagination-wrapper');
if (paginationWrapper && !paginationWrapper.contains(addButton)) {
paginationWrapper.appendChild(addButton);
}
}
async function addFriendToList(friendData) {
const targetUL = document.querySelector(".user-info-blacklist-wrap");
if (targetUL) {
if (addedFriends.has(friendData.id)) return; // Prevent adding duplicate friends
// Fetch the latest data every time a friend is added
const latestData = await fetchFriendData(friendData.id);
if (!latestData) return;
// Extract the first word from the status
const firstWordStatus = latestData.status.split(' ')[0];
const newLI = document.createElement("li");
newLI.setAttribute('data-id', friendData.id);
newLI.innerHTML = `
<div class="delete">
<i class="delete-user"></i>
</div>
<div class="acc-wrapper">
<div class="expander left">
<span class="honor-text-wrap blue big" style="display: block; text-align: center; width: 100%;">
<a href="${latestData.profileUrl}" title="${latestData.name} [${latestData.id}]" style="text-decoration: none; color:white;">
${latestData.name}
</a>
</span>
<div class="d-hide expand right">
<i class="collapse-arrow"></i>
</div>
</div>
<div class="acc-body">
<div class="level left">
<span class="d-hide bold">Level:</span> ${latestData.level}
</div>
<div class="status left">
<span class="d-hide bold">Status:</span>
<div class="status-description" style="text-overflow:elipsis;">${firstWordStatus}</div>
</div>
<div class="description">
<div class="left"></div>
<div class="text left t-overflow">${friendData.description}</div>
</div>
</div>
</div>
`;
targetUL.appendChild(newLI);
addedFriends.add(friendData.id); // Mark as added to prevent duplicates
const deleteButton = newLI.querySelector('.delete-user');
deleteButton.addEventListener('click', () => deleteFriend(friendData.id, newLI));
}
}
function deleteFriend(id, listItem) {
listItem.remove();
addedFriends.delete(id); // Remove from the added friends set
const friends = loadFriends().filter(friend => friend.id !== id);
saveFriends(friends);
}
function loadAndDisplayFriends() {
if (friendsLoaded) return; // Prevent loading friends again
const friends = loadFriends();
friends.forEach(friendData => {
// Avoid re-adding the same friend
if (!addedFriends.has(friendData.id)) {
addFriendToList(friendData);
}
});
friendsLoaded = true; // Set the flag to prevent reloading
}
const observer = new MutationObserver(() => {
if (document.querySelector('.pagination-wrapper')) {
addFriendButton();
}
if (document.querySelector('.user-info-blacklist-wrap')) {
loadAndDisplayFriends();
}
});
observer.observe(document.body, { childList: true, subtree: true });
})();