NeoBoards By web crawler 0 installs Rating 0.0 (0) approved

Neopets – Neoboards Auto-Bumper

Auto-bump a Neoboards topic every X–Y seconds with a control modal, random delays, saved settings, and inline countdown.
auto-bumper neoboards neopets
https://www.scriptneo.com/script/neopets-neoboards-auto-bumper

Version selector


SHA256
b1dbed8330c5cc58658d016aadb955c38d6d20d892dcbbe95a177d8f5575f2ee
No scan flags on this version.

Source code

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