// ==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();
}
})();