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