basic slash commands bot

This commit is contained in:
Snowflake107 2021-06-13 13:51:19 +05:45
parent 5239e5f9cc
commit b97e8e61fc
11 changed files with 239 additions and 106 deletions

View file

@ -1,5 +1,5 @@
{ {
"printWidth": 120, "printWidth": 200,
"trailingComma": "none", "trailingComma": "none",
"singleQuote": false, "singleQuote": false,
"tabWidth": 4, "tabWidth": 4,

View file

@ -1,78 +1,199 @@
import { Client, Message } from "discord.js"; import { Client, GuildMember, Message, TextChannel } from "discord.js";
import { Player, Queue } from "../src/index"; import { Player, Queue, Track } from "../src/index";
import { config } from "./config"; import { config } from "./config";
// use this in prod.
// import { Player, Queue } from "discord-player";
const client = new Client({ const client = new Client({
intents: ['GUILD_VOICE_STATES', 'GUILD_MESSAGES', 'GUILDS'] intents: ["GUILD_VOICE_STATES", "GUILD_MESSAGES", "GUILDS"]
}); });
client.on("ready", () => {
console.log("Bot is online!");
client.user.setActivity({
name: "🎶 | Music Time",
type: "LISTENING"
});
});
client.on("error", console.error);
client.on("warn", console.warn);
// instantiate the player
const player = new Player(client); const player = new Player(client);
player.on("error", console.error);
player.on("trackStart", (queue, track) => { player.on("trackStart", (queue, track) => {
const guildQueue = queue as Queue<Message>; const guildQueue = queue as Queue<TextChannel>;
guildQueue.metadata.channel.send(`🎶 | Now playing: **${track.title}** in **${guildQueue.connection.channel.name}**!`); guildQueue.metadata.send(`🎶 | Started playing: **${track.title}** in **${guildQueue.connection.channel.name}**!`);
}); });
client.on("ready", () => console.log("Bot is online!")); player.on("trackAdd", (queue, track) => {
const guildQueue = queue as Queue<TextChannel>;
guildQueue.metadata.send(`🎶 | Track **${track.title}** queued!`);
});
client.on("message", async message => { client.on("message", async (message) => {
if (!client.application.owner) await client.application.fetch(); if (message.author.bot || !message.guild) return;
if (message.author.id !== client.application.owner.id) return; if (!client.application?.owner) await client.application?.fetch();
if (message.content.startsWith("!np") && message.guild.me.voice.channelID) { if (message.content === "!deploy" && message.author.id === client.application?.owner?.id) {
const conn = player.getQueue(message.guild.id); await message.guild.commands.set([
if (!conn) return; {
return void message.channel.send(`Now Playing: **${conn.current.title}** (Played **${Math.floor(conn.connection.streamTime / 1000)} seconds**)`); name: "play",
description: "Plays a song from youtube",
options: [
{
name: "query",
type: "STRING",
description: "The song you want to play",
required: true
}
]
},
{
name: "volume",
description: "Sets music volume",
options: [
{
name: "amount",
type: "INTEGER",
description: "The volume amount to set (0-100)",
required: false
}
]
},
{
name: "skip",
description: "Skip to the current song"
},
{
name: "queue",
description: "See the queue"
},
{
name: "pause",
description: "Pause the current song"
},
{
name: "resume",
description: "Resume the current song"
},
{
name: "stop",
description: "Stop the player"
},
{
name: "np",
description: "Now Playing"
}
]);
await message.reply("Deployed!");
}
});
client.on("interaction", async (interaction) => {
if (!interaction.isCommand() || !interaction.guildID) return;
if (!(interaction.member instanceof GuildMember) || !interaction.member.voice.channel) {
return void interaction.reply({ content: "You are not in a voice channel!", ephemeral: true });
} }
if (message.content.startsWith("!pause") && message.guild.me.voice.channelID) { if (interaction.guild.me.voice.channelID && interaction.member.voice.channelID !== interaction.guild.me.voice.channelID) {
const conn = player.getQueue(message.guild.id); return void interaction.reply({ content: "You are not in my voice channel!", ephemeral: true });
if (!conn) return;
conn.setPaused(true);
return void message.channel.send("Paused!");
} }
if (message.content.startsWith("!resume") && message.guild.me.voice.channelID) { if (interaction.commandName === "play") {
const conn = player.getQueue(message.guild.id); await interaction.defer();
if (!conn) return;
conn.setPaused(false);
return void message.channel.send("Resumed!");
}
if (message.content.startsWith("!skip") && message.guild.me.voice.channelID) { const query = interaction.options.get("query")!.value! as string;
const conn = player.getQueue(message.guild.id); const searchResult = (await player.search(query, interaction.user).catch(() => [])) as Track[];
if (!conn) return; if (!searchResult.length) return void interaction.followUp({ content: "No results were found!" });
conn.skip();
return void message.channel.send("Done!");
}
if (message.content.startsWith("!queue") && message.guild.me.voice.channelID) { const queue = await player.createQueue(interaction.guild, {
const conn = player.getQueue(message.guild.id); metadata: interaction.channel
if (!conn) return;
return void message.channel.send({ content: conn.toString(), split: true });
}
if (message.content.startsWith("!vol") && message.guild.me.voice.channelID) {
const conn = player.getQueue(message.guild.id);
if (!conn) return;
conn.connection.setVolume(parseInt(message.content.slice(4).trim()));
return void message.channel.send("Volume changed!");
}
if (message.content.startsWith("!p") && message.member.voice.channelID) {
const queue = player.createQueue<Message>(message.guild, {
metadata: message
}); });
const song = await player.search(message.content.slice(2).trim(), message.author).then(x => x[0]);
queue.addTrack(song);
if (!queue.connection) { try {
queue.connect(message.member.voice.channel) if (!queue.connection) await queue.connect(interaction.member.voice.channel);
.then(async q => { } catch {
await q.play(); void player.deleteQueue(interaction.guildID);
return void interaction.followUp({ content: "Could not join your voice channel!" });
}
await interaction.followUp({ content: "⏱ | Loading your track..." });
await queue.play(searchResult[0]);
} else if (interaction.commandName === "volume") {
await interaction.defer();
const queue = player.getQueue(interaction.guildID);
if (!queue || !queue.playing) return void interaction.followUp({ content: "❌ | No music is being played!" });
const vol = interaction.options.get("amount");
if (!vol) return void interaction.followUp({ content: `🎧 | Current volume is **${queue.volume}**%!` });
if ((vol.value as number) < 0 || (vol.value as number) > 100) return void interaction.followUp({ content: "❌ | Volume range must be 0-100" });
const success = queue.setVolume(vol.value as number);
return void interaction.followUp({
content: success ? `✅ | Volume set to **${vol.value}%**!` : "❌ | Something went wrong!"
}); });
} else if (interaction.commandName === "skip") {
await interaction.defer();
const queue = player.getQueue(interaction.guildID);
if (!queue || !queue.playing) return void interaction.followUp({ content: "❌ | No music is being played!" });
const currentTrack = queue.current;
const success = queue.skip();
return void interaction.followUp({
content: success ? `✅ | Skipped **${currentTrack}**!` : "❌ | Something went wrong!"
});
} else if (interaction.commandName === "queue") {
await interaction.defer();
const queue = player.getQueue(interaction.guildID);
if (!queue || !queue.playing) return void interaction.followUp({ content: "❌ | No music is being played!" });
const currentTrack = queue.current;
const tracks = queue.tracks
.slice(0, 10)
.map((m, i) => {
return `${i + 1}. **${m.title}**`;
})
.join("\n");
return void interaction.followUp({
embeds: [
{
title: "Server Queue",
description: `${tracks}${queue.tracks.length > tracks.length ? `...${queue.tracks.length - tracks.length} more tracks` : ""}`,
color: 0xff0000,
fields: [{ name: "Now Playing", value: `🎶 | **${currentTrack.title}**` }]
}
]
});
} else if (interaction.commandName === "pause") {
await interaction.defer();
const queue = player.getQueue(interaction.guildID);
if (!queue || !queue.playing) return void interaction.followUp({ content: "❌ | No music is being played!" });
const success = queue.setPaused(true);
return void interaction.followUp({ content: success ? "⏸ | Paused!" : "❌ | Something went wrong!" });
} else if (interaction.commandName === "resume") {
await interaction.defer();
const queue = player.getQueue(interaction.guildID);
if (!queue || !queue.playing) return void interaction.followUp({ content: "❌ | No music is being played!" });
const success = queue.setPaused(false);
return void interaction.followUp({ content: success ? "▶ | Resumed!" : "❌ | Something went wrong!" });
} else if (interaction.commandName === "stop") {
await interaction.defer();
const queue = player.getQueue(interaction.guildID);
if (!queue || !queue.playing) return void interaction.followUp({ content: "❌ | No music is being played!" });
queue.destroy();
return void interaction.followUp({ content: "🛑 | Stopped the player!" });
} else if (interaction.commandName === "np") {
await interaction.defer();
const queue = player.getQueue(interaction.guildID);
if (!queue || !queue.playing) return void interaction.followUp({ content: "❌ | No music is being played!" });
return void interaction.followUp({ content: `🎶 | Current song: **${queue.current.title}**!` });
} else { } else {
message.channel.send(`🎶 | Queued: **${song.title}**!`); interaction.reply({
} content: "Unknown command!",
ephemeral: true
});
} }
}); });

View file

@ -9,8 +9,8 @@
], ],
"scripts": { "scripts": {
"test": "cd example && ts-node index.ts", "test": "cd example && ts-node index.ts",
"build": "tsc", "build": "rimraf lib && tsc",
"format": "prettier --write \"src/**/*.ts\"", "format": "prettier --write \"src/**/*.ts\" \"example/**/*.ts\"",
"lint": "tslint -p tsconfig.json", "lint": "tslint -p tsconfig.json",
"docs": "docgen --jsdoc jsdoc.json --verbose --source src/*.ts src/**/*.ts --custom docs/index.yml --output docs/docs.json", "docs": "docgen --jsdoc jsdoc.json --verbose --source src/*.ts src/**/*.ts --custom docs/index.yml --output docs/docs.json",
"docs:test": "docgen --jsdoc jsdoc.json --verbose --source src/*.ts src/**/*.ts --custom docs/index.yml" "docs:test": "docgen --jsdoc jsdoc.json --verbose --source src/*.ts src/**/*.ts --custom docs/index.yml"
@ -68,10 +68,11 @@
"@types/node": "^14.14.41", "@types/node": "^14.14.41",
"@types/ws": "^7.4.1", "@types/ws": "^7.4.1",
"discord-api-types": "^0.18.1", "discord-api-types": "^0.18.1",
"discord.js": "^13.0.0-dev.f5f3f772865ee98bbb44df938e0e71f9f8865c10", "discord.js": "^13.0.0-dev.02693bc02f45980d8165820a103220f0027b96b7",
"discord.js-docgen": "discordjs/docgen#ts-patch", "discord.js-docgen": "discordjs/docgen#ts-patch",
"jsdoc-babel": "^0.5.0", "jsdoc-babel": "^0.5.0",
"prettier": "^2.2.1", "prettier": "^2.2.1",
"rimraf": "^3.0.2",
"ts-node": "^10.0.0", "ts-node": "^10.0.0",
"tslint": "^6.1.3", "tslint": "^6.1.3",
"tslint-config-prettier": "^1.18.0", "tslint-config-prettier": "^1.18.0",

View file

@ -31,6 +31,12 @@ class DiscordPlayer extends EventEmitter<PlayerEvents> {
return this.queues.get(guild) as Queue<T>; return this.queues.get(guild) as Queue<T>;
} }
deleteQueue<T = unknown>(guild: Snowflake) {
const prev = this.getQueue<T>(guild);
this.queues.delete(guild);
return prev;
}
/** /**
* Search tracks * Search tracks
* @param {string|Track} query The search query * @param {string|Track} query The search query

View file

@ -45,8 +45,7 @@ class Queue<T = unknown> {
} }
async connect(channel: StageChannel | VoiceChannel) { async connect(channel: StageChannel | VoiceChannel) {
if (!["stage", "voice"].includes(channel?.type)) if (!["stage", "voice"].includes(channel?.type)) throw new TypeError(`Channel type must be voice or stage, got ${channel?.type}!`);
throw new TypeError(`Channel type must be voice or stage, got ${channel?.type}!`);
const connection = await this.player.voiceUtils.connect(channel); const connection = await this.player.voiceUtils.connect(channel);
this.connection = connection; this.connection = connection;
@ -69,6 +68,7 @@ class Queue<T = unknown> {
addTrack(track: Track) { addTrack(track: Track) {
this.addTracks([track]); this.addTracks([track]);
this.player.emit("trackAdd", this, track);
} }
addTracks(tracks: Track[]) { addTracks(tracks: Track[]) {
@ -86,12 +86,21 @@ class Queue<T = unknown> {
this.connection.audioResource.encoder.setBitrate(bitrate); this.connection.audioResource.encoder.setBitrate(bitrate);
} }
setVolume(amount: number) {
if (!this.connection) return false;
return this.connection.setVolume(amount);
}
get volume() {
if (!this.connection) return 100;
return this.connection.volume;
}
async play(src?: Track, options: PlayOptions = {}) { async play(src?: Track, options: PlayOptions = {}) {
if (!this.connection || !this.connection.voiceConnection) if (!this.connection || !this.connection.voiceConnection) throw new Error("Voice connection is not available, use <Queue>.connect()!");
throw new Error("Voice connection is not available, use <Queue>.connect()!"); if (src && (this.playing || this.tracks.length) && !options.immediate) return this.addTrack(src);
const track = options.filtersUpdate ? this.current : src ?? this.tracks.shift(); const track = options.filtersUpdate ? this.current : src ?? this.tracks.shift();
if (!track) return; if (!track) return;
let stream; let stream;
if (["youtube", "spotify"].includes(track.raw.source)) { if (["youtube", "spotify"].includes(track.raw.source)) {
stream = ytdl(track.raw.source === "spotify" ? track.raw.engine : track.url, { stream = ytdl(track.raw.source === "spotify" ? track.raw.engine : track.url, {
@ -102,18 +111,13 @@ class Queue<T = unknown> {
seek: options.seek seek: options.seek
}); });
} else { } else {
stream = ytdl.arbitraryStream( stream = ytdl.arbitraryStream(track.raw.source === "soundcloud" ? await track.raw.engine.downloadProgressive() : (track.raw.engine as string), {
track.raw.source === "soundcloud"
? await track.raw.engine.downloadProgressive()
: (track.raw.engine as string),
{
// because we don't wanna decode opus into pcm again just for volume, let discord.js handle that // because we don't wanna decode opus into pcm again just for volume, let discord.js handle that
opusEncoded: false, opusEncoded: false,
fmt: "s16le", fmt: "s16le",
encoderArgs: options.encoderArgs ?? [], encoderArgs: options.encoderArgs ?? [],
seek: options.seek seek: options.seek
} });
);
} }
const resource: AudioResource<Track> = this.connection.createStream(stream, { const resource: AudioResource<Track> = this.connection.createStream(stream, {
@ -124,12 +128,12 @@ class Queue<T = unknown> {
const dispatcher = await this.connection.playStream(resource); const dispatcher = await this.connection.playStream(resource);
dispatcher.setVolume(this.options.initialVolume); dispatcher.setVolume(this.options.initialVolume);
dispatcher.on("start", () => { dispatcher.once("start", () => {
this.playing = true; this.playing = true;
if (!options.filtersUpdate) this.player.emit("trackStart", this, this.current); if (!options.filtersUpdate) this.player.emit("trackStart", this, this.current);
}); });
dispatcher.on("finish", () => { dispatcher.once("finish", () => {
this.playing = false; this.playing = false;
if (options.filtersUpdate) return; if (options.filtersUpdate) return;
@ -138,9 +142,12 @@ class Queue<T = unknown> {
this.player.emit("queueEnd", this); this.player.emit("queueEnd", this);
} else { } else {
const nextTrack = this.tracks.shift(); const nextTrack = this.tracks.shift();
this.play(nextTrack); this.play(nextTrack, { immediate: true });
} }
}); });
dispatcher.on("error", (e) => this.player.emit("error", this, e));
dispatcher.on("debug", (msg) => this.player.emit("debug", this, msg));
} }
*[Symbol.iterator]() { *[Symbol.iterator]() {

View file

@ -51,16 +51,11 @@ class BasicStreamDispatcher extends EventEmitter<VoiceEvents> {
} }
} else if (newState.status === VoiceConnectionStatus.Destroyed) { } else if (newState.status === VoiceConnectionStatus.Destroyed) {
this.end(); this.end();
} else if ( } else if (!this.connectPromise && (newState.status === VoiceConnectionStatus.Connecting || newState.status === VoiceConnectionStatus.Signalling)) {
!this.connectPromise &&
(newState.status === VoiceConnectionStatus.Connecting ||
newState.status === VoiceConnectionStatus.Signalling)
) {
this.connectPromise = entersState(this.voiceConnection, VoiceConnectionStatus.Ready, 20000) this.connectPromise = entersState(this.voiceConnection, VoiceConnectionStatus.Ready, 20000)
.then(() => undefined) .then(() => undefined)
.catch(() => { .catch(() => {
if (this.voiceConnection.state.status !== VoiceConnectionStatus.Destroyed) if (this.voiceConnection.state.status !== VoiceConnectionStatus.Destroyed) this.voiceConnection.destroy();
this.voiceConnection.destroy();
}) })
.finally(() => (this.connectPromise = undefined)); .finally(() => (this.connectPromise = undefined));
} }
@ -140,8 +135,7 @@ class BasicStreamDispatcher extends EventEmitter<VoiceEvents> {
async playStream(resource: AudioResource<Track> = this.audioResource) { async playStream(resource: AudioResource<Track> = this.audioResource) {
if (!resource) throw new PlayerError("Audio resource is not available!"); if (!resource) throw new PlayerError("Audio resource is not available!");
if (!this.audioResource) this.audioResource = resource; if (!this.audioResource) this.audioResource = resource;
if (this.voiceConnection.state.status !== VoiceConnectionStatus.Ready) if (this.voiceConnection.state.status !== VoiceConnectionStatus.Ready) await entersState(this.voiceConnection, VoiceConnectionStatus.Ready, 20000);
await entersState(this.voiceConnection, VoiceConnectionStatus.Ready, 20000);
this.audioPlayer.play(resource); this.audioPlayer.play(resource);
return this; return this;
@ -155,6 +149,12 @@ class BasicStreamDispatcher extends EventEmitter<VoiceEvents> {
return true; return true;
} }
get volume() {
if (!this.audioResource || !this.audioResource.volume) return 100;
const currentVol = this.audioResource.volume.volume;
return Math.round(Math.pow(currentVol, 1 / 1.660964) * 200);
}
get streamTime() { get streamTime() {
if (!this.audioResource) return 0; if (!this.audioResource) return 0;
return this.audioResource.playbackDuration; return this.audioResource.playbackDuration;

View file

@ -119,7 +119,8 @@ export interface PlayerEvents {
botDisconnect: () => any; botDisconnect: () => any;
channelEmpty: () => any; channelEmpty: () => any;
connectionCreate: () => any; connectionCreate: () => any;
error: () => any; debug: (queue: Queue, message: string) => any;
error: (queue: Queue, error: Error) => any;
musicStop: () => any; musicStop: () => any;
noResults: () => any; noResults: () => any;
playlistAdd: () => any; playlistAdd: () => any;
@ -130,7 +131,7 @@ export interface PlayerEvents {
searchCancel: () => any; searchCancel: () => any;
searchInvalidResponse: () => any; searchInvalidResponse: () => any;
searchResults: () => any; searchResults: () => any;
trackAdd: () => any; trackAdd: (queue: Queue, track: Track) => any;
trackStart: (queue: Queue, track: Track) => any; trackStart: (queue: Queue, track: Track) => any;
} }
@ -143,4 +144,7 @@ export interface PlayOptions {
/** Time to seek to before playing */ /** Time to seek to before playing */
seek?: number; seek?: number;
/** If it should start playing provided track immediately */
immediate?: boolean;
} }

View file

@ -70,15 +70,11 @@ const FilterList = {
}, },
get names() { get names() {
return Object.keys(this).filter( return Object.keys(this).filter((p) => !["names", "length"].includes(p) && typeof this[p as FiltersName] !== "function");
(p) => !["names", "length"].includes(p) && typeof this[p as FiltersName] !== "function"
);
}, },
get length() { get length() {
return Object.keys(this).filter( return Object.keys(this).filter((p) => !["names", "length"].includes(p) && typeof this[p as FiltersName] !== "function").length;
(p) => !["names", "length"].includes(p) && typeof this[p as FiltersName] !== "function"
).length;
}, },
toString() { toString() {

View file

@ -6,11 +6,9 @@ import { validateURL as SoundcloudValidateURL } from "soundcloud-scraper";
// scary things below *sigh* // scary things below *sigh*
const spotifySongRegex = /https?:\/\/(?:embed\.|open\.)(?:spotify\.com\/)(?:track\/|\?uri=spotify:track:)((\w|-){22})/; const spotifySongRegex = /https?:\/\/(?:embed\.|open\.)(?:spotify\.com\/)(?:track\/|\?uri=spotify:track:)((\w|-){22})/;
const spotifyPlaylistRegex = const spotifyPlaylistRegex = /https?:\/\/(?:embed\.|open\.)(?:spotify\.com\/)(?:playlist\/|\?uri=spotify:playlist:)((\w|-){22})/;
/https?:\/\/(?:embed\.|open\.)(?:spotify\.com\/)(?:playlist\/|\?uri=spotify:playlist:)((\w|-){22})/;
const spotifyAlbumRegex = /https?:\/\/(?:embed\.|open\.)(?:spotify\.com\/)(?:album\/|\?uri=spotify:album:)((\w|-){22})/; const spotifyAlbumRegex = /https?:\/\/(?:embed\.|open\.)(?:spotify\.com\/)(?:album\/|\?uri=spotify:album:)((\w|-){22})/;
const vimeoRegex = const vimeoRegex = /(http|https)?:\/\/(www\.|player\.)?vimeo\.com\/(?:channels\/(?:\w+\/)?|groups\/([^/]*)\/videos\/|video\/|)(\d+)(?:|\/\?)/;
/(http|https)?:\/\/(www\.|player\.)?vimeo\.com\/(?:channels\/(?:\w+\/)?|groups\/([^/]*)\/videos\/|video\/|)(\d+)(?:|\/\?)/;
const facebookRegex = /(https?:\/\/)(www\.|m\.)?(facebook|fb).com\/.*\/videos\/.*/; const facebookRegex = /(https?:\/\/)(www\.|m\.)?(facebook|fb).com\/.*\/videos\/.*/;
const reverbnationRegex = /https:\/\/(www.)?reverbnation.com\/(.+)\/song\/(.+)/; const reverbnationRegex = /https:\/\/(www.)?reverbnation.com\/(.+)\/song\/(.+)/;
const attachmentRegex = const attachmentRegex =

View file

@ -1860,10 +1860,10 @@ discord.js-docgen@discordjs/docgen#ts-patch:
tsubaki "^1.3.2" tsubaki "^1.3.2"
yargs "^14.0.0" yargs "^14.0.0"
discord.js@^13.0.0-dev.f5f3f772865ee98bbb44df938e0e71f9f8865c10: discord.js@^13.0.0-dev.02693bc02f45980d8165820a103220f0027b96b7:
version "13.0.0-dev.f5f3f772865ee98bbb44df938e0e71f9f8865c10" version "13.0.0-dev.02693bc02f45980d8165820a103220f0027b96b7"
resolved "https://registry.yarnpkg.com/discord.js/-/discord.js-13.0.0-dev.f5f3f772865ee98bbb44df938e0e71f9f8865c10.tgz#0fe3389e4befffd429ba37fc669b9153e87f33e5" resolved "https://registry.yarnpkg.com/discord.js/-/discord.js-13.0.0-dev.02693bc02f45980d8165820a103220f0027b96b7.tgz#5baa6c758b970a1e6175d06373d4ab33eb6a4164"
integrity sha512-VuGJMzXStqeeHmB3DDEShGHXGey215573kkbFxQinzrFYt1UNnQZ5d1ZmB7Y0oY3JSivpv3Whc5M/YtH41NMXQ== integrity sha512-nzbmF5MLSjpdr8DS7SpC9q291NQ8XkHlledM4lz+uFJ4YgnMmTpi6e0FgF7v2kpTJPqTsXDRY3rWBv6Dat+08A==
dependencies: dependencies:
"@discordjs/collection" "^0.1.6" "@discordjs/collection" "^0.1.6"
"@discordjs/form-data" "^3.0.1" "@discordjs/form-data" "^3.0.1"