Play YouTube videos from the chat box and/or add custom commands to StumbleChat
当前为
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.
StumbleBot extends the standard StumbleChat client by:
🧠 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.HH:20 (e.g. 4:20 🌿).| 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. |
this._send(...) continue to work automatically.localStorage.Install directly from GreasyFork:
👉 Install StumbleBot
After installation, StumbleBot will automatically activate when visiting a chatroom.
Developed by Goji
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.
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.
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:
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();
}
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();
}
}
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";
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;
}
The new YouTube module supports operator-only access, multiple aliases, URL normalization, and local playback history with .history [page].
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);
}
}
.history CommandAdd 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());
}
.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");