StumbleBot

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

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

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

You will need to install an extension such as Tampermonkey to install this script.

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

您需要先安装一个扩展,例如 篡改猴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\
.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");