StumbleBot

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

当前为 2025-10-18 提交的版本,查看 最新版本

您需要先安装一个扩展,例如 篡改猴Greasemonkey暴力猴,之后才能安装此脚本。

您需要先安装一个扩展,例如 篡改猴暴力猴,之后才能安装此脚本。

您需要先安装一个扩展,例如 篡改猴暴力猴,之后才能安装此脚本。

您需要先安装一个扩展,例如 篡改猴Userscripts ,之后才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。

您需要先安装用户脚本管理器扩展后才能安装此脚本。

(我已经安装了用户脚本管理器,让我安装!)

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

(我已经安装了用户样式管理器,让我安装!)

作者
Goji
评分
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");