Torn Faction Last Action Display

Display faction members' last action times on the faction page

您需要先安裝使用者腳本管理器擴展,如 TampermonkeyGreasemonkeyViolentmonkey 之後才能安裝該腳本。

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyViolentmonkey 後才能安裝該腳本。

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyViolentmonkey 後才能安裝該腳本。

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyUserscripts 後才能安裝該腳本。

你需要先安裝一款使用者腳本管理器擴展,比如 Tampermonkey,才能安裝此腳本

您需要先安裝使用者腳本管理器擴充功能後才能安裝該腳本。

(我已經安裝了使用者腳本管理器,讓我安裝!)

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

(我已經安裝了使用者樣式管理器,讓我安裝!)

// ==UserScript==
// @name         Torn Faction Last Action Display
// @namespace    http://tampermonkey.net/
// @version      1.3
// @description  Display faction members' last action times on the faction page
// @author       ShAdOwCrEsT [3929345]
// @match        https://www.torn.com/factions.php*
// @grant        GM_getValue
// @grant        GM_setValue
// @run-at       document-end
// ==/UserScript==

(function() {
    'use strict';

    const API_KEY_STORAGE = 'torn_faction_api_key';
    const FACTION_ID_STORAGE = 'torn_faction_id';

    function getApiKey() {
        let apiKey = GM_getValue(API_KEY_STORAGE, '');
        if (!apiKey) {
            apiKey = prompt('Please enter your Torn API key:');
            if (apiKey) {
                GM_setValue(API_KEY_STORAGE, apiKey);
            }
        }
        return apiKey;
    }

    async function fetchUserFaction(apiKey) {
        try {
            const response = await fetch(`https://api.torn.com/v2/user/faction?key=${apiKey}`);
            const data = await response.json();

            if (data.error) {
                console.error('API Error:', data.error);
                if (data.error.code === 2) {
                    GM_setValue(API_KEY_STORAGE, '');
                    alert('Invalid API key. Please refresh and enter a valid key.');
                }
                return null;
            }

            if (data.faction && data.faction.id) {
                GM_setValue(FACTION_ID_STORAGE, data.faction.id.toString());
                console.log('Faction ID stored:', data.faction.id);
                return data.faction.id;
            }

            return null;
        } catch (error) {
            console.error('Error fetching user faction:', error);
            return null;
        }
    }

    async function getFactionId(apiKey) {
        let factionId = GM_getValue(FACTION_ID_STORAGE, '');
        if (!factionId) {
            factionId = await fetchUserFaction(apiKey);
        }
        return factionId;
    }

    function getFactionIdFromUrl() {
        const urlParams = new URLSearchParams(window.location.search);
        const factionId = urlParams.get('ID');
        return factionId;
    }

    async function fetchFactionMembers(factionId, apiKey) {
        try {
            const response = await fetch(`https://api.torn.com/v2/faction/${factionId}/members?striptags=true&key=${apiKey}`);
            const data = await response.json();

            if (data.error) {
                console.error('API Error:', data.error);
                if (data.error.code === 2) {
                    GM_setValue(API_KEY_STORAGE, '');
                    GM_setValue(FACTION_ID_STORAGE, '');
                    alert('Invalid API key. Please refresh and enter a valid key.');
                }
                return null;
            }

            return data.members;
        } catch (error) {
            console.error('Error fetching faction data:', error);
            return null;
        }
    }

    function addLastActionColumn(members) {
        const observer = new MutationObserver((mutations, obs) => {
            const tableRows = document.querySelectorAll('ul.table-body li.table-row');

            if (tableRows.length > 0) {
                obs.disconnect();

                const memberMap = {};
                members.forEach(member => {
                    memberMap[member.id] = member;
                });

                addTableHeader();

                const inactiveMembers = [];

                tableRows.forEach(row => {
                    const profileLink = row.querySelector('a[href*="/profiles.php?XID="]');
                    if (profileLink) {
                        const href = profileLink.getAttribute('href');
                        const memberId = href.match(/XID=(\d+)/)?.[1];

                        if (memberId && memberMap[memberId]) {
                            const member = memberMap[memberId];
                            addLastActionCell(row, member.last_action.relative);

                            if (isInactiveForThreeDays(member.last_action.relative)) {
                                inactiveMembers.push({
                                    name: member.name,
                                    lastAction: member.last_action.relative
                                });
                            }
                        }
                    }
                });

                if (inactiveMembers.length > 0) {
                    const memberList = inactiveMembers.map(m => `${m.name} (${m.lastAction})`).join('\n');
                    alert(`Inactive members (3+ days):\n\n${memberList}\n\nTotal: ${inactiveMembers.length} member(s)`);
                }
            }
        });

        observer.observe(document.body, {
            childList: true,
            subtree: true
        });
    }

    function addTableHeader() {
        const headerRow = document.querySelector('ul.table-header');
        if (headerRow && !document.querySelector('.last-action-header')) {
            const statusHeader = headerRow.querySelector('.table-cell.status');
            if (statusHeader) {
                const lastActionHeader = document.createElement('div');
                lastActionHeader.className = 'table-cell last-action-header';
                lastActionHeader.textContent = 'LA';
                lastActionHeader.title = 'Last Action';
                lastActionHeader.style.cssText = 'font-weight: bold; padding: 5px; text-align: center;';
                statusHeader.parentNode.insertBefore(lastActionHeader, statusHeader);
            }
        }
    }

    function formatTimeShort(relativeTime) {
        const match = relativeTime.match(/(\d+)\s+(second|minute|hour|day|week|month|year)s?\s+ago/i);
        if (match) {
            const value = match[1];
            const unit = match[2].toLowerCase()[0];
            return `${value}${unit}`;
        }

        if (relativeTime.includes('0 minutes') || relativeTime.toLowerCase() === 'now') {
            return 'now';
        }

        return relativeTime;
    }

    function isInactiveForThreeDays(relativeTime) {
        const match = relativeTime.match(/(\d+)\s+(day|week|month|year)s?\s+ago/i);
        if (match) {
            const value = parseInt(match[1]);
            const unit = match[2].toLowerCase();

            if (unit === 'day' && value >= 3) {
                return true;
            }
            if (unit === 'week' || unit === 'month' || unit === 'year') {
                return true;
            }
        }
        return false;
    }

    function isInactiveForOneDay(relativeTime) {
        const match = relativeTime.match(/(\d+)\s+(day)s?\s+ago/i);
        if (match) {
            const value = parseInt(match[1]);
            const unit = match[2].toLowerCase();

            if (unit === 'day' && value >= 1 && value < 3) {
                return true;
            }
        }
        return false;
    }

    function addLastActionCell(row, lastAction) {
        if (row.querySelector('.last-action-cell')) {
            return;
        }

        const statusCell = row.querySelector('.table-cell.status');
        if (statusCell) {
            const lastActionCell = document.createElement('div');
            lastActionCell.className = 'table-cell last-action-cell';
            lastActionCell.textContent = formatTimeShort(lastAction);
            lastActionCell.title = lastAction;

            let cellStyle = 'padding: 5px; text-align: center; white-space: nowrap;';

            if (isInactiveForThreeDays(lastAction)) {
                cellStyle += ' background-color: rgba(255, 0, 0, 0.3);';
            }
            else if (isInactiveForOneDay(lastAction)) {
                cellStyle += ' background-color: rgba(255, 255, 0, 0.3);';
            }

            lastActionCell.style.cssText = cellStyle;
            statusCell.parentNode.insertBefore(lastActionCell, statusCell);
        }
    }

    async function init() {
        const isYourFactionPage = window.location.href.includes('factions.php?step=your');
        const isProfilePage = window.location.href.includes('factions.php?step=profile');

        if (!isYourFactionPage && !isProfilePage) {
            return;
        }

        const apiKey = getApiKey();
        if (!apiKey) {
            console.error('No API key provided');
            return;
        }

        let factionId;

        if (isProfilePage) {
            factionId = getFactionIdFromUrl();
            console.log('Faction ID from URL:', factionId);

            if (!factionId) {
                factionId = await getFactionId(apiKey);
                console.log('Faction ID from storage:', factionId);
            }
        } else {
            console.log('Getting faction ID...');
            factionId = await getFactionId(apiKey);
        }

        if (!factionId) {
            console.error('Could not retrieve faction ID');
            return;
        }

        console.log('Fetching faction members data for faction:', factionId);
        const members = await fetchFactionMembers(factionId, apiKey);

        if (members) {
            console.log('Members data received:', members.length, 'members');
            addLastActionColumn(members);
        }
    }

    if (document.readyState === 'loading') {
        document.addEventListener('DOMContentLoaded', init);
    } else {
        init();
    }
})();