Skip to content

Commit

Permalink
✨Slash command support
Browse files Browse the repository at this point in the history
  • Loading branch information
MotiCAT committed Dec 3, 2023
1 parent 15446cb commit 5b7be8b
Show file tree
Hide file tree
Showing 13 changed files with 339 additions and 0 deletions.
47 changes: 47 additions & 0 deletions src/Events/onInteractionCreate.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
import { embeds } from '../embeds';
import { interactions } from '../interactions';
import { BaseInteraction, Awaitable } from 'discord.js';

export async function onInteractionCreate(interaction: BaseInteraction): Promise<Awaitable<void>> {
if (!interaction.isChatInputCommand()) return;
if (!interaction.guild) {
interaction.reply({ content: 'This command can only be used in a server!', ephemeral: true });
}
const commandName = interaction.commandName;

switch (commandName) {
case 'play':
interactions.play(interaction);
break;
case 'stop':
interactions.stop(interaction);
break;
case 'pause':
interactions.pause(interaction);
break;
case 'resume':
interactions.resume(interaction);
break;
case 'loop':
interactions.loop(interaction);
break;
case 'skip':
interactions.skip(interaction);
break;
case 'queue':
interactions.queue(interaction);
break;
case 'help':
interactions.help(interaction);
break;
case 'volume':
interactions.changeVolume(interaction);
break;
case 'nowplaying':
interactions.nowplaying(interaction);
break;
default:
interaction.reply(embeds.unknownCommand);
break;
}
}
50 changes: 50 additions & 0 deletions src/deploy-commands.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
import { REST, Routes, SlashCommandBuilder } from 'discord.js';
import { config } from 'dotenv';

config();

const commands = [
new SlashCommandBuilder()
.setName('play')
.setDescription('Play a song')
.addStringOption((option) => option.setName('url').setDescription('The song to play').setRequired(true)),
new SlashCommandBuilder().setName('pause').setDescription('Pause the current song'),
new SlashCommandBuilder().setName('resume').setDescription('Resume the current song'),
new SlashCommandBuilder().setName('skip').setDescription('Skip the current song'),
new SlashCommandBuilder().setName('stop').setDescription('Stop the current song'),
new SlashCommandBuilder().setName('queue').setDescription('View the current queue'),
new SlashCommandBuilder()
.setName('loop')
.setDescription('Loop the current song')
.addStringOption((option) =>
option
.setName('mode')
.setDescription('The loop mode')
.addChoices(
{ name: 'off', value: 'none' },
{ name: 'queue', value: 'queue' },
{ name: 'track', value: 'track' }
)
.setRequired(false)
),
new SlashCommandBuilder()
.setName('volume')
.setDescription('Change the volume')
.addIntegerOption((option) => option.setName('volume').setDescription('The volume to change to').setRequired(true)),
new SlashCommandBuilder().setName('nowplaying').setDescription('View the currently playing song'),
new SlashCommandBuilder().setName('help').setDescription('View the help menu')
];

const rest = new REST().setToken(process.env.TOKEN!);

(async () => {
try {
console.log('Started refreshing application (/) commands.');

await rest.put(Routes.applicationCommands('1138100173759856701'), { body: commands });

console.log('Successfully reloaded application (/) commands.');
} catch (error) {
console.error(error);
}
})();
20 changes: 20 additions & 0 deletions src/interactions/changeVolume.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import { player } from './play';
import { ChatInputCommandInteraction } from 'discord.js';

export async function changeVolumeCommand(interaction: ChatInputCommandInteraction) {
if (typeof player === 'undefined') return interaction.reply({ content: '動画が再生されていません。' });
const number = interaction.options.getNumber('volume');
if (!number) return interaction.reply({ content: `現在の音量は${player.volume}です。` });
if (number > 100) {
player.changeVolume(100 / 10);
return interaction.reply({ content: 'ボリュームを最大に設定しました。' });
}

if (number < 0) {
player.changeVolume(0 / 10);
return interaction.reply({ content: 'ミュートに設定しました。' });
}

player.changeVolume(number / 10);
return interaction.reply({ content: `ボリュームを${player.volume * 10}に変更しました。` });
}
6 changes: 6 additions & 0 deletions src/interactions/help.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
import { embeds } from '../embeds';
import { ChatInputCommandInteraction } from 'discord.js';

export async function helpCommand(interaction: ChatInputCommandInteraction): Promise<void> {
interaction.reply(embeds.help);
}
23 changes: 23 additions & 0 deletions src/interactions/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import { changeVolumeCommand } from './changeVolume';
import { helpCommand } from './help';
import { loopCommand } from './loop';
import { nowplayingCommand } from './nowplaying';
import { pauseCommand } from './pause';
import { playCommand } from './play';
import { queueCommand } from './queue';
import { resumeCommand } from './resume';
import { skipCommand } from './skip';
import { stopCommand } from './stop';

export const interactions = {
changeVolume: changeVolumeCommand,
help: helpCommand,
loop: loopCommand,
nowplaying: nowplayingCommand,
pause: pauseCommand,
play: playCommand,
queue: queueCommand,
resume: resumeCommand,
skip: skipCommand,
stop: stopCommand
};
30 changes: 30 additions & 0 deletions src/interactions/loop.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import { queueManager, Queue } from '../classes/queue';
import { embeds } from '../embeds';
import { player } from './play';
import { ChatInputCommandInteraction } from 'discord.js';

export async function loopCommand(interaction: ChatInputCommandInteraction) {
if (typeof player === 'undefined') return interaction.reply(embeds.videoNotPlaying);
const queue = queueManager.queues.get(interaction.guildId!) as Queue;
const settings = interaction.options.getString('mode') as string;
if (!settings) {
queue.loop === 'none' ? queue.setLoop('queue') : queue.setLoop('none');
} else if (settings) {
switch (settings) {
case 'none':
queue?.setLoop('none');
break;
case 'queue':
queue?.setLoop('queue');
break;
case 'track':
queue?.setLoop('track');
break;
default:
interaction.reply(embeds.commandNotFound);
break;
}
}

interaction.reply(new embeds.embed().addFields({ name: 'Looping', value: queue.loop! }).setColor('Green').build());
}
13 changes: 13 additions & 0 deletions src/interactions/nowplaying.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import { getSongInfo } from '../Utils/songResolver';
import { queueManager, Queue } from '../classes/queue';
import { embeds } from '../embeds';
import { player } from './play';
import { ChatInputCommandInteraction } from 'discord.js';

export async function nowplayingCommand(interaction: ChatInputCommandInteraction) {
if (typeof player === 'undefined') return interaction.reply(embeds.videoNotPlaying);
const queue = queueManager.getQueue(interaction.guild?.id as string) as Queue;
if (!queue.currentSong) return interaction.reply(embeds.queueEmpty);
const info = await getSongInfo(queue.currentSong);
return interaction.reply(info);
}
18 changes: 18 additions & 0 deletions src/interactions/pause.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import { embeds } from '../embeds';
import { player } from './play';
import { AudioPlayerStatus } from '@discordjs/voice';
import { ChatInputCommandInteraction } from 'discord.js';

export async function pauseCommand(interaction: ChatInputCommandInteraction) {
if (typeof player === 'undefined') return interaction.reply(embeds.videoNotPlaying);

if (player.player.state.status === AudioPlayerStatus.Playing) {
player.pause();
interaction.reply(embeds.videoPaused);
} else if (player.player.state.status === AudioPlayerStatus.Paused) {
player.resume();
interaction.reply(embeds.videoResumed);
} else {
interaction.reply(embeds.videoNotPlaying);
}
}
66 changes: 66 additions & 0 deletions src/interactions/play.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
import { YTPlayer } from '../classes/player';
import { Queue, queueManager } from '../classes/queue';
import { embeds } from '../embeds';
import { ChannelType, VoiceBasedChannel, ChatInputCommandInteraction, GuildMember } from 'discord.js';
import ytdl from 'ytdl-core';

export let player: YTPlayer | undefined;

export let url: string;

export async function playCommand(interaction: ChatInputCommandInteraction) {
if (typeof queueManager.getQueue(interaction.guild?.id as string) === 'undefined') {
queueManager.setQueue(interaction.guild?.id as string, new Queue());
}
const queue = queueManager.getQueue(interaction.guild?.id as string) as Queue;
if (!interaction.channel) return;
if (!(interaction.member instanceof GuildMember)) return;
if (typeof player === 'undefined') {
player = new YTPlayer(
interaction.guild?.id as string,
interaction.member?.voice.channel as VoiceBasedChannel,
interaction.channel?.id
);
}
url = interaction.options.getString('url') as string;
const channel = interaction.member?.voice.channel;
if (!url) return interaction.reply(embeds.noUrl);
if (!ytdl.validateURL(url)) return interaction.reply(embeds.invaildUrl);
if (!channel) return interaction.reply(embeds.voiceChannelJoin);
if (channel.type !== ChannelType.GuildVoice) return;
if (!channel.joinable) return interaction.reply(embeds.voiceChannnelJoined);
if (!channel.speakable) return interaction.reply(embeds.voiceChannnelPermission);

if (!queue.length || !player.isPlaying) {
queue.addSong(url);
const info = await ytdl.getInfo(url);
interaction.reply(
new embeds.embed()
.setTitle('Success')
.setDescription(`**[${info.videoDetails.title}](${info.videoDetails.video_url})を再生します。**`)
.addFields({
name: info.videoDetails.title,
value: `投稿者: [${info.videoDetails.author.name}](${info.videoDetails.author.channel_url})`
})
.setImage(info.videoDetails.thumbnails[0].url.split('?')[0])
.setColor('Green')
.build()
);
if (queue.length === 1) return player.play();
} else {
queue.addSong(url);
const info = await ytdl.getInfo(url);
interaction.reply(
new embeds.embed()
.setTitle('Info')
.setDescription(`**[${info.videoDetails.title}](${info.videoDetails.video_url})をキューに追加しました。**`)
.addFields({
name: info.videoDetails.title,
value: `投稿者: [${info.videoDetails.author.name}](${info.videoDetails.author.channel_url})`
})
.setImage(info.videoDetails.thumbnails[0].url.split('?')[0])
.setColor('Yellow')
.build()
);
}
}
27 changes: 27 additions & 0 deletions src/interactions/queue.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import { songResolver } from '../Utils/songResolver';
import { Queue, queueManager } from '../classes/queue';
import { embeds } from '../embeds';
import { player } from './play';
import { ChatInputCommandInteraction } from 'discord.js';
import ytdl from 'ytdl-core';

export async function queueCommand(interaction: ChatInputCommandInteraction) {
if (typeof player === 'undefined') return interaction.reply(embeds.videoNotPlaying);
const queue = queueManager.getQueue(interaction.guildId!) as Queue;
if (!queue.length) return interaction.reply(embeds.queueEmpty);

const embed = new embeds.embed().setTitle('Queue').setColor('Blue').setTimestamp();
for (let i = 0; i < queue.length; i++) {
const url = queue.store[i];
const info = await ytdl.getInfo(url);
const song = songResolver(info, interaction.user.username, interaction.user.displayAvatarURL()!);
embed.addFields({
name: `${i + 1}. ${song.title}`,
value: `[${song.author}](${song.authorUrl})`
});
embed.setFooter({
text: `Queue: ${queue.store.length} songs`
});
}
interaction.reply(embed.build());
}
16 changes: 16 additions & 0 deletions src/interactions/resume.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import { embeds } from '../embeds';
import { player } from './play';
import { AudioPlayerStatus } from '@discordjs/voice';
import { ChatInputCommandInteraction } from 'discord.js';

export async function resumeCommand(interaction: ChatInputCommandInteraction) {
if (typeof player === 'undefined') return interaction.reply(embeds.videoNotPlaying);
if (player.player.state.status === AudioPlayerStatus.Paused) {
player.resume();
interaction.reply(embeds.videoResumed);
} else if (player.player.state.status === AudioPlayerStatus.Playing) {
interaction.reply(embeds.videoNotPaused);
} else {
interaction.reply(embeds.videoNotPlaying);
}
}
11 changes: 11 additions & 0 deletions src/interactions/skip.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import { Queue, queueManager } from '../classes/queue';
import { embeds } from '../embeds';
import { player } from './play';
import { ChatInputCommandInteraction } from 'discord.js';

export async function skipCommand(interaction: ChatInputCommandInteraction) {
if (typeof player === 'undefined') return interaction.reply(embeds.videoNotPlaying);
const queue = queueManager.queues.get(interaction.guildId!) as Queue;
if (!queue.length) return interaction.reply(embeds.queueEmpty);
player.skip();
}
12 changes: 12 additions & 0 deletions src/interactions/stop.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import { queueManager } from '../classes/queue';
import { embeds } from '../embeds';
import { ChatInputCommandInteraction } from 'discord.js';

export async function stopCommand(interaction: ChatInputCommandInteraction): Promise<void> {
if (interaction.guild?.members.me?.voice.channel) {
interaction.guild.members.me.voice.disconnect();
interaction.reply(embeds.videoStopped);
}

queueManager.queues.delete(interaction.guildId!);
}

0 comments on commit 5b7be8b

Please sign in to comment.