StumbleBot

Play YouTube videos from the chat box and/or add custom commands to StumbleChat

您需要先安裝使用者腳本管理器擴展,如 TampermonkeyGreasemonkeyViolentmonkey 之後才能安裝該腳本。

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyViolentmonkey 後才能安裝該腳本。

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyViolentmonkey 後才能安裝該腳本。

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyUserscripts 後才能安裝該腳本。

你需要先安裝一款使用者腳本管理器擴展,比如 Tampermonkey,才能安裝此腳本

您需要先安裝使用者腳本管理器擴充功能後才能安裝該腳本。

(我已經安裝了使用者腳本管理器,讓我安裝!)

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

(我已經安裝了使用者樣式管理器,讓我安裝!)

作者
Goji
今日安裝
0
安裝總數
15
評價
0 0 0
版本
1.1.2
建立日期
2025-02-09
更新日期
2025-10-18
尺寸
17.4 KB
授權條款
未知
腳本執行於

🧩 StumbleBot (Queued Edition)

StumbleBot is a user script written in JavaScript that enhances the StumbleChat experience by adding modular commands, media playback, and smart message handling — all while maintaining chat stability with a built-in global message queue.


✨ Overview

StumbleBot extends the standard StumbleChat client by:

  • Intercepting and augmenting WebSocket traffic.
  • Automatically queueing bot messages (1-second delay) to avoid spam or rate-limits.
  • Tracking users, nicknames, and roles across sessions.
  • Supporting operator-only commands for moderation and media control.
  • Providing utility commands for identity lookup, YouTube playback, and fun interactions.

⚙️ Key Features

  • 🧠 Global Message Queue
    Ensures messages are sent at a controlled 1-second interval using a global send queue.

  • 🎥 YouTube Playback
    Operators can play YouTube videos using .yt, .play, .video, or .youtube.

  • 🧾 Persistent User Tracking
    Saves usernames, handles, nicknames, and roles (Operator, Moderator, Owner, etc.) locally.

  • 🔎 Identity Commands

    • .self — Displays your username, nickname, handle, and current role.
    • .whois <user|nick|handle> — Looks up another user's stored info.
  • 📜 YouTube History
    View recently added tracks using .history [page].

  • 💬 Custom & Utility Commands

    • .me [message] — Sends a message as your nickname.
    • .commands or .help — Lists all available commands.
    • ping — Responds with PONG.
    • Hourly demo alert at HH:20 (e.g. 4:20 🌿).

🧩 Command List

Command Description
.yt [query] Play a YouTube video (Operator+).
.play [query] Alias of .yt.
.video [query] Alias of .yt.
.youtube [query] Alias of .yt.
.history [page] View YouTube playback history (paginated).
.self Display your user info and role.
.whois [user/nick/handle] Look up another user’s info.
.me [message] Send a message as yourself.
.commands / .help Show all available commands.
ping Responds with PONG.

🧰 Technical Notes

  • Backward Compatible: Existing commands using this._send(...) continue to work automatically.
  • Storage: User data (handles, nicknames, roles, and YouTube history) is persisted in localStorage.
  • Queue System: Uses a 1000 ms global cooldown between sends to prevent message flooding.
  • Safe JSON Parsing: All server messages are parsed safely to avoid breaking on malformed input.

🚀 Installation

Install directly from GreasyFork:
👉 Install StumbleBot

After installation, StumbleBot will automatically activate when visiting a chatroom.


🧑‍💻 Author

Developed by Goji


⚙️ Updating from Previous Versions

If you already have a custom StumbleBot or other userscript with a lot of existing commands, you don’t need to start over.
You can simply add the new queue system, user tracking, and YouTube system manually.


🧠 1. Add the Global Send Queue

This system adds a global 1-second delay between all bot messages to avoid spam or throttling.

Find this in your existing script:

WebSocket.prototype.send = function (data) {
    this._send(data);
};

Replace it with this:

// === Global 1000ms send queue ===
const SEND_COOLDOWN_MS = 1000;
const _socketState = new WeakMap();

function getState(ws) {
    let s = _socketState.get(ws);
    if (!s) {
        s = { queue: [], busy: false, lastSentAt: 0 };
        _socketState.set(ws, s);
    }
    return s;
}

function enqueueSend(ws, payloadObj) {
    const state = getState(ws);
    state.queue.push(payloadObj);
    if (!state.busy) processQueue(ws);
}

function processQueue(ws) {
    const state = getState(ws);
    if (state.busy) return;
    const next = state.queue.shift();
    if (!next) return;

    const now = Date.now();
    const wait = Math.max(0, state.lastSentAt + SEND_COOLDOWN_MS - now);
    state.busy = true;

    setTimeout(() => {
        try {
            const raw = typeof next === "string" ? next : JSON.stringify(next);
            const native = ws.__nativeSend || WebSocket.prototype.send;
            native.call(ws, raw);
        } catch (e) {
            console.error("Queue send failed:", e, next);
        } finally {
            state.lastSentAt = Date.now();
            state.busy = false;
            if (state.queue.length) processQueue(ws);
        }
    }, wait);
}

// Optional helper for timers or delayed messages
window.stumbleBotSend = function (textOrPayload) {
    const ws = window._ssbSocket;
    if (!ws) return;
    ws._send(textOrPayload);
};

// Capture native sender
const __NATIVE_WS_SEND__ = WebSocket.prototype.send;
WebSocket.prototype._send = __NATIVE_WS_SEND__;
WebSocket.prototype.send = function (data) {
    if (!this.__nativeSend) this.__nativeSend = __NATIVE_WS_SEND__;
    if (!this.__ssbBound) {
        this.addEventListener("message", handleMessage.bind(this), false);
        this.__ssbBound = true;
        window._ssbSocket = this;
    }
    this.__nativeSend.call(this, data);
    if (!this.__queuedApplied) {
        const self = this;
        this._send = (payload) => enqueueSend(self, payload);
        this.__queuedApplied = true;
    }
};

That’s it! All your existing commands using this._send() (or even respondWithMessage.call(this, ...)) will now send messages through the 1-second queue automatically.


🧾 2. Add Persistent User Tracking

This tracks usernames, nicknames, handles, and roles across sessions — needed for commands like .self, .whois, and role checks.

Add this near the top of your script (before handleMessage):

let userHandles = JSON.parse(localStorage.getItem("userHandles") || "{}");
let userNicknames = JSON.parse(localStorage.getItem("userNicknames") || "{}");

function saveIdentity() {
    localStorage.setItem("userHandles", JSON.stringify(userHandles));
    localStorage.setItem("userNicknames", JSON.stringify(userNicknames));
}

const ROLE_ORDER = ["none", "guest", "regular", "operator", "moderator", "super", "owner"];
function roleRank(role) {
    const idx = ROLE_ORDER.indexOf((role || "none").toLowerCase());
    return idx >= 0 ? idx : 0;
}
function hasMinimumRole(username, minimumRole) {
    const rec = userNicknames[username];
    const current = rec?.role || "none";
    return roleRank(current) >= roleRank(minimumRole);
}

Then inside your handleMessage function, add these snippets to keep user data updated:

When users join:

if (wsmsg.stumble === "join" && wsmsg.username && wsmsg.handle) {
    const username = wsmsg.username;
    let nickname = wsmsg.nick;
    const handle = wsmsg.handle;
    if (/^guest-\d+$/i.test(nickname)) nickname = username;

    userNicknames[username] = {
        handle,
        username,
        nickname,
        modStatus: wsmsg.mod ? "Moderator" : "Regular",
        role: wsmsg.mod >= 4 ? "owner" :
              wsmsg.mod === 3 ? "super" :
              wsmsg.mod === 2 ? "moderator" :
              wsmsg.mod === 1 ? "operator" : "none"
    };
    userHandles[handle] = username;
    saveIdentity();
}

When nicknames change:

if (wsmsg.stumble === "nick" && wsmsg.handle && wsmsg.nick) {
    const handle = wsmsg.handle;
    const newNick = wsmsg.nick;
    const username = userHandles[handle];
    if (userNicknames[username]) {
        userNicknames[username].nickname = newNick;
        saveIdentity();
    }
}

When users leave:

if (wsmsg.stumble === "quit" && wsmsg.handle) {
    const handle = wsmsg.handle;
    delete userHandles[handle];
    saveIdentity();
}

✅ Once that’s added, you can safely use:

const username = userHandles[wsmsg.handle];
const nickname = userNicknames[username]?.nickname || username;
const role = userNicknames[username]?.role || "none";

💡 Bonus: Role Checking Example

If you want a command restricted to operators or higher:

if (!hasMinimumRole(username, "operator")) {
    respondWithMessage.call(this, "🚫 You need Operator or higher to use this command.");
    return;
}

🎥 3. If You Want to Update the YouTube System

The new YouTube module supports operator-only access, multiple aliases, URL normalization, and local playback history with .history [page].


🧩 Step 1 — Replace Your Old YouTube Command Block

Find your existing YouTube command (anything like .yt or .youtube handling) and replace the whole section with this:

// YouTube --------------------------------------------------------------------------------------------------------------------------
//-----------------------------------------------------------------------------------------------------------------------------------

// Define aliases for YouTube-related commands
const youtubeKeywords = ['.youtube', '.video', '.play', '.yt'];

// Universal YouTube History Storage
let youtubeHistory = JSON.parse(localStorage.getItem("youtubeHistory") || "[]");
function saveYT() {
    localStorage.setItem("youtubeHistory", JSON.stringify(youtubeHistory));
}

// Convert all YT link formats to watch?v= form
function convertToRegularYouTubeLink(url) {
    const videoIdRegex = /(?:https?:\/\/)?(?:www\.)?(?:youtu\.be\/|youtube\.com\/(?:watch\?v=|embed\/|v\/|shorts\/|.*[?&]v=))([\w-]+)/i;
    const match = String(url || "").trim().match(videoIdRegex);
    return (match && match[1]) ? ("https://www.youtube.com/watch?v=" + match[1]) : null;
}

// Save to local history
function saveToHistory(requester, trackName) {
    if (!trackName) return;
    youtubeHistory.push({ requester: requester || null, track: trackName });
    saveYT();
}

// Detect YouTube command
for (const kw of youtubeKeywords) {
    if (wsmsg.text.toLowerCase().startsWith(kw)) {
        const handle = wsmsg.handle;
        const username = userHandles[handle];
        const nickname = userNicknames[username]?.nickname || username;

        // Role check (requires operator or higher)
        if (!hasMinimumRole(username, "operator")) {
            respondWithMessage.call(this, "🚫 You need Operator or higher to play YouTube videos.");
            break;
        }

        const query = wsmsg.text.slice(kw.length).trim();
        if (query) {
            const finalLink = convertToRegularYouTubeLink(query) || query;
            this._send({ stumble: "youtube", type: "add", id: finalLink, time: 0 });
        }
        break;
    }
}

// Detect system message confirming a video was added
if (wsmsg.stumble === "sysmsg" && wsmsg.text.includes("has added YouTube track:")) {
    const lines = wsmsg.text.split("\n");
    const trackName = (lines.pop() || "").trim();
    if (trackName) {
        const match = lines[0].match(/^(.*?) \((.*?)\) has added YouTube track:/);
        const requester = match ? match[2] : null;
        saveToHistory(requester, trackName);
    }
}

🧾 Step 2 — Add the .history Command

Add this inside your handleMessage function (near your other chat commands):

// .history [page] — paginated YouTube playback history
if (wsmsg.text.toLowerCase().startsWith(".history")) {
    const args = wsmsg.text.split(" ");
    const page = parseInt(args[1]) || 1;
    const itemsPerPage = 5;

    const reversed = [...youtubeHistory].reverse(); // newest first
    const totalPages = Math.max(1, Math.ceil(reversed.length / itemsPerPage));

    if (reversed.length === 0) {
        respondWithMessage.call(this, "🤖 No recent YouTube tracks played.");
        return;
    }
    if (page < 1 || page > totalPages) {
        respondWithMessage.call(this, `⚠️ Invalid page. Use \`.history [1-${totalPages}]\`.`);
        return;
    }

    const start = (page - 1) * itemsPerPage;
    const end = start + itemsPerPage;
    const slice = reversed.slice(start, end);

    let resp = `📺 YouTube History — Page ${page}/${totalPages}\n`;
    slice.forEach((entry, i) => {
        const nick = entry.requester
            ? userNicknames[entry.requester]?.nickname || entry.requester
            : null;
        resp += `${start + i + 1}. ${nick ? nick + " played: " : ""}${entry.track}\n`;
    });

    respondWithMessage.call(this, resp.trim());
}

💡 Optional Enhancements

  • Add .commands list entry:
  "- .yt/.play/.video/.youtube <id|url|search> - Play a YouTube video (Operator+)"
  "- .history [page] - Show recent YouTube tracks"
  • The youtubeHistory array is stored in localStorage, so it persists across sessions.

  • You can clear history manually by running this in your browser console:

  localStorage.removeItem("youtubeHistory");