// ==UserScript== // @name Neopets – Neoboards Auto-Bumper // @namespace http://scriptneo.com/ // @version 1.0.0 // @description Auto-bump a Neoboards topic every X–Y seconds with a control modal, random delays, saved settings, and inline countdown. // @match https://www.neopets.com/neoboards/topic.phtml* // @run-at document-idle // @grant none // @downloadURL https://www.scriptneo.com/scripts/download.php?id=33 // @updateURL https://www.scriptneo.com/scripts/download.php?id=33 // ==/UserScript== (function () { 'use strict'; /*********************** * BASIC PAGE CHECKS ***********************/ const form = document.forms['message_form']; if (!form) return; // no reply form, nothing to do const replyTextarea = form.elements['message']; if (!replyTextarea) return; // Try to get topic ID from URL first, then from hidden input const url = new URL(window.location.href); const topicFromUrl = url.searchParams.get('topic'); const topicFromForm = form.querySelector('input[name="topic_id"]')?.value; const topicId = topicFromUrl || topicFromForm || ''; if (!topicId) return; // Safety: no topic ID const STORAGE_KEY = 'NB_AutoBump_' + topicId; /*********************** * STATE ***********************/ let state = { running: false, timerId: null, bumpsSent: 0, minDelay: 30, maxDelay: 90, maxBumps: 0, // 0 = unlimited messages: 'bump', }; // Countdown info (not stored in localStorage) let nextDelaySeconds = null; let countdownSecondsRemaining = null; let countdownIntervalId = null; /*********************** * DOM HELPERS ***********************/ function createEl(tag, props, children) { const el = document.createElement(tag); if (props) { Object.keys(props).forEach((k) => { if (k === 'class') el.className = props[k]; else if (k === 'text') el.textContent = props[k]; else el.setAttribute(k, props[k]); }); } if (children) { children.forEach((c) => { if (typeof c === 'string') el.appendChild(document.createTextNode(c)); else if (c) el.appendChild(c); }); } return el; } /*********************** * STYLE ***********************/ const style = document.createElement('style'); style.textContent = ` .nbab-trigger-btn { margin-left: 6px; padding: 3px 8px; font-size: 11px; font-family: Arial, sans-serif; cursor: pointer; border-radius: 3px; border: 1px solid #444; background: #3b3b3b; color: #fff; } .nbab-trigger-btn:hover { background: #505050; } .nbab-overlay { position: fixed; inset: 0; background: rgba(0,0,0,0.6); display: flex; align-items: center; justify-content: center; z-index: 999998; font-family: Arial, sans-serif; } .nbab-hidden { display: none !important; } .nbab-modal { background: #f7f7f7; border-radius: 6px; border: 1px solid #444; width: 420px; max-width: 95vw; box-shadow: 0 0 10px rgba(0,0,0,0.7); font-size: 12px; } .nbab-header { padding: 8px 10px; background: #333; color: #fff; display: flex; justify-content: space-between; align-items: center; border-bottom: 1px solid #000; border-radius: 6px 6px 0 0; font-weight: bold; } .nbab-close { background: transparent; border: none; color: #fff; font-size: 16px; cursor: pointer; } .nbab-body { padding: 8px 10px 10px; } .nbab-row { margin-bottom: 6px; } .nbab-row label { display: block; margin-bottom: 2px; font-weight: bold; } .nbab-row input[type="number"], .nbab-row input[type="text"], .nbab-row textarea { width: 100%; box-sizing: border-box; font-size: 12px; padding: 3px; border: 1px solid #aaa; border-radius: 3px; font-family: Arial, sans-serif; } .nbab-row textarea { resize: vertical; min-height: 60px; max-height: 200px; } .nbab-actions { margin-top: 8px; text-align: right; } .nbab-actions button { margin-left: 4px; padding: 3px 8px; font-size: 12px; cursor: pointer; border-radius: 3px; border: 1px solid #444; } .nbab-start { background: #228b22; color: #fff; } .nbab-stop { background: #b22222; color: #fff; } .nbab-status { margin-top: 6px; font-size: 11px; padding: 4px; background: #eee; border-radius: 3px; border: 1px solid #ccc; max-height: 80px; overflow-y: auto; white-space: pre-line; } .nbab-status-running { border-color: #228b22; } .nbab-status-stopped { border-color: #b22222; } .nbab-small { font-size: 10px; color: #555; } .nbab-info { padding: 5px; background: #fff7d5; border: 1px solid #e0c878; border-radius: 3px; font-size: 11px; margin-bottom: 6px; } .nbab-inline-msg { margin: 4px 0 8px 0; font-size: 11px; font-family: Arial, sans-serif; color: #555; } `; document.head.appendChild(style); /*********************** * TRIGGER BUTTON (AFTER SUBMIT) ***********************/ const submitBtnForTrigger = form.querySelector('input[type="submit"], .topicReplySubmit'); if (!submitBtnForTrigger) return; const triggerBtn = createEl('button', { type: 'button', class: 'nbab-trigger-btn', id: 'nbab-trigger', title: 'Open Auto-Bump controls', }, ['Auto-Bump']); // Insert Auto-Bump button immediately AFTER the Submit button submitBtnForTrigger.insertAdjacentElement('afterend', triggerBtn); /*********************** * INLINE MESSAGE AFTER "Reply to this topic" ***********************/ const replyTitle = document.querySelector('.topicReplyTitle'); let inlineMsgEl = null; if (replyTitle) { inlineMsgEl = createEl('div', { id: 'nbab-inline-msg', class: 'nbab-inline-msg', }, ['Auto-bumping is stopped.']); replyTitle.insertAdjacentElement('afterend', inlineMsgEl); } function updateInlineMessage() { if (!inlineMsgEl) return; if (!state.running) { inlineMsgEl.textContent = 'Auto-bumping is stopped.'; return; } let sec = countdownSecondsRemaining; if (typeof sec !== 'number' || sec < 0) { sec = state.minDelay; } inlineMsgEl.textContent = 'Auto-bumping board in ' + sec + ' seconds...'; } function clearCountdown() { if (countdownIntervalId !== null) { clearInterval(countdownIntervalId); countdownIntervalId = null; } } function setupCountdown() { clearCountdown(); if (!state.running) { updateInlineMessage(); return; } countdownIntervalId = window.setInterval(() => { if (!state.running) { clearCountdown(); return; } if (typeof countdownSecondsRemaining !== 'number') { clearCountdown(); return; } if (countdownSecondsRemaining > 0) { countdownSecondsRemaining -= 1; updateInlineMessage(); } else { clearCountdown(); } }, 1000); updateInlineMessage(); } /*********************** * MODAL / OVERLAY ***********************/ const overlay = createEl('div', { id: 'nbab-overlay', class: 'nbab-overlay nbab-hidden', }); const modal = createEl('div', { class: 'nbab-modal' }); const header = createEl('div', { class: 'nbab-header' }, [ createEl('span', null, ['Neoboards Auto-Bump']), (function () { const btn = createEl('button', { class: 'nbab-close', type: 'button' }, ['×']); btn.addEventListener('click', () => hideModal()); return btn; })(), ]); const body = createEl('div', { class: 'nbab-body' }); // Info message const infoRow = createEl('div', { class: 'nbab-row' }); const infoBox = createEl('div', { class: 'nbab-info' }, [ 'This tool will automatically submit replies at random intervals for this topic. ', 'Use it carefully and only when you\'re okay with repeated bumps being posted on your behalf.', ]); infoRow.appendChild(infoBox); // Topic / URL const topicRow = createEl('div', { class: 'nbab-row' }); const topicLabel = createEl('label', null, ['Topic ID / URL']); const topicInput = createEl('input', { type: 'text', readonly: 'readonly', value: topicId, }); const topicHint = createEl('div', { class: 'nbab-small' }, [ 'Current URL: ', window.location.href, ]); topicRow.appendChild(topicLabel); topicRow.appendChild(topicInput); topicRow.appendChild(topicHint); // Delay row const delayRow = createEl('div', { class: 'nbab-row' }); const delayLabel = createEl('label', null, ['Random delay (seconds, min–max)']); const delayInputsWrapper = createEl('div', null); const minInput = createEl('input', { type: 'number', min: '5', max: '600', value: '30', style: 'width:48%;display:inline-block;', }); const maxInput = createEl('input', { type: 'number', min: '5', max: '600', value: '90', style: 'width:48%;display:inline-block;margin-left:4%;', }); delayInputsWrapper.appendChild(minInput); delayInputsWrapper.appendChild(maxInput); delayRow.appendChild(delayLabel); delayRow.appendChild(delayInputsWrapper); // Max bumps const maxBumpsRow = createEl('div', { class: 'nbab-row' }); const maxBumpsLabel = createEl('label', null, ['Max bumps (0 = unlimited)']); const maxBumpsInput = createEl('input', { type: 'number', min: '0', max: '1000', value: '0', }); maxBumpsRow.appendChild(maxBumpsLabel); maxBumpsRow.appendChild(maxBumpsInput); // Messages textarea const msgRow = createEl('div', { class: 'nbab-row' }); const msgLabel = createEl('label', null, ['Bump messages (one per line; random each time)']); const msgTextarea = createEl('textarea', null, ['bump\nup\nboop']); const msgHint = createEl('div', { class: 'nbab-small' }, [ 'Each bump uses a random non-empty line. Keep under 400 characters.', ]); msgRow.appendChild(msgLabel); msgRow.appendChild(msgTextarea); msgRow.appendChild(msgHint); // Actions const actionsRow = createEl('div', { class: 'nbab-actions' }); const startBtn = createEl('button', { type: 'button', class: 'nbab-start', }, ['Start']); const stopBtn = createEl('button', { type: 'button', class: 'nbab-stop', }, ['Stop']); actionsRow.appendChild(startBtn); actionsRow.appendChild(stopBtn); // Status const statusDiv = createEl('div', { id: 'nbab-status', class: 'nbab-status nbab-status-stopped', }, ['Stopped.']); body.appendChild(infoRow); body.appendChild(topicRow); body.appendChild(delayRow); body.appendChild(maxBumpsRow); body.appendChild(msgRow); body.appendChild(actionsRow); body.appendChild(statusDiv); modal.appendChild(header); modal.appendChild(body); overlay.appendChild(modal); document.body.appendChild(overlay); /*********************** * STORAGE ***********************/ function saveState() { const toSave = { running: state.running, bumpsSent: state.bumpsSent, minDelay: state.minDelay, maxDelay: state.maxDelay, maxBumps: state.maxBumps, messages: state.messages, topicId: topicId, }; try { localStorage.setItem(STORAGE_KEY, JSON.stringify(toSave)); } catch (e) { // ignore storage issues } } function loadState() { try { const raw = localStorage.getItem(STORAGE_KEY); if (!raw) return; const data = JSON.parse(raw); if (!data || data.topicId !== topicId) return; state = Object.assign(state, data); } catch (e) { // ignore } } loadState(); /*********************** * STATUS / UI UPDATES ***********************/ function updateInputsFromState() { minInput.value = state.minDelay; maxInput.value = state.maxDelay; maxBumpsInput.value = state.maxBumps; msgTextarea.value = state.messages; } function readInputsIntoState() { let minVal = parseInt(minInput.value, 10); let maxVal = parseInt(maxInput.value, 10); let maxB = parseInt(maxBumpsInput.value, 10); if (isNaN(minVal) || minVal < 5) minVal = 5; if (isNaN(maxVal) || maxVal < minVal) maxVal = Math.max(minVal, 5); if (isNaN(maxB) || maxB < 0) maxB = 0; state.minDelay = minVal; state.maxDelay = maxVal; state.maxBumps = maxB; state.messages = msgTextarea.value || ''; } function formatStatus() { const lines = []; lines.push(`Status: ${state.running ? 'RUNNING' : 'STOPPED'}`); lines.push(`Topic: ${topicId}`); lines.push(`Delays: ${state.minDelay}s–${state.maxDelay}s}`); lines.push(`Max bumps: ${state.maxBumps === 0 ? 'unlimited' : state.maxBumps}`); lines.push(`Bumps sent (this topic): ${state.bumpsSent}`); lines.push(''); if (state.running) { lines.push('Auto-bump is active. This will keep posting until you hit Stop, reach Max bumps, or leave this page.'); } else { lines.push('Press Start to begin auto-bumping.'); } return lines.join('\n'); } function updateStatus() { statusDiv.textContent = formatStatus(); if (state.running) { statusDiv.classList.remove('nbab-status-stopped'); statusDiv.classList.add('nbab-status-running'); } else { statusDiv.classList.remove('nbab-status-running'); statusDiv.classList.add('nbab-status-stopped'); } updateInlineMessage(); } function showModal() { overlay.classList.remove('nbab-hidden'); } function hideModal() { overlay.classList.add('nbab-hidden'); } /*********************** * CORE LOGIC ***********************/ function getRandomMessage() { const raw = state.messages || ''; const lines = raw.split(/\r?\n/).map((s) => s.trim()).filter(Boolean); if (!lines.length) return ''; const idx = Math.floor(Math.random() * lines.length); return lines[idx]; } function getRandomDelayMs() { const min = state.minDelay; const max = state.maxDelay; const delta = Math.max(0, max - min); const seconds = min + Math.random() * delta; return Math.round(seconds * 1000); } function clearTimer() { if (state.timerId !== null) { clearTimeout(state.timerId); state.timerId = null; } } function stopAutoBump() { state.running = false; clearTimer(); clearCountdown(); saveState(); updateStatus(); } function scheduleNextBump() { clearTimer(); const delayMs = getRandomDelayMs(); nextDelaySeconds = Math.round(delayMs / 1000); countdownSecondsRemaining = nextDelaySeconds; setupCountdown(); state.timerId = window.setTimeout(doBump, delayMs); saveState(); updateStatus(); } function doBump() { if (!state.running) return; // Check limit if (state.maxBumps > 0 && state.bumpsSent >= state.maxBumps) { alert('[Auto-Bump] Reached max bumps. Stopping.'); stopAutoBump(); return; } const msg = getRandomMessage(); if (!msg.trim()) { alert('[Auto-Bump] No valid bump message configured. Stopping.'); stopAutoBump(); return; } if (msg.length > 400) { alert('[Auto-Bump] Message is over 400 characters. Shorten it. Stopping.'); stopAutoBump(); return; } // Make sure form still exists if (!form || !replyTextarea) { alert('[Auto-Bump] Reply form not found. Stopping.'); stopAutoBump(); return; } // Fill message replyTextarea.value = msg; // Trigger Neo's counter if present try { if (typeof textCounter === 'function') { textCounter(form.message, form.remLen, (window.NeoboardPens && NeoboardPens.maxPostLength) || 400); } } catch (e) { // ignore } state.bumpsSent++; saveState(); updateStatus(); // Submit form. Page will normally reload. const submitBtn = form.querySelector('input[type="submit"], .topicReplySubmit'); if (submitBtn) { submitBtn.click(); } else { form.submit(); } // If for some reason the page does NOT reload, schedule another bump. // If it reloads (normal case), a new timer will be scheduled on the fresh load. scheduleNextBump(); } function startAutoBump(resetCounter) { readInputsIntoState(); if (resetCounter) { state.bumpsSent = 0; } state.running = true; saveState(); updateStatus(); scheduleNextBump(); } function startAutoBumpIfNeededFromStorage() { updateInputsFromState(); updateStatus(); if (state.running) { // Auto-resume for this topic scheduleNextBump(); } } /*********************** * EVENT BINDINGS ***********************/ triggerBtn.addEventListener('click', () => { updateInputsFromState(); updateStatus(); showModal(); }); startBtn.addEventListener('click', () => { // Starting manually resets bump counter startAutoBump(true); alert('[Auto-Bump] Started. The script will post automatically until you press Stop, reach Max bumps, or leave this page.'); }); stopBtn.addEventListener('click', () => { stopAutoBump(); alert('[Auto-Bump] Stopped.'); }); // Close modal when clicking outside content overlay.addEventListener('click', (e) => { if (e.target === overlay) hideModal(); }); // Update state when inputs change [minInput, maxInput, maxBumpsInput, msgTextarea].forEach((el) => { el.addEventListener('change', () => { readInputsIntoState(); saveState(); updateStatus(); }); }); /*********************** * INIT ***********************/ updateInputsFromState(); updateStatus(); startAutoBumpIfNeededFromStorage(); })();