// ==UserScript==
// @name WTF Flight Club
// @namespace https://github.com/Silverdark/TornScripts
// @version 2025-05-05.1
// @description Flight Club Helper tools. Currently no support for TORN PDA.
// @author Silverdark [3503183], neth [3564828]
// @icon data:image/png;base64,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
// @match https://www.torn.com/item.php
// @grant GM_xmlhttpRequest
// @grant GM_getValue
// @grant GM_setValue
// @grant GM_deleteValue
// @grant GM_registerMenuCommand
// @connect travel.wtf-torn.app
// ==/UserScript==
(async function() {
'use strict';
const allowedItemIds = [
// Flowers
"282", // African Violet
"617", // Banana Orchid
"271", // Ceibo
"277", // Cherry Blossom
"263", // Crocus
"260", // Dahlia
"272", // Edelweiss
"267", // Heather
"264", // Orchid
"276", // Peony
"385", // Tribulus Omanens
// Plushies
"384", // Camel
"273", // Chamois
"258", // Jaguar
"215", // Kitten
"281", // Lion
"269", // Monkey
"266", // Nessie
"274", // Panda
"268", // Red Fox
"186", // Sheep
"618", // Stingray
"187", // Teddy Bear
"261", // Wolverine
];
const travelWebsiteUrl = "https://travel.wtf-torn.app/";
const dataKey_flightClub = "flightClubData";
const dataKey_publicApiKey = "publicApiKey";
const event_FlightClubDataChanged = 'flight-club-data-changed';
const event_FlightClubItemRowChanged = 'flight-club-itemrow-changed';
const event_PublicApiKeyChanged = 'public-api-key-changed';
const supportedFlightClubDataVersion = "1.0";
const flightClubCacheTimeInSeconds = 120;
const listeners = {};
const remainingByItemName = new Map();
let apiKey = await GM.getValue(dataKey_publicApiKey, null);
on(event_FlightClubItemRowChanged, itemRow => {
const actionsWrap = itemRow.querySelector(".actions-wrap");
const fcSendContainer = actionsWrap.children[2];
const fcOriginalSendButton = actionsWrap.children[1];
// Use the "sell" class as indicator if there is already a send button
if (fcSendContainer.classList.contains("sell")) return;
fcSendContainer.classList.add("sell");
const fcButton = createFlightClubSendButton();
fcButton.addEventListener("click", function (btnEvt) {
btnEvt.stopPropagation();
fcOriginalSendButton.click();
const actionsNode = btnEvt.target.closest(".cont-wrap");
if (!actionsNode) return;
waitForElm(actionsNode, ".user-id-label").then(() => {
updateSendDetailsToTargetFlightClub(actionsNode);
});
});
fcSendContainer.appendChild(fcButton);
});
on(event_FlightClubItemRowChanged, itemRow => {
updateStatusFlightClubItemRow(itemRow);
});
on(event_FlightClubDataChanged, async () => {
const itemRows = await getFlightClubItemRows();
for (const itemRow of itemRows) {
updateStatusFlightClubItemRow(itemRow);
}
});
on(event_PublicApiKeyChanged, async () => {
remainingByItemName.clear();
await GM.deleteValue(dataKey_flightClub);
await initFlightClubData();
});
init();
// Helper functions
function init() {
initFlightClubData();
initFlightClubItemRowChange();
GM_registerMenuCommand('Set API Key', () => {
createApiKeyInput();
});
}
async function initFlightClubItemRowChange() {
const categoryWrapper = await waitForElm(document.body, "#category-wrap");
const observer = new MutationObserver((mutations) => {
for (const mutation of mutations) {
const node = mutation.target;
if (!isItemRowNode(node)) continue;
emit(event_FlightClubItemRowChanged, node);
}
});
observer.observe(categoryWrapper, {
subtree: true,
attributes: true
});
}
async function getFlightClubItemRows() {
const categoryWrapper = await waitForElm(document.body, "#category-wrap");
const nodes = document.querySelectorAll("#category-wrap li");
return Array.from(nodes).filter(x => isItemRowNode(x));
}
function isItemRowNode(node) {
const itemId = node.dataset.item;
if (!itemId || !allowedItemIds.includes(itemId)) return false;
const itemName = node.dataset.sort;
if (!itemName) return false;
return true;
}
// Send button
function createFlightClubSendButton() {
const fcSpan = document.createElement("span");
fcSpan.className = "icon-h";
fcSpan.title = "Send to Flight Club";
const fcButton = document.createElement("button");
fcButton.className = "wai-btn";
fcButton.style.width = "34px";
const fcImage = document.createElement("img");
fcImage.src = "data:image/png;base64,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";
fcImage.alt = "WTF Flight Club";
fcImage.style.width = "18px";
fcImage.style.height = "18px";
fcImage.style.verticalAlign = "middle";
const optSpan = document.createElement("span");
optSpan.className = "opt-name";
optSpan.textContent = "Flight";
fcButton.appendChild(fcImage);
fcSpan.appendChild(fcButton);
fcSpan.appendChild(optSpan);
return fcSpan;
}
function updateSendDetailsToTargetFlightClub(node) {
const hiddenAmountTextInput = node.querySelector("input[type=hidden].amount");
const amountTextInput = node.querySelector("input[type=text].amount");
hiddenAmountTextInput.value = amountTextInput.dataset.max;
amountTextInput.value = amountTextInput.dataset.max;
const receiverTextInput = node.querySelector("input[type=text].user-id");
receiverTextInput.value = "Hecle [3099100]";
}
// Flight Club Goal status
async function initFlightClubData() {
// Disable, when no API key defined
if (!apiKey || apiKey.trim() === '') {
emit(event_FlightClubDataChanged);
return;
}
// Read data from storage
let data = await GM.getValue(dataKey_flightClub, null);
if (data && data.version !== supportedFlightClubDataVersion) {
await GM.deleteValue(dataKey_flightClub);
data = null;
}
// Load data, if required
const currentTimestamp = new Date().getTime();
if (!data || currentTimestamp > data.lastUpdate + flightClubCacheTimeInSeconds * 1000) {
await GM.deleteValue(dataKey_flightClub);
await ensureFlightClubLoggedIn();
const itemDataByItemName = await getCurrentItemDataByItemName();
data = {
version: supportedFlightClubDataVersion,
lastUpdate: currentTimestamp,
itemDataByItemName,
};
await GM.setValue(dataKey_flightClub, data);
}
// Transform raw data
for (const [itemName, itemData] of Object.entries(data.itemDataByItemName)) {
remainingByItemName.set(itemName, itemData.remaining);
}
emit(event_FlightClubDataChanged);
}
async function ensureFlightClubLoggedIn() {
const loginPageResponse = await gmFetch(`${travelWebsiteUrl}`, 'GET');
const { responseText } = loginPageResponse;
if (responseText.includes('You are logged in as')) return;
const parser = new DOMParser();
const doc = parser.parseFromString(responseText, 'text/html');
const token = doc.querySelector('#loginForm > input[type=hidden]').value;
await gmFetch(`${travelWebsiteUrl}login`, 'POST',
{
'Content-Type': 'application/x-www-form-urlencoded',
},
`_token=${encodeURIComponent(token)}&apikey=${encodeURIComponent(apiKey)}`
);
}
async function getCurrentItemDataByItemName() {
const plushieDataByItemName = await getItemDataByItemNameFromPage(`${travelWebsiteUrl}dashboard`);
const flowerDataByItemName = await getItemDataByItemNameFromPage(`${travelWebsiteUrl}dashboard/flowers`);
return Object.assign({}, plushieDataByItemName, flowerDataByItemName);
}
async function getItemDataByItemNameFromPage(page) {
const response = await gmFetch(page, 'GET');
const responseText = response.responseText;
const itemDataByItemName = {};
const parser = new DOMParser();
const doc = parser.parseFromString(responseText, 'text/html');
const table = doc.querySelector('#overview_table > tbody');
if (!table) return itemDataByItemName;
for (const row of table.children) {
const itemName = Array.from(row.children[0].childNodes)
.find(x => x.nodeType === Node.TEXT_NODE)
.textContent
.trim();
itemDataByItemName[itemName] = {
received: row.children[1].textContent,
remaining: row.children[2].textContent,
done: row.children[3].textContent.trim(),
};
}
return itemDataByItemName;
}
function updateStatusFlightClubItemRow(itemRow) {
const itemName = itemRow.dataset.sort;
if (!itemName) return;
let remaining = null;
for (let [name, val] of remainingByItemName) {
if (!itemName.includes(name)) continue;
remaining = val;
break;
}
const nameWrap = itemRow.querySelector(".name-wrap");
let statusTextNode = nameWrap.querySelector(".flight-club-status");
if (!remaining) {
if (statusTextNode) {
statusTextNode.remove();
}
return;
}
if (!statusTextNode) {
statusTextNode = document.createElement("span");
statusTextNode.className = "qty flight-club-status";
nameWrap.appendChild(statusTextNode);
}
statusTextNode.innerHTML = `(${remaining})`;
statusTextNode.style.color = remaining < 0 ? "red" : "green";
}
// Set API key function
function createApiKeyInput() {
const headerRoot = document.getElementById('header-root');
if (!headerRoot) return;
const apiKeyInputForm = document.getElementById('apiKeyputId');
if (apiKeyInputForm) return;
const wrapper = document.createElement('div');
wrapper.id = 'apiKeyputId';
wrapper.style.width = '100%';
wrapper.style.height = '50px';
wrapper.style.display = 'flex';
wrapper.style.flexDirection = 'row';
wrapper.style.gap = '10px';
wrapper.style.justifyContent = 'center';
wrapper.style.alignItems = 'center';;
wrapper.style.padding = '4px';
const label = document.createElement('label');
label.innerText = 'Enter WTF Flight API Key';
const input = document.createElement('input');
input.type = 'text';
input.value = apiKey;
input.style.width = '140px';
input.style.padding = '4px';
const submit = document.createElement('button');
submit.classList.add('torn-btn');
submit.innerText = 'Submit';
submit.onclick = () => {
apiKey = input.value;
GM_setValue(dataKey_publicApiKey, apiKey);
wrapper.remove();
emit(event_PublicApiKeyChanged);
}
wrapper.appendChild(label);
wrapper.appendChild(input);
wrapper.appendChild(submit);
headerRoot.parentNode.insertBefore(wrapper, headerRoot);
}
// Utilities
// Source: https://stackoverflow.com/a/61511955
function waitForElm(parent, selector) {
return new Promise(resolve => {
if (parent.querySelector(selector)) {
return resolve(parent.querySelector(selector));
}
const observer = new MutationObserver(mutations => {
if (parent.querySelector(selector)) {
observer.disconnect();
resolve(parent.querySelector(selector));
}
});
observer.observe(document.body, {
childList: true,
subtree: true
});
});
}
function gmFetch(url, method, headers = {}, data = '') {
return new Promise((resolve, reject) => {
GM_xmlhttpRequest({
method: method,
url: url,
headers: headers,
data: data,
onload: resolve,
onerror: reject,
})
})
}
// Event handling
function on(event, callback) {
if (!listeners[event]) listeners[event] = [];
listeners[event].push(callback);
}
function emit(event, data) {
(listeners[event] || []).forEach(cb => cb(data));
}
})();