Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
39 changes: 20 additions & 19 deletions package.json
Original file line number Diff line number Diff line change
@@ -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"
}
}
54 changes: 48 additions & 6 deletions src/ClientHandler.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,16 @@
import { Channel, ChannelResolvable, Client, Intents } from 'discord.js';
import {
Channel,
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;

Expand All @@ -20,14 +31,31 @@ 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();
client.on('interactionCreate', async interaction => {
if (interaction.isButton()) {
try {
await createInteraction(interaction).execute();
} catch (error) {
if (!(error instanceof InteractionDoesntExistError)) {
throw error;
}

console.warn(error.message);
}
}
});
};

Expand All @@ -41,3 +69,17 @@ export const getClient = () => {

export const getChannel = (channelId: string): Channel | null =>
client.channels.resolve(channelId);

export const sendMessage = (
channelId: string,
message: string | MessagePayload | MessageOptions
) => {
const channel = getChannel(channelId);
if (!channel || !channel.isText()) {
console.warn(
`Tried to send message to non existing or voice channel! \nMessage: ${message} \nChannelId: ${channelId}`
);
}

return (channel as TextBasedChannels).send(message);
};
15 changes: 2 additions & 13 deletions src/QueueManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 } = {};

Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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';
Expand Down
55 changes: 41 additions & 14 deletions src/YoutubeHandler.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import ytdl from 'ytdl-core';
import fs from 'fs';
import axios from 'axios';
import axios, { AxiosError } from 'axios';
import { RawYoutubeVideo } from './types/RawYoutubeVideo';
import { ParsedYoutubeVideo } from './types/ParsedYoutubeVideo';

export const currentlyDownloading: string[] = [];

Expand Down Expand Up @@ -43,28 +45,53 @@ 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`
);

if (res.status !== 200) {
console.error(`Search request failed: ${res.statusText}
${res.data}`);
export const searchVideo = async (searchQuery: string) => {
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);
}

return;
}
};

const resultItems = res.data.items;
if (resultItems.length === 0) {
return 'no results';
}
export const searchMultipleVideos = async (
searchQuery: string
): Promise<ParsedYoutubeVideo[]> => {
try {
const res = await getYoutubeSearchResults(searchQuery);

return {
id: resultItems[0].id.videoId,
title: resultItems[0].snippet.title,
};
return res.data.items.map((currentVideo: RawYoutubeVideo) => ({
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) =>
Expand Down
13 changes: 13 additions & 0 deletions src/buttonInteractions/ButtonInteraction.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import { ButtonInteraction as discordJSButtonInteraction } from 'discord.js';

export const getNameFromCustomId = (customId: string) => customId.split(' ')[0];

export abstract class ButtonInteraction {
interaction: discordJSButtonInteraction;

constructor(interaction: discordJSButtonInteraction) {
this.interaction = interaction;
}

abstract execute(): void;
}
25 changes: 25 additions & 0 deletions src/buttonInteractions/ButtonInteractionFactory.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import { ButtonInteraction as discordJSButtonInteraction } from 'discord.js';
import { InteractionDoesntExistError } from '../errors/InteractionDoesntExistError';
import { ButtonInteraction, getNameFromCustomId } from './ButtonInteraction';
import { PlayInteraction, PlayInteractionName } from './PlayInteraction';

const buttonInterctions: {
[interactionName: string]: new (
interaction: discordJSButtonInteraction
) => ButtonInteraction;
} = {
[PlayInteractionName]: PlayInteraction,
};

const isInteractionExists = (interactionName: string) =>
Object.keys(buttonInterctions).includes(interactionName);

export const createInteraction = (interaction: discordJSButtonInteraction) => {
const interactionName = getNameFromCustomId(interaction.customId);

if (!isInteractionExists(interactionName)) {
throw new InteractionDoesntExistError();
}

return new buttonInterctions[interactionName](interaction);
};
67 changes: 67 additions & 0 deletions src/buttonInteractions/PlayInteraction.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
import { Message, TextBasedChannels } from 'discord.js';
import { establishPlayer } from '../AudioHandler';
import { sendMessage } from '../ClientHandler';
import { deleteAllSearchMessages } from '../commands/SearchCommand';
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);

deleteAllSearchMessages(this.interaction.channel as TextBasedChannels);
}
}
5 changes: 4 additions & 1 deletion src/commands/CommandFactory.ts
Original file line number Diff line number Diff line change
@@ -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';
Expand All @@ -7,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';

Expand All @@ -22,6 +24,7 @@ const commands: {
skip: SkipCommand,
remove: RemoveCommand,
queue: QueueCommand,
search: SearchCommand,
};

export const isCommandExists = (commandName: string) =>
Expand All @@ -30,7 +33,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);
Expand Down
Loading