Merge pull request #1 from YuzuZensai/music-rework

Music
This commit is contained in:
2022-02-05 00:48:23 +07:00
committed by GitHub
25 changed files with 1758 additions and 105 deletions
+98
View File
@@ -0,0 +1,98 @@
Dockerfile
.dockerignore
# See http://help.github.com/ignore-files/ for more about ignoring files.
/build
/configs/*
!/configs/*.example.json
# compiled output
/dist
/tmp
/out-tsc
# Runtime data
pids
*.pid
*.seed
*.pid.lock
# Directory for instrumented libs generated by jscoverage/JSCover
lib-cov
# Coverage directory used by tools like istanbul
coverage
# nyc test coverage
.nyc_output
# Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files)
.grunt
# Bower dependency directory (https://bower.io/)
bower_components
# node-waf configuration
.lock-wscript
# IDEs and editors
.idea
.project
.classpath
.c9/
*.launch
.settings/
*.sublime-workspace
# IDE - VSCode
.vscode/*
#!.vscode/settings.json Not needed for now
!.vscode/tasks.json
!.vscode/launch.json
!.vscode/extensions.json
# misc
.sass-cache
connect.lock
typings
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
# Dependency directories
node_modules/
jspm_packages/
# Optional npm cache directory
.npm
# Optional eslint cache
.eslintcache
# Optional REPL history
.node_repl_history
# Output of 'npm pack'
*.tgz
# Yarn Integrity file
.yarn-integrity
# dotenv environment variables file
.env
# next.js build output
.next
# Lerna
lerna-debug.log
# System Files
.DS_Store
Thumbs.db
+12
View File
@@ -0,0 +1,12 @@
NODE_ENV=development
#development
#production
DATABASE_URL=postgresql://user:password@host:port/dbName?schema=public
DISCORD_TOKEN=SuperSecretDiscordToken
DEVELOPER_IDS=DEV1_ID,DEV2_ID
PRIVATE_BOT=true
OSU_API_KEY=SuperSecretosu!Key
YOUTUBE_COOKIE_BASE64=SuperSecretCookie_EncodeItInBase64_AndThen_RemoveTheLastEqualToo
+2 -1
View File
@@ -1,7 +1,8 @@
# See http://help.github.com/ignore-files/ for more about ignoring files.
/build
/configs
/configs/*
!/configs/*.example.json
# compiled output
/dist
+23
View File
@@ -0,0 +1,23 @@
FROM debian
RUN mkdir -p /home/node/app/node_modules
WORKDIR /home/node/app
COPY package*.json ./
COPY prisma ./prisma/
RUN DEBIAN_FRONTEND=noninteractive
RUN apt-get update -y
RUN apt-get -y install curl gnupg git python2 make g++ iputils-ping ffmpeg
RUN curl -sL https://deb.nodesource.com/setup_17.x | bash -
RUN apt-get -y install nodejs
RUN npm i -g yarn
RUN yarn
RUN yarn prisma generate
COPY . .
RUN yarn build
CMD [ "yarn", "start"]
+15
View File
@@ -0,0 +1,15 @@
[
{
"type": "ping",
"title": "☁️Internet | ",
"host": "1.1.1.1"
},
{
"type": "discordwebsocket",
"title": "🚀 Discord Websocket | "
},
{
"type": "discordhttp",
"title": "🏓 Discord HTTP | "
}
]
+55
View File
@@ -0,0 +1,55 @@
{
"News": {
"color": "#FAEDF0",
"title": "📰 Newsletter",
"description": "\u200B\n**{bot_username} have been super-charged and upgraded!**\nThe long-awaited update is now here, running latest and greatest version!\n\n**What's new?**\n\u200B\n",
"fields": [
{
"name": "🔥 Feature 1!",
"value": "\u200B\nSuch wow! New feature so cool 10/10!!!!\n\u200B\n",
"inline": false
},
{
"name": "✨ Shiny and new feature!",
"value": "\u200B\nNow brighter than ever!\n\u200B\n",
"inline": false
},
{
"name": "🪲 Bugs squashed",
"value": "\u200B\nBe gone, bugs!\n\u200B\n",
"inline": false
}
],
"thumbnail": {
"url": "bot_avatar"
}
},
"Maintenance": {
"color": "#383b80",
"title": "🔧 Maintenance",
"description": "{bot_username} will be down for maintenance\nThe maintenance includes feature improvement to bug fixes\n\nDuring the maintenance, {bot_username}'s commands and features will be unavailable",
"fields": [
{ "name": "Start", "value": "<t:1812519286:R>", "inline": true},
{ "name": "End", "value": "<t:3390442486:R>", "inline": true}
],
"thumbnail": {
"url": "bot_avatar"
}
},
"Message": {
"color": "#A1DE93",
"title": "✉️Message",
"description": "Hello everyone!",
"thumbnail": {
"url": "bot_avatar"
}
},
"Alert": {
"color": "#EC255A",
"title": "🚨 Alert",
"description": "{bot_username} currently has an issue with lack of food, {bot_username} need more FOOD! MORE AND MORE!.\nWe're fixing it ASAP",
"thumbnail": {
"url": "bot_avatar"
}
}
}
+13 -1
View File
@@ -1,19 +1,29 @@
{
"dependencies": {
"@discordjs/builders": "^0.7.0",
"@discordjs/opus": "^0.7.0",
"@discordjs/rest": "^0.3.0",
"@discordjs/voice": "^0.8.0",
"@types/color-thief-node": "^1.0.1",
"bluebird": "^3.7.2",
"bufferutil": "^4.0.6",
"color-thief-node": "^1.0.4",
"country-code-emoji": "^2.3.0",
"country-code-lookup": "^0.0.19",
"discord-api-types": "^0.26.1",
"discord.js": "^13.6.0",
"dotenv": "^10.0.0",
"libsodium-wrappers": "^0.7.9",
"node-osu": "^2.2.1",
"ping": "^0.4.1",
"play-dl": "^1.8.6",
"ts-custom-error": "^3.2.0",
"typescript": "^4.4.4",
"utf-8-validate": "^5.0.8",
"validator": "^13.7.0",
"winston": "^3.3.3"
"winston": "3.3.3",
"zlib-sync": "^0.1.7"
},
"devDependencies": {
"@prisma/client": "^3.3.0",
@@ -27,6 +37,8 @@
"scripts": {
"tsc": "tsc",
"development": "ts-node-dev ./src/index.ts --respawn --transpileOnly",
"build": "tsc -b",
"start": "node ./build/index.js",
"production": "tsc && node ./build/index.js",
"test": "mocha -r ts-node/register tests/**/*.ts"
},
+23 -8
View File
@@ -17,22 +17,37 @@ const EMBEDS = {
let _e = makeInfoEmbed({
icon: "💌",
title: `Help - ${DiscordProvider.client.user?.username}`,
description: `\u200b\n📰 **Info**\nYumi is currently undergoing complete rewrite, as expected, losing many of her functionality. All of the features will be re-implemented.\n\n🤔 **Why?**\nYumi was specifically written for use in a only 1 private server, now it's time to extend the border and globalize it (just kidding lol)\n\n🏷️**Prefix**\nYou can call me using \`\`${prefix.replaceAll('`','`')}\`\`, <@${DiscordProvider.client.user?.id}> or \`\`/slash command\`\`\n\n💻 **Available commands**`,
fields: [
{
name: 'Info',
value: `Yumi is currently undergoing complete rewrite, as expected, losing many of her functionality. All of the features will be re-implemented.`
name: '☕ General',
value: `help, ping, invite, userinfo`,
inline: true
},
{
name: 'Why?',
value: `Yumi was specifically written for use in a only 1 private server, now it's time to extend the border and globalize it (just kidding lol)`
name: '🎵 Music',
value: `play (p), search, skip, queue, nowplaying (np), join, leave`,
inline: true
},
{
name: 'Prefix',
value: `You can call me using \`\`${prefix.replaceAll('`','`')}\`\`, <@${DiscordProvider.client.user?.id}> or \`\`/slash command\`\``
name: '🎮 Games',
value: `osu`,
inline: true
},
{
name: 'Available commands',
value: '``help``\n``ping``\n``invite``\n``osu``\n``membershipscreening (ms)``\n``interaction`'
name: '🔐 Admin',
value: `settings, say, membershipscreening (ms)`,
inline: true
},
{
name: '🔧 Developer',
value: 'interactions, serviceannouncement',
inline: true
},
{
name: '\u200b',
value: '**Made with 💖 and [open source](https://github.com/YuzuZensai/Yumi)**',
inline: false
}
],
user: (data instanceof Interaction) ? data.user : data.author
+221
View File
@@ -0,0 +1,221 @@
import { Message, CommandInteraction, Interaction, VoiceChannel, Permissions, GuildMember, DMChannel, StageChannel, MessageActionRow, MessageButton, MessageAttachment, TextChannel } from "discord.js";
import { getEmotes, makeSuccessEmbed, makeErrorEmbed, sendMessage, sendMessageOrInteractionResponse, makeInfoEmbed } from "../../utils/DiscordMessage";
import DiscordProvider from "../../providers/Discord";
import Users from "../../services/Users"
import Environment from "../../providers/Environment";
import DiscordMusicPlayer, { PlayerPlayingEvent, ValidTracks, DiscordMusicPlayerInstance } from "../../providers/DiscordMusicPlayer";
import Discord from "../../providers/Discord";
const EMBEDS = {
VOICECHANNEL_JOINED: (data: Message | Interaction) => {
return makeSuccessEmbed({
title: `Success`,
description: `Joined voice channel`,
user: (data instanceof Interaction) ? data.user : data.author
});
},
VOICECHANNEL_INUSE: (data: Message | Interaction) => {
let haveForceMove = (data instanceof Message) ? (data as Message).member!.permissions.has([Permissions.FLAGS.MOVE_MEMBERS]) : ((data as Interaction).member! as GuildMember).permissions.has([Permissions.FLAGS.MOVE_MEMBERS]);
return makeErrorEmbed({
title: `I can't open portal to parallel universe`,
description: `I can't join mutiple voice channel on the same guild.
I wish I had a superpower, maybe... one day I will.
There are also somebody listening to the music in the voice channel I'm currently in.${!haveForceMove ? ` Wait until I finish playing there or I'm alone there. Better yet, join them!` : " Wait until I finish playing there or I'm alone there. Better yet, join them! \n\n**Or... just (ab)use your admin permissions and move me to where you want!**"}`,
user: (data instanceof Interaction) ? data.user : data.author
});
},
VOICECHANNEL_ALREADY_JOINED: (data: Message | Interaction) => {
return makeInfoEmbed({
title: `I'm already here`,
description: `Already in the voice channel you are currently connected to`,
user: (data instanceof Interaction) ? data.user : data.author
});
},
USER_NOT_IN_VOICECHANNEL: (data: Message | Interaction) => {
return makeErrorEmbed({
title: `You need to be in the voice channel first!`,
user: (data instanceof Interaction) ? data.user : data.author
});
},
NOW_PLAYING: (data: Message | Interaction, track: ValidTracks) => {
const embed = makeInfoEmbed({
title: 'Now playing',
icon: '🎵',
description: `${track.title}`,
user: DiscordProvider.client.user
});
embed.setImage(track.thumbnails[0].url);
return embed;
}
}
export async function joinVoiceChannelProcedure (data: Interaction | Message, instance: (DiscordMusicPlayerInstance | null), voiceChannel: (VoiceChannel | StageChannel)) {
const isSlashCommand = data instanceof CommandInteraction && data.isCommand();
const isAcceptableInteraction = data instanceof Interaction && data.isSelectMenu();
const isMessage = data instanceof Message;
if ((!isSlashCommand && !isAcceptableInteraction) && !isMessage) return;
if (!data.member) return;
if (!data.channel) return;
if (!data.guildId) return;
if (data.channel instanceof DMChannel) return;
if (!(data.channel instanceof TextChannel)) return;
const channel: any = isMessage ? data.member.voice.channel : DiscordProvider.client.guilds.cache.get((data as Interaction).guildId!)!.members.cache.get((data as Interaction).user.id)?.voice.channel;
if (!channel) return;
// If already in VoiceChannel
if (DiscordProvider.client.guilds.cache.get(data.guildId!)!.me!.voice.channelId) {
// But, no music instance yet (The bot might just restarted)
if (!instance) {
// User is in different VoiceChannel
if (channel.id !== DiscordProvider.client.guilds.cache.get(data.guildId)?.me?.voice) {
//Disconnect it
await DiscordProvider.client.guilds.cache.get(data.guildId)?.me?.voice.disconnect();
}
//Create instance for a new one
DiscordMusicPlayer.createGuildInstance(data.guildId, voiceChannel);
instance = DiscordMusicPlayer.getGuildInstance(data.guildId);
instance!.joinVoiceChannel(voiceChannel, data.channel);
if(!isAcceptableInteraction)
await sendMessageOrInteractionResponse(data, { embeds: [EMBEDS.VOICECHANNEL_JOINED(data)] });
}
// And, already have instance on the guild
else {
// And, User VoiceChannel is same as the instance
if (channel.id === instance.voiceChannel.id) {
if(!isAcceptableInteraction)
return await sendMessageOrInteractionResponse(data, { embeds: [EMBEDS.VOICECHANNEL_ALREADY_JOINED(data)] });
else return;
}
// But, not the same VoiceChannel
else {
let activeMembers = instance.voiceChannel.members.filter(member => member.user.bot === false);
// And someone else is using the bot
if(activeMembers.size > 0) {
if(!isAcceptableInteraction)
return await sendMessageOrInteractionResponse(data, { embeds: [EMBEDS.VOICECHANNEL_INUSE(data)] });
else return;
}
// And alone in the VoiceChannel
else {
// Move to the new voice channel
await DiscordProvider.client.guilds.cache.get(data.guildId)?.me?.voice.setChannel(voiceChannel);
if(!isAcceptableInteraction)
return await sendMessageOrInteractionResponse(data, { embeds: [EMBEDS.VOICECHANNEL_JOINED(data)] });
else return;
}
}
}
}
// Not in any VoiceChannel
else {
if (instance)
await DiscordMusicPlayer.destoryGuildInstance(data.guildId!);
DiscordMusicPlayer.createGuildInstance(data.guildId, voiceChannel);
instance = DiscordMusicPlayer.getGuildInstance(data.guildId);
instance!.joinVoiceChannel(voiceChannel, data.channel);
if(!isAcceptableInteraction)
await sendMessageOrInteractionResponse(data, { embeds: [EMBEDS.VOICECHANNEL_JOINED(data)] });
}
if (!instance) return;
let nowPlayingMessage: any;
// Register Event Listeners
instance.events.on('playing', async (event: PlayerPlayingEvent) => {
const row = new MessageActionRow()
.addComponents(
new MessageButton()
.setEmoji('▶️')
.setLabel('Open on YouTube')
.setURL(encodeURI(`https://www.youtube.com/watch?v=${event.instance.queue.track[0].id}`))
.setStyle('LINK'),
)
if (event.instance.textChannel) {
await sendMessage(event.instance.textChannel, undefined, { embeds: [EMBEDS.NOW_PLAYING(data, event.instance.queue.track[0])], components: [row] });
}
})
}
export default class Join {
async onCommand(command: string, args: any, message: Message) {
if (command.toLowerCase() !== 'join') return;
await this.process(message, args);
}
async interactionCreate(interaction: CommandInteraction) {
if (interaction.isCommand()) {
if (typeof interaction.commandName === 'undefined') return;
if ((interaction.commandName).toLowerCase() !== 'join') return;
await this.process(interaction, interaction.options);
}
}
async process(data: Interaction | Message, args: any) {
const isSlashCommand = data instanceof CommandInteraction && data.isCommand();
const isMessage = data instanceof Message;
if (!isSlashCommand && !isMessage) return;
if (!data.member) return;
this.joinCommands(data, args);
}
public async joinCommands(data: Interaction | Message, args: any) {
const isSlashCommand = data instanceof CommandInteraction && data.isCommand();
const isMessage = data instanceof Message;
if (!isSlashCommand && !isMessage) return;
if (!data.member) return;
if (!data.channel) return;
if (!data.guildId) return;
if (data.channel instanceof DMChannel) return;
if (!(data.channel instanceof TextChannel)) return;
const channel: any = isMessage ? data.member.voice.channel : DiscordProvider.client.guilds.cache.get((data as Interaction).guildId!)!.members.cache.get((data as Interaction).user.id)?.voice.channel;
if (!channel) return;
if (!data.member.voice.channel)
return await sendMessageOrInteractionResponse(data, { embeds: [EMBEDS.USER_NOT_IN_VOICECHANNEL(data)] });
let voiceChannel = data.member.voice.channel;
//if(!DiscordMusicPlayer.isGuildInstanceExists(data.guildId)) {
// DiscordMusicPlayer.createGuildInstance(data.guildId, voiceChannel);
//}
let instance = DiscordMusicPlayer.getGuildInstance(data.guildId);
await joinVoiceChannelProcedure(data, instance!, voiceChannel);
//if(!instance.isConnected())
// instance.joinVoiceChannel(voiceChannel, data.channel ? data.channel : undefined);
}
}
+80
View File
@@ -0,0 +1,80 @@
import { Message, CommandInteraction, Interaction, VoiceChannel } from "discord.js";
import { getEmotes, makeSuccessEmbed, makeErrorEmbed, sendMessage, sendMessageOrInteractionResponse, makeInfoEmbed } from "../../utils/DiscordMessage";
import DiscordProvider from "../../providers/Discord";
import Users from "../../services/Users"
import Environment from "../../providers/Environment";
import DiscordMusicPlayer, { DiscordMusicPlayerInstance } from "../../providers/DiscordMusicPlayer";
const EMBEDS = {
VOICECHANNEL_LEFT: (data: Message | Interaction) => {
return makeSuccessEmbed({
title: `Success`,
description: `Left the voice channel`,
user: (data instanceof Interaction) ? data.user : data.author
});
},
USER_NOT_IN_VOICECHANNEL: (data: Message | Interaction) => {
return makeErrorEmbed({
title: `You need to be in the voice channel first!`,
user: (data instanceof Interaction) ? data.user : data.author
});
},
NO_MUSIC_PLAYING: (data: Message | Interaction) => {
return makeErrorEmbed({
title: `There are no music playing`,
user: (data instanceof Interaction) ? data.user : data.author
});
},
USER_NOT_IN_SAME_VOICECHANNEL: (data: Message | Interaction) => {
return makeErrorEmbed({
title: `You are not in the same voice channel!`,
user: (data instanceof Interaction) ? data.user : data.author
});
}
}
export default class Leave {
async onCommand(command: string, args: any, message: Message) {
if(command.toLowerCase() !== 'leave') return;
await this.process(message, args);
}
async interactionCreate(interaction: CommandInteraction) {
if(interaction.isCommand()) {
if(typeof interaction.commandName === 'undefined') return;
if((interaction.commandName).toLowerCase() !== 'leave') return;
await this.process(interaction, interaction.options);
}
}
async process(data: Interaction | Message, args: any) {
const isSlashCommand = data instanceof CommandInteraction && data.isCommand();
const isMessage = data instanceof Message;
if(!isSlashCommand && !isMessage) return;
if(!data.member) return;
if(!data.guildId) return;
const channel: any = isMessage ? data.member.voice.channel : DiscordProvider.client.guilds.cache.get((data as Interaction).guildId!)!.members.cache.get((data as Interaction).user.id)?.voice.channel;
if (!data.member.voice.channel)
return await sendMessageOrInteractionResponse(data, { embeds: [EMBEDS.USER_NOT_IN_VOICECHANNEL(data)] });
if(!channel) return;
let instance = DiscordMusicPlayer.getGuildInstance(data.guildId);
if(!instance)
return await sendMessageOrInteractionResponse(data, { embeds: [EMBEDS.NO_MUSIC_PLAYING(data)] });
if(instance.voiceChannel.id !== data.member.voice.channel.id)
return await sendMessageOrInteractionResponse(data, { embeds: [EMBEDS.USER_NOT_IN_SAME_VOICECHANNEL(data)] });
DiscordMusicPlayer.destoryGuildInstance(data.guild?.id!);
await sendMessageOrInteractionResponse(data, { embeds:[EMBEDS.VOICECHANNEL_LEFT(data)] });
return;
}
}
+70
View File
@@ -0,0 +1,70 @@
import { Message, CommandInteraction, Interaction, MessageActionRow, TextChannel, MessageButton } from "discord.js";
import { getEmotes, makeSuccessEmbed, makeErrorEmbed, sendMessage, sendMessageOrInteractionResponse, makeInfoEmbed } from "../../utils/DiscordMessage";
import DiscordProvider from "../../providers/Discord";
import Users from "../../services/Users"
import Environment from "../../providers/Environment";
import DiscordMusicPlayer, { DiscordMusicPlayerInstance, ValidTracks, Queue } from "../../providers/DiscordMusicPlayer";
const EMBEDS = {
NOW_PLAYING: (data: Message | Interaction, track: ValidTracks) => {
const embed = makeInfoEmbed({
title: 'Now playing',
icon: '🎵',
description: `${track.title}`,
user: DiscordProvider.client.user
});
embed.setImage(track.thumbnails[0].url);
return embed;
},
NO_MUSIC_PLAYING: (data: Message | Interaction) => {
return makeErrorEmbed({
title: `There are no music playing`,
user: (data instanceof Interaction) ? data.user : data.author
});
}
}
export default class NowPlaying {
async onCommand(command: string, args: any, message: Message) {
if(command.toLowerCase() !== 'nowplaying' && command.toLowerCase() !== 'np') return;
await this.process(message, args);
}
async interactionCreate(interaction: CommandInteraction) {
if(interaction.isCommand()) {
if(typeof interaction.commandName === 'undefined') return;
if((interaction.commandName).toLowerCase() !== 'nowplaying') return;
await this.process(interaction, interaction.options);
}
}
async process(data: Interaction | Message, args: any) {
const isSlashCommand = data instanceof CommandInteraction && data.isCommand();
const isMessage = data instanceof Message;
if(!isSlashCommand && !isMessage) return;
if(!data.member) return;
if(!data.guildId) return;
if(!(data.channel instanceof TextChannel)) return;
//const channel: any = isMessage ? data.member.voice.channel : DiscordProvider.client.guilds.cache.get((data as Interaction).guildId!)!.members.cache.get((data as Interaction).user.id)?.voice.channel;
//if(!channel) return;
const instance = DiscordMusicPlayer.getGuildInstance(data.guildId);
if(!instance || !instance.queue.track[0]) return await sendMessageOrInteractionResponse(data, { embeds: [EMBEDS.NO_MUSIC_PLAYING(data)] });
const row = new MessageActionRow()
.addComponents(
new MessageButton()
.setEmoji('▶️')
.setLabel('Open on YouTube')
.setURL(encodeURI(`https://www.youtube.com/watch?v=${instance.queue.track[0].id}`))
.setStyle('LINK'),
)
return await sendMessageOrInteractionResponse(data, { embeds: [EMBEDS.NOW_PLAYING(data, instance.queue.track[0])], components: [row] });
}
}
+204
View File
@@ -0,0 +1,204 @@
import { Message, CommandInteraction, Interaction, VoiceChannel, MessageActionRow, Permissions, DMChannel, TextChannel } from "discord.js";
import { makeErrorEmbed, makeSuccessEmbed, makeProcessingEmbed, sendMessage, sendMessageOrInteractionResponse, makeInfoEmbed } from "../../utils/DiscordMessage";
import DiscordProvider from "../../providers/Discord";
import Environment from "../../providers/Environment";
import DiscordMusicPlayer, { ValidTracks } from "../../providers/DiscordMusicPlayer";
import Prisma from "../../providers/Prisma";
import { joinVoiceChannelProcedure } from "./Join";
const EMBEDS = {
YOUTUBE_ONLY: (data: Message | Interaction) => {
return makeErrorEmbed({
title: `Only youtube.com link is supported for now`,
user: (data instanceof Interaction) ? data.user : data.author
});
},
INVALID_LINK: (data: Message | Interaction) => {
return makeErrorEmbed({
title: `Invalid Link`,
user: (data instanceof Interaction) ? data.user : data.author
});
},
ADDED_QUEUE: (data: Message | Interaction, track: ValidTracks) => {
let embed = makeSuccessEmbed({
title: `Song added to queue!`,
description: `${track.title}`,
user: (data instanceof Interaction) ? data.user : data.author
});
embed.setImage(track.thumbnails[0].url);
return embed;
},
USER_NOT_IN_VOICECHANNEL: (data: Message | Interaction) => {
return makeErrorEmbed({
title: `You need to be in the voice channel first!`,
user: (data instanceof Interaction) ? data.user : data.author
});
},
USER_NOT_IN_SAME_VOICECHANNEL: (data: Message | Interaction) => {
return makeErrorEmbed({
title: `You are not in the same voice channel!`,
user: (data instanceof Interaction) ? data.user : data.author
});
},
}
export default class Play {
async onCommand(command: string, args: any, message: Message) {
if (command.toLowerCase() !== 'play' && command.toLowerCase() !== 'p') return;
await this.process(message, args);
}
async interactionCreate(interaction: Interaction) {
if (interaction.isCommand()) {
if (typeof interaction.commandName === 'undefined') return;
if ((interaction.commandName).toLowerCase() !== 'play') return;
await this.process(interaction, interaction.options);
}
else if (interaction.isSelectMenu()) {
if(!interaction.guild || !interaction.guildId) return;
if (!this.tryParseJSONObject(interaction.customId)) return;
let payload = JSON.parse(interaction.customId);
if(!interaction.member?.user?.id) return;
/*
Discord have 100 char custom id char limit
So we need to shorten our json.
MP_SM stands for MusicPlayer_SearchMenu
data.r stands for data.requester
data.v stands for data.voiceChannel
*/
if (typeof payload.module === 'undefined' ||
typeof payload.action === 'undefined' ||
payload.module !== 'MP_SM' ||
payload.action !== 'play') return;
await interaction.deferReply();
let voiceChannel = DiscordProvider.client.guilds.cache.get(interaction.guildId)?.channels.cache.get(payload.data.v);
let member = DiscordProvider.client.guilds.cache.get(interaction.guildId)?.members.cache.get(interaction.member?.user?.id);
if(!member) return;
if (!voiceChannel || !(voiceChannel instanceof VoiceChannel)) return;
if (!member.voice.channel)
return await sendMessageOrInteractionResponse(interaction, { embeds: [EMBEDS.USER_NOT_IN_VOICECHANNEL(interaction)] }, true);
if (!DiscordMusicPlayer.isGuildInstanceExists(interaction.guildId)) {
await joinVoiceChannelProcedure(interaction, null, voiceChannel);
}
let instance = DiscordMusicPlayer.getGuildInstance(interaction.guildId);
if(instance!.voiceChannel.id !== member.voice.channel.id)
return await sendMessageOrInteractionResponse(interaction, { embeds: [EMBEDS.USER_NOT_IN_SAME_VOICECHANNEL(interaction)] }, true);
if (!instance!.isConnected()) {
await joinVoiceChannelProcedure(interaction, instance!, voiceChannel);
instance = DiscordMusicPlayer.getGuildInstance(interaction.guildId);
}
if (!instance) return;
let result = await DiscordMusicPlayer.searchYouTubeByYouTubeLink(DiscordMusicPlayer.parseYouTubeLink(interaction.values[0]));
if (!result) return;
instance.addTrackToQueue(result);
//interaction.editReply({ embeds: [EMBEDS.ADDED_QUEUE(interaction, result)] });
return await sendMessageOrInteractionResponse(interaction, { embeds: [EMBEDS.ADDED_QUEUE(interaction, result)] }, true);
}
}
async process(data: Interaction | Message, args: any) {
const isSlashCommand = data instanceof CommandInteraction && data.isCommand();
const isMessage = data instanceof Message;
if (!isSlashCommand && !isMessage) return;
if (!data.member) return;
if (!data.guild) return;
if (!data.guildId) return;
let query;
if (isMessage) {
if (data === null || !data.guildId || data.member === null || data.guild === null) return;
if (args.length === 0) {
// TODO: Show info
//return await sendMessageOrInteractionResponse(data, { embeds: [EMBEDS.MSINFO(data)] });
return;
}
query = args[0];
}
else if (isSlashCommand) {
query = args.getSubcommand();
}
if (!query) return;
if (!(data.channel instanceof TextChannel)) return;
if (!data.member.voice.channel)
return await sendMessageOrInteractionResponse(data, { embeds: [EMBEDS.USER_NOT_IN_VOICECHANNEL(data)] });
let voiceChannel = data.member.voice.channel;
if (!DiscordMusicPlayer.isGuildInstanceExists(data.guildId)) {
await joinVoiceChannelProcedure(data, null, voiceChannel);
}
let instance = DiscordMusicPlayer.getGuildInstance(data.guildId);
if(instance!.voiceChannel.id !== data.member.voice.channel.id)
return await sendMessageOrInteractionResponse(data, { embeds: [EMBEDS.USER_NOT_IN_SAME_VOICECHANNEL(data)] }, true);
if (!instance!.isConnected()) {
await joinVoiceChannelProcedure(data, instance!, voiceChannel);
instance = DiscordMusicPlayer.getGuildInstance(data.guildId);
}
if (!instance) return;
if (DiscordMusicPlayer.isYouTubeLink(query)) {
let result = await DiscordMusicPlayer.searchYouTubeByYouTubeLink(DiscordMusicPlayer.parseYouTubeLink(query));
if (!result) return;
if (isMessage && (data as Message).embeds.length > 0) {
(data as Message).suppressEmbeds(true);
}
instance.addTrackToQueue(result);
return await sendMessageOrInteractionResponse(data, { embeds: [EMBEDS.ADDED_QUEUE(data, result)] });
} else {
let result = await DiscordMusicPlayer.searchYouTubeByQuery(query);
if (!result) return;
instance.addTrackToQueue(result[0]);
// TODO: Let user choose the video
return await sendMessageOrInteractionResponse(data, { embeds: [EMBEDS.ADDED_QUEUE(data, result[0])] });
}
return;
}
tryParseJSONObject(jsonString: string) {
try {
let o = JSON.parse(jsonString);
if (o && typeof o === "object") {
return true;
//return o;
}
}
catch (e) { }
return false;
}
}
+86
View File
@@ -0,0 +1,86 @@
import { Message, CommandInteraction, Interaction, VoiceChannel, TextChannel } from "discord.js";
import { getEmotes, makeSuccessEmbed, makeErrorEmbed, sendMessage, sendMessageOrInteractionResponse, makeInfoEmbed } from "../../utils/DiscordMessage";
import DiscordProvider from "../../providers/Discord";
import Users from "../../services/Users"
import Environment from "../../providers/Environment";
import DiscordMusicPlayer, { DiscordMusicPlayerInstance, ValidTracks, Queue } from "../../providers/DiscordMusicPlayer";
const EMBEDS = {
QUEUE: (data: Message | Interaction, queue: Queue) => {
if(!queue.track[0])
return makeInfoEmbed({
title: `Queue`,
description: `The queue is empty!`,
user: (data instanceof Interaction) ? data.user : data.author
});
else {
if(queue.track.length == 1) {
let queueString = `1. [${queue.track[0].title}](${queue.track[0].url})`
return makeInfoEmbed({
title: `Queue`,
description: `Now playing: ${queue.track[0].title}\n\nThere are ${queue.track.length} song in the queue!\n${queueString}`,
user: (data instanceof Interaction) ? data.user : data.author
});
}
else if(queue.track.length >= 10) {
let first10 = queue.track.slice(0, 10);
let queueString = first10.map((track, index) => `${index + 1}. [${track.title}](${track.url})`).join("\n");
return makeInfoEmbed({
title: `Queue`,
description: `Now playing: ${queue.track[0].title}\nUpcoming song: ${queue.track[1].title}\n\nThere are ${queue.track.length} songs in the queue!\n${queueString}\n${queue.track.length > 10 ? `...${queue.track.length - 10} more songs` : ""}`,
user: (data instanceof Interaction) ? data.user : data.author
});
} else {
let queueString = queue.track.map((track, index) => `${index + 1}. [${track.title}](${track.url})`).join("\n");
return makeInfoEmbed({
title: `Queue`,
description: `Now playing: ${queue.track[0].title}\nUpcoming song: ${queue.track[1].title}\n\nThere are ${queue.track.length} songs in the queue!\n${queueString}`,
user: (data instanceof Interaction) ? data.user : data.author
});
}
}
},
NO_MUSIC_PLAYING: (data: Message | Interaction) => {
return makeErrorEmbed({
title: `There are no music playing`,
user: (data instanceof Interaction) ? data.user : data.author
});
}
}
export default class QueueCommand {
async onCommand(command: string, args: any, message: Message) {
if(command.toLowerCase() !== 'queue') return;
await this.process(message, args);
}
async interactionCreate(interaction: CommandInteraction) {
if(interaction.isCommand()) {
if(typeof interaction.commandName === 'undefined') return;
if((interaction.commandName).toLowerCase() !== 'queue') return;
await this.process(interaction, interaction.options);
}
}
async process(data: Interaction | Message, args: any) {
const isSlashCommand = data instanceof CommandInteraction && data.isCommand();
const isMessage = data instanceof Message;
if(!isSlashCommand && !isMessage) return;
if(!data.member) return;
if(!data.guildId) return;
if(!(data.channel instanceof TextChannel)) return;
//const channel: any = isMessage ? data.member.voice.channel : DiscordProvider.client.guilds.cache.get((data as Interaction).guildId!)!.members.cache.get((data as Interaction).user.id)?.voice.channel;
//if(!channel) return;
const instance = DiscordMusicPlayer.getGuildInstance(data.guildId);
if(!instance) return await sendMessageOrInteractionResponse(data, { embeds: [EMBEDS.NO_MUSIC_PLAYING(data)] });
await sendMessageOrInteractionResponse(data, { embeds:[EMBEDS.QUEUE(data, instance.queue)] });
return;
}
}
+158
View File
@@ -0,0 +1,158 @@
import { Message, CommandInteraction, Interaction, VoiceChannel, MessageActionRow, Permissions, DMChannel, TextChannel, MessageSelectMenu, MessageSelectOptionData } from "discord.js";
import { makeErrorEmbed, makeSuccessEmbed, makeProcessingEmbed, sendMessage, sendMessageOrInteractionResponse, makeInfoEmbed } from "../../utils/DiscordMessage";
import DiscordProvider from "../../providers/Discord";
import Environment from "../../providers/Environment";
import DiscordMusicPlayer, { ValidTracks } from "../../providers/DiscordMusicPlayer";
import Prisma from "../../providers/Prisma";
import { joinVoiceChannelProcedure } from "./Join";
const EMBEDS = {
SEARCH_RESULT: (data: Message | Interaction, result: ValidTracks[]) => {
return makeInfoEmbed({
title: `Search result`,
description: `${result.length} results found\n\n${result.map(track => `- [${track.title}](${track.url})`).join('\n')}`,
user: (data instanceof Interaction) ? data.user : data.author
});
},
INVALID_LINK: (data: Message | Interaction) => {
return makeErrorEmbed({
title: `Invalid Link`,
user: (data instanceof Interaction) ? data.user : data.author
});
},
ADDED_QUEUE: (data: Message | Interaction, track: ValidTracks) => {
let embed = makeSuccessEmbed({
title: `Song added to queue!`,
description: `${track.title}`,
user: (data instanceof Interaction) ? data.user : data.author
});
embed.setImage(track.thumbnails[0].url);
return embed;
},
USER_NOT_IN_VOICECHANNEL: (data: Message | Interaction) => {
return makeErrorEmbed({
title: `You need to be in the voice channel first!`,
user: (data instanceof Interaction) ? data.user : data.author
});
},
USER_NOT_IN_SAME_VOICECHANNEL: (data: Message | Interaction) => {
return makeErrorEmbed({
title: `You are not in the same voice channel!`,
user: (data instanceof Interaction) ? data.user : data.author
});
}
}
export default class Search {
async onCommand(command: string, args: any, message: Message) {
if (command.toLowerCase() !== 'search') return;
await this.process(message, args);
}
async interactionCreate(interaction: CommandInteraction) {
if (interaction.isCommand()) {
if (typeof interaction.commandName === 'undefined') return;
if ((interaction.commandName).toLowerCase() !== 'search') return;
await this.process(interaction, interaction.options);
}
}
async process(data: Interaction | Message, args: any) {
const isSlashCommand = data instanceof CommandInteraction && data.isCommand();
const isMessage = data instanceof Message;
if (!isSlashCommand && !isMessage) return;
if (!data.member) return;
if (!data.guild) return;
if (!data.guildId) return;
let query;
if (isMessage) {
if (data === null || !data.guildId || data.member === null || data.guild === null) return;
if (args.length === 0) {
// TODO: Show info
//return await sendMessageOrInteractionResponse(data, { embeds: [EMBEDS.MSINFO(data)] });
return;
}
query = args[0];
}
else if (isSlashCommand) {
query = args.getSubcommand();
}
if (!query) return;
if (!(data.channel instanceof TextChannel)) return;
if (!data.member.voice.channel)
return await sendMessageOrInteractionResponse(data, { embeds: [EMBEDS.USER_NOT_IN_VOICECHANNEL(data)] });
let voiceChannel = data.member.voice.channel;
if (!DiscordMusicPlayer.isGuildInstanceExists(data.guildId)) {
await joinVoiceChannelProcedure(data, null, voiceChannel);
}
let instance = DiscordMusicPlayer.getGuildInstance(data.guildId);
if (instance!.voiceChannel.id !== voiceChannel.id)
return await sendMessageOrInteractionResponse(data, { embeds: [EMBEDS.USER_NOT_IN_SAME_VOICECHANNEL(data)] });
if (!instance!.isConnected()) {
await joinVoiceChannelProcedure(data, instance!, voiceChannel);
instance = DiscordMusicPlayer.getGuildInstance(data.guildId);
}
if (!instance) return;
let result = await DiscordMusicPlayer.searchYouTubeByQuery(query);
if (!result) return;
const menuOptions: MessageSelectOptionData[] = [];
const messageSelectMenu = new MessageSelectMenu();
/*
Discord have 100 char custom id char limit
So we need to shorten our json.
MP_SM stands for MusicPlayer_SearchMenu
data.r stands for data.requester
data.v stands for data.voiceChannel
*/
messageSelectMenu.setCustomId(JSON.stringify({
module: 'MP_SM',
action: 'play',
data: {
r: data.member.id,
v: voiceChannel.id
}
}))
messageSelectMenu.setPlaceholder('Nothing selected');
for(let song of result) {
if(!song.title) continue;
menuOptions.push({
label: song.title,
description: `${song.durationInSec} sec | ${song.views} views`,
value: song.url
});
}
messageSelectMenu.addOptions(menuOptions);
const row = new MessageActionRow();
row.addComponents(messageSelectMenu);
await sendMessageOrInteractionResponse(data, { embeds: [EMBEDS.SEARCH_RESULT(data, result)], components: [row] });
return;
}
}
+92
View File
@@ -0,0 +1,92 @@
import { Message, CommandInteraction, Interaction, VoiceChannel } from "discord.js";
import { getEmotes, makeSuccessEmbed, makeErrorEmbed, sendMessage, sendMessageOrInteractionResponse, makeInfoEmbed } from "../../utils/DiscordMessage";
import DiscordProvider from "../../providers/Discord";
import Users from "../../services/Users"
import Environment from "../../providers/Environment";
import DiscordMusicPlayer from "../../providers/DiscordMusicPlayer";
const EMBEDS = {
SKIPPED: (data: Message | Interaction) => {
return makeSuccessEmbed({
title: `Skipped`,
user: (data instanceof Interaction) ? data.user : data.author
});
},
SKIPPED_LASTSONG: (data: Message | Interaction) => {
return makeSuccessEmbed({
title: `Skipped, that was the last song`,
user: (data instanceof Interaction) ? data.user : data.author
});
},
NO_MUSIC_PLAYING: (data: Message | Interaction) => {
return makeErrorEmbed({
title: `There are no music playing`,
user: (data instanceof Interaction) ? data.user : data.author
});
},
USER_NOT_IN_VOICECHANNEL: (data: Message | Interaction) => {
return makeErrorEmbed({
title: `You need to be in the voice channel first!`,
user: (data instanceof Interaction) ? data.user : data.author
});
},
USER_NOT_IN_SAME_VOICECHANNEL: (data: Message | Interaction) => {
return makeErrorEmbed({
title: `You are not in the same voice channel!`,
user: (data instanceof Interaction) ? data.user : data.author
});
}
}
export default class Skip {
async onCommand(command: string, args: any, message: Message) {
if (command.toLowerCase() !== 'skip') return;
await this.process(message, args);
}
async interactionCreate(interaction: CommandInteraction) {
if (interaction.isCommand()) {
if (typeof interaction.commandName === 'undefined') return;
if ((interaction.commandName).toLowerCase() !== 'skip') return;
await this.process(interaction, interaction.options);
}
}
async process(data: Interaction | Message, args: any) {
const isSlashCommand = data instanceof CommandInteraction && data.isCommand();
const isMessage = data instanceof Message;
if (!isSlashCommand && !isMessage) return;
if (!data.member) return;
if (!data.guildId) return;
const channel: any = isMessage ? data.member.voice.channel : DiscordProvider.client.guilds.cache.get((data as Interaction).guildId!)!.members.cache.get((data as Interaction).user.id)?.voice.channel;
if (!data.member.voice.channel)
return await sendMessageOrInteractionResponse(data, { embeds: [EMBEDS.USER_NOT_IN_VOICECHANNEL(data)] });
if (!channel) return;
let voiceChannel = data.member.voice.channel;
if(!DiscordMusicPlayer.isGuildInstanceExists(data.guildId))
return await sendMessageOrInteractionResponse(data, { embeds: [EMBEDS.NO_MUSIC_PLAYING(data)] });
const instance = DiscordMusicPlayer.getGuildInstance(data.guildId);
if(instance!.voiceChannel.id !== data.member.voice.channel.id)
return await sendMessageOrInteractionResponse(data, { embeds: [EMBEDS.USER_NOT_IN_SAME_VOICECHANNEL(data)] }, true);
if(instance?.queue.track.length === 0)
return await sendMessageOrInteractionResponse(data, { embeds: [EMBEDS.NO_MUSIC_PLAYING(data)] });
instance!.skipTrack();
if(instance?.queue.track.length === 0)
return await sendMessageOrInteractionResponse(data, { embeds: [EMBEDS.SKIPPED_LASTSONG(data)] });
else
return await sendMessageOrInteractionResponse(data, { embeds: [EMBEDS.SKIPPED(data)] });
}
}
+33 -43
View File
@@ -14,31 +14,7 @@ enum MeasureType {
DiscordWebsocket = "discordwebsocket"
}
let measureList = [
{
type: MeasureType.Ping,
title: "☁️Internet | ",
host: "1.1.1.1"
},
{
type: MeasureType.DiscordWebsocket,
title: "🚀 Discord Websocket | "
},
{
type: MeasureType.DiscordHTTPPing,
title: "🏓 Discord HTTP | "
}
];
if(fs.existsSync(path.join(process.cwd(), 'configs/Ping.json'))) {
try {
const rawData = fs.readFileSync(path.join(process.cwd(), 'configs/Ping.json'));
const jsonData = JSON.parse(rawData.toString());
measureList = jsonData;
} catch (err) {
Logger.error("Unable to load custom Ping config: " + err);
}
}
let measureList: any = [];
const EMBEDS = {
PING_INFO: (data: Message | Interaction, description: string) => {
@@ -53,15 +29,29 @@ const EMBEDS = {
export default class Ping {
async init() {
if (fs.existsSync(path.join(process.cwd(), 'configs/Ping.json'))) {
try {
const rawData = fs.readFileSync(path.join(process.cwd(), 'configs/Ping.json'));
const jsonData = JSON.parse(rawData.toString());
measureList = jsonData;
} catch (err) {
Logger.error("Unable to load custom Ping config: " + err);
}
}
}
async onCommand(command: string, args: any, message: Message) {
if(command.toLowerCase() !== 'ping') return;
if (command.toLowerCase() !== 'ping') return;
await this.process(message, args);
}
async interactionCreate(interaction: CommandInteraction) {
if(interaction.isCommand()) {
if(typeof interaction.commandName === 'undefined') return;
if((interaction.commandName).toLowerCase() !== 'ping') return;
async interactionCreate(interaction: CommandInteraction) {
if (interaction.isCommand()) {
if (typeof interaction.commandName === 'undefined') return;
if ((interaction.commandName).toLowerCase() !== 'ping') return;
await this.process(interaction, interaction.options);
}
}
@@ -70,7 +60,7 @@ export default class Ping {
const isSlashCommand = data instanceof CommandInteraction && data.isCommand();
const isMessage = data instanceof Message;
if(!isSlashCommand && !isMessage) return;
if (!isSlashCommand && !isMessage) return;
let placeholder;
@@ -81,7 +71,7 @@ export default class Ping {
});
let beforeEditDate = Date.now();
placeholder = await sendMessageOrInteractionResponse(data, { embeds:[loadingEmbed]} );
placeholder = await sendMessageOrInteractionResponse(data, { embeds: [loadingEmbed] });
let afterEditDate = Date.now();
@@ -89,23 +79,23 @@ export default class Ping {
// TODO: Optimize speed of this
for(let entry of measureList) {
for (let entry of measureList) {
let stringCurrent = `${entry.title}`;
if(entry.type === MeasureType.Ping) {
if (entry.type === MeasureType.Ping) {
const res = await NodePing.promise.probe(entry.host!, { min_reply: 4 });
if(!res.alive) {
if (!res.alive) {
stringCurrent += "Failed";
return desString.push(stringCurrent);
}
if(res.avg === 'unknown' || res.min === 'unknown' || res.max === 'unknown') {
if (res.avg === 'unknown' || res.min === 'unknown' || res.max === 'unknown') {
stringCurrent += "Failed";
return desString.push(stringCurrent);
}
stringCurrent += `${parseFloat(res.avg).toFixed(1)}ms (${parseFloat(res.min).toFixed(1)}ms - ${parseFloat(res.max).toFixed(1)}ms)`;
desString.push(stringCurrent);
} else if (entry.type === MeasureType.DiscordWebsocket) {
@@ -114,15 +104,15 @@ export default class Ping {
} else if (entry.type === MeasureType.DiscordHTTPPing) {
stringCurrent += `${Math.abs(afterEditDate - beforeEditDate).toFixed(1)}ms`;
desString.push(stringCurrent);
}
}
}
desString.push("");
desString.push("💻 Running on " + `${os.hostname()}${Environment.get().NODE_ENV === "development" ? ' / Development Environment' : ''}`);
if(isSlashCommand) return await data.editReply({ embeds: [EMBEDS.PING_INFO(data, desString.join('\n'))] });
else if(typeof placeholder !== "undefined" && isMessage && placeholder instanceof Message) return await placeholder.edit({ embeds: [EMBEDS.PING_INFO(data, desString.join('\n'))] });
if (isSlashCommand) return await data.editReply({ embeds: [EMBEDS.PING_INFO(data, desString.join('\n'))] });
else if (typeof placeholder !== "undefined" && isMessage && placeholder instanceof Message) return await placeholder.edit({ embeds: [EMBEDS.PING_INFO(data, desString.join('\n'))] });
}
}
+43
View File
@@ -0,0 +1,43 @@
import { Message, CommandInteraction, Interaction } from "discord.js";
import { getEmotes, makeSuccessEmbed, makeProcessingEmbed, sendMessage, sendMessageOrInteractionResponse, makeInfoEmbed } from "../utils/DiscordMessage";
import DiscordProvider from "../providers/Discord";
import Users from "../services/Users"
import Environment from "../providers/Environment";
const EMBEDS = {
STATS_INFO: (data: Message | Interaction) => {
let message;
return makeInfoEmbed({
title: `Stats`,
icon: '📊',
description: `Serving in ${DiscordProvider.client.guilds.cache.size} servers\n${DiscordProvider.client.guilds.cache.map((g) => g.memberCount).reduce((a, c) => a + c)} users`,
user: (data instanceof Interaction) ? data.user : data.author
});
}
}
export default class Stats {
async onCommand(command: string, args: any, message: Message) {
if(command.toLowerCase() !== 'stats') return;
await this.process(message, args);
}
async interactionCreate(interaction: CommandInteraction) {
if(interaction.isCommand()) {
if(typeof interaction.commandName === 'undefined') return;
if((interaction.commandName).toLowerCase() !== 'stats') return;
await this.process(interaction, interaction.options);
}
}
async process(data: Interaction | Message, args: any) {
const isSlashCommand = data instanceof CommandInteraction && data.isCommand();
const isMessage = data instanceof Message;
if(!isSlashCommand && !isMessage) return;
return await sendMessageOrInteractionResponse(data, { embeds:[EMBEDS.STATS_INFO(data)] });
}
}
+24 -23
View File
@@ -5,7 +5,7 @@ import { registerAllGuildsCommands, unregisterAllGuildsCommands } from "../../ut
import Prisma from "../../providers/Prisma";
import Users from "../../services/Users";
import {Promise, reject} from "bluebird";
import { Promise, reject } from "bluebird";
import fs from "fs";
import path from "path";
import Logger from "../../libs/Logger";
@@ -124,20 +124,21 @@ let Announcements = {
}
}
if (fs.existsSync(path.join(process.cwd(), 'configs/ServiceAnnouncement.json'))) {
try {
const rawData = fs.readFileSync(path.join(process.cwd(), 'configs/ServiceAnnouncement.json'));
const jsonData = JSON.parse(rawData.toString());
Announcements = jsonData;
} catch (err) {
Logger.error("Unable to load custom ServiceAnnouncement config: " + err);
}
} else {
fs.writeFileSync(path.join(process.cwd(), 'configs/ServiceAnnouncement.json'), JSON.stringify(Announcements, null, 4), 'utf8');
}
export default class InteractionManager {
async init() {
if (fs.existsSync(path.join(process.cwd(), 'configs/ServiceAnnouncement.json'))) {
try {
const rawData = fs.readFileSync(path.join(process.cwd(), 'configs/ServiceAnnouncement.json'));
const jsonData = JSON.parse(rawData.toString());
Announcements = jsonData;
} catch (err) {
Logger.error("Unable to load custom ServiceAnnouncement config: " + err);
}
}
}
async onCommand(command: string, args: any, message: Message) {
if (command.toLowerCase() !== 'serviceannouncement') return;
await this.process(message, args);
@@ -215,7 +216,7 @@ export default class InteractionManager {
toSend.set(Guild, channel);
}
let placeholder = await sendMessage(data.channel!, undefined, { embeds:[EMBEDS.SENDING_SERVICE_ANNOUNCEMENT(data)] });
let placeholder = await sendMessage(data.channel!, undefined, { embeds: [EMBEDS.SENDING_SERVICE_ANNOUNCEMENT(data)] });
await Promise.map(toSend, element => {
return new Promise(async (resolve, reject) => {
@@ -223,14 +224,14 @@ export default class InteractionManager {
let Channel: TextChannel = element[1];
try {
const guildObject = DiscordProvider.client.guilds.cache.get(Guild.id);
if(!guildObject)
if (!guildObject)
throw new Error('Guild not found');
Logger.info(`Sending Service Announcement to ${guildObject?.name} (${guildObject.id}) #${Channel.name} (${Channel.id})`);
await sendMessage(Channel, undefined, { embeds:[EMBEDS.MAKE_PAYLOAD(data, embed)] })
await sendMessage(Channel, undefined, { embeds: [EMBEDS.MAKE_PAYLOAD(data, embed)] })
} catch (err) {
withError = true;
const guildObject = DiscordProvider.client.guilds.cache.get(Guild.id);
if(guildObject)
if (guildObject)
Logger.info(`Unable to send Service Announcement to ${guildObject?.name} (${guildObject.id}) #${Channel.name} (${Channel.id})`);
reject(err);
@@ -241,16 +242,16 @@ export default class InteractionManager {
});
}, { concurrency: 2 });
if(withError) {
if(isMessage)
if (withError) {
if (isMessage)
(placeholder as Message).edit({ embeds: [EMBEDS.SERVICE_ANNOUNCEMENT_SENT_WITH_ERRORS(data)] });
else if(isSlashCommand)
else if (isSlashCommand)
return await sendMessageOrInteractionResponse(data, { embeds: [EMBEDS.SERVICE_ANNOUNCEMENT_SENT_WITH_ERRORS(data)] }, true);
}
else {
if(isMessage)
if (isMessage)
(placeholder as Message).edit({ embeds: [EMBEDS.SERVICE_ANNOUNCEMENT_SENT(data)] });
else if(isSlashCommand)
else if (isSlashCommand)
return await sendMessageOrInteractionResponse(data, { embeds: [EMBEDS.SERVICE_ANNOUNCEMENT_SENT(data)] }, true);
}
}
+1 -1
View File
@@ -8,7 +8,7 @@ import Configuration from './Configuration';
class App {
public readonly version = `0.05${Environment.get().NODE_ENV === "development" ? ' / Development Build' : ''}`;
public readonly version = `0.07${Environment.get().NODE_ENV === "development" ? ' / Development Build' : ''}`;
public loadConfig(): void {
Logger.log('info', 'Loading configuration');
+9
View File
@@ -10,6 +10,15 @@ class Configuration {
if (!fs.existsSync(dir)) {
fs.mkdirSync(dir);
}
this.copyExampleIfNotExists("Ping.json");
this.copyExampleIfNotExists("ServiceAnnouncement.json");
}
private copyExampleIfNotExists(file: string): void {
const dir = path.join(process.cwd(), 'configs/');
if (!fs.existsSync(path.join(dir, file))) {
fs.copyFileSync(path.join(process.cwd(), 'configs/', `${file.replace('.json', '.example.json')}`), path.join(dir, file));
}
}
// TODO: Make a real providers like Environment
+21 -2
View File
@@ -13,6 +13,16 @@ import Discord_InteractionManager from "../discord/InteractionManager";
import Discord_MembershipScreening from "../discord/MembershipScreening";
import Discord_osu from "../discord/osu";
import Discord_UserInfo from "../discord/UserInfo";
import Discord_Stats from "../discord/Stats";
import Discord_MusicPlayer_Play from "../discord/MusicPlayer/Play";
import Discord_MusicPlayer_Skip from "../discord/MusicPlayer/Skip";
import Discord_MusicPlayer_Join from "../discord/MusicPlayer/Join";
import Discord_MusicPlayer_Leave from "../discord/MusicPlayer/Leave";
import Discord_MusicPlayer_Queue from "../discord/MusicPlayer/Queue";
import Discord_MusicPlayer_Search from "../discord/MusicPlayer/Search";
import Discord_MusicPlayer_Search_NowPlaying from "../discord/MusicPlayer/NowPlaying";
import Discord_Developer_ServiceAnnouncement from "../discord/developer/ServiceAnnouncement"
import Cache from "./Cache";
@@ -24,8 +34,8 @@ class Discord {
constructor () {
this.client = new Client({
partials: ['MESSAGE', 'CHANNEL', 'REACTION'],
intents: [Intents.FLAGS.GUILDS, Intents.FLAGS.GUILD_MESSAGES, Intents.FLAGS.GUILD_MEMBERS, Intents.FLAGS.GUILD_PRESENCES]
//partials: ['MESSAGE', 'CHANNEL', 'REACTION'],
intents: [Intents.FLAGS.GUILDS, Intents.FLAGS.GUILD_MESSAGES, Intents.FLAGS.GUILD_MEMBERS, Intents.FLAGS.GUILD_PRESENCES, Intents.FLAGS.GUILD_VOICE_STATES]
});
this.loaded_module = {};
}
@@ -45,6 +55,15 @@ class Discord {
this.loaded_module["Discord_Say"] = new Discord_Say();
this.loaded_module["Discord_osu"] = new Discord_osu();
this.loaded_module["Discord_UserInfo"] = new Discord_UserInfo();
this.loaded_module["Discord_Stats"] = new Discord_Stats();
this.loaded_module["Discord_MusicPlayer_Play"] = new Discord_MusicPlayer_Play();
this.loaded_module["Discord_MusicPlayer_Skip"] = new Discord_MusicPlayer_Skip();
this.loaded_module["Discord_MusicPlayer_Join"] = new Discord_MusicPlayer_Join();
this.loaded_module["Discord_MusicPlayer_Leave"] = new Discord_MusicPlayer_Leave();
this.loaded_module["Discord_MusicPlayer_Queue"] = new Discord_MusicPlayer_Queue();
this.loaded_module["Discord_MusicPlayer_Search"] = new Discord_MusicPlayer_Search();
this.loaded_module["Discord_MusicPlayer_Search_NowPlaying"] = new Discord_MusicPlayer_Search_NowPlaying();
this.loaded_module["MembershipScreening"] = new Discord_MembershipScreening();
+327
View File
@@ -0,0 +1,327 @@
import playdl, { YouTubeVideo, InfoData } from "play-dl";
import { Message, VoiceChannel, Snowflake, TextChannel, StageChannel, Guild } from "discord.js";
import { AudioPlayer, VoiceConnection, createAudioPlayer, joinVoiceChannel, createAudioResource, StreamType, VoiceConnectionStatus, AudioPlayerStatus, NoSubscriberBehavior, VoiceConnectionDestroyedState, VoiceConnectionState, DiscordGatewayAdapterCreator, AudioResource } from "@discordjs/voice";
import { EventEmitter } from "stream";
import DiscordProvider from "./Discord";
import Environment from "./Environment";
export type ValidTracks = YouTubeVideo;
if (Environment.get().YOUTUBE_COOKIE_BASE64) {
playdl.setToken({
youtube: {
cookie: Buffer.from(Environment.get().YOUTUBE_COOKIE_BASE64, 'base64').toString()
}
})
}
export class Queue {
public track: (ValidTracks)[] = [];
}
export class YouTubeLink {
public videoId: string;
public list?: string;
constructor(videoId: string, list: string) {
this.videoId = videoId;
this.list = list;
}
}
export class PlayerPlayingEvent {
public instance: DiscordMusicPlayerInstance;
constructor(instance: DiscordMusicPlayerInstance) {
this.instance = instance;
}
}
export class DiscordMusicPlayerInstance {
public queue: Queue;
public player: AudioPlayer;
public textChannel?: TextChannel;
public voiceChannel: (VoiceChannel | StageChannel);
public voiceConnection?: VoiceConnection;
public readonly events: EventEmitter;
constructor({ voiceChannel }: { voiceChannel: (VoiceChannel | StageChannel) }) {
this.queue = new Queue();
this.player = createAudioPlayer({
behaviors: {
noSubscriber: NoSubscriberBehavior.Pause,
maxMissedFrames: 1000
}
});
this.voiceChannel = voiceChannel;
this.events = new EventEmitter();
this.player.on(AudioPlayerStatus.Idle, async (oldStage, newStage) => {
//The player stopped
if (newStage.status === AudioPlayerStatus.Idle && oldStage.status !== AudioPlayerStatus.Idle) {
if (this.queue.track.length !== 0) {
this.queue.track.shift();
if (this.queue.track.length > 0) {
this.playTrack(this.queue.track[0]);
}
}
}
});
this.player.on(AudioPlayerStatus.Playing, (oldState: any, newState: any) => {
this.events.emit('playing', new PlayerPlayingEvent(this));
});
}
public isReady() {
if (!this.voiceConnection) return false;
return this.voiceConnection.state.status === VoiceConnectionStatus.Ready;
}
public isConnected() {
if (!this.voiceConnection) return false;
if (this.voiceConnection.state.status === VoiceConnectionStatus.Destroyed) return false;
if (this.voiceConnection.state.status === VoiceConnectionStatus.Disconnected) return false;
return true;
}
public joinVoiceChannel(voiceChannel: (VoiceChannel | StageChannel), textChannel?: TextChannel) {
const permissions = voiceChannel.permissionsFor(DiscordProvider.client.user!);
if (!permissions)
throw new Error("No permissions");
if (!voiceChannel.joinable || !permissions.has("CONNECT"))
throw new Error("No permissions");
if (textChannel)
this.textChannel = textChannel;
this.voiceConnection = joinVoiceChannel({
channelId: this.voiceChannel.id,
guildId: this.voiceChannel.guild.id,
adapterCreator: this.voiceChannel.guild.voiceAdapterCreator
});
this.voiceConnection.on(VoiceConnectionStatus.Ready, (oldState: VoiceConnectionState, newState: VoiceConnectionState) => {
let currentVC = DiscordProvider.client.guilds.cache.get(voiceChannel.guild.id)?.me?.voice.channel;
if (currentVC && currentVC.id !== this.voiceChannel.id) {
this.voiceChannel = currentVC;
}
});
this.voiceConnection.on(VoiceConnectionStatus.Disconnected, (oldState: VoiceConnectionState, newState: VoiceConnectionState) => {
// TODO: Handle on Disconnect by user (Right-click > Disconnect)
});
}
public async leaveVoiceChannel() {
if (this.player)
this.player.pause();
if (DiscordProvider.client.guilds.cache.get(this.voiceChannel.guild.id)?.me?.voice) {
await DiscordProvider.client.guilds.cache.get(this.voiceChannel.guild.id)?.me?.voice.disconnect();
}
}
public addTrackToQueue(track: (ValidTracks)) {
if (this.queue.track.length === 0) {
this.queue.track.push(track);
this.playTrack(this.queue.track[0]);
return;
}
this.queue.track.push(track);
}
public async playTrack(track: ValidTracks) {
if (!this.voiceConnection) throw new Error("No voice connection");
const stream = await playdl.stream(track.url);
const resource = createAudioResource(stream.stream, {
inputType: stream.type
});
this.player.play(resource)
this.voiceConnection.subscribe(this.player)
}
public async skipTrack() {
if (!this.voiceConnection) throw new Error("No voice connection");
if (this.queue.track.length > 1) {
this.queue.track.shift();
this.playTrack(this.queue.track[0]);
}
else {
this.queue.track.shift();
this.player.stop();
}
}
public async destroy() {
await this.leaveVoiceChannel();
if (this.voiceConnection) {
this.voiceConnection.removeAllListeners();
if (this.voiceConnection.state.status !== 'destroyed')
this.voiceConnection.destroy();
}
if (this.player) {
this.player.removeAllListeners();
this.player.stop(true);
}
this.queue.track = [];
this.textChannel = undefined;
this.voiceConnection = undefined;
}
}
class DiscordMusicPlayer {
public GuildQueue = new Map();
public getGuildInstance(guildId: Snowflake): (DiscordMusicPlayerInstance | null) {
if (!this.isGuildInstanceExists(guildId)) return null;
return this.GuildQueue.get(guildId);
}
public isGuildInstanceExists(guildId: Snowflake) {
return this.GuildQueue.has(guildId);
}
public createGuildInstance(guildId: Snowflake, voiceChannel: (VoiceChannel | StageChannel)) {
this.GuildQueue.set(guildId, new DiscordMusicPlayerInstance({ voiceChannel }));
}
public async destoryGuildInstance(guild: (Guild | Snowflake)) {
let guildId: Snowflake = guild instanceof Guild ? guild.id : guild;
if (this.isGuildInstanceExists(guildId)) {
await this.GuildQueue.get(guildId).destroy();
this.GuildQueue.delete(guildId);
}
}
public async searchYouTubeByQuery(query: string) {
const searched: YouTubeVideo[] = await playdl.search(query, { source: { youtube: "video" } });
if (searched.length == 0) return null;
return searched;
}
public async searchYouTubeByYouTubeLink(youtubeLink: YouTubeLink) {
// Search the url
const searched: YouTubeVideo[] = await playdl.search("https://www.youtube.com/watch?v=" + youtubeLink.videoId, { source: { youtube: "video" } });
for (let video of searched) {
if (video.id === youtubeLink.videoId)
return video;
}
// Serch the video Id
const searched2: YouTubeVideo[] = await playdl.search(youtubeLink.videoId, { source: { youtube: "video" } });
for (let video of searched2) {
if (video.id === youtubeLink.videoId)
return video;
}
// Last resort, search the title
const videoInfo = await playdl.video_basic_info("https://www.youtube.com/watch?v=" + youtubeLink.videoId);
if (videoInfo?.video_details?.title) {
const searched: YouTubeVideo[] = await playdl.search(videoInfo.video_details.title, { source: { youtube: "video" } });
for (let video of searched) {
if (video.id === youtubeLink.videoId)
return video;
}
}
let yt_info = await playdl.video_info("https://www.youtube.com/watch?v=" + youtubeLink.videoId);
if(yt_info) {
return new YouTubeVideo({
id: yt_info.video_details.id,
url: yt_info.video_details.url,
type: yt_info.video_details.type,
title: yt_info.video_details.title,
description: yt_info.video_details.description,
durationRaw: yt_info.video_details.durationRaw,
durationInSec: yt_info.video_details.durationInSec,
uploadedAt: yt_info.video_details.uploadedAt,
upcoming: yt_info.video_details.upcoming,
views: yt_info.video_details.views,
thumbnails: yt_info.video_details.thumbnails,
channel: yt_info.video_details.channel,
likes: yt_info.video_details.likes,
live: yt_info.video_details.live,
private: yt_info.video_details.private,
tags: yt_info.video_details.tags,
discretionAdvised: yt_info.video_details.discretionAdvised,
music: yt_info.video_details.music
});
}
return null;
}
public isYouTubeLink(link: string): boolean {
try {
this.parseYouTubeLink(link);
return true;
} catch (err) {
return false;
}
}
public parseYouTubeLink(query: string): YouTubeLink {
if (query.startsWith('https://www.youtube.com/watch?v=') || query.startsWith('http://www.youtube.com/watch?v=')) {
let data = this.parseURLQuery(query);
if (!data.v)
throw new Error('YouTube link is invalid');
return {
videoId: data.v,
list: (data.list ? data.list : undefined)
}
}
else if (query.startsWith('https://youtu.be/') || query.startsWith('http://youtu.be/')) {
//Get youtube video id after the url
let videoId = query.split('/')[3];
if (!videoId) throw new Error('YouTube link is invalid');
if (query.split('/')[4]) throw new Error('YouTube link is invalid');
return {
videoId: videoId
}
}
else {
throw new Error('YouTube link is invalid');
}
}
private parseURLQuery(query: string) {
let queryObject: any = {};
if (query.indexOf('?') >= 0) {
let queryString = query.split('?')[1];
let queryArray = queryString.split('&');
for (let i = 0; i < queryArray.length; i++) {
let queryPair = queryArray[i].split('=');
queryObject[queryPair[0]] = queryPair[1];
}
}
return queryObject;
}
}
const DiscordMusicPlayer_Instance = new DiscordMusicPlayer();
export default DiscordMusicPlayer_Instance;
+3 -1
View File
@@ -41,6 +41,7 @@ class Environment {
const PRIVATE_BOT = process.env.PRIVATE_BOT;
const OSU_API_KEY = process.env.OSU_API_KEY;
const YOUTUBE_COOKIE_BASE64 = process.env.YOUTUBE_COOKIE_BASE64;
return {
NODE_ENV,
@@ -49,7 +50,8 @@ class Environment {
DEVELOPER_IDS,
PRIVATE_BOT,
OSU_API_KEY
OSU_API_KEY,
YOUTUBE_COOKIE_BASE64
};
}
+4 -2
View File
@@ -65,7 +65,9 @@ export async function sendMessage(channel: TextChannel | DMChannel | BaseGuildTe
try { message = await channel.send(options); }
catch(error) {
if(typeof user === 'undefined') return;
if(typeof user === 'undefined') {
return Logger.error(`Cannot find available destinations to send the message CID: ${channel.id} C_ERR: ${error}`);
}
try { message = await user.send(options); }
catch(errorDM) {
Logger.error(`Cannot find available destinations to send the message CID: ${channel.id} UID: ${user.id} C_ERR: ${error} DM_ERR: ${errorDM}`);
@@ -99,7 +101,7 @@ export async function sendMessageOrInteractionResponse(data: Message | Interacti
const isSlashCommand = data instanceof Interaction && data.isCommand();
const isMessage = data instanceof Message;
if(isSlashCommand) {
if(isSlashCommand || (data instanceof Interaction && data.isSelectMenu())) {
if(!data.replied) {
let message;
try {
+141 -23
View File
@@ -38,6 +38,54 @@
resolved "https://registry.yarnpkg.com/@discordjs/collection/-/collection-0.4.0.tgz#b6488286a1cc7b41b644d7e6086f25a1c1e6f837"
integrity sha512-zmjq+l/rV35kE6zRrwe8BHqV78JvIh2ybJeZavBi5NySjWXqN3hmmAKg7kYMMXSeiWtSsMoZ/+MQi0DiQWy2lw==
"@discordjs/node-pre-gyp@^0.4.2":
version "0.4.2"
resolved "https://registry.yarnpkg.com/@discordjs/node-pre-gyp/-/node-pre-gyp-0.4.2.tgz#37dd27f8d353eeb87372fcce059c5249550b37ab"
integrity sha512-V239Czn+DXFGLhhuccwEDBoTdgMGrRu30dOlzm1GzrSIjwFj01ZJerNX7x+CEX1NG1Q/1gGfOOkeZFNHjycrRA==
dependencies:
detect-libc "^1.0.3"
https-proxy-agent "^5.0.0"
make-dir "^3.1.0"
node-fetch "^2.6.5"
nopt "^5.0.0"
npmlog "^5.0.1"
rimraf "^3.0.2"
semver "^7.3.5"
tar "^6.1.11"
"@discordjs/opus@^0.7.0":
version "0.7.0"
resolved "https://registry.yarnpkg.com/@discordjs/opus/-/opus-0.7.0.tgz#69e9611a6412ea470f8e640f54478da89326ca3b"
integrity sha512-3Xxa3dh7taSDwBAR5fLALZ/KTxvbMmHCMxYLYve6NlPO7Ms1CLmKqp/R4ZoVzkRGQVUVWEhaB1s0v9jfa2tfDg==
dependencies:
"@discordjs/node-pre-gyp" "^0.4.2"
node-addon-api "^4.2.0"
"@discordjs/rest@^0.3.0":
version "0.3.0"
resolved "https://registry.yarnpkg.com/@discordjs/rest/-/rest-0.3.0.tgz#89e06a42b168c91598891d6bf860353e28fba5d2"
integrity sha512-F9aeP3odlAlllM1ciBZLdd+adiAyBj4VaZBejj4UMj4afE2wfCkNTGvYYiRxrXUE9fN7e/BuDP2ePl0tVA2m7Q==
dependencies:
"@discordjs/collection" "^0.4.0"
"@sapphire/async-queue" "^1.1.9"
"@sapphire/snowflake" "^3.0.1"
discord-api-types "^0.26.1"
form-data "^4.0.0"
node-fetch "^2.6.5"
tslib "^2.3.1"
"@discordjs/voice@^0.8.0":
version "0.8.0"
resolved "https://registry.yarnpkg.com/@discordjs/voice/-/voice-0.8.0.tgz#5d790fc25b883698f6eb7762efe1af00b6440947"
integrity sha512-o0JfVLMs3eLjUzPf6oxMydEeum40I7xzfUc66SLN+RrKpSAsTbngf5qnCF53nm+KDNSvrwg1AZqNm4LEAdxJIA==
dependencies:
"@types/ws" "^8.2.0"
discord-api-types "^0.26.1"
prism-media "^1.3.2"
tiny-typed-emitter "^2.1.0"
tslib "^2.3.1"
ws "^8.4.2"
"@mapbox/node-pre-gyp@^1.0.0":
version "1.0.8"
resolved "https://registry.yarnpkg.com/@mapbox/node-pre-gyp/-/node-pre-gyp-1.0.8.tgz#32abc8a5c624bc4e46c43d84dfb8b26d33a96f58"
@@ -71,14 +119,19 @@
integrity sha512-bHYubuItSN/DGYo36aDu7xJiJmK52JOSHs4MK+KbceAtwS20BCWadRgtpQ3iZ2EXfN/B1T0iCXlNraaNwnpU2w==
"@sapphire/async-queue@^1.1.9":
version "1.1.9"
resolved "https://registry.yarnpkg.com/@sapphire/async-queue/-/async-queue-1.1.9.tgz#ce69611c8753c4affd905a7ef43061c7eb95c01b"
integrity sha512-CbXaGwwlEMq+l1TRu01FJCvySJ1CEFKFclHT48nIfNeZXaAAmmwwy7scUKmYHPUa3GhoMp6Qr1B3eAJux6XgOQ==
version "1.2.0"
resolved "https://registry.yarnpkg.com/@sapphire/async-queue/-/async-queue-1.2.0.tgz#7a56afd318101d338433d7180ebd6af349243268"
integrity sha512-O5ND5Ljpef86X5oy8zXorQ754TMjWALcPSAgPBu4+76HLtDTrNoDyzU2uGE2G4A8Wv51u0MXHzGQ0WZ4GMtpIw==
"@sapphire/snowflake@^3.0.1":
version "3.1.0"
resolved "https://registry.yarnpkg.com/@sapphire/snowflake/-/snowflake-3.1.0.tgz#1c2d9781d3b1de0bcb02b7079898947ce754d394"
integrity sha512-K+OiqXSx4clIaXcoaghrCV56zsm3bZZ5SBpgJkgvAKegFFdETMntHviUfypjt8xVleIuDaNyQA4APOIl3BMcxg==
"@sindresorhus/is@^4.0.1", "@sindresorhus/is@^4.2.0":
version "4.3.0"
resolved "https://registry.yarnpkg.com/@sindresorhus/is/-/is-4.3.0.tgz#344fd9bf808a84567ba563d00cc54b2f428dbab1"
integrity sha512-wwOvh0eO3PiTEivGJWiZ+b946SlMSb4pe+y+Ur/4S87cwo09pYi+FWHHnbrM3W9W7cBYKDqQXcrFYjYUCOJUEQ==
version "4.4.0"
resolved "https://registry.yarnpkg.com/@sindresorhus/is/-/is-4.4.0.tgz#e277e5bdbdf7cb1e20d320f02f5e2ed113cd3185"
integrity sha512-QppPM/8l3Mawvh4rn9CNEYIU9bxpXUCRMaX9yUpvBk1nMKusLKpfXGDEKExKaPhLzcn3lzil7pR6rnJ11HgeRQ==
"@types/bluebird@^3.5.36":
version "3.5.36"
@@ -99,9 +152,9 @@
form-data "^3.0.0"
"@types/node@*":
version "17.0.12"
resolved "https://registry.yarnpkg.com/@types/node/-/node-17.0.12.tgz#f7aa331b27f08244888c47b7df126184bc2339c5"
integrity sha512-4YpbAsnJXWYK/fpTVFlMIcUIho2AYCi4wg5aNPrG1ng7fn/1/RZfCIpRCiBX+12RVa34RluilnvCqD+g3KiSiA==
version "17.0.13"
resolved "https://registry.yarnpkg.com/@types/node/-/node-17.0.13.tgz#5ed7ed7c662948335fcad6c412bb42d99ea754e3"
integrity sha512-Y86MAxASe25hNzlDbsviXl8jQHb0RDvKt4c40ZJQ1Don0AAL0STLZSs4N+6gLEO55pedy7r2cLwS+ZDxPm/2Bw==
"@types/node@^16.11.6":
version "16.11.21"
@@ -128,7 +181,7 @@
resolved "https://registry.yarnpkg.com/@types/validator/-/validator-13.7.1.tgz#cdab1b4779f6b1718a08de89d92d2603b71950cb"
integrity sha512-I6OUIZ5cYRk5lp14xSOAiXjWrfVoMZVjDuevBYgQDYzZIjsf2CAISpEcXOkFAtpAHbmWIDLcZObejqny/9xq5Q==
"@types/ws@^8.2.2":
"@types/ws@^8.2.0", "@types/ws@^8.2.2":
version "8.2.2"
resolved "https://registry.yarnpkg.com/@types/ws/-/ws-8.2.2.tgz#7c5be4decb19500ae6b3d563043cd407bf366c21"
integrity sha512-NOn5eIcgWLOo6qW8AcuLZ7G8PycXu0xTxxkS6Q18VWFxgPUSOwV0pBj2a/4viNZVu25i7RIB7GttdkAIUUXOOg==
@@ -178,7 +231,7 @@ arg@^4.1.0:
resolved "https://registry.yarnpkg.com/arg/-/arg-4.1.3.tgz#269fc7ad5b8e42cb63c896d5666017261c144089"
integrity sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==
async@^3.2.3:
async@^3.1.0:
version "3.2.3"
resolved "https://registry.yarnpkg.com/async/-/async-3.2.3.tgz#ac53dafd3f4720ee9e8a160628f18ea91df196c9"
integrity sha512-spZRyzKL5l5BZQrr/6m/SqFdBN0q3OCI0f9rjfBzCMBIP4p75P620rR3gTmaksNOhmzgdxcaxdNfMy6anrbM0g==
@@ -223,6 +276,13 @@ buffer-from@^1.0.0:
resolved "https://registry.yarnpkg.com/buffer-from/-/buffer-from-1.1.2.tgz#2b146a6fd72e80b4f55d255f35ed59a3a9a41bd5"
integrity sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==
bufferutil@^4.0.6:
version "4.0.6"
resolved "https://registry.yarnpkg.com/bufferutil/-/bufferutil-4.0.6.tgz#ebd6c67c7922a0e902f053e5d8be5ec850e48433"
integrity sha512-jduaYOYtnio4aIAyc6UbvPCVcgq7nYpVnucyxr6eCYg/Woad9Hf/oxxBRDnGGjPfjUm6j5O/uBWhIu4iLebFaw==
dependencies:
node-gyp-build "^4.3.0"
call-bind@^1.0.0:
version "1.0.2"
resolved "https://registry.yarnpkg.com/call-bind/-/call-bind-1.0.2.tgz#b1d4e89e688119c3c9a903ad30abb2f6a919be3c"
@@ -404,7 +464,7 @@ discord-api-types@^0.24.0:
resolved "https://registry.yarnpkg.com/discord-api-types/-/discord-api-types-0.24.0.tgz#9e429b8a1ddb4147134dfb3109093422de7ec549"
integrity sha512-X0uA2a92cRjowUEXpLZIHWl4jiX1NsUpDhcEOpa1/hpO1vkaokgZ8kkPtPih9hHth5UVQ3mHBu/PpB4qjyfJ4A==
discord-api-types@^0.26.0:
discord-api-types@^0.26.0, discord-api-types@^0.26.1:
version "0.26.1"
resolved "https://registry.yarnpkg.com/discord-api-types/-/discord-api-types-0.26.1.tgz#726f766ddc37d60da95740991d22cb6ef2ed787b"
integrity sha512-T5PdMQ+Y1MEECYMV5wmyi9VEYPagEDEi4S0amgsszpWY0VB9JJ/hEvM6BgLhbdnKky4gfmZEXtEEtojN8ZKJQQ==
@@ -657,12 +717,24 @@ kuler@^2.0.0:
resolved "https://registry.yarnpkg.com/kuler/-/kuler-2.0.0.tgz#e2c570a3800388fb44407e851531c1d670b061b3"
integrity sha512-Xq9nH7KlWZmXAtodXDDRE7vs6DU1gTU8zYDHDiWLSip45Egwq3plLHzPn27NgvzL2r1LMPC1vdqh98sQxtqj4A==
libsodium-wrappers@^0.7.9:
version "0.7.9"
resolved "https://registry.yarnpkg.com/libsodium-wrappers/-/libsodium-wrappers-0.7.9.tgz#4ffc2b69b8f7c7c7c5594a93a4803f80f6d0f346"
integrity sha512-9HaAeBGk1nKTRFRHkt7nzxqCvnkWTjn1pdjKgcUnZxj0FyOP4CnhgFhMdrFfgNsukijBGyBLpP2m2uKT1vuWhQ==
dependencies:
libsodium "^0.7.0"
libsodium@^0.7.0:
version "0.7.9"
resolved "https://registry.yarnpkg.com/libsodium/-/libsodium-0.7.9.tgz#4bb7bcbf662ddd920d8795c227ae25bbbfa3821b"
integrity sha512-gfeADtR4D/CM0oRUviKBViMGXZDgnFdMKMzHsvBdqLBHd9ySi6EtYnmuhHVDDYgYpAO8eU8hEY+F8vIUAPh08A==
lodash.isequal@^4.5.0:
version "4.5.0"
resolved "https://registry.yarnpkg.com/lodash.isequal/-/lodash.isequal-4.5.0.tgz#415c4478f2bcc30120c22ce10ed3226f7d3e18e0"
integrity sha1-QVxEePK8wwEgwizhDtMib30+GOA=
logform@^2.3.2:
logform@^2.2.0, logform@^2.3.2:
version "2.3.2"
resolved "https://registry.yarnpkg.com/logform/-/logform-2.3.2.tgz#68babe6a74ab09a1fd15a9b1e6cbc7713d41cb5b"
integrity sha512-V6JiPThZzTsbVRspNO6TmHkR99oqYTs8fivMBYQkjZj6rxW92KxtDCPE6IkAk1DNBnYKNkjm4jYBm6JDUcyhOA==
@@ -761,11 +833,16 @@ ms@^2.1.1:
resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.3.tgz#574c8138ce1d2b5861f0b44579dbadd60c6615b2"
integrity sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==
nan@^2.15.0:
nan@^2.14.0, nan@^2.15.0:
version "2.15.0"
resolved "https://registry.yarnpkg.com/nan/-/nan-2.15.0.tgz#3f34a473ff18e15c1b5626b62903b5ad6e665fee"
integrity sha512-8ZtvEnA2c5aYCZYd1cvgdnU6cqwixRoYg70xPLWUws5ORTa/lnw+u4amixRS/Ac5U5mQVgp9pnlSUnbNWFaWZQ==
node-addon-api@^4.2.0:
version "4.3.0"
resolved "https://registry.yarnpkg.com/node-addon-api/-/node-addon-api-4.3.0.tgz#52a1a0b475193e0928e98e0426a0d1254782b77f"
integrity sha512-73sE9+3UaLYYFmDsFZnqCInzPyh3MqIwZO9cw58yIqAZhONrrabrYyYe3TuIqtIiOuTXVhsGau8hcrhhwSsDIQ==
node-fetch@^2.6.1, node-fetch@^2.6.5:
version "2.6.7"
resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-2.6.7.tgz#24de9fba827e3b4ae44dc8b20256a379160052ad"
@@ -773,6 +850,11 @@ node-fetch@^2.6.1, node-fetch@^2.6.5:
dependencies:
whatwg-url "^5.0.0"
node-gyp-build@^4.3.0:
version "4.3.0"
resolved "https://registry.yarnpkg.com/node-gyp-build/-/node-gyp-build-4.3.0.tgz#9f256b03e5826150be39c764bf51e993946d71a3"
integrity sha512-iWjXZvmboq0ja1pUGULQBexmxq8CV4xBhX7VDOTbL7ZR4FOowwY/VOtRxBN/yKxmdGoIp4j5ysNT4u3S2pDQ3Q==
node-osu@^2.2.1:
version "2.2.1"
resolved "https://registry.yarnpkg.com/node-osu/-/node-osu-2.2.1.tgz#2dcf9bef2e92490cab7a215842559f003d702ac1"
@@ -861,6 +943,23 @@ ping@^0.4.1:
q "1.x"
underscore "^1.12.0"
play-audio@^0.5.2:
version "0.5.2"
resolved "https://registry.yarnpkg.com/play-audio/-/play-audio-0.5.2.tgz#191dd45b95ff64ae20bbd718671da4e987d4edd1"
integrity sha512-ZAqHUKkQLix2Iga7pPbsf1LpUoBjcpwU93F1l3qBIfxYddQLhxS6GKmS0d3jV8kSVaUbr6NnOEcEMFvuX93SWQ==
play-dl@^1.8.6:
version "1.8.6"
resolved "https://registry.yarnpkg.com/play-dl/-/play-dl-1.8.6.tgz#483420c0273ad38f0a07346dcd78fb76ce31c2c9"
integrity sha512-NOnSmg6ikdrf6x6R4ML5QY6AJS4h4KkIIynlKuDx7RVDPE7C2kowgtS+e6RRMuuAXXXESkmlWrpUb+bnZFWy1w==
dependencies:
play-audio "^0.5.2"
prism-media@^1.3.2:
version "1.3.2"
resolved "https://registry.yarnpkg.com/prism-media/-/prism-media-1.3.2.tgz#a1f04423ec15d22f3d62b1987b6a25dc49aad13b"
integrity sha512-L6UsGHcT6i4wrQhFF1aPK+MNYgjRqR2tUoIqEY+CG1NqVkMjPRKzS37j9f8GiYPlD6wG9ruBj+q5Ax+bH8Ik1g==
prisma@^3.3.0:
version "3.8.1"
resolved "https://registry.yarnpkg.com/prisma/-/prisma-3.8.1.tgz#44395cef7cbb1ea86216cb84ee02f856c08a7873"
@@ -1071,6 +1170,11 @@ text-hex@1.0.x:
resolved "https://registry.yarnpkg.com/text-hex/-/text-hex-1.0.0.tgz#69dc9c1b17446ee79a92bf5b884bb4b9127506f5"
integrity sha512-uuVGNWzgJ4yhRaNSiubPY7OjISw4sw4E5Uv0wbjp+OzcbmVU/rsT8ujgcXJhn9ypzsgr5vlzpPqP+MBBKcGvbg==
tiny-typed-emitter@^2.1.0:
version "2.1.0"
resolved "https://registry.yarnpkg.com/tiny-typed-emitter/-/tiny-typed-emitter-2.1.0.tgz#b3b027fdd389ff81a152c8e847ee2f5be9fad7b5"
integrity sha512-qVtvMxeXbVej0cQWKqVSSAHmKZEHAvxdF8HEUBFWts8h+xEo5m/lEiPakuyZ3BnCBjOD8i24kzNOiOLLgsSxhA==
to-regex-range@^5.0.1:
version "5.0.1"
resolved "https://registry.yarnpkg.com/to-regex-range/-/to-regex-range-5.0.1.tgz#1648c44aae7c8d988a326018ed72f5b4dd0392e4"
@@ -1161,6 +1265,13 @@ underscore@^1.12.0:
resolved "https://registry.yarnpkg.com/underscore/-/underscore-1.13.2.tgz#276cea1e8b9722a8dbed0100a407dda572125881"
integrity sha512-ekY1NhRzq0B08g4bGuX4wd2jZx5GnKz6mKSqFL4nqBlfyMGiG10gDFhDTMEfYmDL6Jy0FUIZp7wiRB+0BP7J2g==
utf-8-validate@^5.0.8:
version "5.0.8"
resolved "https://registry.yarnpkg.com/utf-8-validate/-/utf-8-validate-5.0.8.tgz#4a735a61661dbb1c59a0868c397d2fe263f14e58"
integrity sha512-k4dW/Qja1BYDl2qD4tOMB9PFVha/UJtxTc1cXYOe3WwA/2m0Yn4qB7wLMpJyLJ/7DR0XnTut3HsCSzDT4ZvKgA==
dependencies:
node-gyp-build "^4.3.0"
util-deprecate@^1.0.1:
version "1.0.2"
resolved "https://registry.yarnpkg.com/util-deprecate/-/util-deprecate-1.0.2.tgz#450d4dc9fa70de732762fbd2d4a28981419a0ccf"
@@ -1196,7 +1307,7 @@ wide-align@^1.1.2:
dependencies:
string-width "^1.0.2 || 2 || 3 || 4"
winston-transport@^4.4.2:
winston-transport@^4.4.0:
version "4.4.2"
resolved "https://registry.yarnpkg.com/winston-transport/-/winston-transport-4.4.2.tgz#554efe3fce229d046df006e0e3c411d240652e51"
integrity sha512-9jmhltAr5ygt5usgUTQbEiw/7RYXpyUbEAFRCSicIacpUzPkrnQsQZSPGEI12aLK9Jth4zNcYJx3Cvznwrl8pw==
@@ -1205,27 +1316,27 @@ winston-transport@^4.4.2:
readable-stream "^3.4.0"
triple-beam "^1.2.0"
winston@^3.3.3:
version "3.4.0"
resolved "https://registry.yarnpkg.com/winston/-/winston-3.4.0.tgz#7080f24b02a0684f8a37f9d5c6afb1ac23e95b84"
integrity sha512-FqilVj+5HKwCfIHQzMxrrd5tBIH10JTS3koFGbLVWBODjiIYq7zir08rFyBT4rrTYG/eaTqDcfSIbcjSM78YSw==
winston@3.3.3:
version "3.3.3"
resolved "https://registry.yarnpkg.com/winston/-/winston-3.3.3.tgz#ae6172042cafb29786afa3d09c8ff833ab7c9170"
integrity sha512-oEXTISQnC8VlSAKf1KYSSd7J6IWuRPQqDdo8eoRNaYKLvwSb5+79Z3Yi1lrl6KDpU6/VWaxpakDAtb1oQ4n9aw==
dependencies:
"@dabh/diagnostics" "^2.0.2"
async "^3.2.3"
async "^3.1.0"
is-stream "^2.0.0"
logform "^2.3.2"
logform "^2.2.0"
one-time "^1.0.0"
readable-stream "^3.4.0"
stack-trace "0.0.x"
triple-beam "^1.3.0"
winston-transport "^4.4.2"
winston-transport "^4.4.0"
wrappy@1:
version "1.0.2"
resolved "https://registry.yarnpkg.com/wrappy/-/wrappy-1.0.2.tgz#b5243d8f3ec1aa35f1364605bc0d1036e30ab69f"
integrity sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=
ws@^8.4.0:
ws@^8.4.0, ws@^8.4.2:
version "8.4.2"
resolved "https://registry.yarnpkg.com/ws/-/ws-8.4.2.tgz#18e749868d8439f2268368829042894b6907aa0b"
integrity sha512-Kbk4Nxyq7/ZWqr/tarI9yIt/+iNNFOjBXEWgTb4ydaNHBNGgvf2QHbS9fdfsndfjFlFwEd4Al+mw83YkaD10ZA==
@@ -1245,6 +1356,13 @@ yn@3.1.1:
resolved "https://registry.yarnpkg.com/yn/-/yn-3.1.1.tgz#1e87401a09d767c1d5eab26a6e4c185182d2eb50"
integrity sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==
zlib-sync@^0.1.7:
version "0.1.7"
resolved "https://registry.yarnpkg.com/zlib-sync/-/zlib-sync-0.1.7.tgz#b0b24f9428e434943e306ad586428ef8baf3ed60"
integrity sha512-UmciU6ZrIwtwPC8noMzq+kGMdiWwNRZ3wC0SbED4Ew5Ikqx14MqDPRs/Pbk+3rZPh5SzsOgUBs1WRE0iieddpg==
dependencies:
nan "^2.14.0"
zod@^3.11.6:
version "3.11.6"
resolved "https://registry.yarnpkg.com/zod/-/zod-3.11.6.tgz#e43a5e0c213ae2e02aefe7cb2b1a6fa3d7f1f483"