Greasy Fork

来自缓存

Greasy Fork is available in English.

Red Dead Resolver - Forum Report Resolver

Ready, aim, fire! Quickly resolve and report editing threads.

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Greasemonkey 油猴子Violentmonkey 暴力猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Violentmonkey 暴力猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Userscripts ,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展后才能安装此脚本。

(我已经安装了用户脚本管理器,让我安装!)

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

(我已经安装了用户样式管理器,让我安装!)

// ==UserScript==
// @name         Red Dead Resolver - Forum Report Resolver
// @namespace    waiter7
// @version      1.3
// @description  Ready, aim, fire! Quickly resolve and report editing threads.
// @author       waiter7
// @homepageURL  https://gitlab.com/waiter77/red-dead-resolver
// @license      MIT
// @match        https://redacted.sh/forums.php*
// @match        https://orpheus.network/forums.php*
// @match        https://redacted.sh/reports.php*
// @match        https://orpheus.network/reports.php*
// @grant        GM_setValue
// @grant        GM_getValue
// @grant        GM_addStyle
// ==/UserScript==

(function() {
    'use strict';
    
    const CONFIG = {
        DEFAULT_REPLY: safeGM_getValue('defaultReply', "Thanks for reporting! Fixed."),
        AUTO_REFRESH: safeGM_getValue('autoRefresh', true),
        AUTO_SIGNATURE: safeGM_getValue('autoSignature', true),
        FORUMS: { 'redacted.sh': 10, 'orpheus.network': 34 }
    };
    
    const SELECTORS = {
        linkbox: '.linkbox .center',
        reportLink: 'a[href*="reports.php?action=report&type=thread"]',
        replyBox: '#reply_box',
        textarea: '#quickpost',
        submitButton: '#submit_button',
        reportForm: '#report_form',
        reportThreadId: 'input[name="id"]'
    };
    
    // Cross-browser compatible style injection
    function injectStyles(css) {
        try {
            // Try GM_addStyle first (Tampermonkey/Violentmonkey)
            if (typeof GM_addStyle !== 'undefined') {
                GM_addStyle(css);
                return;
            }
        } catch (e) {
            console.warn('GM_addStyle failed, using fallback method');
        }
        
        // Fallback method for Greasemonkey and other cases
        const style = document.createElement('style');
        style.textContent = css;
        document.head.appendChild(style);
    }
    
    // Add styles with cross-browser compatibility
    injectStyles(`
        .resolver-error { 
            color: #ff0000; 
            border: 1px solid rgb(116, 10, 10); 
            padding: 10px; 
            margin-bottom: 10px; 
            border-radius: 4px; 
        }
        .resolver-success { 
            color: #4CAF50; 
            padding: 10px; 
            margin-bottom: 10px; 
            border-radius: 4px; 
            text-align: center; 
        }
        .resolver-checkmark { 
            color: #00ff00; 
            font-weight: bold; 
        }
        .resolver-resolve-button { 
            margin-right: 10px; 
        }
        .resolver-reply-resolve-button {}
        .resolver-settings { 
            margin-left: 10px; 
            font-size: 0.9em; 
        }
        .resolver-dropdown { 
            display: none; 
            margin-top: 10px; 
            padding: 10px !important;
            border: 1px solid #404040;
            border-radius: 4px;
        }
    `);
    
    // Utility functions with cross-browser compatibility
    const $ = (selector) => document.querySelector(selector);
    const $$ = (selector) => document.querySelectorAll(selector);
    
    // Cross-browser compatible GM functions
    function safeGM_getValue(key, defaultValue) {
        try {
            return GM_getValue(key, defaultValue);
        } catch (e) {
            console.warn('GM_getValue failed, using localStorage fallback');
            try {
                const stored = localStorage.getItem(`red_dead_resolver_${key}`);
                return stored ? JSON.parse(stored) : defaultValue;
            } catch (e2) {
                console.error('LocalStorage fallback also failed:', e2);
                return defaultValue;
            }
        }
    }
    
    function safeGM_setValue(key, value) {
        try {
            GM_setValue(key, value);
        } catch (e) {
            console.warn('GM_setValue failed, using localStorage fallback');
            try {
                localStorage.setItem(`red_dead_resolver_${key}`, JSON.stringify(value));
            } catch (e2) {
                console.error('LocalStorage fallback also failed:', e2);
            }
        }
    }
    
    // DOM cache for frequently accessed elements
    const domCache = {
        elements: new Map(),
        get: function(selector) {
            if (!this.elements.has(selector)) {
                this.elements.set(selector, $(selector));
            }
            return this.elements.get(selector);
        },
        clear: function() {
            this.elements.clear();
        }
    };
    
    // Debounce utility for performance
    function debounce(func, wait) {
        let timeout;
        return function executedFunction(...args) {
            const later = () => {
                clearTimeout(timeout);
                func(...args);
            };
            clearTimeout(timeout);
            timeout = setTimeout(later, wait);
        };
    }
    
    function isEditingForum() {
        const hostname = window.location.hostname;
        const forumId = CONFIG.FORUMS[hostname];
        if (!forumId) return false;
        const breadcrumbs = $('.breadcrumbs');
        return breadcrumbs && breadcrumbs.querySelector(`a[href*="forumid=${forumId}"]`);
    }
    
    function isReportPage() {
        return window.location.pathname.includes('reports.php') && 
               window.location.search.includes('action=report') &&
               window.location.search.includes('type=thread');
    }
    
    function getAuthKey() {
        // Try to get auth key from script tag first (RED style)
        const script = Array.from($$('script')).find(s => s.textContent.includes('var authkey'));
        const match = script?.textContent.match(/var authkey = "([^"]+)"/);
        if (match) return match[1];
        
        // Try to get auth key from form input (OPS style)
        const authInput = $('input[name="auth"]');
        if (authInput) return authInput.value;
        
        // Try to get auth key from body data attribute (OPS style)
        const body = document.body;
        if (body && body.dataset.auth) return body.dataset.auth;
        
        return null;
    }
    
    function getThreadId() {
        // Check for thread ID in forum pages - try multiple field names
        const threadIdInputs = [
            $('input[name="thread"]'),      // RED style
            $('input[name="threadid"]'),    // OPS style
            $('input[name="id"]')           // Report page style
        ];
        
        for (const input of threadIdInputs) {
            if (input && input.value) return input.value;
        }
        
        // Try to extract from URL
        const urlMatch = window.location.search.match(/[?&]threadid=(\d+)/);
        if (urlMatch) return urlMatch[1];
        
        return null;
    }
    
    function isResolved() {
        const threadId = getThreadId();
        return threadId && safeGM_getValue('resolvedThreads', []).includes(threadId);
    }
    
    function markAsResolved() {
        const threadId = getThreadId();
        if (!threadId) return;
        
        const resolvedThreads = safeGM_getValue('resolvedThreads', []);
        if (!resolvedThreads.includes(threadId)) {
            resolvedThreads.push(threadId);
            safeGM_setValue('resolvedThreads', resolvedThreads);
        }
        
        const reportLink = $(SELECTORS.reportLink);
        if (reportLink) {
            reportLink.style.opacity = '0.5';
            if (!reportLink.querySelector('.resolver-checkmark')) {
                const checkmark = document.createElement('span');
                checkmark.className = 'resolver-checkmark';
                checkmark.innerHTML = ' ✓';
                reportLink.appendChild(checkmark);
            }
        }
        
        $$('.resolver-resolve-button, .resolver-reply-resolve-button, .resolver-settings-icon').forEach(btn => btn.style.display = 'none');
    }
    
    async function submitForm(endpoint, data) {
        const authKey = getAuthKey();
        const threadId = getThreadId();
        
        if (!authKey || !threadId) {
            console.error('Auth key:', authKey, 'Thread ID:', threadId);
            throw new Error('Could not extract auth key or thread ID');
        }
        
        const formData = new FormData();
        Object.entries(data).forEach(([key, value]) => formData.append(key, value));
        
        const response = await fetch(endpoint, { method: 'POST', body: formData });
        if (!response.ok) throw new Error(`${endpoint} submission failed: ${response.status}`);
        return response;
    }
    
    async function resolveAndReport(customMessage = null) {
        try {
            let message = customMessage || CONFIG.DEFAULT_REPLY;
            
            // Append the resolver signature to the message if enabled
            if (CONFIG.AUTO_SIGNATURE) {
                const resolverSignature = "\n\n\n[size=1][align=right][img=https://ptpimg.me/fdw703.png] Reported as Resolved via [url=https://redacted.sh/forums.php?action=viewthread&threadid=73924]Red Dead Resolver[/url][/align][/size]";
                message += resolverSignature;
            }
            
            const hostname = window.location.hostname;
            const isOPS = hostname === 'orpheus.network';
            
            // Handle different field names for RED vs OPS
            const replyData = {
                action: 'reply',
                auth: getAuthKey()
            };
            
            if (isOPS) {
                // OPS uses threadid and quickpost
                replyData.threadid = getThreadId();
                replyData.quickpost = message;
            } else {
                // RED uses thread and body
                replyData.thread = getThreadId();
                replyData.body = message;
            }
            
            await submitForm('forums.php', replyData);
            
            await submitForm('reports.php', {
                action: 'takereport',
                auth: getAuthKey(),
                id: getThreadId(),
                type: 'thread',
                reason: "Resolved! (via Red Dead Resolver)"
            });
            
            markAsResolved();
            
            const replyBox = domCache.get(SELECTORS.replyBox);
            if (replyBox) {
                const successDiv = document.createElement('div');
                successDiv.className = 'resolver-success';
                successDiv.textContent = 'Successfully resolved and reported (pew pew)!';
                replyBox.insertBefore(successDiv, replyBox.firstChild);
            }
            
            const textarea = domCache.get(SELECTORS.textarea);
            if (textarea) textarea.value = '';
            
            if (CONFIG.AUTO_REFRESH) {
                setTimeout(() => window.location.reload(), 2000);
            }
        } catch (error) {
            console.error('Red Dead Resolver error:', error);
            showError(error.message);
        }
    }
    
    function showError(message) {
        const replyBox = domCache.get(SELECTORS.replyBox);
        if (!replyBox) return;
        
        const existingError = replyBox.querySelector('.resolver-error');
        if (existingError) existingError.remove();
        
        const errorDiv = document.createElement('div');
        errorDiv.className = 'resolver-error';
        errorDiv.textContent = `Error: ${message}. Please reply and resolve manually.`;
        replyBox.insertBefore(errorDiv, replyBox.firstChild);
        errorDiv.scrollIntoView({ behavior: 'smooth' });
        
        const textarea = domCache.get(SELECTORS.textarea);
        if (textarea && !textarea.value.trim()) {
            textarea.value = CONFIG.DEFAULT_REPLY;
        }
    }
    
    // Event handlers
    function handleResolveClick(e) {
        e.preventDefault();
        const btn = e.target;
        
        if (btn.textContent === 'Confirm?') {
            btn.style.pointerEvents = 'none';
            btn.textContent = 'Processing...';
            resolveAndReport();
            return;
        }
        
        const originalText = btn.textContent;
        btn.textContent = 'Confirm?';
        btn.style.color = '#ff8c00';
        
        setTimeout(() => {
            btn.textContent = originalText;
            btn.style.color = '';
        }, 3000);
    }
    
    function handleReplyResolveClick(e) {
        const textarea = $(SELECTORS.textarea);
        let message = textarea?.value.trim() || '';
        
        if (!message) {
            message = CONFIG.DEFAULT_REPLY;
            if (textarea) textarea.value = message;
        }
        
        e.target.disabled = true;
        e.target.value = 'Processing...';
        resolveAndReport(message);
    }
    
    // Debounced toggle to prevent rapid clicking issues
    const debouncedToggle = debounce(function() {
        const dropdown = domCache.get('#resolver-settings-dropdown');
        if (!dropdown) return;
        
        const isVisible = dropdown.style.display !== 'none';
        
        if (isVisible) {
            // Hide dropdown
            dropdown.style.display = 'none';
        } else {
            // Show dropdown
            dropdown.style.display = 'block';
        }
    }, 100);
    
    function toggleSettingsDropdown() {
        debouncedToggle();
    }
    
    function saveSettings() {
        const defaultReply = $('#resolver-default-reply').value;
        const autoRefresh = $('#resolver-auto-refresh').checked;
        const autoSignature = $('#resolver-auto-signature').checked;
        
        safeGM_setValue('defaultReply', defaultReply);
        safeGM_setValue('autoRefresh', autoRefresh);
        safeGM_setValue('autoSignature', autoSignature);
        
        CONFIG.DEFAULT_REPLY = defaultReply;
        CONFIG.AUTO_REFRESH = autoRefresh;
        CONFIG.AUTO_SIGNATURE = autoSignature;
        
        toggleSettingsDropdown();
    }
    
    function handleManualReport() {
        const reportForm = $(SELECTORS.reportForm);
        if (!reportForm) return;
        
        // Add visual indicator that this will be tracked
        const submitButton = reportForm.querySelector('input[type="submit"]');
        if (submitButton) {
            const indicator = document.createElement('div');
            indicator.className = 'resolver-success';
            indicator.style.marginTop = '10px';
            indicator.textContent = '✓ Tracked by Red Dead Resolver';
            submitButton.parentNode.insertBefore(indicator, submitButton.nextSibling);
        }
        
        reportForm.addEventListener('submit', function(e) {
            const threadId = getThreadId();
            if (!threadId) return;
            
            // Mark as resolved when form is submitted
            const resolvedThreads = safeGM_getValue('resolvedThreads', []);
            if (!resolvedThreads.includes(threadId)) {
                resolvedThreads.push(threadId);
                safeGM_setValue('resolvedThreads', resolvedThreads);
            }
        });
    }
    
    // Add UI elements
    function addButtons() {
        const linkbox = domCache.get(SELECTORS.linkbox);
        if (!linkbox) return;
        
        const reportLink = linkbox.querySelector(SELECTORS.reportLink);
        if (!reportLink || isResolved()) return;
        
        // Resolve button (add after "Search this thread")
        const searchThreadLink = linkbox.querySelector('a[id="thread-search"], a[onclick*="searchthread"]');
        if (searchThreadLink) {
            const resolveBtn = document.createElement('a');
            resolveBtn.href = '#';
            resolveBtn.className = 'brackets resolver-resolve-button';
            resolveBtn.textContent = '✓ Quick Resolve';
            searchThreadLink.parentNode.insertBefore(resolveBtn, searchThreadLink.nextSibling);
            
            // Add settings link after the resolve button
            const settingsLink = document.createElement('a');
            settingsLink.href = '#';
            settingsLink.className = 'resolver-settings-icon';
            settingsLink.innerHTML = '<small>(Settings)</small>';
            settingsLink.addEventListener('click', (e) => {
                e.preventDefault();
                toggleSettingsDropdown();
            });
            searchThreadLink.parentNode.insertBefore(settingsLink, resolveBtn.nextSibling);
        }
        
        // Reply and resolve button
        const submitBtn = domCache.get(SELECTORS.submitButton);
        if (submitBtn) {
            const replyBtn = document.createElement('input');
            replyBtn.type = 'button';
            replyBtn.className = 'resolver-reply-resolve-button';
            replyBtn.value = 'Post Reply and Resolve';
            submitBtn.parentNode.appendChild(replyBtn);
        }
        
        // Settings dropdown
        const dropdown = document.createElement('div');
        dropdown.id = 'resolver-settings-dropdown';
        dropdown.className = 'box pad resolver-dropdown';
        dropdown.style.display = 'none';
        dropdown.style.maxWidth = '50%';
        dropdown.style.margin = '0 auto';
        dropdown.style.textAlign = 'center';
        dropdown.innerHTML = `
            <h4 style="text-align: center; margin-bottom: 15px;">Red Dead Resolver Settings</h4>
            <div class="field_div" style="text-align: left;">
                <label for="resolver-default-reply">Default Reply Message:</label>
                <textarea id="resolver-default-reply" rows="3" class="required" style="width: 100%;">${CONFIG.DEFAULT_REPLY}</textarea>
            </div>
            <div class="field_div" style="text-align: left;">
                <label><input type="checkbox" id="resolver-auto-refresh" ${CONFIG.AUTO_REFRESH ? 'checked' : ''}> Auto-refresh on success</label>
            </div>
            <div class="field_div" style="text-align: left;">
                <label><input type="checkbox" id="resolver-auto-signature" ${CONFIG.AUTO_SIGNATURE ? 'checked' : ''}> Auto-add resolver signature</label>
            </div>
            <div style="text-align: center; margin-top: 15px;">
                <input type="button" id="resolver-save-settings" value="Save" class="button-primary">
                <input type="button" id="resolver-cancel-settings" value="Cancel" class="button-secondary" style="margin-left: 10px;">
            </div>
        `;
        linkbox.appendChild(dropdown);
    }
    
    // Event delegation
    document.addEventListener('click', (e) => {
        if (e.target.matches('.resolver-resolve-button')) handleResolveClick(e);
        if (e.target.matches('.resolver-reply-resolve-button')) handleReplyResolveClick(e);
        if (e.target.matches('.resolver-settings') || e.target.matches('.resolver-settings-icon')) toggleSettingsDropdown();
        if (e.target.matches('#resolver-save-settings')) saveSettings();
        if (e.target.matches('#resolver-cancel-settings')) toggleSettingsDropdown();
    });
    
    // Initialize
    function init() {
        // Clear DOM cache on each initialization
        domCache.clear();
        
        // Ensure DOM is ready
        if (!document.body) {
            setTimeout(init, 100);
            return;
        }
        
        if (isReportPage()) {
            handleManualReport();
            return;
        }
        
        if (!isEditingForum()) return;
        
        // Clear any existing draft content in the textarea
        const textarea = domCache.get(SELECTORS.textarea);
        if (textarea) {
            textarea.value = '';
        }
        
        addButtons();
        
        if (isResolved()) {
            markAsResolved();
        }
    }
    
    if (document.readyState === 'loading') {
        document.addEventListener('DOMContentLoaded', init);
    } else {
        init();
    }
})();