Torn Missions Indicator

Adds a status icon when configured number of missions are pending

이 스크립트를 설치하려면 Tampermonkey, Greasemonkey 또는 Violentmonkey와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 Tampermonkey와 같은 확장 프로그램을 설치해야 합니다.

이 스크립트를 설치하려면 Tampermonkey 또는 Violentmonkey와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 Tampermonkey 또는 Userscripts와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 Tampermonkey와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 유저 스크립트 관리자 확장 프로그램이 필요합니다.

(이미 유저 스크립트 관리자가 설치되어 있습니다. 설치를 진행합니다!)

이 스타일을 설치하려면 Stylus와 같은 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 Stylus와 같은 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 Stylus와 같은 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 유저 스타일 관리자 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 유저 스타일 관리자 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 유저 스타일 관리자 확장 프로그램이 필요합니다.

(이미 유저 스타일 관리자가 설치되어 있습니다. 설치를 진행합니다!)

// ==UserScript==
// @name         Torn Missions Indicator
// @namespace    https://github.com/Silverdark/TornScripts
// @version      2025-11-09.1
// @description  Adds a status icon when configured number of missions are pending
// @author       Silverdark [3503183]
// @match        https://www.torn.com/*
// @grant        GM_addStyle
// @grant        GM_xmlhttpRequest
// @grant        GM_getValue
// @grant        GM_setValue
// @grant        GM_deleteValue
// @connect      api.torn.com
// @run-at       document-end
// ==/UserScript==

(function() {
    'use strict';

    // Configuration

    const desktopApiKey = ''; // Set with minimal API key on desktop
    const showIconOnMissionsPending = 3;
    const dataCacheExpirationMinutes = 3 * 60; // 3 hours

    // Configuration end - DO NOT TOUCH

    const apiKey = desktopApiKey === '' ? '###PDA-APIKEY###' : desktopApiKey;
    const dataKey_Missions = 'missionData';
    const supportedDataVersion = "1.0";

    init();

    // Helper functions

    async function init() {
        if (apiKey === '' || apiKey.startsWith('###')) {
            console.error('No API key for mission update found - exit');
            return;
        }

        const missonsData = await getMissionsData();
        const missionsPending = countMissionsPending(missonsData);

        if (missionsPending >= showIconOnMissionsPending) {
            addPendingMissionsIcon();
        }
    }

    async function getMissionsData() {
        // Read data from storage
        let data = await GM.getValue(dataKey_Missions, null);
        if (data && data.version !== supportedDataVersion) {
            await GM.deleteValue(dataKey_Missions);
            data = null;
        }

        // Load data, if required
        const currentTimestamp = new Date().getTime();
        if (!data || currentTimestamp > data.lastUpdate + dataCacheExpirationMinutes * 60 * 1000) {
            console.debug('Mission data cache update required');
            await GM.deleteValue(dataKey_Missions);

            const missionsData = await fetchMissionsData();

            data = {
                version: supportedDataVersion,
                lastUpdate: currentTimestamp,
                response: missionsData
            };

            await GM.setValue(dataKey_Missions, data);
        }

        return data.response;
    }

    async function fetchMissionsData() {
        const missionUrl = `https://api.torn.com/v2/user/missions?comment=MissionCheck&key=${apiKey}`;
        const response = await gmFetch(missionUrl, 'GET');
        return JSON.parse(response.responseText);
    }

    function countMissionsPending(data) {
        let count = 0;

        for (const giver of data.missions.givers) {
            for (const contract of giver.contracts) {
                if (contract.status === "Available" || contract.status === "Accepted") {
                    count++;
                }
            }
        }

        return count;
    }

    function addPendingMissionsIcon() {
        const statusContainer = document.querySelector('ul[class*="status-icons"]');

        if (!statusContainer) return;

        const icon = createPendingMissionsIcon();
        statusContainer.appendChild(icon);
    }

    function createPendingMissionsIcon() {
        const listItem = document.createElement('li');
        listItem.classList.add('custom-mission-icon');
        listItem.innerHTML = `
            <a href="/loader.php?sid=missions">
                <svg xmlns="http://www.w3.org/2000/svg" stroke="transparent" stroke-width="0" width="17" height="17" viewBox="0 .5 16 14.5"><path d="M4,7.67a2.66,2.66,0,0,0,5.18.84l1.49-.86v0a4,4,0,1,1-4-4,3.91,3.91,0,0,1,2,.53l-1.47.85a2.94,2.94,0,0,0-.51,0A2.68,2.68,0,0,0,4,7.67M6.67,6.33A1.34,1.34,0,1,0,8,7.67H8l2.24-1.3a3,3,0,0,1,2.47.1L16,4.55l-2.11-1L14,1.09,10.71,3A3.07,3.07,0,0,1,9.58,5.2L7.32,6.51a1.29,1.29,0,0,0-.65-.18m6.64,1.91A1.42,1.42,0,0,0,12,7.61v.06a5.36,5.36,0,1,1-2.64-4.6,1.79,1.79,0,0,0,.1-1.45A6.55,6.55,0,0,0,6.67,1,6.66,6.66,0,0,0,2.22,12.63l-.87,2.7a.26.26,0,0,0,.43.26l2-1.92a6.66,6.66,0,0,0,5.78,0l2,1.92a.25.25,0,0,0,.42-.26l-.87-2.7a6.65,6.65,0,0,0,2.2-4.39"></path></svg>
            </a>
        `;

        return listItem;
    }

    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,
            })
        })
    }

    GM_addStyle(`
        .custom-mission-icon {
            background-image: none !important;
        }
        .custom-mission-icon svg {
            fill: url(#sidebar_svg_gradient_regular_mobile_active);
            filter: drop-shadow(0 0 1px rgba(255, 255, 255, .5019607843));
        }
    `);

})();