From c14319aa2e9b28708aaf46b0b8cd3f0229d01133 Mon Sep 17 00:00:00 2001 From: oriyafe1 Date: Wed, 22 Sep 2021 00:49:52 +0300 Subject: [PATCH 01/17] Adding a 'CommandDoesntExistError' class, and throwing on createCommand --- src/ClientHandler.ts | 16 ++++++++++------ src/commands/CommandFactory.ts | 3 ++- 2 files changed, 12 insertions(+), 7 deletions(-) diff --git a/src/ClientHandler.ts b/src/ClientHandler.ts index 7ceab60..ae24b6e 100644 --- a/src/ClientHandler.ts +++ b/src/ClientHandler.ts @@ -1,5 +1,6 @@ import { Channel, ChannelResolvable, Client, Intents } from 'discord.js'; import { createCommand, isCommandExists } from './commands/CommandFactory'; +import { CommandDoesntExistError } from './errors/CommandDoesntExistError'; let client: Client; @@ -20,14 +21,17 @@ const createClient = () => { if (message.content[0] !== '!') { return; } - const args = message.content.substring(1).split(' '); - const command = args[0]; - if (!isCommandExists(command)) { - return; + try { + createCommand( + message.content.substring(1).split(' '), + message + ).execute(); + } catch (error) { + if (!(error instanceof CommandDoesntExistError)) { + throw error; + } } - - createCommand(args, message).execute(); }); }; diff --git a/src/commands/CommandFactory.ts b/src/commands/CommandFactory.ts index 2279267..9374556 100644 --- a/src/commands/CommandFactory.ts +++ b/src/commands/CommandFactory.ts @@ -1,4 +1,5 @@ import { Message } from 'discord.js'; +import { CommandDoesntExistError } from '../errors/CommandDoesntExistError'; import { Command } from './Command'; import { GayCommand } from './GayCommand'; import { LeaveCommand } from './LeaveCommand'; @@ -30,7 +31,7 @@ export const isCommandExists = (commandName: string) => export const createCommand = (args: string[], message: Message) => { const commandName = args[0]; if (!isCommandExists(commandName)) { - throw new Error('Tried to create non existing command'); + throw new CommandDoesntExistError(); } return new commands[commandName](args, message); From bd37b93c70bbddaa53dd67deff14f2b5b2f10241 Mon Sep 17 00:00:00 2001 From: oriyafe1 Date: Wed, 22 Sep 2021 01:04:52 +0300 Subject: [PATCH 02/17] Adding error file --- src/errors/CommandDoesntExistError.ts | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 src/errors/CommandDoesntExistError.ts diff --git a/src/errors/CommandDoesntExistError.ts b/src/errors/CommandDoesntExistError.ts new file mode 100644 index 0000000..7fdf379 --- /dev/null +++ b/src/errors/CommandDoesntExistError.ts @@ -0,0 +1,5 @@ +export class CommandDoesntExistError extends Error { + constructor(message: string = 'Tried to create non existing command') { + super(message); + } +} From 3b6e12d091b03882522317c5873633ea1335f94b Mon Sep 17 00:00:00 2001 From: oriyafe1 Date: Wed, 22 Sep 2021 01:23:07 +0300 Subject: [PATCH 03/17] Changed splice to shift --- src/QueueManager.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/QueueManager.ts b/src/QueueManager.ts index 8351275..6f781a1 100644 --- a/src/QueueManager.ts +++ b/src/QueueManager.ts @@ -41,7 +41,7 @@ export const playNext = (guildId: string) => { throw new Error('Tried to play next with an empty queue!'); } - const lastTrack = guildQueue.tracks.splice(0, 1)[0]; + const lastTrack = guildQueue.tracks.shift() as Track; if (guildQueue.tracks.length === 0) { guildQueue.isPlaying = false; stop(guildId); From 656d7d6220e496f7e244f359a904cf9bfa6f878c Mon Sep 17 00:00:00 2001 From: oriyafe1 Date: Wed, 22 Sep 2021 11:23:21 +0300 Subject: [PATCH 04/17] Moving 'sendMessage' to clientHandler from queueManager --- src/ClientHandler.ts | 11 +++++++++++ src/QueueManager.ts | 13 +------------ 2 files changed, 12 insertions(+), 12 deletions(-) diff --git a/src/ClientHandler.ts b/src/ClientHandler.ts index ae24b6e..861c838 100644 --- a/src/ClientHandler.ts +++ b/src/ClientHandler.ts @@ -45,3 +45,14 @@ export const getClient = () => { export const getChannel = (channelId: string): Channel | null => client.channels.resolve(channelId); + +export const sendMessage = (channelId: string, message: string) => { + const channel = getChannel(channelId); + if (!channel || !channel.isText()) { + console.warn( + `Tried to send message to non existing or voice channel! \nMessage: ${message} \nChannelId: ${channelId}` + ); + } + + (channel as TextBasedChannels).send(message); +}; diff --git a/src/QueueManager.ts b/src/QueueManager.ts index 6f781a1..c38f22c 100644 --- a/src/QueueManager.ts +++ b/src/QueueManager.ts @@ -2,7 +2,7 @@ import { TextBasedChannel, TextBasedChannels } from 'discord.js'; import { Queue } from './types/Queue'; import { Track } from './types/Track'; import { pause, play, stop } from './AudioHandler'; -import { getChannel } from './ClientHandler'; +import { getChannel, sendMessage } from './ClientHandler'; let queues: { [guildId: string]: Queue } = {}; @@ -69,17 +69,6 @@ const playTrack = (guildID: string, track: Track) => { play(guildID, track.path); }; -const sendMessage = (channelId: string, message: string) => { - const channel = getChannel(channelId); - if (!channel || !channel.isText()) { - console.warn( - `Tried to send message to non existing or voice channel! \nMessage: ${message} \nChannelId: ${channelId}` - ); - } - - (channel as TextBasedChannels).send(message); -}; - export const queueToString = (queue: Queue) => { if (queue.tracks.length === 0) { return 'Queue is empty'; From f15ed59a11b5d0e52e8b9b1bd6b8eeb96ae9e2b8 Mon Sep 17 00:00:00 2001 From: oriyafe1 Date: Wed, 22 Sep 2021 11:23:49 +0300 Subject: [PATCH 05/17] Adding 'searchMultipleVideos' function --- src/YoutubeHandler.ts | 21 +++++++++++++++++++-- 1 file changed, 19 insertions(+), 2 deletions(-) diff --git a/src/YoutubeHandler.ts b/src/YoutubeHandler.ts index 8746aa4..1c8b5ec 100644 --- a/src/YoutubeHandler.ts +++ b/src/YoutubeHandler.ts @@ -43,13 +43,16 @@ export const getAudioFromVideoId = async (videoId: string) => { return fileName; }; -export const searchVideo = async (searchQuery: string) => { - const res = await axios.get( +const getYoutubeSearchResults = async (searchQuery: string) => + await axios.get( `https://www.googleapis.com/youtube/v3/search?key=${ process.env.YT_API_KEY }&q=${encodeURIComponent(searchQuery)}&part=snippet&type=video` ); +export const searchVideo = async (searchQuery: string) => { + const res = await getYoutubeSearchResults(searchQuery); + if (res.status !== 200) { console.error(`Search request failed: ${res.statusText} ${res.data}`); @@ -67,5 +70,19 @@ ${res.data}`); }; }; +export const searchMultipleVideos = async (searchQuery: string) => { + const res = await getYoutubeSearchResults(searchQuery); + + return res.data.items.map( + (currentVideo: { + id: { videoId: string }; + snippet: { title: string }; + }) => ({ + id: currentVideo.id.videoId, + title: currentVideo.snippet.title, + }) + ); +}; + export const decodeHtmlEntity = (str: string) => str.replace(/&#(\d+);/g, (_, dec) => String.fromCharCode(dec)); From f246d056af9fb2ca310980bd713561aca8b0e651 Mon Sep 17 00:00:00 2001 From: oriyafe1 Date: Wed, 22 Sep 2021 11:26:55 +0300 Subject: [PATCH 06/17] Fix import error --- src/ClientHandler.ts | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/src/ClientHandler.ts b/src/ClientHandler.ts index 861c838..7a2cef8 100644 --- a/src/ClientHandler.ts +++ b/src/ClientHandler.ts @@ -1,4 +1,10 @@ -import { Channel, ChannelResolvable, Client, Intents } from 'discord.js'; +import { + Channel, + ChannelResolvable, + Client, + Intents, + TextBasedChannels, +} from 'discord.js'; import { createCommand, isCommandExists } from './commands/CommandFactory'; import { CommandDoesntExistError } from './errors/CommandDoesntExistError'; From 0208a83ddf5fcb52b1009d1d986d1b90af0de394 Mon Sep 17 00:00:00 2001 From: oriyafe1 Date: Wed, 22 Sep 2021 12:26:27 +0300 Subject: [PATCH 07/17] Adding type to return value of 'searchMultipleVideos' --- src/YoutubeHandler.ts | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/src/YoutubeHandler.ts b/src/YoutubeHandler.ts index 1c8b5ec..6bf72b6 100644 --- a/src/YoutubeHandler.ts +++ b/src/YoutubeHandler.ts @@ -70,16 +70,28 @@ ${res.data}`); }; }; -export const searchMultipleVideos = async (searchQuery: string) => { +export const searchMultipleVideos = async ( + searchQuery: string +): Promise< + { + id: string; + title: string; + thumbnail: string; + }[] +> => { const res = await getYoutubeSearchResults(searchQuery); return res.data.items.map( (currentVideo: { id: { videoId: string }; - snippet: { title: string }; + snippet: { + title: string; + thumbnails: { default: { url: string } }; + }; }) => ({ id: currentVideo.id.videoId, title: currentVideo.snippet.title, + thumbnail: currentVideo.snippet.thumbnails.default.url, }) ); }; From 34eb3d0b6171224f666a46a0bacd7788da61a71e Mon Sep 17 00:00:00 2001 From: oriyafe1 Date: Wed, 22 Sep 2021 12:27:02 +0300 Subject: [PATCH 08/17] =?UTF-8?q?Adding=20searchCommand=20=EF=BF=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/commands/CommandFactory.ts | 2 ++ src/commands/SearchCommand.ts | 38 ++++++++++++++++++++++++++++++++++ 2 files changed, 40 insertions(+) create mode 100644 src/commands/SearchCommand.ts diff --git a/src/commands/CommandFactory.ts b/src/commands/CommandFactory.ts index 9374556..e3b4aa6 100644 --- a/src/commands/CommandFactory.ts +++ b/src/commands/CommandFactory.ts @@ -8,6 +8,7 @@ import { PlayCommand } from './PlayCommand'; import { QueueCommand } from './QueueCommand'; import { RemoveCommand } from './RemoveCommand'; import { ResumeCommand } from './ResumeCommand'; +import { SearchCommand } from './SearchCommand'; import { SkipCommand } from './SkipCommand'; import { SummonCommand } from './SummonCommand'; @@ -23,6 +24,7 @@ const commands: { skip: SkipCommand, remove: RemoveCommand, queue: QueueCommand, + search: SearchCommand, }; export const isCommandExists = (commandName: string) => diff --git a/src/commands/SearchCommand.ts b/src/commands/SearchCommand.ts new file mode 100644 index 0000000..5a2e4e4 --- /dev/null +++ b/src/commands/SearchCommand.ts @@ -0,0 +1,38 @@ +import { + MessageActionRow, + MessageButton, + MessageEmbed, + TextBasedChannels, +} from 'discord.js'; +import { getChannel, sendMessage } from '../ClientHandler'; +import { searchMultipleVideos } from '../YoutubeHandler'; +import { Command } from './Command'; + +export class SearchCommand extends Command { + async execute() { + const searchQuery = this.args.slice(1).join(' '); + + const searchResults = await searchMultipleVideos(searchQuery); + + searchResults.forEach(currSearchResult => { + const VideoEmbed = new MessageEmbed() + .setTitle(currSearchResult.title) + .setURL( + `https://www.youtube.com/watch?v=${currSearchResult.id}` + ) + .setThumbnail(currSearchResult.thumbnail); + + const PlayVideoButton = new MessageActionRow().addComponents( + new MessageButton() + .setCustomId(`play ${currSearchResult.id}`) + .setLabel('Play') + .setStyle('SUCCESS') + ); + + sendMessage(this.message.channelId, { + embeds: [VideoEmbed], + components: [PlayVideoButton], + }); + }); + } +} From 44040a5d947fe64eb3afe0d6751171974910d4e3 Mon Sep 17 00:00:00 2001 From: oriyafe1 Date: Wed, 22 Sep 2021 12:37:52 +0300 Subject: [PATCH 09/17] Adding ButtonInteractions factory --- src/button-interactions/ButtonInteraction.ts | 13 ++++++++++++ .../ButtonInteractionFactory.ts | 21 +++++++++++++++++++ src/errors/InteractionDoesntExistError.ts | 5 +++++ 3 files changed, 39 insertions(+) create mode 100644 src/button-interactions/ButtonInteraction.ts create mode 100644 src/button-interactions/ButtonInteractionFactory.ts create mode 100644 src/errors/InteractionDoesntExistError.ts diff --git a/src/button-interactions/ButtonInteraction.ts b/src/button-interactions/ButtonInteraction.ts new file mode 100644 index 0000000..db96b19 --- /dev/null +++ b/src/button-interactions/ButtonInteraction.ts @@ -0,0 +1,13 @@ +import { Interaction } from 'discord.js'; + +export abstract class ButtonInteraction { + args: string[]; + interaction: Interaction; + + constructor(args: string[], interaction: Interaction) { + this.args = args; + this.interaction = interaction; + } + + abstract execute(): void; +} diff --git a/src/button-interactions/ButtonInteractionFactory.ts b/src/button-interactions/ButtonInteractionFactory.ts new file mode 100644 index 0000000..1b1db6e --- /dev/null +++ b/src/button-interactions/ButtonInteractionFactory.ts @@ -0,0 +1,21 @@ +import { ButtonInteraction, Interaction } from 'discord.js'; +import { InteractionDoesntExistError } from '../errors/InteractionDoesntExistError'; + +const buttonInterctions: { + [interactionName: string]: new ( + args: string[], + interaction: Interaction + ) => ButtonInteraction; +} = {}; + +const isInteractionExists = (interactionName: string) => + Object.keys(buttonInterctions).includes(interactionName); + +export const createInteraction = (args: string[], interaction: Interaction) => { + const interactionName = args[0]; + if (!isInteractionExists(interactionName)) { + throw new InteractionDoesntExistError(); + } + + return new buttonInterctions[interactionName](args, interaction); +}; diff --git a/src/errors/InteractionDoesntExistError.ts b/src/errors/InteractionDoesntExistError.ts new file mode 100644 index 0000000..41266d2 --- /dev/null +++ b/src/errors/InteractionDoesntExistError.ts @@ -0,0 +1,5 @@ +export class InteractionDoesntExistError extends Error { + constructor(message: string = 'Tried to create non existing interaction') { + super(message); + } +} From fd19c612ea0597e2206b74a35e3cd69e3d9542c3 Mon Sep 17 00:00:00 2001 From: oriyafe1 Date: Wed, 22 Sep 2021 12:50:56 +0300 Subject: [PATCH 10/17] Adding run dev script with hot-reload --- package.json | 39 ++++++++++++++++++++------------------- 1 file changed, 20 insertions(+), 19 deletions(-) diff --git a/package.json b/package.json index cb1e4ba..7f37400 100644 --- a/package.json +++ b/package.json @@ -1,21 +1,22 @@ { - "name": "stavbot", - "version": "1.0.0", - "description": "", - "main": "index.js", - "scripts": { - "start": "ts-node index.ts" - }, - "author": "Stav", - "license": "ISC", - "dependencies": { - "@discordjs/opus": "^0.5.3", - "@discordjs/voice": "^0.6.0", - "axios": "^0.21.4", - "discord.js": "^13.1.0", - "dotenv": "^10.0.0", - "ffmpeg-static": "^4.4.0", - "tweetnacl": "^1.0.3", - "ytdl-core": "^4.9.1" - } + "name": "stavbot", + "version": "1.0.0", + "description": "", + "main": "index.js", + "scripts": { + "start": "ts-node index.ts", + "dev": "ts-node-dev index.ts" + }, + "author": "Stav", + "license": "ISC", + "dependencies": { + "@discordjs/opus": "^0.5.3", + "@discordjs/voice": "^0.6.0", + "axios": "^0.21.4", + "discord.js": "^13.1.0", + "dotenv": "^10.0.0", + "ffmpeg-static": "^4.4.0", + "tweetnacl": "^1.0.3", + "ytdl-core": "^4.9.1" + } } From 1bd21fb0d5acc9d0c5053930975d7e05af27d9aa Mon Sep 17 00:00:00 2001 From: oriyafe1 Date: Wed, 22 Sep 2021 12:51:39 +0300 Subject: [PATCH 11/17] Renaming buttonInteractions folder --- .../ButtonInteraction.ts | 0 .../ButtonInteractionFactory.ts | 8 ++++++-- 2 files changed, 6 insertions(+), 2 deletions(-) rename src/{button-interactions => buttonInteractions}/ButtonInteraction.ts (100%) rename src/{button-interactions => buttonInteractions}/ButtonInteractionFactory.ts (77%) diff --git a/src/button-interactions/ButtonInteraction.ts b/src/buttonInteractions/ButtonInteraction.ts similarity index 100% rename from src/button-interactions/ButtonInteraction.ts rename to src/buttonInteractions/ButtonInteraction.ts diff --git a/src/button-interactions/ButtonInteractionFactory.ts b/src/buttonInteractions/ButtonInteractionFactory.ts similarity index 77% rename from src/button-interactions/ButtonInteractionFactory.ts rename to src/buttonInteractions/ButtonInteractionFactory.ts index 1b1db6e..f718949 100644 --- a/src/button-interactions/ButtonInteractionFactory.ts +++ b/src/buttonInteractions/ButtonInteractionFactory.ts @@ -1,12 +1,16 @@ -import { ButtonInteraction, Interaction } from 'discord.js'; +import { Interaction } from 'discord.js'; import { InteractionDoesntExistError } from '../errors/InteractionDoesntExistError'; +import { ButtonInteraction } from './ButtonInteraction'; +import { PlayInteraction } from './PlayInteraction'; const buttonInterctions: { [interactionName: string]: new ( args: string[], interaction: Interaction ) => ButtonInteraction; -} = {}; +} = { + play: PlayInteraction, +}; const isInteractionExists = (interactionName: string) => Object.keys(buttonInterctions).includes(interactionName); From daf5c72c0182db7143f4fbb8c0fb053cb6653096 Mon Sep 17 00:00:00 2001 From: oriyafe1 Date: Wed, 22 Sep 2021 12:51:55 +0300 Subject: [PATCH 12/17] Adding call to buttonInteraction Factory --- src/ClientHandler.ts | 26 +++++++++++++++++++++++++- 1 file changed, 25 insertions(+), 1 deletion(-) diff --git a/src/ClientHandler.ts b/src/ClientHandler.ts index 7a2cef8..add27c6 100644 --- a/src/ClientHandler.ts +++ b/src/ClientHandler.ts @@ -3,10 +3,14 @@ import { ChannelResolvable, Client, Intents, + MessageOptions, + MessagePayload, TextBasedChannels, } from 'discord.js'; +import { createInteraction } from './buttonInteractions/ButtonInteractionFactory'; import { createCommand, isCommandExists } from './commands/CommandFactory'; import { CommandDoesntExistError } from './errors/CommandDoesntExistError'; +import { InteractionDoesntExistError } from './errors/InteractionDoesntExistError'; let client: Client; @@ -39,6 +43,23 @@ const createClient = () => { } } }); + + client.on('interactionCreate', interaction => { + if (interaction.isButton()) { + try { + createInteraction( + interaction.customId.split(' '), + interaction + ).execute(); + } catch (error) { + if (!(error instanceof InteractionDoesntExistError)) { + throw error; + } + + console.warn(error.message); + } + } + }); }; export const getClient = () => { @@ -52,7 +73,10 @@ export const getClient = () => { export const getChannel = (channelId: string): Channel | null => client.channels.resolve(channelId); -export const sendMessage = (channelId: string, message: string) => { +export const sendMessage = ( + channelId: string, + message: string | MessagePayload | MessageOptions +) => { const channel = getChannel(channelId); if (!channel || !channel.isText()) { console.warn( From a8a2a477709fb2073c75ae54f5fb71e0c55893ea Mon Sep 17 00:00:00 2001 From: oriyafe1 Date: Wed, 22 Sep 2021 13:57:52 +0300 Subject: [PATCH 13/17] Fixing PlayButtonInteraction --- src/ClientHandler.ts | 5 +- src/buttonInteractions/ButtonInteraction.ts | 13 ++-- .../ButtonInteractionFactory.ts | 21 +++--- src/buttonInteractions/PlayInteraction.ts | 66 +++++++++++++++++++ src/commands/SearchCommand.ts | 10 ++- 5 files changed, 94 insertions(+), 21 deletions(-) create mode 100644 src/buttonInteractions/PlayInteraction.ts diff --git a/src/ClientHandler.ts b/src/ClientHandler.ts index add27c6..5ee5bab 100644 --- a/src/ClientHandler.ts +++ b/src/ClientHandler.ts @@ -47,10 +47,7 @@ const createClient = () => { client.on('interactionCreate', interaction => { if (interaction.isButton()) { try { - createInteraction( - interaction.customId.split(' '), - interaction - ).execute(); + createInteraction(interaction).execute(); } catch (error) { if (!(error instanceof InteractionDoesntExistError)) { throw error; diff --git a/src/buttonInteractions/ButtonInteraction.ts b/src/buttonInteractions/ButtonInteraction.ts index db96b19..c7b2bc2 100644 --- a/src/buttonInteractions/ButtonInteraction.ts +++ b/src/buttonInteractions/ButtonInteraction.ts @@ -1,11 +1,14 @@ -import { Interaction } from 'discord.js'; +import { + Interaction, + ButtonInteraction as discordJSButtonInteraction, +} from 'discord.js'; + +export const getNameFromCustomId = (customId: string) => customId.split(' ')[0]; export abstract class ButtonInteraction { - args: string[]; - interaction: Interaction; + interaction: discordJSButtonInteraction; - constructor(args: string[], interaction: Interaction) { - this.args = args; + constructor(interaction: discordJSButtonInteraction) { this.interaction = interaction; } diff --git a/src/buttonInteractions/ButtonInteractionFactory.ts b/src/buttonInteractions/ButtonInteractionFactory.ts index f718949..fa2e1ff 100644 --- a/src/buttonInteractions/ButtonInteractionFactory.ts +++ b/src/buttonInteractions/ButtonInteractionFactory.ts @@ -1,25 +1,28 @@ -import { Interaction } from 'discord.js'; +import { + Interaction, + ButtonInteraction as discordJSButtonInteraction, +} from 'discord.js'; import { InteractionDoesntExistError } from '../errors/InteractionDoesntExistError'; -import { ButtonInteraction } from './ButtonInteraction'; -import { PlayInteraction } from './PlayInteraction'; +import { ButtonInteraction, getNameFromCustomId } from './ButtonInteraction'; +import { PlayInteraction, PlayInteractionName } from './PlayInteraction'; const buttonInterctions: { [interactionName: string]: new ( - args: string[], - interaction: Interaction + interaction: discordJSButtonInteraction ) => ButtonInteraction; } = { - play: PlayInteraction, + [PlayInteractionName]: PlayInteraction, }; const isInteractionExists = (interactionName: string) => Object.keys(buttonInterctions).includes(interactionName); -export const createInteraction = (args: string[], interaction: Interaction) => { - const interactionName = args[0]; +export const createInteraction = (interaction: discordJSButtonInteraction) => { + const interactionName = getNameFromCustomId(interaction.customId); + if (!isInteractionExists(interactionName)) { throw new InteractionDoesntExistError(); } - return new buttonInterctions[interactionName](args, interaction); + return new buttonInterctions[interactionName](interaction); }; diff --git a/src/buttonInteractions/PlayInteraction.ts b/src/buttonInteractions/PlayInteraction.ts new file mode 100644 index 0000000..a3d2a31 --- /dev/null +++ b/src/buttonInteractions/PlayInteraction.ts @@ -0,0 +1,66 @@ +import { Message } from 'discord.js'; +import { establishPlayer } from '../AudioHandler'; +import { sendMessage } from '../ClientHandler'; +import { getGuildId } from '../GuildHandler'; +import { addToQueue } from '../QueueManager'; +import { Track } from '../types/Track'; +import { establishVoiceChannel } from '../VoiceChannelHandler'; +import { + decodeHtmlEntity, + getAudioFromVideoId, + searchVideo, +} from '../YoutubeHandler'; +import { ButtonInteraction } from './ButtonInteraction'; + +export const PlayInteractionName = 'PLAY'; + +export const constructPlayCutomId = (videoId: string) => + `${PlayInteractionName} ${videoId}`; + +export const deconstructPlayCustomId = (customId: string) => { + const args = customId.split(' '); + + return { + name: args[0], + videoId: args[1], + }; +}; + +export class PlayInteraction extends ButtonInteraction { + async execute() { + const args = deconstructPlayCustomId(this.interaction.customId); + + if (!establishVoiceChannel(this.interaction as unknown as Message)) { + return; + } + + const guildId = this.interaction.guild?.id; + if (!guildId || !establishPlayer(guildId)) { + return; + } + + const audioURL = await getAudioFromVideoId(args.videoId); + + if (!audioURL) { + sendMessage( + this.interaction.channelId as string, + 'Something went wrong with audio decoding, sorry' + ); + return; + } + + const videoResult = await searchVideo(args.videoId); + + if (!videoResult || videoResult === 'no results') { + return; + } + + const track: Track = { + title: decodeHtmlEntity(videoResult.title), + path: audioURL, + requestChannelId: this.interaction.channelId as string, + }; + + addToQueue(guildId, track); + } +} diff --git a/src/commands/SearchCommand.ts b/src/commands/SearchCommand.ts index 5a2e4e4..d3c13ac 100644 --- a/src/commands/SearchCommand.ts +++ b/src/commands/SearchCommand.ts @@ -4,8 +4,12 @@ import { MessageEmbed, TextBasedChannels, } from 'discord.js'; +import { + constructPlayCutomId, + PlayInteractionName, +} from '../buttonInteractions/PlayInteraction'; import { getChannel, sendMessage } from '../ClientHandler'; -import { searchMultipleVideos } from '../YoutubeHandler'; +import { decodeHtmlEntity, searchMultipleVideos } from '../YoutubeHandler'; import { Command } from './Command'; export class SearchCommand extends Command { @@ -16,7 +20,7 @@ export class SearchCommand extends Command { searchResults.forEach(currSearchResult => { const VideoEmbed = new MessageEmbed() - .setTitle(currSearchResult.title) + .setTitle(decodeHtmlEntity(currSearchResult.title)) .setURL( `https://www.youtube.com/watch?v=${currSearchResult.id}` ) @@ -24,7 +28,7 @@ export class SearchCommand extends Command { const PlayVideoButton = new MessageActionRow().addComponents( new MessageButton() - .setCustomId(`play ${currSearchResult.id}`) + .setCustomId(constructPlayCutomId(currSearchResult.id)) .setLabel('Play') .setStyle('SUCCESS') ); From 986ac55be03e764a3f3924a3acac139979a18dba Mon Sep 17 00:00:00 2001 From: oriyafe1 Date: Wed, 22 Sep 2021 16:32:43 +0300 Subject: [PATCH 14/17] Now removing messages upon new search, and one minute after they are being sent --- src/ClientHandler.ts | 6 ++-- src/buttonInteractions/PlayInteraction.ts | 5 +++- src/commands/SearchCommand.ts | 35 +++++++++++++++++++++-- 3 files changed, 40 insertions(+), 6 deletions(-) diff --git a/src/ClientHandler.ts b/src/ClientHandler.ts index 5ee5bab..a9b3fa0 100644 --- a/src/ClientHandler.ts +++ b/src/ClientHandler.ts @@ -44,10 +44,10 @@ const createClient = () => { } }); - client.on('interactionCreate', interaction => { + client.on('interactionCreate', async interaction => { if (interaction.isButton()) { try { - createInteraction(interaction).execute(); + await createInteraction(interaction).execute(); } catch (error) { if (!(error instanceof InteractionDoesntExistError)) { throw error; @@ -81,5 +81,5 @@ export const sendMessage = ( ); } - (channel as TextBasedChannels).send(message); + return (channel as TextBasedChannels).send(message); }; diff --git a/src/buttonInteractions/PlayInteraction.ts b/src/buttonInteractions/PlayInteraction.ts index a3d2a31..4dd2e44 100644 --- a/src/buttonInteractions/PlayInteraction.ts +++ b/src/buttonInteractions/PlayInteraction.ts @@ -1,6 +1,7 @@ -import { Message } from 'discord.js'; +import { Message, TextBasedChannels } from 'discord.js'; import { establishPlayer } from '../AudioHandler'; import { sendMessage } from '../ClientHandler'; +import { deleteAllSearchMessages } from '../commands/SearchCommand'; import { getGuildId } from '../GuildHandler'; import { addToQueue } from '../QueueManager'; import { Track } from '../types/Track'; @@ -62,5 +63,7 @@ export class PlayInteraction extends ButtonInteraction { }; addToQueue(guildId, track); + + deleteAllSearchMessages(this.interaction.channel as TextBasedChannels); } } diff --git a/src/commands/SearchCommand.ts b/src/commands/SearchCommand.ts index d3c13ac..03aeedd 100644 --- a/src/commands/SearchCommand.ts +++ b/src/commands/SearchCommand.ts @@ -1,4 +1,5 @@ import { + Message, MessageActionRow, MessageButton, MessageEmbed, @@ -12,13 +13,41 @@ import { getChannel, sendMessage } from '../ClientHandler'; import { decodeHtmlEntity, searchMultipleVideos } from '../YoutubeHandler'; import { Command } from './Command'; +const filterPlayButtonMessages = (messages: Message[]) => + messages.filter(currMessage => + currMessage.components.some(currComponent => + currComponent.components.some(currSubComponent => + currSubComponent.customId?.startsWith(PlayInteractionName) + ) + ) + ); + +export const deleteAllSearchMessages = async (channel: TextBasedChannels) => { + filterPlayButtonMessages([ + ...(await channel.messages.fetch({ limit: 100 })).values(), + ]).forEach(currMessage => { + if (!currMessage.deleted) { + currMessage.delete(); + } + }); +}; + +const MESSAGE_DELETE_TIMOUT = 10000; + +const deleteMessagesAfterTimeout = (channel: TextBasedChannels) => + setTimeout(async () => { + await deleteAllSearchMessages(channel); + }, MESSAGE_DELETE_TIMOUT); + export class SearchCommand extends Command { async execute() { + await deleteAllSearchMessages(this.message.channel); + const searchQuery = this.args.slice(1).join(' '); const searchResults = await searchMultipleVideos(searchQuery); - searchResults.forEach(currSearchResult => { + searchResults.forEach(async currSearchResult => { const VideoEmbed = new MessageEmbed() .setTitle(decodeHtmlEntity(currSearchResult.title)) .setURL( @@ -33,10 +62,12 @@ export class SearchCommand extends Command { .setStyle('SUCCESS') ); - sendMessage(this.message.channelId, { + await sendMessage(this.message.channelId, { embeds: [VideoEmbed], components: [PlayVideoButton], }); }); + + deleteMessagesAfterTimeout(this.message.channel); } } From fe5f33c93acc45f05ad123ae76344b231180322f Mon Sep 17 00:00:00 2001 From: oriyafe1 Date: Wed, 22 Sep 2021 16:33:09 +0300 Subject: [PATCH 15/17] Update timeout time --- src/commands/SearchCommand.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/commands/SearchCommand.ts b/src/commands/SearchCommand.ts index 03aeedd..7672417 100644 --- a/src/commands/SearchCommand.ts +++ b/src/commands/SearchCommand.ts @@ -32,7 +32,7 @@ export const deleteAllSearchMessages = async (channel: TextBasedChannels) => { }); }; -const MESSAGE_DELETE_TIMOUT = 10000; +const MESSAGE_DELETE_TIMOUT = 60000; const deleteMessagesAfterTimeout = (channel: TextBasedChannels) => setTimeout(async () => { From 9dc172215075118704c960ceaba88df79b9b0047 Mon Sep 17 00:00:00 2001 From: stavby Date: Wed, 22 Sep 2021 17:39:31 +0300 Subject: [PATCH 16/17] refactor search stuff --- src/YoutubeHandler.ts | 28 ++++++------------- src/buttonInteractions/ButtonInteraction.ts | 5 +--- .../ButtonInteractionFactory.ts | 5 +--- src/buttonInteractions/PlayInteraction.ts | 2 -- src/commands/SearchCommand.ts | 22 +++++++++------ src/types/ParsedYoutubeVideo.ts | 5 ++++ src/types/RawYoutubeVideo.ts | 7 +++++ 7 files changed, 35 insertions(+), 39 deletions(-) create mode 100644 src/types/ParsedYoutubeVideo.ts create mode 100644 src/types/RawYoutubeVideo.ts diff --git a/src/YoutubeHandler.ts b/src/YoutubeHandler.ts index 6bf72b6..8bfa261 100644 --- a/src/YoutubeHandler.ts +++ b/src/YoutubeHandler.ts @@ -1,6 +1,8 @@ import ytdl from 'ytdl-core'; import fs from 'fs'; import axios from 'axios'; +import { RawYoutubeVideo } from './types/RawYoutubeVideo'; +import { ParsedYoutubeVideo } from './types/ParsedYoutubeVideo'; export const currentlyDownloading: string[] = []; @@ -72,28 +74,14 @@ ${res.data}`); export const searchMultipleVideos = async ( searchQuery: string -): Promise< - { - id: string; - title: string; - thumbnail: string; - }[] -> => { +): Promise => { const res = await getYoutubeSearchResults(searchQuery); - return res.data.items.map( - (currentVideo: { - id: { videoId: string }; - snippet: { - title: string; - thumbnails: { default: { url: string } }; - }; - }) => ({ - id: currentVideo.id.videoId, - title: currentVideo.snippet.title, - thumbnail: currentVideo.snippet.thumbnails.default.url, - }) - ); + return res.data.items.map((currentVideo: RawYoutubeVideo) => ({ + id: currentVideo.id.videoId, + title: currentVideo.snippet.title, + thumbnailURL: currentVideo.snippet.thumbnails.default.url, + })); }; export const decodeHtmlEntity = (str: string) => diff --git a/src/buttonInteractions/ButtonInteraction.ts b/src/buttonInteractions/ButtonInteraction.ts index c7b2bc2..b1ca291 100644 --- a/src/buttonInteractions/ButtonInteraction.ts +++ b/src/buttonInteractions/ButtonInteraction.ts @@ -1,7 +1,4 @@ -import { - Interaction, - ButtonInteraction as discordJSButtonInteraction, -} from 'discord.js'; +import { ButtonInteraction as discordJSButtonInteraction } from 'discord.js'; export const getNameFromCustomId = (customId: string) => customId.split(' ')[0]; diff --git a/src/buttonInteractions/ButtonInteractionFactory.ts b/src/buttonInteractions/ButtonInteractionFactory.ts index fa2e1ff..23203cc 100644 --- a/src/buttonInteractions/ButtonInteractionFactory.ts +++ b/src/buttonInteractions/ButtonInteractionFactory.ts @@ -1,7 +1,4 @@ -import { - Interaction, - ButtonInteraction as discordJSButtonInteraction, -} from 'discord.js'; +import { ButtonInteraction as discordJSButtonInteraction } from 'discord.js'; import { InteractionDoesntExistError } from '../errors/InteractionDoesntExistError'; import { ButtonInteraction, getNameFromCustomId } from './ButtonInteraction'; import { PlayInteraction, PlayInteractionName } from './PlayInteraction'; diff --git a/src/buttonInteractions/PlayInteraction.ts b/src/buttonInteractions/PlayInteraction.ts index 4dd2e44..1175c94 100644 --- a/src/buttonInteractions/PlayInteraction.ts +++ b/src/buttonInteractions/PlayInteraction.ts @@ -2,7 +2,6 @@ import { Message, TextBasedChannels } from 'discord.js'; import { establishPlayer } from '../AudioHandler'; import { sendMessage } from '../ClientHandler'; import { deleteAllSearchMessages } from '../commands/SearchCommand'; -import { getGuildId } from '../GuildHandler'; import { addToQueue } from '../QueueManager'; import { Track } from '../types/Track'; import { establishVoiceChannel } from '../VoiceChannelHandler'; @@ -41,7 +40,6 @@ export class PlayInteraction extends ButtonInteraction { } const audioURL = await getAudioFromVideoId(args.videoId); - if (!audioURL) { sendMessage( this.interaction.channelId as string, diff --git a/src/commands/SearchCommand.ts b/src/commands/SearchCommand.ts index 7672417..1fba8e2 100644 --- a/src/commands/SearchCommand.ts +++ b/src/commands/SearchCommand.ts @@ -9,10 +9,12 @@ import { constructPlayCutomId, PlayInteractionName, } from '../buttonInteractions/PlayInteraction'; -import { getChannel, sendMessage } from '../ClientHandler'; +import { sendMessage } from '../ClientHandler'; import { decodeHtmlEntity, searchMultipleVideos } from '../YoutubeHandler'; import { Command } from './Command'; +const MESSAGE_DELETE_TIMOUT = 60000; + const filterPlayButtonMessages = (messages: Message[]) => messages.filter(currMessage => currMessage.components.some(currComponent => @@ -32,8 +34,6 @@ export const deleteAllSearchMessages = async (channel: TextBasedChannels) => { }); }; -const MESSAGE_DELETE_TIMOUT = 60000; - const deleteMessagesAfterTimeout = (channel: TextBasedChannels) => setTimeout(async () => { await deleteAllSearchMessages(channel); @@ -43,19 +43,23 @@ export class SearchCommand extends Command { async execute() { await deleteAllSearchMessages(this.message.channel); - const searchQuery = this.args.slice(1).join(' '); + if (this.args.length <= 1) { + this.message.reply('Search what exactly?'); + return; + } + const searchQuery = this.args.slice(1).join(' '); const searchResults = await searchMultipleVideos(searchQuery); searchResults.forEach(async currSearchResult => { - const VideoEmbed = new MessageEmbed() + const videoEmbed = new MessageEmbed() .setTitle(decodeHtmlEntity(currSearchResult.title)) .setURL( `https://www.youtube.com/watch?v=${currSearchResult.id}` ) - .setThumbnail(currSearchResult.thumbnail); + .setThumbnail(currSearchResult.thumbnailURL); - const PlayVideoButton = new MessageActionRow().addComponents( + const playVideoButton = new MessageActionRow().addComponents( new MessageButton() .setCustomId(constructPlayCutomId(currSearchResult.id)) .setLabel('Play') @@ -63,8 +67,8 @@ export class SearchCommand extends Command { ); await sendMessage(this.message.channelId, { - embeds: [VideoEmbed], - components: [PlayVideoButton], + embeds: [videoEmbed], + components: [playVideoButton], }); }); diff --git a/src/types/ParsedYoutubeVideo.ts b/src/types/ParsedYoutubeVideo.ts new file mode 100644 index 0000000..8367058 --- /dev/null +++ b/src/types/ParsedYoutubeVideo.ts @@ -0,0 +1,5 @@ +export type ParsedYoutubeVideo = { + id: string; + title: string; + thumbnailURL: string; +}; diff --git a/src/types/RawYoutubeVideo.ts b/src/types/RawYoutubeVideo.ts new file mode 100644 index 0000000..5c273e8 --- /dev/null +++ b/src/types/RawYoutubeVideo.ts @@ -0,0 +1,7 @@ +export type RawYoutubeVideo = { + id: { videoId: string }; + snippet: { + title: string; + thumbnails: { default: { url: string } }; + }; +}; From f83cfda83e207e1da5dae600307d9545df061857 Mon Sep 17 00:00:00 2001 From: oriyafe1 Date: Wed, 22 Sep 2021 17:47:56 +0300 Subject: [PATCH 17/17] Adding axios try catch --- src/YoutubeHandler.ts | 70 ++++++++++++++++++++++++------------------- 1 file changed, 40 insertions(+), 30 deletions(-) diff --git a/src/YoutubeHandler.ts b/src/YoutubeHandler.ts index 6bf72b6..a8da769 100644 --- a/src/YoutubeHandler.ts +++ b/src/YoutubeHandler.ts @@ -1,6 +1,6 @@ import ytdl from 'ytdl-core'; import fs from 'fs'; -import axios from 'axios'; +import axios, { AxiosError } from 'axios'; export const currentlyDownloading: string[] = []; @@ -51,23 +51,25 @@ const getYoutubeSearchResults = async (searchQuery: string) => ); export const searchVideo = async (searchQuery: string) => { - const res = await getYoutubeSearchResults(searchQuery); + try { + const res = await getYoutubeSearchResults(searchQuery); + + const resultItems = res.data.items; + if (resultItems.length === 0) { + return 'no results'; + } + + return { + id: resultItems[0].id.videoId, + title: resultItems[0].snippet.title, + }; + } catch (error) { + if (axios.isAxiosError(error)) { + console.error(error.response); + } - if (res.status !== 200) { - console.error(`Search request failed: ${res.statusText} -${res.data}`); return; } - - const resultItems = res.data.items; - if (resultItems.length === 0) { - return 'no results'; - } - - return { - id: resultItems[0].id.videoId, - title: resultItems[0].snippet.title, - }; }; export const searchMultipleVideos = async ( @@ -79,21 +81,29 @@ export const searchMultipleVideos = async ( thumbnail: string; }[] > => { - const res = await getYoutubeSearchResults(searchQuery); - - return res.data.items.map( - (currentVideo: { - id: { videoId: string }; - snippet: { - title: string; - thumbnails: { default: { url: string } }; - }; - }) => ({ - id: currentVideo.id.videoId, - title: currentVideo.snippet.title, - thumbnail: currentVideo.snippet.thumbnails.default.url, - }) - ); + try { + const res = await getYoutubeSearchResults(searchQuery); + + return res.data.items.map( + (currentVideo: { + id: { videoId: string }; + snippet: { + title: string; + thumbnails: { default: { url: string } }; + }; + }) => ({ + id: currentVideo.id.videoId, + title: currentVideo.snippet.title, + thumbnail: currentVideo.snippet.thumbnails.default.url, + }) + ); + } catch (error: any | AxiosError) { + if (axios.isAxiosError(error)) { + console.error(error.response); + } + + return []; + } }; export const decodeHtmlEntity = (str: string) =>