Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
52 commits
Select commit Hold shift + click to select a range
001f92a
initial extraction of player structure
ShadowLp174 Nov 15, 2023
f59c7d6
first component done (Player), functionality missing
ShadowLp174 Nov 16, 2023
32ec60a
added queue and song-item component, still missing features
ShadowLp174 Nov 20, 2023
0b7448f
added search input component
ShadowLp174 Nov 21, 2023
5f1cef9
minor changes
ShadowLp174 Nov 21, 2023
306cd5c
more queue functions
ShadowLp174 Nov 22, 2023
ab3ea61
added timer to player component
ShadowLp174 Nov 22, 2023
4b15dd7
very small addition
ShadowLp174 Nov 23, 2023
81c1e4a
updated revoice.js
ShadowLp174 Jan 13, 2024
89d19fd
added api class kinda implemented play/pause buttons
ShadowLp174 Jan 14, 2024
6ae5028
translated colour algorithms into their own module
ShadowLp174 Jan 14, 2024
7c4fcc7
added notification api
ShadowLp174 Jan 16, 2024
c98fc10
ported notification api to web components
ShadowLp174 Jan 17, 2024
c622bdf
fixed critical security bug
ShadowLp174 Jan 17, 2024
373cdda
socket io connecion + live time updates
ShadowLp174 Jan 17, 2024
b5f0370
implemented volume and queue updates
ShadowLp174 Jan 17, 2024
6e9064a
implemented various missing features
ShadowLp174 Jan 19, 2024
34ad871
connected search input to api, full window search still missing
ShadowLp174 Jan 20, 2024
e0b1a8c
prepared server list component
ShadowLp174 Jan 20, 2024
fd42319
added server list and server item
ShadowLp174 Jan 21, 2024
1c1a1ae
added voice channel list, missing initJoin
ShadowLp174 Jan 21, 2024
99ee566
added channel selector
ShadowLp174 Jan 21, 2024
2c333a7
added categories; doesn't render correctly for some reason
ShadowLp174 Jan 21, 2024
1dc5af4
fixed user mapping bug
ShadowLp174 Feb 18, 2024
9fbd5a4
improving the channel selector
ShadowLp174 Feb 18, 2024
6c99eca
Wrapper approach for message and reaction handling.
ShadowLp174 Mar 4, 2026
a7adce7
removed masquerading until settings work again
ShadowLp174 Mar 4, 2026
41c82cb
embed editing, small fixes
ShadowLp174 Mar 4, 2026
0026efd
added basic permission checking for replying with embeds and text
ShadowLp174 Mar 5, 2026
b8d8bdf
added Channel Wrapper and PageBuilder for pagination
ShadowLp174 Mar 5, 2026
5de25cc
added pagination, improved sending messages and embeds
ShadowLp174 Mar 6, 2026
d743525
started CommandHandler rewrite
ShadowLp174 Mar 6, 2026
e51818a
processCommand requirements checking; started option parsing
ShadowLp174 Mar 6, 2026
cc6125b
added jsdocs to CommandBuilder, CommandRequirement, Option and Flag
ShadowLp174 Mar 6, 2026
3ec4ffa
completed command processing; added text wrap errors
ShadowLp174 Mar 7, 2026
11fccbc
added Settings
ShadowLp174 Mar 7, 2026
d848cf3
PrefixManager and HelpHandler; Help handling not completed yet
ShadowLp174 Mar 7, 2026
534a4d9
getCommandHelp
ShadowLp174 Mar 9, 2026
a4d07ee
subcommand help handling
ShadowLp174 Mar 9, 2026
3a1e5d8
Completed HelpHandler, help handling logic in CommandHandler adjusted
ShadowLp174 Mar 10, 2026
a6bee61
forgot to push Settings.mjs
ShadowLp174 Mar 10, 2026
be74715
added CommandLoader
ShadowLp174 Mar 10, 2026
c207ac3
added join method to Channels; added channel user message listeners
ShadowLp174 Mar 10, 2026
2599bb1
started port of main bot to new system; More work on other components
ShadowLp174 Mar 10, 2026
24df60f
added PlayerManager for creating and managing Player instances
ShadowLp174 Mar 10, 2026
a084423
initiated Player.mjs; play() and fetchResults() missing, more or less
ShadowLp174 Mar 11, 2026
916b730
added missing Player features, expanded index.mjs
ShadowLp174 Mar 11, 2026
0bff99c
updated command files to work with new system
ShadowLp174 Mar 11, 2026
76535cf
playback possible
ShadowLp174 Mar 11, 2026
4b5496c
forgot to push play.mjs
ShadowLp174 Mar 11, 2026
8f04478
fixed stats command, fixes to MessageHandler and imports
ShadowLp174 Mar 13, 2026
da7c0db
Merge branch 'refactor' of github.com:remix-bot/stoat into dashboard-…
ShadowLp174 Mar 13, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1,250 changes: 627 additions & 623 deletions Player.js

Large diffs are not rendered by default.

4 changes: 2 additions & 2 deletions commands/play.js → commands/.play.js
Original file line number Diff line number Diff line change
Expand Up @@ -20,10 +20,10 @@ module.exports = {
const p = await this.getPlayer(message);
if (!p) return;
const query = data.get("query").value;
message.reply(this.em("Searching...", message), false).then((msg) => {
message.replyEmbed("Searching...").then((msg) => {
const messages = p.play(query, false, data.get("provider").value);
messages.on("message", (d) => {
msg.edit(this.em(d, message));
msg.editEmbed(d);
});
});
}
Expand Down
2 changes: 1 addition & 1 deletion commands/clear.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,6 @@ module.exports = {
const p = await this.getPlayer(msg);
if (!p) return;
p.clear();
msg.channel.sendMessage(this.em("✅ Queue cleared.", msg));
msg.channel.sendEmbed("✅ Queue cleared.");
}
}
12 changes: 12 additions & 0 deletions commands/dashboard.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
const { CommandBuilder } = require("../Commands.js");

module.exports = {
command: new CommandBuilder() // TODO: maybe move to own website category?
.setName("dashboard")
.setDescription("Display information about the dashboard.")
.setCategory("util"),
run: async function (msg, data) { // TODO: temporary login (without creating account)
const url = this.config.dashboardUrl;
msg.reply(this.em("## Dashboard\n\nThe Dashboard is accessible under [" + url + "](" + url + "). Please note that it is very early and experimental version and is thus subject to many bugs.\n\nAccessing the actual dashboard will require you to connect to a Stoat account. However, this does not require access to your actual Stoat login information. You will have to provide your User ID or your Username + Discriminator. Afterwards you'll have to confirm your identity by sending a command with a certain token to Remix, verifying you. That is all that will happen and none of your data passes our servers.", msg), false);
}
}
2 changes: 1 addition & 1 deletion commands/debug.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ module.exports = {
switch(data.get("target").value) {
case "voice":
var servers = [];
var iterator = this.playerMap.entries();
var iterator = this.players.playerMap.entries();
for (let v = iterator.next(); !v.done; v = iterator.next()) {
servers.push(v.value[1]);
};
Expand Down
10 changes: 5 additions & 5 deletions commands/forceleave.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,10 +14,10 @@ module.exports = {
),
run: async function(msg, data) {
const cid = data.get("channelId").value;
if (msg.channel.server.id !== this.client.channels.get(cid)?.server.id) return msg.reply(this.em("This command has to be run in the same server as the voice channel.", msg), false)
const p = this.playerMap.get(cid);
if (!p) return msg.reply(this.em("Player not found", msg));
if (!p.connection) return msg.reply(this.em("Player not initialized.", msg), false);
this.leaveChannel.call(this, msg, cid, p);
if (msg.channel.server.id !== this.client.channels.get(cid)?.server.id) return msg.replyEmbed("This command has to be run in the same server as the voice channel.");
const p = this.players.playerMap.get(cid);
if (!p) return msg.replyEmbed("Player not found");
if (!p.connection) return msg.replyEmbed("Player not initialized.");
this.players.leave(msg, cid);
}
}
16 changes: 8 additions & 8 deletions commands/join.js
Original file line number Diff line number Diff line change
@@ -1,15 +1,15 @@
const { CommandBuilder } = require("../Commands.js");
const RevoltPlayer = require("../Player.js");

/* */
function joinChannel(message, cid, cb=()=>{}, ecb=()=>{}) {
if (!this.client.channels.has(cid)) {
ecb();
return message.reply(this.em("Couldn't find the channel `" + cid + "`\nUse the help command to learn more about this. (`%help join`)", message), false)
return message.replyEmbed("Couldn't find the channel `" + cid + "`\nUse the help command to learn more about this. (`%help join`)")
}

if (this.playerMap.has(cid)) {
cb(this.playerMap.get(cid));
return message.reply(this.em("Already joined <#" + cid + ">.", message), false);
return message.replyEmbed("Already joined <#" + cid + ">.");
}
this.channels.push(cid);
const settings = this.getSettings(message);
Expand All @@ -28,7 +28,7 @@ function joinChannel(message, cid, cb=()=>{}, ecb=()=>{}) {
innertube: this.innertube
});
p.on("autoleave", async () => {
message.channel.sendMessage(this.em("Left channel <#" + cid + "> because of inactivity.", message));
message.channel.sendEmbed("Left channel <#" + cid + "> because of inactivity.");
const port = p.port - 3050;
this.playerMap.delete(cid);
p.destroy();
Expand All @@ -44,7 +44,7 @@ function joinChannel(message, cid, cb=()=>{}, ecb=()=>{}) {
});
p.on("message", (m) => {
if ((this.getSettings(message).get("songAnnouncements")) == "false") return;
message.channel.sendMessage(this.em(m, message))
message.channel.sendEmbed(m);
});
p.on("roomfetched", () => {
p.connection.users.forEach(user => {
Expand All @@ -55,9 +55,9 @@ function joinChannel(message, cid, cb=()=>{}, ecb=()=>{}) {
});
});
this.playerMap.set(cid, p);
message.reply(this.em("Joining Channel...", message), false).then((message) => {
message.replyEmbed("Joining Channel...").then((message) => {
p.join(cid).then(() => {
message.edit(this.em(`✅ Successfully joined <#${cid}>`, message));
message.editEmbed(`✅ Successfully joined <#${cid}>`);
cb(p);

p.connection.on("userjoin", (user) => {
Expand Down Expand Up @@ -101,7 +101,7 @@ module.exports = {
),
run: function(message, data) {
const cid = data.getById("cid").value || this.checkVoiceChannels(message);
joinChannel.call(this, message, cid);
this.players.initPlayer(message, cid);
},
export: {
name: "joinChannel",
Expand Down
8 changes: 4 additions & 4 deletions commands/leave.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,12 @@ function leaveChannel(msg, cid, p) {
return new Promise(async (res) => {
this.playerMap.delete(cid);
const port = p.port - 3050;
const m = await msg.reply(this.em("Leaving...", msg), false);
const m = await msg.replyEmbed("Leaving...");
const left = p.leave();
//p.leave().then(async left => {
p.destroy(); // wait for the ports to be open again
this.freed.push(port);
m.edit(this.em((left) ? `✅ Successfully Left` : `Not connected to any voice channel`, msg));
m.editEmbed((left) ? `✅ Successfully Left` : `Not connected to any voice channel`);
res();
});
}
Expand All @@ -22,9 +22,9 @@ module.exports = {
run: async function(msg) {
const p = await this.getPlayer(msg, false, false);
if (!p) return;
if (!p.connection) return msg.reply(this.em("Player not initialized.", msg), false);
if (!p.connection) return msg.replyEmbed("Player not initialized.");
const cid = p.connection.channelId;
leaveChannel.call(this, msg, cid, p);
this.players.leave(msg, cid);
},
export: {
name: "leaveChannel",
Expand Down
2 changes: 1 addition & 1 deletion commands/loop.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,6 @@ module.exports = {
const p = await this.getPlayer(message, data);
if (!p) return;
const res = p.loop(data.options[0].value);
message.channel.sendMessage(this.em(res, message));
message.channel.sendEmbed(res);
}
}
10 changes: 5 additions & 5 deletions commands/lyrics.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,13 +10,13 @@ module.exports = {
const p = await this.getPlayer(message);
if (!p) return;

const n = message.reply(this.em("Fetching lyrics from genius...", message), false);
const n = message.replyEmbed("Fetching lyrics from genius...");

var messages = await p.lyrics();
if (!messages) return (await n).edit(this.em("Couldn't find the lyrics for this song on genius!", message), false);
if (messages.length == 0) return message.reply(this.em("There's nothing playing at the moment.", message), false);
if (!messages) return (await n).editEmbed("Couldn't find the lyrics for this song on genius!");
if (messages.length == 0) return message.replyEmbed("There's nothing playing at the moment.");
messages = messages.split("\n");
(await n).delete();
this.pagination("Lyrics for " + p.getVidName(p.data.current) + ": \n```\n$content\n```\nPage $currPage/$maxPage", messages, message, 15)
(await n).message.delete();
this.pagination("Lyrics for " + p.getVideoName(p.queue.getCurrent()) + ": \n```\n$content\n```\nPage $currPage/$maxPage", messages, message, 15)
}
}
8 changes: 4 additions & 4 deletions commands/np.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,11 +8,11 @@ module.exports = {
run: async function(msg) {
const p = await this.getPlayer(msg);
if (!p) return;
msg.reply(this.em("Loading...", msg)).then(async m => {
msg.replyEmbed("Loading...").then(async m => {
let data = await p.nowPlaying();
let embed = this.em(data.msg, m);
if (data.image) embed.embeds[0].media = data.image;
m.edit(embed);
m.editEmbed(data.msg, {
media: data.image
});
});
}
}
2 changes: 1 addition & 1 deletion commands/pause.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,6 @@ module.exports = {
const p = await this.getPlayer(message);
if (!p) return;
let res = p.pause() || `✅ The song has been paused!`;
message.channel.sendMessage(this.em(res, message));
message.channel.sendEmbed(res);
}
}
28 changes: 28 additions & 0 deletions commands/play.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import { CommandBuilder } from "../src/CommandHandler.mjs";
export const command = new CommandBuilder()
.setName("play")
.setId("play")
.setDescription("Play a youtube video from url/query or a playlist by url. Other services are supported as well.\nSearches will be done on `Youtube Music` by default.\nIf you want to search on `YouTube` you will have to specify that with `-u yt` explicitly.", "commands.play")
.addExamples("$prefixplay take over league of legends", "$prefixplay -provider yt 'take over league of legends'", "$prefixp take over league of legends")
.addTextOption((option) =>
option.setName("query")
.setDescription("A YouTube query/url, playlist url, or a link to a Spotify, SoundCloud, or YouTube Music song.", "options.play.query")
.setRequired(true)
).addChoiceOption(o =>
o.setName("provider")
.setDescription("The search result provider (YouTube, YouTube Music or SoundCloud). Default: SoundCloud", "options.search.provider") // same as search provider flag
.addFlagAliases("p", "u", "use")
.addChoices("ytm", "yt", "scld")
.setDefault("ytm")
, true).addAlias("p");
export const run = async function(message, data) {
const p = await this.getPlayer(message);
if (!p) return;
const query = data.get("query").value;
message.replyEmbed("Searching...").then((msg) => {
const messages = p.play(query, false, data.get("provider").value);
messages.on("message", (d) => {
msg.editEmbed(d);
});
});
}
32 changes: 18 additions & 14 deletions commands/player.js
Original file line number Diff line number Diff line change
@@ -1,47 +1,51 @@
const { CommandBuilder } = require("../Commands.js");
const { Message } = require("../src/MessageHandler.mjs");

module.exports = {
command: new CommandBuilder()
.setName("player")
.setDescription("Create an emoji player control for your voice channel", "commands.player"),
/**
* @param {Message} msg
*/
run: async function(msg) {
const p = await this.getPlayer(msg);
if (!p) return;

const Timeout = this.config.playerAFKTimeout || 10 * 6000;

const controls = ["▶️", "⏸️", "⏭️", "🔁", "🔀"];
const form = "Currently Playing: $current\n\n$lastMsg";
var em = this.embedify(form.replace(/\$current/gi, p.getCurrent()).replace(/\$lastMsg/gi, "Control updates will appear here"));
msg.reply({
var lastContent = form.replace(/\$current/gi, p.getCurrent()).replace(/\$lastMsg/gi, "Control updates will appear here");
msg.replyEmbed({
content: " ",
embeds: [em],
embedText: lastContent,
interactions: {
restrict_reactions: true,
reactions: controls
}
}, false).then((m) => {
}).then((m) => {

var suspensionTimeout = setTimeout(() => close(), Timeout);

var lastUpdate = "Control updates will appear here";
const update = (s=lastUpdate) => {
em = this.embedify(form.replace(/\$current/gi, p.getCurrent()).replace(/\$lastMsg/gi, s))
m.edit({ embeds: [ em ]});
const update = (s = lastUpdate) => {
lastContent = form.replace(/\$current/gi, p.getCurrent()).replace(/\$lastMsg/gi, s);
m.editEmbed(lastContent);
lastUpdate = s;
}
const close = () => {
this.unobserveReactions(oid);
m.edit({
unobserve();
m.editEmbed({
content: "Player Session Closed",
embeds: [
this.embedify(em.description + "\n\nSession Closed. The player controls **won't respond** from here.", "red")
]
embedText: lastContent + "\n\nSession Closed. The player controls **won't respond** from here.",
}, {
colour: "red"
});
}
p.on("message", () => {
update();
});
const oid = this.observeReactions(m, controls, (e, ms) => {
const unobserve = m.onReaction(controls, (e) => {
var reply = "";
switch (e.emoji_id) {
case controls[0]:
Expand Down
4 changes: 2 additions & 2 deletions commands/playnext.js
Original file line number Diff line number Diff line change
Expand Up @@ -19,10 +19,10 @@ module.exports = {
const p = await this.getPlayer(message);
if (!p) return;
const query = data.get("query").value; // only 1 text option registered
message.reply(this.em("Searching...", message), false).then((msg) => {
message.replyEmbed("Searching...").then((msg) => {
const messages = p.playFirst(query, data.get("provider").value);
messages.on("message", (d) => {
msg.edit(this.em(d, message));
msg.editEmbed(d, message);
});
});
}
Expand Down
6 changes: 3 additions & 3 deletions commands/radio.js
Original file line number Diff line number Diff line change
Expand Up @@ -25,17 +25,17 @@ module.exports = {
m += " \n\nUse the name in the brackets to select that station in the radio command.\n\n";
m += "Example: `%radio zamrock`";

msg.reply(this.em(m, msg), false);
msg.replyEmbed(m);
return;
}

const p = await this.getPlayer(msg);
if (!p) return;

const radio = this.config.radio.find(e => e.name === data.get("station").value);
msg.channel.sendMessage(this.em("Adding radio station to queue...", msg)).then(m => {
msg.channel.sendEmbed("Adding radio station to queue...").then(m => {
const _messages = p.playRadio(radio);
m.edit(this.em("Added `" + radio.detailedName + "` to the queue.", msg));
m.editEmbed("Added `" + radio.detailedName + "` to the queue.");
});
}
}
2 changes: 1 addition & 1 deletion commands/remove.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,6 @@ module.exports = {
const p = await this.getPlayer(message);
if (!p) return;
let res = p.remove(data.options[0].value);
message.channel.sendMessage(this.em(res, message));
message.channel.sendEmbed(res);
}
}
2 changes: 1 addition & 1 deletion commands/resume.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,6 @@ module.exports = {
const p = await this.getPlayer(message);
if (!p) return;
let res = p.resume() || `✅ The song has been resumed!`;
message.channel.sendMessage(this.em(res, message));
message.channel.sendEmbed(res);
}
}
25 changes: 14 additions & 11 deletions commands/search.js
Original file line number Diff line number Diff line change
@@ -1,18 +1,22 @@
const { CommandBuilder } = require("../Commands.js");
const { MessageHandler } = require("../src/MessageHandler.mjs");

function awaitMessage(msg, count, player) {
const oid = this.observeUser(msg.authorId, msg.channelId, (m) => {
/** @type {MessageHandler} */
const messages = this.messages;
const channel = messages.getChannel(msg.channel.id);
const unobserve = channel.onMessageUser((m) => {
if (m.content.trim().toLowerCase() == "x") {
this.unobserveUser(oid);
return m.reply(this.em("Cancelled!", m));
unobserve();
return m.replyEmbed("Cancelled!");
}
let c = parseInt(m.content.trim().replace(/\./g, ""));
if (isNaN(c)) return m.reply(this.em("Invalid number! (Send 'x' to cancel)", m));
if (c < 0 || c > count) return m.reply(this.em("Index out of range! (`1 - " + count + "`)", m));
if (isNaN(c)) return m.replyEmbed("Invalid number! (Send 'x' to cancel)");
if (c < 0 || c > count) return m.replyEmbed("Index out of range! (`1 - " + count + "`)");
let v = player.playResult(msg.authorId, c - 1);
m.reply(this.em((typeof v == "string") ? v : `Added [${v.title}](${v.url}) to the queue!`, m));
this.unobserveUser(oid);
});
m.replyEmbed((typeof v == "string") ? v : `Added [${v.title}](${v.url}) to the queue!`);
unobserve();
}, msg.author);
}

module.exports = {
Expand All @@ -36,11 +40,10 @@ module.exports = {
if (!p) return;
let query = data.get("query").value;
let provider = data.get("provider")?.value;
msg.reply(this.em("Loading results...", msg)).then(async m => {
msg.replyEmbed("Loading results...").then(async m => {
let res = await p.fetchResults(query, msg.authorId, provider);
m.edit(this.em(res.m, msg));
m.editEmbed(res.m);
awaitMessage.call(this, msg, res.count, p);
});
}
}

Loading
Loading