Compare commits

...

14 Commits

67 changed files with 11863 additions and 1922 deletions

5
.gitignore vendored
View File

@@ -141,4 +141,7 @@ docs/_book
# TODO: where does this rule come from? # TODO: where does this rule come from?
test/ test/
data/ data/
__DEBUG.js
__TEST.js

View File

@@ -1,20 +1,19 @@
# **Subsonics - Chopin** # **Subsonics - Chopin**
> Cette version est une refonte complète et intégrale de [Subsonics - Web](https://git.raphix.fr/subsonics/web) > Cette version est une refonte complète et intégrale de [Subsonics - Web](https://git.raphix.fr/subsonics/web)
### Bienvenue sur Chopin, la nouvelle version de Subsonics. ## Bienvenue sur Chopin, la nouvelle version de Subsonics
### **Fonctionnalités** ## **Fonctionnalités**
> - Lecture de vidéos depuis Youtube, Spotify et SoundClound > - Lecture de vidéos depuis Youtube, Spotify et SoundClound
> - Lecture de fichiers locaux *(Uniquement sur le site)* > - Lecture de fichiers locaux *(Uniquement sur le site)*
> - Gestion et lecture de playlist > - Gestion et lecture de playlist
> - Accéder à votre propre historique et à l'historique du Bot > - Accéder à votre propre historique et à l'historique du Bot
> - Affichage des paroles de la musique en cours > - Affichage des paroles de la musique en cours
> - Une interface refaite pour toutes les platformes. > - Une interface refaite pour toutes les platformes.
> - Récupération de vos recommendations & Playlists Spotify / Youtube > - Récupération de vos recommendations & Playlists Spotify / Youtube
Le FrontEnd est gérée par VueJS ✌et le BackEnd a été entièrement refait localement pour des réponses plus rapide et plus stable Le FrontEnd est gérée par VueJS ✌et le BackEnd a été entièrement refait localement pour des réponses plus rapide et plus stable
[CHANGELOG](https://git.raphix.fr/subsonics/chopin/src/branch/main/changelog.md) [CHANGELOG](https://git.raphix.fr/subsonics/chopin/src/branch/main/changelog.md)

3
TODOS.md Normal file
View File

@@ -0,0 +1,3 @@
# List
TODO: Récupération des recommendations, playlists Youtube et Spotify

1717
backend/package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,30 +0,0 @@
{
"name": "chopin-backend",
"version": "0.1.0",
"description": "Discord Bot for music - Fetching everywhere !",
"main": "src/main.js",
"nodemonConfig": {
"ext": "js, html",
"ignore": [
"*.json",
"*.html"
],
"delay": "2000000"
},
"scripts": {
"start": "nodemon src/main.js"
},
"keywords": [],
"author": "Raphix",
"license": "ISC",
"dependencies": {
"cors": "^2.8.5",
"discord.js": "^14.18.0",
"express": "^4.21.2",
"loguix": "^1.4.2",
"nodemon": "^3.1.9",
"socket.io": "^4.8.1",
"uuid": "^11.1.0",
"webmetrik": "^0.1.4"
}
}

View File

@@ -1,78 +0,0 @@
const { Client, GatewayIntentBits, Collection, ActivityType, REST, Routes } = require("discord.js")
const fs = require("node:fs")
const path = require("path")
const { __glob } = require("../utils/GlobalVars")
const { LogType } = require("loguix")
const config = require("../utils/Database/Configuration")
const metric = require("webmetrik")
const dlog = new LogType("Discord")
const client = new Client({
intents:[GatewayIntentBits.Guilds, GatewayIntentBits.GuildVoiceStates, GatewayIntentBits.GuildMembers],
})
//Getter for the client
function getClient() {
return client
}
function init() {
client.once('ready', () => {
dlog.log("Connexion au Bot Discord réussi ! Connecté en tant que : " + client.user.tag)
const Activity = require("./Activity")
Activity.idleActivity()
const CommandUpdater = require("./CommandUpdater")
CommandUpdater.init()
const commandManager = client.application.commands;
if (!commandManager) {
dlog.error('Command manager not available.');
} else {
commandManager.set([]);
}
dlog.step.end("d_init")
});
client.on("interactionCreate", (interaction) => {
if(!interaction.isCommand()) return;
var numberOfCommands = new metric.Metric("numberOfCommands", "Nombre de commandes éxécutées")
numberOfCommands.setValue(numberOfCommands.getValue() + 1)
const command = client.commands.get(interaction.commandName)
try {
// Create a metric to count the number of commands executed by each user
const userCommand = new metric.Metric("userCommand_" + interaction.member.user.username, "Nombre de commandes éxécutées par l'utilisateur : " + interaction.member.user.username)
userCommand.setValue(userCommand.getValue() + 1)
dlog.log(interaction.member.user.username + "-> /" + interaction.commandName)
command.execute(client, interaction)
} catch(error) {
dlog.error(interaction.member.user.username + "-> /" + interaction.commandName + " : ERREUR RENCONTRE")
dlog.error(error)
interaction.reply({content:"Erreur lors de l'éxécution de la commande !", ephemeral: true})
}
})
client.login(config.getToken())
}
module.exports = {init, getClient}

View File

@@ -1,14 +0,0 @@
const { Command } = require('../Command');
const { Embed } = require('../Embed');
const command = new Command("web", "Affiche le lien vers le site web pour contrôler le bot", (client, interaction) => {
const embed = new Embed()
embed.setColor(0xffffff)
embed.setTitle('Subsonics - Chopin')
embed.addBotPicture(client)
embed.addField('Lien',"https://subsonics.raphix.fr/")
embed.send(interaction)
})
module.exports = {command}

View File

@@ -1,33 +0,0 @@
const {Database} = require("./Database")
const {__glob} = require("../GlobalVars")
const {LogType} = require("loguix")
const path = require("path")
const clog = new LogType("Configuration")
const config = new Database("config", __glob.DATA + path.sep + "config.json", {
token: "",
report: {
channel : "",
contact : ""
}
})
function getToken() {
return config.data.token
}
function getReportChannel() {
return config.data.report.channel
}
function getReportContact() {
return config.data.report.contact
}
if(getToken() == "") {
clog.error("Impossible de démarrer sans token valide")
process.exit(1)
}
module.exports = {getToken, getReportChannel, getReportContact}

View File

@@ -1,17 +0,0 @@
# **Changelog**
- Express JS
- Vue JS
- Discord JS
- Player Youtube
## **Légende de version** :
* Version X.Y.Z
> **X** : Indique une version de travail (Période d'activité) \
> **Y** : Indique l'ajout d'une fonctionnalité \
> **Z** : Indique la modification ou la réparation d'une fonctionnalité
* Tags
> **-alpha** : Indique une version de dévelopement inutilisable \
> **-rcX** : Indique une sous-version qui ne modifie rien mais qui peux corriger un bug de facon très superficiel

6944
package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

47
package.json Normal file
View File

@@ -0,0 +1,47 @@
{
"name": "chopin-backend",
"version": "1.1.1",
"description": "Discord Bot for music - Fetching everywhere !",
"main": "src/main.js",
"nodemonConfig": {
"ext": "js, html",
"ignore": [
"*.json",
"*.html"
],
"delay": "2000000"
},
"scripts": {
"start": "nodemon src/main.js",
"stop": "ssh raphix@raphix.fr sudo -S -u gitlab-ci pm2 stop 'Subsonics'",
"restart": "ssh raphix@raphix.fr sudo -S -u gitlab-ci pm2 start 'Subsonics'"
},
"keywords": [],
"author": "Raphix",
"license": "ISC",
"dependencies": {
"@discordjs/voice": "^0.18.0",
"@distube/ytdl-core": "^4.16.10",
"@distube/ytsr": "2.0.4",
"cors": "^2.8.5",
"discord-player": "^7.1.0",
"discord.js": "^14.18.0",
"express": "^4.21.2",
"ffmpeg-static": "^5.2.0",
"ffprobe": "^1.1.2",
"ffprobe-static": "^3.1.0",
"fluent-ffmpeg": "^2.1.3",
"googleapis": "^149.0.0",
"libsodium-wrappers": "^0.7.15",
"loguix": "^1.4.2",
"mime-types": "^3.0.1",
"nodemon": "^3.1.10",
"pm2": "^5.4.3",
"socket.io": "^4.8.1",
"soundcloud.ts": "^0.6.3",
"spotify-web-api-node": "^5.0.2",
"uuid": "^11.1.0",
"webmetrik": "^0.1.4",
"ytfps": "^1.2.0"
}
}

View File

@@ -13,10 +13,7 @@ function setMusicActivity(songName, artistName, imageUrl) {
const client = bot.getClient() const client = bot.getClient()
client.user.setActivity(`${songName} - ${artistName}`,{ client.user.setActivity(`${songName} - ${artistName}`,{
type: ActivityType.Listening, type: ActivityType.Listening,
/*assets: { url: imageUrl
largeImage: imageUrl,
largeText: songName
}*/
}); });
dlog.log(`Activité mise à jour : ${songName} - ${artistName}`) dlog.log(`Activité mise à jour : ${songName} - ${artistName}`)
} }

231
src/discord/Bot.js Normal file
View File

@@ -0,0 +1,231 @@
const { Client, GatewayIntentBits, Collection, ActivityType, REST, Routes } = require("discord.js")
const fs = require("node:fs")
const path = require("path")
const { __glob } = require("../utils/GlobalVars")
const { LogType } = require("loguix")
const config = require("../utils/Database/Configuration")
const metric = require("webmetrik")
const { Player } = require("../player/Player")
const {refreshAllUserInformation} = require("../server/auth/User")
const dlog = new LogType("Discord")
const glog = new LogType("GuildUpdater")
dlog.log("Initialisation du Bot Discord")
const membersVoices = new Map()
const timers = new Map()
const guilds = new Map()
var operational = false
const client = new Client({
intents:[GatewayIntentBits.Guilds, GatewayIntentBits.GuildVoiceStates, GatewayIntentBits.GuildMembers],
})
//Getter for the client
function getClient() {
return client
}
function getGuilds() {
return guilds
}
function getMembersVoices() {
return membersVoices
}
function isReady() {
return operational
}
function getGuildMembers(guildId) {
const guild = client.guilds.cache.get(guildId)
if(!guild) {
dlog.error("Guild not found: " + guildId)
return []
}
return guild.members.cache.map(member => member.user.id)
}
function getChannel(guildId, channelId) {
return client.guilds.cache.get(guildId).channels.cache.get(channelId)
}
function init() {
client.once('ready', async () => {
dlog.log("Connexion au Bot Discord réussi ! Connecté en tant que : " + client.user.tag)
await refreshGuilds()
await refreshAllUserInformation()
const Activity = require("./Activity")
Activity.idleActivity()
const CommandUpdater = require("./CommandUpdater")
CommandUpdater.init()
const commandManager = client.application.commands
if (!commandManager) {
dlog.error('Command manager not available.')
} else {
commandManager.set([])
}
dlog.step.end("d_init")
operational = true
})
client.on("interactionCreate", (interaction) => {
if(!interaction.isCommand()) return;
var numberOfCommands = new metric.Metric("numberOfCommands", "Nombre de commandes éxécutées")
numberOfCommands.setValue(numberOfCommands.getValue() + 1)
const command = client.commands.get(interaction.commandName)
try {
// Create a metric to count the number of commands executed by each user
const userCommand = new metric.Metric("userCommand_" + interaction.member.user.username, "Nombre de commandes éxécutées par l'utilisateur : " + interaction.member.user.username)
userCommand.setValue(userCommand.getValue() + 1)
dlog.log(interaction.member.user.username + "-> /" + interaction.commandName)
command.execute(client, interaction)
} catch(error) {
dlog.error(interaction.member.user.username + "-> /" + interaction.commandName + " : ERREUR RENCONTRE")
dlog.error(error)
interaction.reply({content:"Erreur lors de l'éxécution de la commande !", ephemeral: true})
}
})
client.on("guildMemberAdd", async (member) => {
dlog.log("Nouveau membre dans la guilde : " + member.guild.name + " (" + member.guild.id + ") - Membre : " + member.user.username + " (" + member.user.id + ")")
await refreshGuilds()
process.emit("USERS_UPDATE")
})
client.on("guildMemberRemove", async (member) => {
dlog.log("Membre quitté la guilde : " + member.guild.name + " (" + member.guild.id + ") - Membre : " + member.user.username + " (" + member.user.id + ")")
await refreshGuilds()
membersVoices.delete(member.user.id)
process.emit("USERS_UPDATE")
})
// If a new guild is added, we will add it to the guilds map
client.on("guildCreate", async (guild) => {
await refreshGuilds()
glog.log("Guilde ajoutée : " + guild.name + " (" + guild.id + ")")
process.emit("USERS_UPDATE")
})
client.on("guildDelete", (guild) => {
dlog.log("Guilde supprimée : " + guild.name)
guilds.delete(guild.id)
glog.log("Guilde supprimée : " + guild.name + " (" + guild.id + ")")
process.emit("USERS_UPDATE")
})
client.on('guildUpdate', async () => {
await refreshGuilds()
process.emit("USERS_UPDATE")
})
client.on("voiceStateUpdate", (oldMember, newMember) => {
membersVoices.set(newMember.id, {
guildId: newMember.guild.id,
channelId: newMember.channelId,
})
const player = new Player(newMember.guild.id)
if(player.connection && player.channelId) {
client.channels.fetch(player.channelId).then(channel => {
if(channel.members.size <= 1) {
// If the player is alone in the channel, we will destroy it in 10 minutes
// 10 minutes = 600000 ms
// 10 second = 10000 ms
timers.set(newMember.guild.id, setTimeout(() => {
const getPlayer = new Player(newMember.guild.id)
if(getPlayer.connection && player.channelId) {
getPlayer.leave()
dlog.log("[Automatic Task] Guild Id :" + newMember.guild.id + " - Player supprimé : " + channel.name)
}
}, 600000))
dlog.log("[Automatic Task] Guild Id :" + newMember.guild.id + " - Player supprimé dans 10 minutess : " + channel.name)
} else {
dlog.log("[Automatic Task] Guild Id :" + newMember.guild.id + " - Player n'est pas seul dans le channel : " + channel.name)
clearTimeout(timers.get(newMember.guild.id))
timers.delete(newMember.guild.id)
}
})
}
})
client.login(config.getToken())
}
async function refreshGuilds() {
glog.step.init("d_refresh_guilds", "Rafraichissement des guildes")
await client.guilds.fetch()
for(const guild of client.guilds.cache.values()) {
await guild.members.fetch()
var allMembersOfGuild = guild.members.cache.map(member => member.user.id)
const missingPermissions = checkRequiredPermission(guild.members.me)
if(missingPermissions.length > 0) {
dlog.error("Le bot n'a pas les permissions nécessaires pour rejoindre la guilde : " + guild.name)
guild.leave()
return
}
guilds.set(guild.id, {
id: guild.id,
name: guild.name,
allMembers: allMembersOfGuild,
icon: guild.iconURL(),
banner: guild.bannerURL(),
description: guild.description,
features: guild.features,
owner: guild.ownerId,
joinedAt: guild.joinedAt,
createdAt: guild.createdAt,
})
glog.log("Guilde rafraichie : " + guild.name + " (" + guild.id + ")")
}
glog.step.end("d_refresh_guilds")
}
function checkRequiredPermission(guildMember) {
const requiredPermissions = [
'CreateInstantInvite', 'AddReactions',
'Stream', 'ViewChannel',
'SendMessages', 'SendTTSMessages',
'EmbedLinks', 'AttachFiles',
'ReadMessageHistory', 'UseExternalEmojis',
'Connect', 'Speak',
'UseVAD', 'ChangeNickname',
'UseApplicationCommands', 'RequestToSpeak',
'CreatePublicThreads', 'CreatePrivateThreads',
'UseExternalStickers', 'SendMessagesInThreads',
'UseEmbeddedActivities', 'UseSoundboard',
'UseExternalSounds', 'SendVoiceMessages',
'SendPolls', 'UseExternalApps'
]
return requiredPermissions.filter(permission => !guildMember.permissions.has(permission));
}
module.exports = {init, getClient, getGuilds, getMembersVoices, getChannel, getGuildMembers, isReady}

26
src/discord/Button.js Normal file
View File

@@ -0,0 +1,26 @@
const { ButtonBuilder, ButtonStyle } = require('discord.js');
class Button extends ButtonBuilder {
constructor(label, customId, style = ButtonStyle.Primary, link = null) {
super()
.setLabel(label)
if (link) {
this.setURL(link);
this.setStyle(ButtonStyle.Link);
} else{
this.setCustomId(customId)
}
this.setStyle(style);
}
setDisabled(disabled) {
return this.setDisabled(disabled);
}
setEmoji(emoji) {
return this.setEmoji(emoji);
}
}
module.exports = { Button };

View File

@@ -53,10 +53,18 @@ class Command {
}) })
SlashCommand.addStringOption(option => option.setName(SelOption.name).setDescription(SelOption.description).setRequired(SelOption.required).addChoices(choices)) SlashCommand.addStringOption(option => option.setName(SelOption.name).setDescription(SelOption.description).setRequired(SelOption.required).addChoices(choices))
} }
if(SelOption.type === "FILE") {
SlashCommand.addAttachmentOption(option => option.setName(SelOption.name).setDescription(SelOption.description).setRequired(SelOption.required))
}
}) })
} }
/**
* @type {SlashCommandBuilder}
* @param {Client} client
* @param {Interaction} interaction
*/
this.data = {data: SlashCommand, async execute(client, interaction) {callback(client, interaction)}} this.data = {data: SlashCommand, async execute(client, interaction) {callback(client, interaction)}}
} }

View File

@@ -50,10 +50,6 @@ client.commands = new Collection()
} }
dlog.step.end("d_commands_refresh") dlog.step.end("d_commands_refresh")
} catch (error) { } catch (error) {
// And of course, make sure you catch and log any errors! // And of course, make sure you catch and log any errors!

View File

@@ -10,8 +10,8 @@ const command = new Command("about", "Affiche des informations sur le bot", (cli
const minutes = Math.floor((uptime % 3600) / 60); const minutes = Math.floor((uptime % 3600) / 60);
const seconds = Math.floor(uptime % 60); const seconds = Math.floor(uptime % 60);
const embed = new Embed() const embed = new Embed(interaction)
embed.setColor(0xb0f542) embed.setColor(237, 12, 91)
embed.setThumbnail("https://cdn.discordapp.com/avatars/" + client.user.id + "/" + client.user.avatar + ".png") embed.setThumbnail("https://cdn.discordapp.com/avatars/" + client.user.id + "/" + client.user.avatar + ".png")
embed.setTitle('Subsonics - Chopin') embed.setTitle('Subsonics - Chopin')
embed.addField('Informations',"") embed.addField('Informations',"")
@@ -20,15 +20,18 @@ const command = new Command("about", "Affiche des informations sur le bot", (cli
embed.addField("Ping", `${client.ws.ping} ms `, true) embed.addField("Ping", `${client.ws.ping} ms `, true)
embed.addField("Réalisé par", "Raphix - 2025", true) embed.addField("Réalisé par", "Raphix - 2025", true)
embed.addColumn() embed.addColumn()
embed.addField('Versions',"") embed.addField('Versions :',"")
embed.addField('Node.js', process.version,true) embed.addField('Node.js', process.version,true)
embed.addField('Discord.js', packageJson.dependencies["discord.js"].replace("^", ""),true) embed.addField('Discord.js', packageJson.dependencies["discord.js"].replace("^", ""),true)
embed.addColumn() embed.addColumn()
embed.addField('Webmetrik', packageJson.dependencies["webmetrik"].replace("^", ""),true) embed.addField('Webmetrik', packageJson.dependencies["webmetrik"].replace("^", ""),true)
embed.addField('Loguix', packageJson.dependencies["loguix"].replace("^", ""),true) embed.addField('Loguix', packageJson.dependencies["loguix"].replace("^", ""),true)
embed.addColumn() embed.addColumn()
embed.addField('FFmpeg', packageJson.dependencies["ffmpeg-static"].replace("^", ""),true)
embed.addField('Ytdl', packageJson.dependencies["@distube/ytdl-core"].replace("^", ""),true)
embed.addColumn()
embed.send(interaction) embed.send()
}) })

View File

@@ -3,7 +3,7 @@ const { Embed } = require('../Embed');
const command = new Command("help", "Affiche la liste des commandes", (client, interaction) => { const command = new Command("help", "Affiche la liste des commandes", (client, interaction) => {
const embed = new Embed() const embed = new Embed(interaction)
embed.setColor(0x03ff2d) embed.setColor(0x03ff2d)
embed.setTitle('Comment assister au concert ?') embed.setTitle('Comment assister au concert ?')
embed.setDescription("**Eh ! Tu as eu ton ticket ? Tant mieux ! Voici la liste des commandes à utiliser dans le salon prévu à cet effet !**") embed.setDescription("**Eh ! Tu as eu ton ticket ? Tant mieux ! Voici la liste des commandes à utiliser dans le salon prévu à cet effet !**")
@@ -18,7 +18,7 @@ const command = new Command("help", "Affiche la liste des commandes", (client, i
option.choices.forEach(choice => { option.choices.forEach(choice => {
choices.push(choice.name) choices.push(choice.name)
}) })
CommandName += " " + choices.join(" | ") CommandName += " <" + choices.join(" | ") +">"
} }
}) })
} }
@@ -28,7 +28,7 @@ const command = new Command("help", "Affiche la liste des commandes", (client, i
}) })
embed.addField("La queue et la gestion du redémarrage se fait par le site https://subsonics.raphix.fr/", ":star:" ) embed.addField("La queue et la gestion du redémarrage se fait par le site https://subsonics.raphix.fr/", ":star:" )
embed.setThumbnail("https://static.wikia.nocookie.net/codelyoko/images/9/95/Subdigitals.jpg/revision/latest/scale-to-width-down/180?cb=20120105180510&path-prefix=fr"); embed.setThumbnail("https://static.wikia.nocookie.net/codelyoko/images/9/95/Subdigitals.jpg/revision/latest/scale-to-width-down/180?cb=20120105180510&path-prefix=fr");
embed.send(interaction) embed.send()
}) })
module.exports = {command} module.exports = {command}

View File

@@ -0,0 +1,20 @@
const {Command } = require("../Command")
const {Embed, EmbedError} = require("../Embed")
const {Button} = require("../Button")
const command = new Command("invite", "Invite moi sur d'autres serveurs", (client, interaction) => {
const embed = new Embed(interaction)
embed.setColor(0xFF007F)
embed.setTitle('**Inviter le bot sur d\'autres serveurs**')
embed.setDescription('Vous pouvez m\'inviter sur d\'autres serveurs en cliquant sur le bouton ci-dessous.')
embed.addBotPicture(client)
const linkButton = new Button("Invite", null, 5, "https://discord.com/oauth2/authorize?client_id=" + client.user.id + "&scope=bot+applications.commands&permissions=8")
embed.addButton(linkButton)
embed.send()
})
module.exports = {command}

View File

@@ -0,0 +1,34 @@
const {Command} = require("../Command")
const {Embed, EmbedError} = require("../Embed")
const {Player, AllPlayers} = require("../../player/Player")
const command = new Command("leave", "Quitter le salon vocal", (client, interaction) => {
if(!interaction.member.voice.channel) return new EmbedError("Vous devez rejoindre un salon vocal pour arrêter le bot !", interaction)
const channel = interaction.member.voice.channel
var embed = new Embed(interaction)
if(AllPlayers.has(channel.guildId)) {
const player = AllPlayers.get(channel.guildId)
if(!player?.connected) {
return embed.returnError("Le bot n'est pas connecté à ce salon vocal")
}
player.leave()
embed.setColor(200, 20, 20)
embed.setTitle('**Déconnexion**')
embed.setDescription('Déconnexion du salon vocal')
embed.setThumbnail("https://www.iconsdb.com/icons/download/white/phone-51-64.png")
embed.send()
} else {
embed.returnError("Le bot n'est pas connecté à ce salon vocal")
}
})
module.exports = {command}

View File

@@ -0,0 +1,55 @@
const {Command} = require('../Command');
const {Embed, EmbedError} = require('../Embed');
const { Player } = require('../../player/Player');
const { Song } = require('../../player/Song');
const history = require('../../playlists/History');
const command = new Command("media", "Lire un média MP3/WAV dans un salon vocal", async (client, interaction) => {
if(!interaction.member.voice.channel) return new EmbedError("Vous devez rejoindre un salon vocal pour jouer un média !", interaction, true)
const media = interaction.options.get("media")
const now = interaction.options.getBoolean("now") || false
if(media.attachment.contentType != "audio/mpeg" && media.attachment.contentType != "audio/wav") return new EmbedError("Le média doit être un fichier audio MP3 ou WAV !", interaction)
const embed = new Embed(interaction)
embed.setColor(0x15e6ed)
const channel = interaction.member.voice.channel
const song = new Song()
await song.processMedia(media, interaction.user.username)
const player = new Player(channel.guildId)
player.join(channel)
if(now) {
player.play(song)
embed.setTitle('**Lecture immédiate**')
} else {
player.add(song)
embed.setTitle('**Ajout à liste de lecture**')
}
history.addToPersonalHistory(interaction.user.id, song)
embed.setDescription('**Titre : **' + song.title)
embed.addField('**Durée : **', song.readduration)
embed.addField("**Artiste : **",song.author)
embed.addField('**Demandé par **' + interaction.member.user.username, "")
embed.setThumbnail(song.thumbnail)
embed.send()
}, [{type: "FILE", name: "media", description: "Fichier audio à lire", required: true},
{type:"BOOLEAN", name: "now", description: "Lire le média maintenant", required: false}]
)
module.exports = {command}

View File

@@ -0,0 +1,44 @@
const {Command} = require("../Command")
const {Embed, EmbedError} = require("../Embed")
const { Player } = require("../../player/Player")
const command = new Command("pause", "Mettre en pause / Reprendre la musique en cours", (client, interaction) => {
if(!interaction.member.voice.channel) return new EmbedError("Vous devez rejoindre un salon vocal pour mettre en pause la musique !", interaction)
const channel = interaction.member.voice.channel
const player = new Player(channel.guildId)
const result = player.pause()
var embed = new Embed(interaction)
embed.setColor(0x03ff2d)
result.then((pause) => {
if(pause == "no_music") {
embed.returnError("Il n'y a pas de musique en cours de lecture")
} else if(pause) {
embed.setTitle('Musique en pause')
embed.setDescription("La musique a été mise en pause")
embed.setThumbnail("https://www.iconsdb.com/icons/download/white/pause-64.png")
} else {
embed.setTitle('Musique reprise')
embed.setDescription("La musique a été reprise")
embed.setThumbnail("https://www.iconsdb.com/icons/download/white/play-64.png")
}
embed.send()
})
// Réponse en embed
})
module.exports = {command}

View File

@@ -0,0 +1,93 @@
const { Command } = require("../Command");
const { Embed, EmbedError } = require("../Embed");
const { Player } = require("../../player/Player");
const Finder = require("../../player/Finder");
const { Playlist } = require("../../playlists/Playlist");
const spotify = require("../../media/SpotifyInformation");
const history = require("../../playlists/History");
const command = new Command("play", "Jouer une musique à partir d'un lien dans un salon vocal", async (client, interaction) => {
if(!interaction.member.voice.channel) return new EmbedError("Vous devez rejoindre un salon vocal pour jouer une musique !", interaction)
const url = interaction.options.get("url")
const channel = interaction.member.voice.channel
const now = interaction.options.getBoolean("now") || false
const embed = new Embed(interaction)
await Finder.search(url.value).then(async (song) => {
if(!song) return embed.returnError("Impossible de trouver la musique à partir du lien donné ou des mots clés donnés")
const player = new Player(channel.guildId)
player.join(channel)
embed.setColor(0x15e6ed)
// Check if song is playlist
if(song instanceof Playlist) {
embed.setDescription('**Playlist : **' + song.songs.length + ' musiques')
embed.addField('**Titre : **' + song.title, "")
embed.addField('**Demandé par : **', interaction.member.user.username,)
embed.addField('**Auteur : **', song.author)
embed.addField('**Provient de : **', song.type.replace(/^\w/, (c) => c.toUpperCase()))
if(!song.type == "spotify") {
embed.addField('**Durée : **', song.readduration)
}
embed.addField('**Lien : **', song.url)
embed.addField(":warning: La récupération des musiques peut prendre du temps", "Veuillez patienter ... et éviter de lancer d'autres commandes")
embed.setThumbnail(song.thumbnail)
} else {
embed.setDescription('**Titre : **' + song.title)
embed.addField('**Durée : **', song.readduration)
embed.addField("**Artiste : **",song.author)
embed.addField('**Demandé par **' + interaction.member.user.username, "")
embed.addField("**Lien :** ", song.url)
embed.setThumbnail(song.thumbnail)
}
if(now) {
embed.setTitle("Lecture immédiate")
} else {
embed.setTitle("Ajoutée à la file d'attente")
}
embed.send()
if(song instanceof Playlist) {
if(song.type == "spotify") {
song = await spotify.getTracks(song)
}
player.readPlaylist(song, now)
} else {
if(now) {
player.play(song)
} else {
player.add(song)
}
history.addToPersonalHistory(interaction.user.id, song)
}
})
}, [{type: "STRING", name: "url", description: "Recherche / Lien audio (Youtube / Soundclound / Spotify)", required: true},
{type:"BOOLEAN", name: "now", description: "Lire le média maintenant", required: false}]
)
module.exports = {command}

View File

@@ -0,0 +1,51 @@
const {Command} = require("../Command")
const {Embed, EmbedError} = require("../Embed")
const { Player, AllPlayers } = require("../../player/Player")
const command = new Command("previous", "Passe à la musique précédente", (client, interaction) => {
if(!interaction.member.voice.channel) return new EmbedError("Vous devez rejoindre un salon vocal pour passer à la musique suivante !", interaction)
const channel = interaction.member.voice.channel
var embed = new Embed(interaction)
if(AllPlayers.has(channel.guildId)) {
const player = new Player(channel.guildId)
const result = player.previous()
embed.setColor(0x15e6ed)
result.then((song) => {
if(song == "no_music") {
embed.returnError("Il n'y a pas de musique précédemment jouée")
} else if(song) {
// Result is a song
embed.setTitle('**Musique précédente !**')
embed.setDescription('**Titre : **' + song.title)
embed.addField('**Durée : **'+ song.readduration, "")
embed.addField("**Artiste : **" + song.author, "")
embed.setThumbnail(song.thumbnail)
embed.send()
}
})
} else {
return embed.returnError("Le bot n'est pas connecté", interaction)
}
})
module.exports = {command}

View File

@@ -0,0 +1,43 @@
const {Command} = require("../Command")
const {Embed, EmbedError} = require("../Embed")
const { Player, AllPlayers } = require("../../player/Player")
const command = new Command("liste", "Affiche la file d'attente", (client, interaction) => {
const channel = interaction.member.voice.channel
var embed = new Embed(interaction)
if(AllPlayers.has(channel.guildId)) {
const player = new Player(channel.guildId)
const queue = player.queue.getNext()
if(queue.length == 0) {
embed.returnError("Il n'y a pas de musique en file d'attente")
} else if(queue.length > 0) {
// Result is a song
embed.setColor(0x15e6ed)
embed.setThumbnail("https://www.iconsdb.com/icons/download/white/list-2-64.png")
embed.setTitle('**File d\'attente :**')
embed.setDescription('**' + queue.length + ' musiques**')
queue.forEach((song, index) => {
// max 24 fields
if(index > 10) return
embed.addField(`**${index+1} - ${song.title}**`, `**Durée : **${song.readduration}\n**Artiste : **${song.author}`)
})
embed.send()
}
} else {
embed.returnError("Le bot n'est pas connecté")
}
})
module.exports = {command}

View File

@@ -5,7 +5,7 @@ const { Report } = require('../ReportSender');
const command = new Command("report", "Signaler un problème avec le bot", (client, interaction) => { const command = new Command("report", "Signaler un problème avec le bot", (client, interaction) => {
const report = new Report(interaction.user.username, interaction.options.getString("type"), interaction.options.getString("description")) const report = new Report(interaction.user.username, interaction.options.getString("type"), interaction.options.getString("description"))
const result = report.send() const result = report.send()
const embed = new Embed() const embed = new Embed(interaction)
result.then((res) => { result.then((res) => {
@@ -20,7 +20,7 @@ const command = new Command("report", "Signaler un problème avec le bot", (clie
embed.setDescription("Votre rapport a bien été envoyé !") embed.setDescription("Votre rapport a bien été envoyé !")
} }
embed.send(interaction) embed.send()
}) })

View File

@@ -0,0 +1,27 @@
const {Embed} = require("../Embed")
const {Command} = require("../Command")
const {restart} = require("../../utils/Maintenance")
const users = require("../../server/auth/User")
// Nécéssite une raison pour redémarrer le bot
const command = new Command("restart", "Redémarre le bot", (client, interaction) => {
// Check if user is admin from users list
const user = users.getUserById(interaction.user.id)
if(!user || !user.isAdmin()) {
interaction.reply({content: "Vous n'êtes pas admin", ephemeral: true})
return
}
const reason = interaction.options.getString("reason")
restart(reason)
const embed = new Embed(interaction)
embed.setColor(150, 20, 20)
embed.setTitle('Redémarrage')
embed.setDescription("Veuillez patientez, le bot va redémarrer dans un instant ! :arrows_counterclockwise:")
embed.addField('Raison', reason)
embed.send()
},
[{type: "STRING", name: "reason", description: "Raison du redémarrage", required: true}]
)
module.exports = {command}

View File

@@ -0,0 +1,48 @@
const {Command} = require("../Command")
const {Embed, EmbedError} = require("../Embed")
const { Player, AllPlayers } = require("../../player/Player")
const command = new Command("skip", "Passe à la musique suivante", (client, interaction) => {
if(!interaction.member.voice.channel) return new EmbedError("Vous devez rejoindre un salon vocal pour passer à la musique suivante !", interaction)
const channel = interaction.member.voice.channel
var embed = new Embed(interaction)
if(AllPlayers.has(channel.guildId)) {
const player = new Player(channel.guildId)
const result = player.skip()
embed.setColor(0x15e6ed)
result.then((song) => {
if(song == "no_music") {
embed.returnError("Il n'y a pas de musique en file d'attente", interaction)
} else if(song) {
// Result is a song
embed.setColor(0x15e6ed)
embed.setTitle('**Musique suivante !**')
embed.setDescription('**Titre : **' + song.title)
embed.addField('**Durée : **'+ song.readduration, "")
embed.addField("**Artiste : **" + song.author, "")
embed.setThumbnail(song.thumbnail)
}
embed.send()
})
} else {
return embed.returnError("Le bot n'est pas connecté", interaction)
}
})
module.exports = {command}

View File

@@ -0,0 +1,34 @@
const {Command} = require("../Command")
const {Embed, EmbedError} = require("../Embed")
const {Player} = require("../../player/Player")
const command = new Command("state", "Affiche la musique en cours", (client, interaction) => {
const channel = interaction.member.voice.channel
const player = new Player(channel.guildId)
const song = player.queue.getCurrent()
if(!song) {
var embed = new EmbedError("Il n'y a pas de musique en cours de lecture", interaction)
} else if(song) {
var embed = new Embed(interaction)
// Result is a song
embed.setColor(0x15e6ed)
embed.setTitle('**Musique en cours :**')
embed.setDescription('**Titre : **' + song.title)
embed.addField('**Durée : **', song.readduration)
embed.addField("**Artiste : **", song.author)
embed.setThumbnail(song.thumbnail)
embed.send()
}
})
module.exports = {command}

View File

@@ -0,0 +1,20 @@
const { Command } = require('../Command');
const { Button } = require('../Button');
const { Embed } = require('../Embed');
const config = require('../../utils/Database/Configuration')
const command = new Command("web", "Affiche le lien vers le site web pour contrôler le bot", (client, interaction) => {
const embed = new Embed(interaction)
embed.setColor(0xffffff)
embed.setTitle('Subsonics - Chopin')
embed.addBotPicture(client)
embed.setDescription('Vous pouvez contrôler le bot depuis le site web ! \n Nécéssite une connexion avec votre compte Discord.')
const linkButton = new Button("Site web", null, 5, config.getWebsiteLink())
embed.addButton(linkButton)
embed.send()
})
module.exports = {command}

View File

@@ -1,10 +1,20 @@
const { EmbedBuilder } = require("discord.js"); const { EmbedBuilder, ActionRowBuilder } = require("discord.js");
class Embed { class Embed {
fields; fields;
constructor() { buttons;
constructor (interaction, ephemeral) {
this.embed = new EmbedBuilder().setTimestamp() this.embed = new EmbedBuilder().setTimestamp()
this.fields = [] this.fields = []
this.buttons = []
this.isSended = false
if(interaction) {
interaction.deferReply({ ephemeral: ephemeral }).then(() => {
this.isSended = true
})
this.interaction = interaction
this.ephemeral = ephemeral
}
} }
setTitle(title) { setTitle(title) {
@@ -75,15 +85,48 @@ class Embed {
return this return this
} }
addButton(button) {
this.buttons.push(button)
return this
}
build() { build() {
//Add Fields to an object //Add Fields to an object
this.embed.addFields(this.fields) this.embed.addFields(this.fields)
if(this.buttons.length > 0) {
this.actionRow = new ActionRowBuilder()
.addComponents(this.buttons);
}
return this.embed return this.embed
} }
send(interaction) { async send() {
interaction.reply({embeds: [this.build()]}) // Add a secutiry check to avoid sending an embed if the interaction is not defined and retry one again
while(!this.isSended) {
await new Promise(resolve => setTimeout(resolve, 50));
}
if(this.ephemeral === undefined) this.ephemeral = false;
this.interaction.editReply({ embeds: [this.build()], components: this.buttons.length > 0 ? [this.actionRow] : [] })
}
async returnError(message) {
this.setColor(150, 20, 20)
this.setTitle('Erreur')
this.setThumbnail("https://upload.wikimedia.org/wikipedia/commons/thumb/9/97/Dialog-error-round.svg/2048px-Dialog-error-round.svg.png")
this.setDescription(message)
await this.send()
} }
} }
module.exports = {Embed} class EmbedError extends Embed {
constructor(message, interaction, ephemeral) {
super(interaction, ephemeral)
this.setColor(150, 20, 20)
this.setTitle('Erreur')
this.setThumbnail("https://upload.wikimedia.org/wikipedia/commons/thumb/9/97/Dialog-error-round.svg/2048px-Dialog-error-round.svg.png")
this.setDescription(message)
this.send()
}
}
module.exports = {Embed, EmbedError}

109
src/discord/MediaBase.js Normal file
View File

@@ -0,0 +1,109 @@
const {LogType} = require("loguix")
const config = require("../utils/Database/Configuration")
const wlog = new LogType("MediaBase")
const { Database } = require("../utils/Database/Database")
const {__glob} = require("../utils/GlobalVars")
const { AttachmentBuilder } = require("discord.js")
const discordBot = require("./Bot")
var connected = false
var mediaDB = new Database("media", __glob.MEDIA_DB, [])
wlog.step.init("init_db", "Initialisation de la base de données multimédia")
if(!config.getMediaGuildId() || !config.getMediaChannelId()) {
wlog.warn("La configuration de la base de données multimédia n'est pas définie, vérifiez le fichier de configuration.")
wlog.step.error("init_db","Impossible d'initialiser la base de données multimédia, vérifiez le fichier de configuration.")
}
var channel = null
discordBot.getClient().on("ready", () => {
try {
channel = discordBot.getChannel(config.getMediaGuildId(), config.getMediaChannelId())
} catch (e) {
}
if(!channel) {
wlog.warn("Le canal multimédia n'existe pas, vérifiez le fichier de configuration.")
wlog.step.error("init_db","Impossible d'initialiser la base de données multimédia, vérifiez le fichier de configuration.")
}
try {
const dateTime = new Date()
const date = dateTime.toLocaleDateString('fr-FR', { timeZone: 'Europe/Paris' })
const time = dateTime.toLocaleTimeString('fr-FR', { timeZone: 'Europe/Paris' })
const message = `[LOGS] La base de données multimédia a été initialisée le ${date} à ${time}`
channel.send(message)
wlog.log("La base de données multimédia a été initialisée avec succès.")
wlog.step.end("init_db")
connected = true
} catch (e) {
wlog.error("Impossible d'envoyer un message au canal multimédia, vérifiez le fichier de configuration.")
wlog.step.error("init_db","Impossible d'envoyer un message au canal multimédia, vérifiez le fichier de configuration.")
connected = false
}
})
// SEND FILE TO DISCORD AND GET THE URL ID
async function postMedia(file) {
if(!connected) {
wlog.error("La base de données multimédia n'est pas connectée, impossible d'envoyer le fichier.")
return null
}
try {
const attachment = new AttachmentBuilder(file.file)
attachment.setName(file.name) // Set the name of the file
attachment.setDescription("Fichier envoyé par Subsonics Chopin - Raphix")
const message = await channel.send({ files: [attachment] })
const url = message.attachments.first().url
wlog.log(`Fichier envoyé avec succès : ${url}`)
// add the file to the database
mediaDB.data.push({
id: message.id,
url: url,
name: file.name,
size: file.size,
createdAt: new Date().toISOString()
})
mediaDB.save()
return url
} catch (error) {
wlog.error(`Erreur lors de l'envoi du fichier : ${error.message}`)
return null
}
}
async function getMedia(id) {
if(!connected) {
wlog.error("La base de données multimédia n'est pas connectée, impossible de récupérer le fichier.")
return null
}
const media = mediaDB.data.find(m => m.id === id)
if(!media) {
wlog.error(`Aucun média trouvé avec l'ID : ${id}`)
return null
}
try {
return media.url
} catch (error) {
wlog.error(`Erreur lors de la récupération du média : ${error.message}`)
return null
}
}
module.exports = {
postMedia,
getMedia,
}

View File

@@ -46,6 +46,7 @@ class Report {
} }
} }
module.exports = {Report} module.exports = {Report}

58
src/lyrics/Lyrics.js Normal file
View File

@@ -0,0 +1,58 @@
const { LogType } = require("loguix");
const plog = new LogType('Lyrics');
const urls = require('./urls.json');
// Make sure Url exists and get lyrics for the first item only
async function getLyrics(name) {
let result = null;
try {
const searchResponse = await fetch(`${urls.urlSearch}${encodeURIComponent(name)}`, {
method: 'GET',
headers: {
'content-type': 'application/json'
}
});
const searchData = await searchResponse.json();
// Check if data exists and has at least one item
if (searchData && searchData.data && searchData.data.length > 0) {
const firstItem = searchData.data[0];
const artist = firstItem.artist && firstItem.artist.name ? firstItem.artist.name : null;
const title = firstItem.title || null;
if (artist && title) {
try {
const lyricsResponse = await fetch(`${urls.urlGet}${encodeURIComponent(artist)}/${encodeURIComponent(title)}`, {
method: 'GET',
headers: {
'content-type': 'application/json'
}
});
const lyricsData = await lyricsResponse.json();
console.log(lyricsData);
if (lyricsData && lyricsData && lyricsData.lyrics) {
result = lyricsData.lyrics;
} else {
plog.error('Invalid response structure:', lyricsData);
return null;
}
} catch (error) {
plog.error('Error fetching lyrics data:', error);
return null;
}
} else {
plog.error('Artist or title missing in search result');
return null;
}
} else {
plog.error('No search results found');
return null;
}
} catch (error) {
plog.error('Error fetching search data:', error);
return null;
}
return result;
}
module.exports = { getLyrics };

4
src/lyrics/urls.json Normal file
View File

@@ -0,0 +1,4 @@
{
"urlSearch": "http://api.deezer.com/search?limit=5&q=",
"urlGet": "https://api.lyrics.ovh/v1/"
}

View File

@@ -5,7 +5,6 @@
*/ */
const { LogType } = require('loguix');
const { __glob } = require("./utils/GlobalVars") const { __glob } = require("./utils/GlobalVars")
require("loguix").setup(__glob.LOGS, __glob.PACKAGEINFO) require("loguix").setup(__glob.LOGS, __glob.PACKAGEINFO)
const config = require("./utils/Database/Configuration") const config = require("./utils/Database/Configuration")
@@ -18,7 +17,9 @@ metric.publishMetrics("8001", "raphraph")
setup(); setup();
function setup() { async function setup() {
const DiscordBot = require("./discord/Bot") const DiscordBot = require("./discord/Bot")
DiscordBot.init() await DiscordBot.init()
const Server = require("./server/Server")
await Server.init()
} }

View File

@@ -0,0 +1,58 @@
const ffprobe = require('ffprobe');
const ffprobeStatic = require('ffprobe-static');
const { getReadableDuration } = require('../utils/TimeConverter');
const clog = require("loguix").getInstance("Song")
async function getMediaInformation(instance, media, provider) {
try {
const info = await ffprobe(media.attachment.url, { path: ffprobeStatic.path });
if (info.streams?.[0]?.duration_ts) {
instance.duration = info.streams[0].duration;
instance.readduration = getReadableDuration(instance.duration)
}
// Vérification pour éviter une erreur si `streams[0]` ou `tags` n'existe pas
instance.thumbnail = info.streams?.[0]?.tags?.thumbnail ??
"https://radomisol.fr/wp-content/uploads/2016/08/cropped-note-radomisol-musique.png";
// Obtenir le titre (sinon utiliser le nom du fichier)
instance.title = info.streams?.[0]?.tags?.title ?? media.attachment.name;
// Obtenir l'auteur (s'il existe)
instance.author = info.streams?.[0]?.tags?.artist ?? instance.author;
} catch (err) {
clog.error("Impossible de récupérer les informations de la musique : " + media.attachment.name)
clog.error(err)
return null;
}
}
async function getMediaInformationFromUrl(instance, url) {
try {
const info = await ffprobe(url, { path: ffprobeStatic.path });
if (info.streams?.[0]?.duration_ts) {
instance.duration = info.streams[0].duration;
instance.readduration = getReadableDuration(instance.duration);
}
// Vérification pour éviter une erreur si `streams[0]` ou `tags` n'existe pas
instance.thumbnail = info.streams?.[0]?.tags?.thumbnail ??
"https://radomisol.fr/wp-content/uploads/2016/08/cropped-note-radomisol-musique.png";
// Obtenir le titre (sinon utiliser le nom du fichier)
instance.title = info.streams?.[0]?.tags?.title ?? "Titre inconnu";
// Obtenir l'auteur (s'il existe)
instance.author = info.streams?.[0]?.tags?.artist ?? "Auteur inconnu";
} catch (err) {
clog.error("Impossible de récupérer les informations de la musique depuis l'URL : " + url);
console.log(err)
clog.error(err);
return null;
}
}
module.exports = {getMediaInformation, getMediaInformationFromUrl};

View File

@@ -0,0 +1,85 @@
const {LogType} = require('loguix');
const clog = new LogType("SoundcloudInformation");
const {Song} = require('../player/Song');
const {Playlist} = require('../playlists/Playlist');
const {Soundcloud} = require('soundcloud.ts')
const {getReadableDuration} = require('../utils/TimeConverter');
const soundcloud = new Soundcloud();
async function getTrack(url) {
try {
const info = await soundcloud.tracks.get(url)
if(!info) {
clog.error("Impossible de récupérer les informations de la piste Soundcloud à partir de l'URL");
return null;
}
const song = new Song();
song.title = info.title;
song.author = info.user.username;
song.url = info.permalink_url;
song.thumbnail = info.artwork_url;
song.id = info.id;
song.duration = info.duration / 1000;
song.readduration = getReadableDuration(info.duration / 1000);
song.type = "soundcloud";
return song;
} catch (error) {
clog.error('Erreur lors de la recherche Soundcloud (Track): ' + error);
return null;
}
}
async function getPlaylist(url) {
try {
const info = await soundcloud.playlists.get(url)
if(!info) {
clog.error("Impossible de récupérer les informations de la playlist Soundcloud à partir de l'URL");
return null;
}
const playlist = new Playlist();
playlist.title = info.title;
playlist.author = info.user.username;
playlist.url = info.permalink_url;
playlist.thumbnail = info.artwork_url;
playlist.id = info.id;
playlist.duration = 0;
playlist.songs = [];
playlist.type = "soundcloud";
for(const track of info.tracks) {
const song = new Song();
song.title = track.title;
song.author = track.user.username;
song.url = track.permalink_url;
song.thumbnail = track.artwork_url;
song.id = track.id;
song.duration = track.duration / 1000;
song.readduration = getReadableDuration(track.duration / 1000);
song.type = "soundcloud";
playlist.duration += track.duration / 1000;
playlist.songs.push(song);
}
playlist.readduration = getReadableDuration(playlist.duration);
return playlist;
} catch (error) {
clog.error('Erreur lors de la recherche Soundcloud (Playlist): ' + error);
return null;
}
}
module.exports = {getTrack, getPlaylist}

View File

@@ -0,0 +1,179 @@
const {LogType} = require('loguix');
const clog = new LogType("SpotifyInformation");
const config = require('../utils/Database/Configuration');
const SPOTIFY_CLIENT_ID = config.getSpotifyClientId()
const SPOTIFY_CLIENT_SECRET = config.getSpotifyClientSecret()
const SpotifyWebApi = require('spotify-web-api-node');
const {Playlist} = require('../playlists/Playlist');
const {Song} = require('../player/Song');
const youtube = require("../media/YoutubeInformation");
const {getReadableDuration} = require('../utils/TimeConverter');
const spotifyApi = new SpotifyWebApi({
clientId: SPOTIFY_CLIENT_ID,
clientSecret: SPOTIFY_CLIENT_SECRET,
});
async function getSong(url) {
try {
const data = await spotifyApi.clientCredentialsGrant();
spotifyApi.setAccessToken(data.body['access_token']);
const parts = url.split('/');
const trackId = parts[parts.length - 1];
if(!trackId) {
clog.error("Impossible de récupérer l'identifiant de la piste Spotify à partir de l'URL");
return null;
}
const trackInfo = await spotifyApi.getTrack(trackId);
const trackName = trackInfo.body.name;
const artistName = trackInfo.body.artists[0].name;
return `${trackName} - ${artistName}`;
} catch (error) {
clog.error("Impossible de récupérer les informations de la piste Spotify à partir de l'URL");
clog.error(error);
return null;
}
}
async function getAlbum(url) {
try {
const creditdata = await spotifyApi.clientCredentialsGrant();
spotifyApi.setAccessToken(creditdata.body['access_token']);
const parts = url.split('/');
const albumId = parts[parts.indexOf('album') + 1].split('?')[0];
const data = await spotifyApi.getAlbum(albumId);
const info = data.body;
if(!info) {
clog.error("Impossible de récupérer les informations de l'album Spotify à partir de l'URL");
return null;
}
clog.log("Informations de l'album récupérées : " + info.name);
const playlist = new Playlist()
playlist.title = info.name;
playlist.author = info.artists[0].name;
playlist.authorId = info.artists[0].id;
playlist.thumbnail = info.images[0].url;
playlist.url = info.external_urls.spotify;
playlist.id = albumId;
playlist.type = "spotify";
playlist.songs = info.tracks.items;
return playlist;
} catch (error) {
clog.error("Impossible de récupérer les informations de l'album Spotify à partir de l'URL");
clog.error(error);
return null;
}
}
async function getPlaylist(url) {
// Get the playlist and return a Playlist Object
try {
const creditdata = await spotifyApi.clientCredentialsGrant();
spotifyApi.setAccessToken(creditdata.body['access_token']);
const parts = url.split('/');
const playlistId = parts[parts.indexOf('playlist') + 1].split('?')[0];
const data = await spotifyApi.getPlaylist(playlistId)
const info = data.body;
if(!info) {
clog.error("Impossible de récupérer les informations de la playlist Spotify à partir de l'URL");
return null;
}
clog.log("Informations de la playlist récupérées : " + info.name);
const playlist = new Playlist()
playlist.title = info.name;
playlist.author = info.owner.display_name;
playlist.authorId = info.owner.id;
playlist.thumbnail = info.images[0].url;
playlist.url = info.external_urls.spotify;
playlist.id = playlistId;
playlist.type = "spotify";
for(const track of info.tracks.items) {
playlist.songs.push(track.track);
}
return playlist;
} catch (error) {
clog.error("Impossible de récupérer les informations de l'album Spotify à partir de l'URL");
clog.error(error);
return null;
}
}
async function getTracks(playlist) {
const tracks = playlist.songs
playlistSongs = [];
for(const track of tracks) {
var trackName = track.name;
var artistName = track.artists[0].name;
var queryForYoutube = `${trackName} - ${artistName}`;
var urlYoutubeFounded = await youtube.getQuery(queryForYoutube).then(function(songFind) {
if(!songFind) return null;
return songFind.url;
});
clog.log("URL de la vidéo YouTube trouvée : " + urlYoutubeFounded);
if(!urlYoutubeFounded) {
clog.error("Impossible de récupérer l'URL de la vidéo YouTube à partir de la requête " + queryForYoutube);
} else {
const song = new Song();
song.title = track.name;
song.author = track.artists[0].name;
song.url = urlYoutubeFounded;
song.thumbnail = playlist.thumbnail;
song.id = track.id;
song.duration = track.duration_ms / 1000;
song.readduration = getReadableDuration(track.duration_ms / 1000);
song.type = "youtube";
playlist.duration += track.duration_ms / 1000;
playlistSongs.push(song);
}
// When finish do this
}
playlist.readduration = getReadableDuration(playlist.duration);
playlist.songs = playlistSongs;
return playlist;
}
module.exports = {getSong, getAlbum, getPlaylist, getTracks}

View File

@@ -0,0 +1,135 @@
const { LogType } = require('loguix');
const clog = new LogType("YoutubeInformation");
const { Song } = require('../player/Song');
const { Playlist } = require('../playlists/Playlist');
const { getReadableDuration, getSecondsDuration } = require('../utils/TimeConverter');
const ytsr = require('@distube/ytsr');
const ytfps = require('ytfps');
async function getQuery(query, multiple) {
if (!query || typeof query !== 'string') {
clog.error("Impossible de rechercher une vidéo YouTube, car la requête est nulle");
return null;
}
try {
const limit = multiple ? 25 : 1;
const searchResults = await ytsr(query, { limit });
const videos = searchResults.items.filter(item => item.type === 'video');
if (videos.length === 0) {
clog.error("Impossible de récupérer le lien de la vidéo YouTube à partir de la requête");
return null;
}
const songs = await Promise.all(videos.map(video => getVideo(video.url)));
return multiple ? songs.filter(song => song !== null) : songs[0];
} catch (error) {
clog.error('Erreur lors de la recherche YouTube: ' + error);
return null;
}
}
async function getVideo(url) {
const videoId = url.match(/(?:youtu\.be\/|youtube\.com\/|music\.youtube\.com\/)(?:watch\?v=)?([a-zA-Z0-9_-]{11})/);
if (videoId === null) {
clog.error("Impossible de récupérer l'identifiant de la vidéo YouTube à partir de l'URL");
return null;
}
try {
const searchResults = await ytsr(videoId[1], { limit: 1 });
const video = searchResults.items.find(item => item.type === 'video');
if (video) {
const songReturn = new Song();
await songReturn.processYoutubeVideo(video);
return songReturn;
} else {
clog.error("Impossible de récupérer la vidéo YouTube à partir de l'identifiant");
return null;
}
} catch (error) {
clog.error('Erreur lors de la recherche de la vidéo YouTube:' + error);
return null;
}
}
async function getPlaylist(url) {
if (url === null || typeof url !== 'string') {
clog.error("Impossible de rechercher une playlist YouTube, car la requête est nulle");
return null;
}
try {
// If watch?v= is present in the url with list?=, remove it and the code behind and transform it to playlist?list=
var playlistId;
// Get &list= in the url until the first & or ?
if (url.includes("list=")) {
playlistId = url.match(/(list=)([a-zA-Z0-9_-]+)/);
}
console.log(playlistId);
if (playlistId === null) {
clog.error("Impossible de récupérer l'identifiant de la playlist YouTube à partir de l'URL");
return null;
}
const playlistInfo = await ytfps(playlistId[2]);
if (!playlistInfo) {
clog.error("Impossible de récupérer la playlist YouTube à partir de l'identifiant");
return null;
}
const playlist = new Playlist();
playlist.type = "youtube";
playlist.author = playlistInfo.author.name;
playlist.authorId = playlistInfo.author.url;
playlist.title = playlistInfo.title;
playlist.thumbnail = playlistInfo.thumbnail_url;
playlist.description = playlistInfo.description;
playlist.url = `https://www.youtube.com/playlist?list=${playlistId[2]}`;
playlist.id = playlistId[2];
for (const video of playlistInfo.videos) {
const song = new Song();
await song.processYoutubeVideo(video, true);
playlist.duration += song.duration;
playlist.songs.push(song);
}
playlist.readduration = getReadableDuration(playlist.duration);
return playlist;
} catch (error) {
clog.error('Erreur lors de la recherche YouTube: ' + error);
return null;
}
}
async function getSecondsFromUrl(url) {
const videoId = url.match(/(?:youtu\.be\/|youtube\.com\/|music\.youtube\.com\/)(?:watch\?v=)?([a-zA-Z0-9_-]{11})/);
if (videoId === null) {
clog.error("Impossible de récupérer l'identifiant de la vidéo YouTube à partir de l'URL");
return null;
}
try {
const searchResults = await ytsr(videoId[1], { limit: 1 });
const video = searchResults.items.find(item => item.type === 'video');
console.log(video);
if (video) {
return getSecondsDuration(video.duration); // Convert seconds to milliseconds
} else {
clog.error("Impossible de récupérer la vidéo YouTube à partir de l'identifiant");
return null;
}
} catch (error) {
clog.error('Erreur lors de la recherche de la vidéo YouTube:' + error);
return null;
}
}
module.exports = { getQuery, getVideo, getPlaylist, getSecondsFromUrl };

82
src/player/Finder.js Normal file
View File

@@ -0,0 +1,82 @@
const Resolver = require('../utils/Resolver');
const { QueryType } = require('../utils/QueryType');
const { Links } = require('../utils/Links');
const youtube = require("../media/YoutubeInformation")
const spotify = require("../media/SpotifyInformation")
const soundcloud = require("../media/SoundcloudInformation")
async function search(query, multiple, forceType) {
if(!query) return null
if(!multiple) multiple = false
if(!forceType) forceType = null
if(forceType == "PLAYLIST") {
if(query.includes("spotify")) {
return await spotify.getPlaylist(query)
} else if(query.includes("soundcloud")) {
return await soundcloud.getPlaylist(query)
} else if(query.includes("youtube")) {
return await youtube.getPlaylist(query)
} else {
return null
}
}
if(forceType == "SONG") {
if(query.includes("spotify")) {
return await spotify.getSong(query)
} else if(query.includes("soundcloud")) {
return await soundcloud.getTrack(query)
} else if(query.includes("youtube")) {
return await youtube.getQuery(query, multiple)
} else {
return null
}
}
if(forceType == "ALBUM") {
if(query.includes("spotify")) {
return await spotify.getAlbum(query)
} else if(query.includes("youtube")) {
return await youtube.getQuery(query, multiple)
} else {
return null
}
}
const type = Resolver.getQueryType(query)
if(type == QueryType.YOUTUBE_SEARCH) {
return await youtube.getQuery(query, multiple)
}
if(type == QueryType.YOUTUBE_VIDEO) {
return await youtube.getVideo(query)
}
if(type == QueryType.YOUTUBE_PLAYLIST) {
return await youtube.getPlaylist(query)
}
if(type == QueryType.SPOTIFY_SONG) {
return await youtube.getQuery(await spotify.getSong(query))
}
if(type == QueryType.SPOTIFY_ALBUM) {
return await spotify.getAlbum(query)
}
if(type == QueryType.SPOTIFY_PLAYLIST) {
return await spotify.getPlaylist(query)
}
if(type == QueryType.SOUNDCLOUD_TRACK) {
return await soundcloud.getTrack(query)
}
if(type == QueryType.SOUNDCLOUD_PLAYLIST) {
return await soundcloud.getPlaylist(query)
}
//MORELATER: Add more providers
}
module.exports = {search}

239
src/player/List.js Normal file
View File

@@ -0,0 +1,239 @@
const { Database } = require('../utils/Database/Database')
const { __glob } = require('../utils/GlobalVars')
const PreviousDB = new Database("previous", __glob.PREVIOUSFILE, {})
const {LogType} = require("loguix")
const clog = new LogType("List")
const { Song } = require('./Song')
const AllLists = new Map() // Map<guildId, List>
class List {
next;
current;
shuffle;
guildId;
constructor(guildId) {
if(guildId === null) {
clog.error("Impossible de créer une liste, car guildId est null")
return
}
if(AllLists.has(guildId)) {
return AllLists.get(guildId)
}
// Add PreviousDB.data[this.guildId]
if(PreviousDB.data[guildId] === undefined) {
PreviousDB.data[guildId] = new Array()
savePrevious()
}
AllLists.set(guildId, this)
this.next = new Array();
this.current = null;
this.shuffle = false;
this.guildId = guildId;
}
getNext() {
return this.next;
}
getNextSong() {
if(this.next.length > 0) {
return this.next[0];
} else {
return null;
}
}
nextSong() {
if(this.current != null) {
this.addPreviousSong(this.current)
}
var song = null;
if(!this.shuffle) {
song = this.next[0]
this.next.splice(0, 1)
} else {
const randomIndex = Math.floor(Math.random() * this.next.length);
song = this.next[randomIndex]
this.next.splice(randomIndex, 1)
}
this.setCurrent(song)
process.emit("PLAYERS_UPDATE")
return song
}
clearNext() {
this.next = new Array();
process.emit("PLAYERS_UPDATE")
}
addNextSong(song) {
this.next.push(song)
process.emit("PLAYERS_UPDATE")
}
firstNext(song) {
this.next.unshift(song)
process.emit("PLAYERS_UPDATE")
}
removeNextByIndex(index) {
this.next.splice(index, 1)
process.emit("PLAYERS_UPDATE")
}
moveSongToUpNext(index) {
const song = this.next[index]
this.next.splice(index, 1)
this.next.unshift(song)
process.emit("PLAYERS_UPDATE")
}
getPrevious() {
const previousList = new Array()
for(const song of PreviousDB.data[this.guildId]) {
previousList.push(new Song(song))
}
return previousList;
}
getPreviousSong() {
if(PreviousDB.data[this.guildId].length > 0) {
return new Song(PreviousDB.data[this.guildId][0])
} else {
return null;
}
}
previousSong() {
if(this.current != null) {
this.firstNext(this.current)
}
if(PreviousDB.data[this.guildId].length > 0) {
const song = PreviousDB.data[this.guildId][0]
// Remove the song from the previous list
PreviousDB.data[this.guildId].splice(0, 1)
savePrevious()
return new Song(song)
} else {
return null;
}
}
clearPrevious() {
PreviousDB.data[this.guildId] = new Array();
savePrevious();
process.emit("PLAYERS_UPDATE")
}
addPreviousSongToNextByIndex(index) {
const song = PreviousDB.data[this.guildId][index]
this.next.push(song)
process.emit("PLAYERS_UPDATE")
}
addPreviousSong(song) {
PreviousDB.data[this.guildId].unshift(song)
savePrevious()
process.emit("PLAYERS_UPDATE")
}
getCurrent() {
return this.current;
}
setCurrent(value) {
this.current = value;
process.emit("PLAYERS_UPDATE")
}
destroy() {
this.clearNext();
this.current = null
this.shuffle = false;
AllLists.delete(this.guildId)
process.emit("PLAYERS_UPDATE")
}
setShuffle() {
this.shuffle = !this.shuffle;
}
isShuffle() {
return this.shuffle;
}
// Play the song with the index in the queue and delete it from the queue
playByIndex(index, typelist) {
var index = data[0]
var list = data[1]
if(typelist == ListType.NEXT) {
const song = this.next[index]
this.next.splice(index, 1)
return song
} else if(typelist == ListType.PREVIOUS) {
const song = PreviousDB.data[this.guildId][index]
return song
}
process.emit("PLAYERS_UPDATE")
}
addNextPlaylist(playlist, firstAlreadyPlayed) {
if(firstAlreadyPlayed) {
playlist.songs.shift()
}
for(const song of playlist.songs) {
this.addNextSong(song)
}
process.emit("PLAYERS_UPDATE")
}
moveNext(fromIndex, toIndex) {
// Check if fromIndex and toIndex are valid
if(fromIndex < 0 || fromIndex >= this.next.length + 1 || toIndex < 0 || toIndex >= this.next.length + 1) {
clog.error("Impossible de déplacer la musique, car l'index est invalide, GuildId : " + this.guildId)
return
}
if(fromIndex == toIndex) return;
const song = this.next[fromIndex]
this.next.splice(fromIndex, 1)
this.next.splice(toIndex, 0, song)
process.emit("PLAYERS_UPDATE")
}
}
const ListType = {
NEXT: "0",
PREVIOUS: "1"
}
function savePrevious() {
for(const guildId in PreviousDB.data) {
if(PreviousDB.data[guildId].length > 50) {
PreviousDB.data[guildId].splice(50, PreviousDB.data[guildId].length - 50)
}
}
PreviousDB.save();
}
module.exports = { List, ListType }

View File

@@ -0,0 +1,21 @@
const {createAudioResource, VoiceConnectionStatus, createAudioPlayer, StreamType} = require('@discordjs/voice');
const {LogType} = require('loguix')
const clog = new LogType("Media")
const plog = require("loguix").getInstance("Player")
const ffmpeg = require('fluent-ffmpeg')
async function getStream(song) {
try {
return song.url;
} catch(e) {
clog.error("Erreur lors de la lecture de la musique : " + song.title)
clog.error(e)
}
}
module.exports = {getStream}

View File

@@ -0,0 +1,25 @@
const {createAudioResource, VoiceConnectionStatus, createAudioPlayer, StreamType} = require('@discordjs/voice');
const {LogType} = require('loguix')
const clog = new LogType("Soundcloud-Stream")
const {Soundcloud} = require('soundcloud.ts')
const ffmpeg = require('fluent-ffmpeg')
const soundcloud = new Soundcloud();
async function getStream(song) {
try {
var stream = await soundcloud.util.streamTrack(song.url)
return stream
} catch(e) {
clog.error("Erreur lors de la récupération du stream : " + song.title)
clog.error(e)
}
}
module.exports = {getStream}

View File

@@ -0,0 +1,30 @@
const {createAudioResource, VoiceConnectionStatus, createAudioPlayer, StreamType} = require('@discordjs/voice');
const {LogType} = require('loguix')
const clog = new LogType("Youtube-Stream")
const ytdl = require('@distube/ytdl-core')
const ffmpeg = require('fluent-ffmpeg')
const { getRandomIPv6 } = require("@distube/ytdl-core/lib/utils");
async function getStream(song) {
try {
let stream = ytdl(song.url, {
quality: 'highestaudio',
highWaterMark: 1 << 30,
liveBuffer: 20000,
dlChunkSize: 0,
bitrate: 128,
});
return stream
} catch(e) {
clog.error("Erreur lors de la récupération du stream : " + song.title)
clog.error(e)
}
}
module.exports = {getStream}

417
src/player/Player.js Normal file
View File

@@ -0,0 +1,417 @@
const { joinVoiceChannel, getVoiceConnection, VoiceConnectionStatus, createAudioPlayer, AudioPlayerStatus, StreamType, createAudioResource } = require('@discordjs/voice');
const {List} = require('./List')
const {LogType} = require("loguix");
const songCheck = require('./SongCheck')
const ffmpeg = require('fluent-ffmpeg')
const fs = require('fs')
const { PassThrough } = require('stream');
const plog = new LogType("Player")
const clog = new LogType("Signal")
const media = require('./Method/Media');
const youtube = require('./Method/Youtube');
const soundcloud = require('./Method/Soundcloud');
const AllPlayers = new Map()
class Player {
connection;
connected = false;
player;
guildId;
channelId;
queue;
currentResource;
loop = false;
constructor(guildId) {
if(this.guildId === null) {
clog.error("Impossible de créer un Player, car guildId est null")
return
}
if(AllPlayers.has(guildId)) {
return AllPlayers.get(guildId)
}
this.connection = null
this.player = null
this.guildId = guildId
this.queue = new List(guildId)
AllPlayers.set(guildId, this)
}
async join(channel) {
if(getVoiceConnection(channel.guild.id)) {
clog.log(`GUILD : ${this.guildId} - Une connexion existe déjà pour ce serveur`)
return
}
this.joinChannel(channel)
this.player = createAudioPlayer()
this.generatePlayerEvents()
}
isConnected() {
return this.connected
}
joinChannel(channel) {
this.channelId = channel.id
this.connection = joinVoiceChannel({
channelId: channel.id,
guildId: channel.guild.id,
adapterCreator: channel.guild.voiceAdapterCreator,
selfDeaf: false,
selfMute: false
});
this.connection.on('stateChange', (oldState, newState) => {
clog.log(`GUILD : ${this.guildId} - [STATE] OLD : "${oldState.status}" NEW : "${newState.status}"`);
// Si la connection est fermée, on détruit le player
if(newState.status === VoiceConnectionStatus.Disconnected) {
this.leave()
}
});
this.connected = true
process.emit("PLAYERS_UPDATE")
}
generatePlayerEvents() {
const Activity = require('../discord/Activity');
this.player.on('error', error => {
plog.error(`GUILD : ${this.guildId} - Une erreur est survenue dans le player`);
plog.error(error);
console.error(error);
process.emit("PLAYERS_UPDATE")
});
this.player.on(AudioPlayerStatus.Idle, () => {
if(this.checkConnection()) return
// Si la musique est en boucle, on relance la musique
if(this.loop) {
this.play(this.queue.current)
return
}
// Si la musique n'est pas en boucle, on passe à la musique suivante
Activity.idleActivity()
this.queue.setCurrent(null)
if(this.queue.next.length > 0) {
this.play(this.queue.nextSong())
}
process.emit("PLAYERS_UPDATE")
});
this.player.on(AudioPlayerStatus.Playing, () => {
if(this.checkConnection()) return
plog.log(`GUILD : ${this.guildId} - Le player est en train de jouer le contenu suivant : ${this.queue.current.title}`);
Activity.setMusicActivity(this.queue.current.title, this.queue.current.author, this.queue.current.thumbnail)
process.emit("PLAYERS_UPDATE")
});
}
checkConnection() {
if(this.connection === null) {
clog.error(`GUILD : ${this.guildId} - La connection n'est pas définie`)
return true
}
if(this.player === null) {
plog.error(`GUILD : ${this.guildId} - Le player n'est pas défini`)
return true
}
}
getState() {
const playerStatus = this.player?.state?.status ?? false;
const connectionStatus = this.connection?.state?.status ?? false;
const state = {
current: this.queue.current,
next: this.queue.next,
previous: this.queue.previous,
loop: this.loop,
shuffle: this.queue.shuffle,
paused: playerStatus === AudioPlayerStatus.Paused,
playing: playerStatus === AudioPlayerStatus.Playing,
duration: this.getDuration(),
playerState: playerStatus,
connectionState: connectionStatus,
channelId: this.channelId,
guildId: this.guildId,
}
return state
}
async setLoop() {
if(this.checkConnection()) return
this.loop = !this.loop
if(this.loop) {
plog.log(`GUILD : ${this.guildId} - La musique est en boucle`)
} else {
plog.log(`GUILD : ${this.guildId} - La musique n'est plus en boucle`)
}
process.emit("PLAYERS_UPDATE")
}
async setShuffle() {
if(this.checkConnection()) return
this.queue.shuffle = !this.queue.shuffle
if(this.queue.shuffle) {
plog.log(`GUILD : ${this.guildId} - La musique est en mode aléatoire`)
} else {
plog.log(`GUILD : ${this.guildId} - La musique n'est plus en mode aléatoire`)
}
process.emit("PLAYERS_UPDATE")
}
async play(song) {
if(!songCheck.checkSong(song)) return
if(this.checkConnection()) return
if(this.queue.current != null) {
this.player.stop()
}
this.queue.setCurrent(song)
this.stream = await this.getStream(song)
if(this.stream === null) {
plog.error(`GUILD : ${this.guildId} - Impossible de lire la musique : ${song.title} avec le type : ${song.type}`)
return
}
this.playStream(this.stream)
plog.log(`GUILD : ${this.guildId} - Lecture de la musique : ${song.title} - Type : ${song.type}`)
}
async getStream(song) {
let stream = null
if(song.type == "attachment") {
stream = await media.getStream(song)
}
if(song.type == 'youtube') {
stream = await youtube.getStream(song)
}
if(song.type == "soundcloud") {
stream = await soundcloud.getStream(song)
}
return stream
}
async add(song) {
if(this.player?.state?.status == AudioPlayerStatus.Idle && this.queue.current === null && this.queue.next.length === 0) {
this.play(song)
return
}
this.queue.addNextSong(song)
plog.log(`GUILD : ${this.guildId} - La musique a été ajoutée à la liste de lecture : ${song.title}`)
}
async readPlaylist(playlist, now) {
if(this.player?.state?.status == AudioPlayerStatus.Idle && this.queue.current === null && this.queue.next.length === 0) {
this.play(playlist.songs[0])
this.queue.addNextPlaylist(playlist, true)
return
}
if(now) this.play(playlist.songs[0])
this.queue.addNextPlaylist(playlist, now)
plog.log(`GUILD : ${this.guildId} - La playlist a été ajoutée à la liste de lecture : ${playlist.title}`)
}
async pause() {
if(this.checkConnection()) return "no_music"
if(this.player.state.status == AudioPlayerStatus.Paused) {
this.player.unpause()
plog.log(`GUILD : ${this.guildId} - La musique a été reprise`)
process.emit("PLAYERS_UPDATE")
return false
} else {
this.player.pause()
plog.log(`GUILD : ${this.guildId} - La musique a été mise en pause`)
process.emit("PLAYERS_UPDATE")
return true
}
const { LogType } = require('loguix')
}
async leave() {
const Activity = require('../discord/Activity');
if(this.checkConnection()) return
if(this.queue.current != null) {
this.queue.addPreviousSong(this.queue.current)
}
// Détruit la connection et le player et l'enlève de la liste des
this.connection.destroy()
this.player.stop()
this.player = null
this.connection = null
this.channelId = null
this.connected = false
Activity.idleActivity()
this.queue.destroy()
AllPlayers.delete(this.guildId)
clog.log("Connection détruite avec le guildId : " + this.guildId)
plog.log("Player détruit avec le guildId : " + this.guildId)
process.emit("PLAYERS_UPDATE")
}
async setDuration(duration) {
//FIXME: SET DURATION FONCTIONNE TRES LENTEMENT
if (this.checkConnection()) return;
if (this.queue.current == null) return;
if (this.currentResource == null) return;
const maxDuration = this.queue.current.duration;
if (duration > maxDuration) {
plog.error(`GUILD : ${this.guildId} - La durée demandée dépasse la durée maximale de la musique.`);
return;
}
this.stream = await this.getStream(this.queue.current);
if (this.stream === null) {
plog.error(`GUILD : ${this.guildId} - Impossible de lire la musique : ${this.queue.current.title} avec le type : ${this.queue.current.type}`);
return;
}
// Si stream est un lien, ouvrir le stream à partir du lien
if(typeof this.stream === "string") {
this.stream = fs.createReadStream(this.stream)
}
const passThroughStream = new PassThrough();
ffmpeg(this.stream)
.setStartTime(duration) // Démarrer à la position demandée (en secondes)
.outputOptions('-f', 'mp3') // Specify output format if needed
.on('error', (err) => {
plog.error(`GUILD : ${this.guildId} - Une erreur est survenue avec ffmpeg : ${err.message}`);
})
.pipe(passThroughStream, { end: true });
this.stream = passThroughStream;
this.playStream(this.stream); // Jouer le nouveau flux
this.currentResource.playbackDuration = duration * 1000; // Mettre à jour la durée de lecture du resource
plog.log(`GUILD : ${this.guildId} - Lecture déplacée à ${duration}s.`);
}
playStream(stream) {
if(this.checkConnection()) return
if(this.player !== null) this.player.stop();
this.player = createAudioPlayer()
this.generatePlayerEvents()
const resource = createAudioResource(stream, { inputType: StreamType.Arbitrary });
this.setCurrentResource(resource)
this.player.play(resource);
this.connection.subscribe(this.player);
process.emit("PLAYERS_UPDATE")
}
getDuration() {
// Return the duration of player
if(this.checkConnection()) return
if(this.queue.current == null) return
if(this.currentResource == null) return
return this.currentResource.playbackDuration / 1000
}
setCurrentResource(value) {
this.currentResource = value;
}
changeChannel(channel) {
if(this.checkConnection()) return
if(this.connection === null) return
if(this.connection.channelId === channel.id) return
this.connection.destroy()
this.joinChannel(channel)
// Si la musique est en cours de lecture, on la relance avec le bon timecode
if(this.player) {
this.connection.subscribe(this.player);
}
process.emit("PLAYERS_UPDATE")
}
async skip() {
if(this.checkConnection()) return "no_music"
if(this.queue.next.length === 0) {
return "no_music"
}
const songSkip = this.queue.nextSong()
this.play(songSkip)
process.emit("PLAYERS_UPDATE")
return songSkip
}
async previous() {
if(this.checkConnection()) return "no_music"
if(this.queue.getPrevious().length === 0) {
return "no_music"
}
const songPrevious = this.queue.previousSong()
this.play(songPrevious)
process.emit("PLAYERS_UPDATE")
return songPrevious
}
}
/**
*
* @param {string} guildId
* @returns {Player} player
*/
function getPlayer(guildId) {
if(AllPlayers.has(guildId)) {
return AllPlayers.get(guildId)
} else {
return new Player(guildId)
}
}
function getAllPlayers() {
const players = new Array()
AllPlayers.forEach((player) => {
players.push(player)
})
}
function isPlayer(guildId) {
return AllPlayers.has(guildId)
}
module.exports = {Player, AllPlayers, getPlayer, isPlayer, getAllPlayers}
/*
You can access created connections elsewhere in your code without having to track the connections yourself. It is best practice to not track the voice connections yourself as you may forget to clean them up once they are destroyed, leading to memory leaks.
const connection = getVoiceConnection(myVoiceChannel.guild.id);
*/

84
src/player/Song.js Normal file
View File

@@ -0,0 +1,84 @@
const {LogType} = require('loguix')
const clog = new LogType("Song")
const MediaInformation = require('../media/MediaInformation')
const { getReadableDuration, getSecondsDuration } = require('../utils/TimeConverter');
class Song {
title = "Aucun titre";
id = "Aucun fichier";
author = "Auteur inconnu"
authorId;
url;
thumbnail = "https://radomisol.fr/wp-content/uploads/2016/08/cropped-note-radomisol-musique.png" ;
duration;
readduration;
form = "SONG";
type;
userAddedId;
constructor(properties) {
if(properties) {
this.type = properties.type ?? this.type
this.title = properties.title ?? this.title
this.id = properties.id ?? this.id
this.author = properties.author ?? this.author
this.url = properties.url ?? this.url
this.thumbnail = properties.thumbnail ?? this.thumbnail
this.duration = properties.duration ?? this.duration
this.readduration = properties.readduration ?? this.readduration
this.type = properties.type ?? this.type
this.authorId = properties.authorId ?? this.authorId
}
}
async processMedia(media, provider) {
if(provider) this.author = provider;
if(provider) this.authorId = provider;
// Check if media is a file or a link
if(media.attachment) {
this.url = media.attachment.url
this.id = media.attachment.name
this.type = "attachment"
// In face, duration is null, get the metadata of the file to get the duration
await MediaInformation.getMediaInformation(this, media)
} else {
clog.error("Impossible de traiter le média")
}
}
async processYoutubeVideo(video, playlist) {
if(playlist) {
this.title = video.title
this.author = video.author.name
this.authorId = video.author.channel_url
this.thumbnail = video.thumbnail_url
this.url = video.url
this.type = "youtube"
this.id = video.id
this.duration = video.milis_length / 1000
this.readduration = getReadableDuration(this.duration)
} else {
this.title = video.name
this.author = video.author.name
this.authorId = video.author.url
this.thumbnail = video.thumbnail
this.url = video.url
this.type = "youtube"
this.id = video.id
this.duration = getSecondsDuration(video.duration)
this.readduration = getReadableDuration(this.duration)
}
return this
}
}
module.exports = {Song}

44
src/player/SongCheck.js Normal file
View File

@@ -0,0 +1,44 @@
const {LogType} = require("loguix")
const {Song} = require("./Song")
const slog = new LogType("SongCheck")
function checkSong(song) {
if(!(song instanceof Song)) {
slog.error("La musique n'est pas une instance de la classe Song")
// Check if the song is valid and if it has all the required properties
if(song.title && song.id && song.author && song.url && song.duration && song.readduration && song.type) {
slog.log("Acceptation de la musique : " + song.title)
return true
} else {
slog.error("La musique n'est pas valide")
return false
}
}
if(!song.url) {
slog.error("La musique n'a pas d'url")
return false
}
if(!song.title) {
slog.error("La musique n'a pas de titre")
return false
}
if(!song.author) {
slog.error("La musique n'a pas d'auteur")
return false
}
if(!song.duration) {
slog.error("La musique n'a pas de durée")
return false
}
if(!song.readduration) {
slog.error("La musique n'a pas de durée lisible")
return false
}
if(!song.type) {
slog.error("La musique n'a pas de type")
return false
}
return true
}
module.exports = {checkSong}

View File

@@ -0,0 +1,87 @@
const { LogType } = require('loguix');
const alog = new LogType("GoogleOAuth2");
const { google } = require('googleapis');
const config = require("../../utils/Database/Configuration");
const Users = require('../../server/auth/User');
const clientId = config.getYoutubeApiClientId();
const clientSecret = config.getYoutubeApiClientSecret();
const redirectUri = config.getWebsiteLink() + "/oauth2callback";
const oAuth2Map = new Map();
function createAuthUrl(userId) {
if(!checkCredientials()) return null;
var oAuth2Client;
const user = Users.getUserById(userId);
if (!user) {
alog.error(`User with ID ${userId} not found.`);
return null;
}
if (!clientId || !clientSecret) {
alog.error("YouTube API client ID or secret is not set in the configuration.");
} else {
oAuth2Client = new google.auth.OAuth2(
clientId,
clientSecret,
redirectUri
);
alog.log("Google OAuth2 client initialized successfully.");
}
if (!oAuth2Client) {
alog.error("OAuth2 client is not initialized. Please check your configuration.");
return null;
}
oAuth2Map.set(userId, oAuth2Client);
alog.log(`OAuth2 client created for user ${userId}.`);
return oAuth2Client.generateAuthUrl({
access_type: 'offline',
scope: SCOPES,
});
}
async function getAuthorization(userId, code) {
if(!checkCredientials()) return null;
try {
const user = Users.getUserById(userId);
if (!user) {
alog.error(`User with ID ${userId} not found.`);
return null;
}
oAuth2Client = oAuth2Map.get(userId);
if (!oAuth2Client) {
alog.error(`OAuth2 client for user ${userId} not found. Please create an OAuth2 client first.`);
return null;
}
const { tokens } = await oAuth2Client.getToken(code);
oAuth2Client.setCredentials(tokens);
alog.log(`OAuth2 client credentials set for user ${userId}.`);
return oAuth2Client;
} catch (error) {
alog.error(`Error during OAuth2 authorization for user ${userId}:`, error);
return null;
}
}
function checkCredientials() {
if (!clientId || !clientSecret) {
alog.error("YouTube API client ID or secret is not set in the configuration.");
return false;
}
return true;
}
module.exports = {
createAuthUrl,
getAuthorization,
getOAuth2Client: (userId) => oAuth2Map.get(userId),
oAuth2Map
};
const SCOPES = ['https://www.googleapis.com/auth/youtube.readonly'];

View File

@@ -0,0 +1,62 @@
const { google } = require('googleapis');
const { LogType } = require('loguix');
const alog = new LogType("YoutubeAPI");
const OAuth2 = require('./OAuth2');
const Users = require('../../server/auth/User');
async function getYoutubePlaylists(userId) {
const user = Users.getUserById(userId);
if (!user) {
alog.error(`User with ID ${userId} not found.`);
return null;
}
const oAuth2Client = OAuth2.getOAuth2Client(userId);
if (!oAuth2Client) {
alog.error(`OAuth2 client for user ${userId} not found. Please authenticate first.`);
return null;
}
const youtube = google.youtube({ version: 'v3', auth: oAuth2Client });
try {
const response = await youtube.playlists.list({
part: 'snippet,contentDetails',
mine: true,
maxResults: 50
});
alog.log(`Retrieved playlists for user ${userId}.`);
return response.data.items;
} catch (error) {
alog.error(`Error retrieving playlists for user ${userId}:`, error);
return null;
}
}
function getYoutubePlaylistSongs(playlistId, userId) {
const user = Users.getUserById(userId);
if (!user) {
alog.error(`User with ID ${userId} not found.`);
return null;
}
const oAuth2Client = OAuth2.getOAuth2Client(userId);
if (!oAuth2Client) {
alog.error(`OAuth2 client for user ${userId} not found. Please authenticate first.`);
return null;
}
const youtube = google.youtube({ version: 'v3', auth: oAuth2Client });
return youtube.playlistItems.list({
part: 'snippet',
playlistId: playlistId,
maxResults: 50
});
}
module.exports = {
getYoutubePlaylists,
getYoutubePlaylistSongs
};

54
src/playlists/History.js Normal file
View File

@@ -0,0 +1,54 @@
const {LogType} = require("loguix")
const hlog = new LogType("PersonalHistory")
const {__glob} = require("../utils/GlobalVars")
const { Database } = require("../utils/Database/Database")
const historyDb = new Database("history", __glob.HISTORY_DB, {})
historyDb.load()
/**
* @param {string} userId
* @returns {Array<Object>}
* @description Renvoie l'historique personnel de l'utilisateur
*/
function getPersonalHistory(userId) {
if (historyDb.data[userId]) {
return historyDb.data[userId];
} else {
hlog.log(`Création d'une clé pour l'utilisateur : ${userId}`);
historyDb.data[userId] = [];
historyDb.save();
return historyDb.data[userId];
}
}
/**
* @param {string} userId
* @param {Object} entry
* @description Ajoute une entrée à l'historique personnel de l'utilisateur
*/
function addToPersonalHistory(userId, entry) {
hlog.log(`Ajout d'une entrée à l'historique personnel de l'utilisateur : ${userId}`);
const history = getPersonalHistory(userId);
// Limit to 25 entries
if (history.length >= 25) {
history.shift();
}
history.push(entry)
historyDb.save();
}
/**
* @param {string} userId
* @description Vide l'historique personnel de l'utilisateur
*/
function clearPersonalHistory(userId) {
hlog.log(`Vidage de l'historique personnel de l'utilisateur : ${userId}`);
historyDb.data[userId] = [];
historyDb.save();
}
module.exports = {
getPersonalHistory,
addToPersonalHistory,
clearPersonalHistory
};

36
src/playlists/Playlist.js Normal file
View File

@@ -0,0 +1,36 @@
const { getReadableDuration } = require("../utils/TimeConverter");
class Playlist {
title = "Aucun titre";
id;
url;
author = "Auteur inconnu";
authorId;
songs = new Array();
thumbnail = "https://radomisol.fr/wp-content/uploads/2016/08/cropped-note-radomisol-musique.png" ;
duration = 0;
readduration;
description;
form = "PLAYLIST";
type;
constructor(title, url, author, authorId, songs, thumbnail, duration, readduration, description) {
this.title = title;
this.url = url;
this.author = author;
this.authorId = authorId;
this.songs = songs || new Array();
this.thumbnail = thumbnail;
// Make the some of durations of the songs
if(this.songs.length > 0) {
this.duration = this.songs.reduce((acc, song) => acc + song.duration, 0);
this.readduration = getReadableDuration(this.duration);
}
this.description = description;
if(!this.url) {
this.type = "playlist";
}
}
}
module.exports = {Playlist};

View File

@@ -0,0 +1,278 @@
const {Database} = require('../utils/Database/Database');
const {__glob} = require('../utils/GlobalVars');
const {Playlist} = require('./Playlist');
const {LogType} = require('loguix');
const clog = new LogType("PlaylistManager");
const Finder = require('../player/Finder');
const spotify = require('../media/SpotifyInformation');
const { getYoutubePlaylistSongs } = require('./Google/YoutubeList');
const { auth } = require('googleapis/build/src/apis/abusiveexperiencereport');
const { getReadableDuration } = require('../utils/TimeConverter');
const { getSecondsFromUrl } = require('../media/YoutubeInformation');
const playlistDB = new Database("Playlists", __glob.PLAYLISTFILE, {});
/**
* @param {string} id
* @param {string} name
* @returns {Array<Playlist>}
* @description Renvoie la liste des playlists de l'utilisateur
*/
function getPlaylistsOfUser(id) {
if (playlistDB.data[id]) {
return playlistDB.data[id];
} else {
// Creaete a key with the user id and an empty array
playlistDB.data[id] = new Array();
clog.log(`Création d'une clé pour l'utilisateur : ${id}`);
playlistDB.save();
return playlistDB.data[id];
}
}
/**
* @param {string} id
* @param {string} name
* @returns {Playlist}
*/
function getPlaylistOfUser(id, name) {
const playlists = getPlaylistsOfUser(id);
const playlist = playlists.find(p => p.title === name);
if (!playlist) {
clog.warn(`La playlist ${name} n'existe pas pour l'utilisateur ${id}`);
return null;
}
return playlist;
}
async function addPlaylist(id, name, url) {
const playlists = getPlaylistsOfUser(id);
var playlist = new Playlist(name, url);
if (playlists.find(p => p.title === name)) {
clog.warn(`La playlist ${name} existe déjà pour l'utilisateur ${id}`);
return;
}
var failed;
if(url) {
await Finder.search(url, false, "PLAYLIST").then(async (playlistFounded) => {
if(!playlistFounded) {
failed = true;
}
if(playlistFounded instanceof Playlist) {
playlist = playlistFounded;
}
if(playlist.type === "spotify") {
playlist.songs = await spotify.getTracks(playlist);
}
})
}
if(failed) {
clog.error(`Impossible de trouver la playlist ${name} pour l'utilisateur ${id}`);
return null;
}
playlists.push(playlist);
playlistDB.save();
clog.log(`Ajout de la playlist ${name} pour l'utilisateur ${id}`);
return playlist;
}
function removePlaylist(id, name) {
const playlists = getPlaylistsOfUser(id);
const index = playlists.findIndex(p => p.title === name);
if (index === -1) {
clog.warn(`La playlist ${name} n'existe pas pour l'utilisateur ${id}`);
return;
}
playlists.splice(index, 1);
playlistDB.save();
clog.log(`Suppression de la playlist ${name} pour l'utilisateur ${id}`);
}
function getPlaylist(id, name) {
const playlists = getPlaylistsOfUser(id);
const playlist = playlists.find(p => p.title === name);
if (!playlist) {
clog.warn(`La playlist ${name} n'existe pas pour l'utilisateur ${id}`);
return null;
}
return playlist;
}
function copyPlaylist(fromId, toId, name) {
const playlists = getPlaylistsOfUser(fromId);
const playlist = playlists.find(p => p.title === name);
if (!playlist) {
clog.warn(`La playlist ${name} n'existe pas pour l'utilisateur ${fromId}`);
return null;
}
const toPlaylists = getPlaylistsOfUser(toId);
// Check if the playlist already exists in the target user
if (toPlaylists.find(p => p.title === name)) {
clog.warn(`La playlist ${name} existe déjà pour l'utilisateur ${toId}`);
return null;
}
toPlaylists.push(playlist);
playlistDB.save();
clog.log(`Copie de la playlist ${name} de l'utilisateur ${fromId} vers l'utilisateur ${toId}`);
return false;
}
function renamePlaylist(id, oldName, newName) {
const playlists = getPlaylistsOfUser(id);
const playlist = playlists.find(p => p.title === oldName);
if (!playlist) {
clog.warn(`La playlist ${oldName} n'existe pas pour l'utilisateur ${id}`);
return null;
}
// Check if the new name already exists
if (playlists.find(p => p.title === newName)) {
clog.warn(`La playlist ${newName} existe déjà pour l'utilisateur ${id}`);
return null;
}
playlist.title = newName;
playlistDB.save();
clog.log(`Renommage de la playlist ${oldName} en ${newName} pour l'utilisateur ${id}`);
}
function addSong(id, playlistName, song) {
if(typeof song === "string") {
try {
song = JSON.parse(song)
} catch (e) {
clog.error(`La chanson ${song} n'est pas valide`);
return null;
}
}
// Check if the song is a valid object
if (typeof song !== 'object' || !song) {
clog.error(`La chanson ${song} n'est pas valide`);
return null;
}
const playlists = getPlaylistsOfUser(id);
const playlist = playlists.find(p => p.title === playlistName);
if (!playlist) {
clog.warn(`La playlist ${playlistName} n'existe pas pour l'utilisateur ${id}`);
return null;
}
// Check the integrity of the song
if (!song.id || !song.title || !song.url) {
clog.error(`La chanson ${song.title} n'est pas valide`);
return null;
}
playlist.songs.push(song);
playlistDB.save();
clog.log(`Ajout de la chanson ${song.title} à la playlist ${playlistName} pour l'utilisateur ${id}`);
}
function removeSong(id, playlistName, songId) {
const playlists = getPlaylistsOfUser(id);
const playlist = playlists.find(p => p.title === playlistName);
if (!playlist) {
clog.warn(`La playlist ${playlistName} n'existe pas pour l'utilisateur ${id}`);
return null;
}
const index = playlist.songs.findIndex(s => s.id === songId);
if (index === -1) {
clog.warn(`La chanson ${songId} n'existe pas dans la playlist ${playlistName} pour l'utilisateur ${id}`);
return null;
}
playlist.songs.splice(index, 1);
playlistDB.save();
clog.log(`Suppression de la chanson ${songId} de la playlist ${playlistName} pour l'utilisateur ${id}`);
}
async function processYoutubeData(userId, data) {
if (!data || data.length === 0) {
clog.warn(`Aucune donnée YouTube trouvée pour l'utilisateur ${userId}`);
return [];
}
const playlists = [];
for (const item of data) {
if (item.snippet && item.contentDetails) {
const playlist = new Playlist();
playlist.id = item.id;
playlist.title = item.snippet.title;
playlist.url = `https://www.youtube.com/playlist?list=${item.id}`;
playlist.description = item.snippet.description || "Aucune description disponible";
playlist.author = item.snippet.channelTitle;
playlist.thumbnail = item.snippet.thumbnails.default.url;
playlist.authorId = `https://www.youtube.com/channel/${item.snippet.channelId}`;
playlist.songs = []; // You can fetch songs later if needed
await getYoutubePlaylistSongs(item.id, userId).then(songsData => {
if (songsData && songsData.data && songsData.data.items) {
playlist.songs = songsData.data.items.map(song => ({
id: song.snippet.resourceId.videoId,
title: song.snippet.title,
author: song.snippet.videoOwnerChannelTitle,
authorId: `https://www.youtube.com/channel/${song.snippet.videoOwnerChannelId}`,
url: `https://www.youtube.com/watch?v=${song.snippet.resourceId.videoId}`,
thumbnail: song.snippet?.thumbnails?.default?.url || "https://radomisol.fr/wp-content/uploads/2016/08/cropped-note-radomisol-musique.png",
}));
// Add readduration for every items in songs
} else {
clog.warn(`Aucune chanson trouvée pour la playlist ${item.id}`);
}
}).catch(err => {
clog.error(`Erreur lors de la récupération des chansons pour la playlist ${item.id}:`, err);
});
for (const song of playlist.songs) {
// If authorId is not defined, delete the song
if (song.authorId == "https://www.youtube.com/channel/undefined") {
clog.warn(`L'auteur de la chanson ${song.title} (${song.id}) n'est pas défini. Suppression de la chanson.`);
playlist.songs.splice(playlist.songs.indexOf(song), 1);
continue; // Skip this song
}
song.duration = await getSecondsFromUrl(song.url);
if (song.duration === null) {
clog.warn(`Impossible de récupérer la durée de la chanson ${song.title} (${song.id})`);
song.duration = 0; // Set to 0 if duration cannot be fetched
} else {
song.readduration = getReadableDuration(song.duration);
playlist.duration += song.duration; // Initialize duration if not set
}
}
playlist.readduration = getReadableDuration(playlist.duration);
playlist.type = "youtube";
playlists.push(playlist);
} else {
clog.warn(`Données YouTube manquantes pour l'élément ${item.id}`);
}
};
clog.log(`Traitement des données YouTube pour l'utilisateur ${userId} terminé. Nombre de playlists trouvées : ${playlists.length}`);
// Save the playlists to the user's playlist collection
const userPlaylists = getPlaylistsOfUser(userId);
// Remove existing playlists with the same IDs to avoid duplicates
for (const playlist of playlists) {
const existingIndex = userPlaylists.findIndex(p => p.id === playlist.id);
if (existingIndex !== -1) {
userPlaylists.splice(existingIndex, 1); // Remove existing playlist with the same ID
}
}
userPlaylists.push(...playlists);
playlistDB.save();
clog.log(`Playlists ajoutées pour l'utilisateur ${userId}. Nombre total de playlists : ${userPlaylists.length}`);
return playlists;
}
module.exports = {
getPlaylistsOfUser,
getPlaylistOfUser,
addPlaylist,
removePlaylist,
getPlaylist,
copyPlaylist,
renamePlaylist,
addSong,
removeSong,
processYoutubeData
}

940
src/server/Server.js Normal file
View File

@@ -0,0 +1,940 @@
const {LogType} = require('loguix')
const wlog = new LogType("Server")
const fs = require("fs")
const path = require("path")
const {Server} = require('socket.io')
const {createServer} = require('http')
const session = require("../server/auth/Session")
const users = require("../server/auth/User")
const players = require("../player/Player")
const {Player} = require("../player/Player")
const discordBot = require("../discord/Bot")
const discordAuth = require("../server/auth/DiscordAuth")
const {Report} = require("../discord/ReportSender")
const Finder = require("../player/Finder")
const {__glob} = require("../utils/GlobalVars")
const playlists = require("../playlists/PlaylistManager")
const history = require("../playlists/History")
const lyrics = require("../lyrics/Lyrics")
const mediaBase = require("../discord/MediaBase")
const googleApis = require("../playlists/Google/OAuth2")
const youtubeApi = require("../playlists/Google/YoutubeList")
const configuration = require("../utils/Database/Configuration")
const { List } = require('../player/List')
const { restart } = require('../utils/Maintenance')
const { isAudioFile } = require('../utils/AudioBufferCheck')
const { Song } = require('../player/Song')
const { getMediaInformationFromUrl } = require('../media/MediaInformation')
const allConnectedUsers = new Array()
const guildConnectedUsers = new Map()
const UsersBySocket = new Map()
//TODO: Refactor this file to implement the fact that server can be joined and leaved and all the events are now handled, so guildId is not required for every event
function init() {
wlog.step.init("server_init", "Initialisation du serveur Socket.IO")
const httpServer = createServer()
const io = new Server(httpServer, {
cors: {
origin: "*"
},
})
process.on("PLAYERS_UPDATE", () => {
if(io) {
// Get all players and send them to client subscribed to the guild
for(var guild of discordBot.getGuilds().keys()) {
const player = players.getPlayer(guild)
if(player) {
if(!player.isConnected()) continue;
io.to(player.guildId).emit("/PLAYER/UPDATE", player.getState())
wlog.log("Envoi de l'état du player de la guilde : " + player.guildId + " à tous les utilisateurs connectés")
}
}
}
})
process.on("USERS_UPDATE", () => {
if(io) {
// Get all players and send them to client subscribed to the guild
for(var guild of discordBot.getGuilds().keys()) {
if(guildConnectedUsers.has(guild)) {
io.to(guild).emit("/USERS/UPDATE", {"id": guild, "members": guildConnectedUsers.get(guild)} )
io.to("ADMIN").emit("ALL_USERS_UPDATE", allConnectedUsers)
wlog.log("Envoi de la liste des utilisateurs connectés (" + guildConnectedUsers.get(guild).length +") à la guilde : " + guild + " à tous les utilisateurs connectés")
}
}
io.sockets.emit("/USER/READY")
}
})
io.on("connection", async (socket) => {
var socketUser;
// Make sure Discord Bot is loaded and make an interruption until it is loaded
while(!await discordBot.isReady()) {
wlog.warn("Attente de traitement : "+ socket.id + " : Le bot Discord n'est pas encore chargé, attente de 0.5 seconde... (Avoid Rate Limit)")
await new Promise(resolve => setTimeout(resolve, 500))
}
wlog.log(`Connexion d'un client : ${socket.id}`)
socket.on("disconnect", () => {
handleDisconnect()
wlog.log("Déconnexion du client : " + socket.id)
})
socket.on("error", (error) => {
handleDisconnect()
wlog.error("Erreur sur le socket : " + socket.id + " - " + error)
})
if(socket.handshake.auth == undefined || socket.handshake.auth == {}) {
wlog.warn("Authentification manquant pour le client :" + socket.id)
sendSession()
return
}
var token = socket.handshake.auth.token
var sessionId = socket.handshake.auth.sessionId
var auth_code = socket.handshake.auth.auth_code
var inLogin = false
if(sessionId) {
if(!session.checkSession(sessionId)) {
wlog.warn("Session invalide pour le client : " + socket.id)
sendSession()
return;
} else {
if(auth_code) {
const discordUser = await discordAuth.getDiscordUser(sessionId, auth_code)
session.removeSession(sessionId)
if(discordUser == "USER_INFO_ERROR" || discordUser == "ACCESS_TOKEN_ERROR") {
wlog.warn("Erreur lors de la récupération des informations de l'utilisateur Discord associé à la session : " + sessionId)
socket.emit("AUTH_ERROR", discordUser)
socket.disconnect()
return
} else {
const loggedUser = await users.addUser(discordUser.auth, discordUser.identity)
for(var guild of discordUser.guilds) {
if(guild.owner) {
users.setGuildOwner(loggedUser.identity.id, guild.id, true)
}
}
const newToken = await loggedUser.createToken()
socket.emit("NEW_TOKEN", newToken)
token = newToken
inLogin = true
wlog.log("Utilisateur Discord associé à la session : " + sessionId + " récupéré avec succès")
}
} else {
wlog.warn("Code d'authentification manquant pour le client :" + socket.id)
socket.emit("AUTH_ERROR", "Code manquant invalide")
socket.disconnect()
return
}
}
}
if(!token) {
wlog.warn("Token manquant pour le client :" + socket.id)
socket.emit("AUTH_ERROR", "Token invalide")
sendSession()
return
}
socketUser = users.getUserByToken(token)
if(!socketUser) {
wlog.warn("Token invalide pour le client :" + socket.id)
socket.emit("AUTH_ERROR", "Token invalide")
sendSession()
return
} else {
if(!socketUser.auth) {
wlog.warn("L'utilisateur '" + socketUser.identity.username + "' n'a pas d'authentification Discord Valide")
socketUser.clearToken()
socket.emit("AUTH_ERROR", "L'authentification Discord de l'utilisateur n'est pas valide")
socket.disconnect()
return
}
if(!inLogin) {
if(socketUser.needUpdate()) {
if (!(await users.updateIdentity(socketUser.identity.id))) {
wlog.error("Erreur lors de la mise à jour des informations de l'utilisateur : " + socketUser.identity.id);
socket.emit("AUTH_ERROR", "Mise à jour des informations de l'utilisateur impossible");
wlog.log("Déconnexion de l'utilisateur : " + socketUser.identity.username + " (" + socketUser.identity.id + ") - Socket : " + socket.id)
socket.disconnect();
return;
}
socketUser.justUpdated()
} else {
wlog.log("Pas de mise à jour des informations de l'utilisateur : " + socketUser.identity.id + " car l'utilisateur vient de se connecter")
}
} else {
wlog.log("L'utilisateur '" + socketUser.identity.username + "' s'est connecté via la session : " + sessionId)
}
}
socketUser = users.getUserByToken(token)
if(socketUser) {
var actualGuildId = null
if(allConnectedUsers.includes(socketUser.identity)) {
wlog.warn("L'utilisateur '" + socketUser.identity.username + "' est déjà connecté sur un autre appareil")
return
} else {
allConnectedUsers.push(socketUser.identity)
UsersBySocket.set(socketUser.identity.id, socket.id)
}
wlog.log("Utilisateur connecté : " + socketUser.identity.username + " (" + socketUser.identity.id + ") - Socket : " + socket.id)
if(socketUser.isFullBanned()) {
wlog.warn("Utilisateur banni : " + socketUser.identity.username + " (" + socketUser.identity.id + ") - Socket : " + socket.id)
socket.emit("AUTH_ERROR", "Vous êtes banni du serveur")
socket.disconnect()
}
if(socketUser.isAdmin()) {
socket.join("ADMIN")
wlog.log("Utilisateur admin identifié : " + socketUser.identity.username + " (" + socketUser.identity.id + ")")
}
process.emit("USERS_UPDATE")
// USERS
// CHECKED : 24/04/2025
IORequest("/USER/INFO", () => {
var guildPresents = new Array();
var guildsOfBot = discordBot.getGuilds()
for(var guild of guildsOfBot) {
if(guild[1].allMembers.includes(socketUser.identity.id)) {
const guildData = guild[1]
guildData['members'] = new Array()
guildData.serverMember = guild[1].allMembers.length
for(var user of guildConnectedUsers.get(guild[0]) || []) {
const userData = users.getUserById(user.id)
if(userData && userData.identity.id != socketUser.identity.id) {
let infos = {
id: userData.identity.id,
username: userData.identity.username,
avatar: userData.identity.avatar,
isAdmin: userData.isAdmin(),
isOwner: userData.isOwner(guild[0]),
isMod: userData.isMod(guild[0]),
}
guildData.members.push(infos)
}
}
// Send if the bot is connected to the guild
if(players.getPlayer(guild[0]) && players.getPlayer(guild[0]).isConnected()) {
guildData.connected = true
} else {
guildData.connected = false
}
// Leave the room if the user is not in the guild
if(socket.rooms.has(guild[0]) && !checkUserGuild(socketUser, guild[0])) {
socket.leave(guild[0])
removeGuildConnectedUser(socketUser.identity)
wlog.warn("L'utilisateur '" + socketUser.identity.username + "' quitte la room de la guilde : " + guild[0] + " car il n'est pas dans la guilde) /!\\")
}
guildPresents.push(guildData)
}
}
IOAnswer("/USER/INFO", {
identity: socketUser.identity,
guilds: guildPresents,
labels: socketUser.labels,
history: history.getPersonalHistory(socketUser.identity.id),
})
wlog.log("Envoi des informations Discord de '" + socketUser.identity.id + "' à '" + socket.id + "'" )
})
IORequest("/USER/HISTORY", () => {
IOAnswer("/USER/HISTORY", history.getPersonalHistory(socketUser.identity.id))
})
//CHECKED : 24/04/2025
IORequest("/USER/SIGNOUT", () => {
socketUser.removeToken(token)
IOAnswer("/USER/SIGNOUT", true)
socket.disconnect()
})
// CHECKED : 24/04/2025
IORequest("/USERS/LIST", () => {
if(!checkUserGuild(socketUser, actualGuildId)) return
if(!guildConnectedUsers.has(actualGuildId)) return IOAnswer("/USERS/LIST", false)
IOAnswer("/USERS/LIST", guildConnectedUsers.get(actualGuildId))
})
// PLAYERS
IORequest("/PLAYER/LYRICS", async () => {
if(!checkUserGuild(socketUser, actualGuildId)) return
const player = await verifyPlayerAction(actualGuildId)
if(!player) return IOAnswer("/PLAYER/LYRICS", false)
if(!player.queue?.current) {
wlog.warn("Le player de la guilde : " + actualGuildId + " n'a pas de musique en cours")
IOAnswer("/PLAYER/LYRICS", false)
return
}
const song = player.queue.current
const lyricsData = await lyrics.getLyrics(song.title + " " + song.author)
if(!lyricsData) {
wlog.warn("Aucune lyrics trouvée pour la musique : " + song.title + " de l'artiste : " + song.author)
IOAnswer("/PLAYER/LYRICS", false)
return
}
IOAnswer("/PLAYER/LYRICS", lyricsData)
})
//CHECKED : 03/05/2025
IORequest("/PLAYER/PREVIOUS/LIST", () => {
if(!checkUserGuild(socketUser, actualGuildId)) return
const list = new List(actualGuildId)
IOAnswer("/PLAYER/PREVIOUS/LIST", list.getPrevious())
})
// ChECKED : 03/05/2025
IORequest("/GUILD/JOIN", async (guildId) => {
if(!checkUserGuild(socketUser, guildId)) return IOAnswer("/GUILD/JOIN", "No guild found or not in the guild")
if(socket.rooms.has(guildId)) {
wlog.warn("L'utilisateur '" + socketUser.identity.username + "' est déjà dans la room de la guilde : " + guildId)
} else {
// Make him to leave all the other rooms except the ADMIN room if he is admin
await socket.rooms.forEach((room) => {
if(room != "ADMIN" && room != guildId && room != socket.id) {
socket.leave(room)
wlog.log("L'utilisateur '" + socketUser.identity.username + "' quitte la room de la guilde: " + room)
removeGuildConnectedUser(socketUser.identity)
}
})
socket.join(guildId)
wlog.log("L'utilisateur '" + socketUser.identity.username + "' rejoint la room de la guilde : " + guildId)
addGuildConnectedUser(socketUser.identity, guildId)
actualGuildId = guildId
IOAnswer("/GUILD/JOIN", true)
process.emit("PLAYERS_UPDATE")
process.emit("USERS_UPDATE")
}
})
// CHECKED : 03/05/2025
IORequest("/PLAYER/STATE", async () => {
const plaryer = await verifyPlayerAction(actualGuildId)
if(!player) return IOAnswer("/PLAYER/STATE", false)
IOAnswer("/PLAYER/STATE", await player.getState())
})
// CHECKED : 03/05/2025
IORequest("/PLAYER/PAUSE", () => {
handlePlayerAction(actualGuildId, (player) => player.pause(), "/PLAYER/PAUSE");
});
// CHECKED : 03/05/2025
IORequest("/PLAYER/BACKWARD", () => {
handlePlayerAction(actualGuildId, (player) => player.previous(), "/PLAYER/BACKWARD");
});
// CHECKED : 03/05/2025
IORequest("/PLAYER/FORWARD", () => {
handlePlayerAction(actualGuildId, (player) => player.skip(), "/PLAYER/FORWARD");
});
// CHECKED : 03/05/2025
IORequest("/PLAYER/LOOP", () => {
handlePlayerAction(actualGuildId, (player) => player.setLoop(), "/PLAYER/LOOP");
});
// CHECKED : 03/05/2025
IORequest("/PLAYER/SHUFFLE", () => {
handlePlayerAction(actualGuildId, (player) => player.setShuffle(), "/PLAYER/SHUFFLE");
});
// CHECKED : 03/05/2025
IORequest("/PLAYER/DISCONNECT", () => {
handlePlayerAction(actualGuildId, (player) => player.leave(), "/PLAYER/DISCONNECT");
});
// CHECKED : 03/05/2025
IORequest("/PLAYER/CHANNEL/CHANGE", () => {
handlePlayerAction(actualGuildId, (player) => {
const channel = getUserChannel()
if(!channel) {
IOAnswer("/PLAYER/CHANNEL/CHANGE", false)
return
}
player.changeChannel(channel)
}, "/PLAYER/CHANNEL/CHANGE");
});
// CHECKED : 03/05/2025
IORequest("/PLAYER/SEEK", (time) => {
if(!time) return IOAnswer("/PLAYER/SEEK", false)
handlePlayerAction(actualGuildId, (player) => {
// Check if current is not null
if(player.queue.current == null) {
wlog.warn("Le player de la guilde : " + guildId + " n'a pas de musique en cours")
IOAnswer("/PLAYER/SEEK", false)
return
}
player.setDuration(time)
}, "/PLAYER/SEEK");
});
// CHECKED : 04/05/2025
IORequest("/QUEUE/PLAY", (data) => {
if(!data) return IOAnswer("/QUEUE/PLAY/NOW", false)
const {index, listType, now} = data
if(!index) return IOAnswer("/QUEUE/PLAY/NOW", false)
if(!listType) return IOAnswer("/QUEUE/PLAY/NOW", false)
if(!checkUserGuild(socketUser, actualGuildId)) return
const player = new Player(actualGuildId)
if(!connectToPlayer(actualGuildId, player)) return IOAnswer("/QUEUE/PLAY", false)
var song;
if(listType == "previous") {
const previous = player.queue.getPrevious()
song = previous[index]
} else if(listType == "next") {
const next = player.queue.getNext()
song = next[index]
}
if(!song) return IOAnswer("/QUEUE/PLAY/NOW", false)
if(listType == "next") player.queue.removeNextByIndex(index)
if(now) {
player.play(song)
} else {
player.add(song)
}
history.addToPersonalHistory(socketUser.identity.id, song)
IOAnswer("/QUEUE/PLAY/NOW", true)
})
// CHECKED : 04/05/2025
IORequest("/QUEUE/NEXT/DELETE", (index) => {
if(!index) return IOAnswer("/QUEUE/NEXT/DELETE", false)
handlePlayerAction(actualGuildId, (player) => {
const next = player.queue.getNext()
if(!next[index]) return IOAnswer("/QUEUE/NEXT/DELETE", false);
player.queue.removeNextByIndex(index)
}, "/QUEUE/NEXT/DELETE")
})
// CHECKED : 04/05/2025
IORequest("/QUEUE/NEXT/DELETEALL", () => {
handlePlayerAction(actualGuildId, (player) => player.queue.clearNext(), "/QUEUE/NEXT/DELETEALL")
})
// CHECKED : 04/05/2025
IORequest("/QUEUE/NEXT/MOVE", (data) => {
if(!data) return IOAnswer("/QUEUE/NEXT/MOVE", false)
const {index, newIndex} = data
if(!index) return IOAnswer("/QUEUE/NEXT/MOVE", false)
if(!newIndex) return IOAnswer("/QUEUE/NEXT/MOVE", false)
handlePlayerAction(actualGuildId, (player) => {
const next = player.queue.getNext()
if(!next[index]) return IOAnswer("/QUEUE/NEXT/MOVE", false);
player.queue.moveNext(index, newIndex)
}, "/QUEUE/NEXT/MOVE")
})
// SEARCH
// CHECKED : 24/04/2025
IORequest("/SEARCH", async (query) => {
IOAnswer("/SEARCH", await Finder.search(query, true))
})
// CHECKED : 03/05/2025
IORequest("/SEARCH/PLAY", async (data) => {
if(!data) return IOAnswer("/SEARCH/PLAY", false)
var {song, now} = data
if(!song) return IOAnswer("/SEARCH/PLAY", false)
if(typeof song == "string") {
song = JSON.parse(song)
}
if(!checkUserGuild(socketUser, actualGuildId)) return
const player = new Player(actualGuildId)
if(!connectToPlayer(actualGuildId, player)) return IOAnswer("/SEARCH/PLAY", false)
if(now) {
player.play(song)
} else {
player.add(song)
}
history.addToPersonalHistory(socketUser.identity.id, song)
IOAnswer("/SEARCH/PLAY", true)
})
// CHECKED : 05/05/2025
IORequest("/SEARCH/PLAYLIST", async (data) => {
if(!data) return IOAnswer("/SEARCH/PLAYLIST", false)
const {url, now} = data
if(!url) return IOAnswer("/SEARCH/PLAYLIST", false)
const playlist = await Finder.search(url, true, "PLAYLIST")
if(!playlist) return IOAnswer("/SEARCH/PLAYLIST", false)
const player = new Player(actualGuildId)
if(!connectToPlayer(actualGuildId, player)) return IOAnswer("/SEARCH/PLAYLIST", false)
player.readPlaylist(playlist, now)
IOAnswer("/SEARCH/PLAYLIST", true)
})
IORequest("/SEARCH/LYRICS", async (name) => {
if(!name) return IOAnswer("/SEARCH/LYRICS", false)
const lyricsData = await lyrics.getLyrics(name)
if(!lyricsData) return IOAnswer("/SEARCH/LYRICS", false)
IOAnswer("/SEARCH/LYRICS", lyricsData)
})
// UPLOAD
// CHECKED : 29/05/2025
IORequest("/UPLOAD/FILE", async (data) => {
if(!data) return IOAnswer("/UPLOAD/FILE", false)
if(!data.name) return IOAnswer("/UPLOAD/FILE", false)
const file = data.file
// Check wav or mp3
if(isAudioFile(file) == false) {
wlog.warn("Le fichier envoyé n'est pas un fichier audio valide (MP3/WAV)")
return IOAnswer("/UPLOAD/FILE", false)
}
const url = await mediaBase.postMedia(data)
if(!url) return IOAnswer("/UPLOAD/FILE", false)
IOAnswer("/UPLOAD/FILE", {"url": url, "name": data.name})
})
// CHECKED : 29/05/2025
IORequest("/UPLOAD/FILE/GET_SONG", async (data) => {
if(!data) return IOAnswer("/UPLOAD/FILE/GET_SONG", false)
const {name, url} = data
if(!url) return IOAnswer("/UPLOAD/FILE/GET_SONG", false)
if(!name) return IOAnswer("/UPLOAD/FILE/GET_SONG", false)
const song = new Song()
if(!song) return IOAnswer("/UPLOAD/FILE/GET_SONG", false)
await getMediaInformationFromUrl(song, url)
song.type = "attachment"
song.author = socketUser.identity.username
song.authorId = socketUser.identity.id
song.title = name
song.url = url
IOAnswer("/UPLOAD/FILE/GET_SONG", song)
})
// GOOGLE API
IORequest("/GOOGLE/AUTH", () => {
IOAnswer("/GOOGLE/AUTH", googleApis.createAuthUrl(socketUser.identity.id))
})
IORequest("/GOOGLE/YOUTUBE/ADD_PLAYLIST", async (code) => {
if(!code) {
IOAnswer("/GOOGLE/YOUTUBE/ADD_PLAYLIST", false)
}
const token = await googleApis.getAuthorization(socketUser.identity.id, code)
if(!token) {
IOAnswer("/GOOGLE/YOUTUBE/ADD_PLAYLIST", false)
return
}
playlists.processYoutubeData(socketUser.identity.id, await youtubeApi.getYoutubePlaylists(socketUser.identity.id))
IOAnswer("/GOOGLE/YOUTUBE/ADD_PLAYLIST", true)
})
// PLAYLISTS
// CHECKED : 30/04/2025
IORequest("/PLAYLISTS/CREATE", async (data) => {
if(!data) return IOAnswer("/PLAYLISTS/CREATE", false)
const {name, url} = data
if(!name) return IOAnswer("/PLAYLISTS/CREATE", false)
const playlist = await playlists.addPlaylist(socketUser.identity.id, name, url)
if(!playlist) return IOAnswer("/PLAYLISTS/CREATE", false)
IOAnswer("/PLAYLISTS/CREATE", true)
})
// CHECKED : 30/04/2025
IORequest("/PLAYLISTS/DELETE", (data) => {
if(!data) return IOAnswer("/PLAYLISTS/DELETE", false)
const {name} = data
if(!name) return IOAnswer("/PLAYLISTS/DELETE", false)
playlists.removePlaylist(socketUser.identity.id, name)
IOAnswer("/PLAYLISTS/DELETE", true)
})
// CHECKED : 24/04/2025
IORequest("/PLAYLISTS/LIST", () => {
const playlist = playlists.getPlaylistsOfUser(socketUser.identity.id)
IOAnswer("/PLAYLISTS/LIST", playlist)
})
// CHECKED : 30/04/2025
IORequest("/PLAYLISTS/SEND", (data) => {
if(!data) return IOAnswer("/PLAYLISTS/SEND", false)
const {name, toId} = data
if(!name || !toId) return IOAnswer("/PLAYLISTS/SEND", false)
// Check if toId is in the same guilds as the user
// Check if the toId exists and have a playlist with the same name
const toUser = users.getUserById(toId)
if(!toUser) return IOAnswer("/PLAYLISTS/SEND", false)
const toPlaylists = playlists.getPlaylistsOfUser(toUser.identity.id)
const fromPlaylist = playlists.getPlaylistOfUser(socketUser.identity.id, name)
if(!fromPlaylist) return IOAnswer("/PLAYLISTS/SEND", false)
if(toPlaylists.find(p => p.name == name)) return IOAnswer("/PLAYLISTS/SEND", false)
playlists.copyPlaylist(socketUser.identity.id, toUser.identity.id, name)
IOAnswer("/PLAYLISTS/SEND", true)
})
// CHECKED : 30/04/2025
IORequest("/PLAYLISTS/RENAME", (data) => {
if(!data) return IOAnswer("/PLAYLISTS/RENAME", false)
const {name, newName} = data
if(!name || !newName) return IOAnswer("/PLAYLISTS/RENAME", false)
const playlist = playlists.getPlaylistOfUser(socketUser.identity.id, name)
if(!playlist) return IOAnswer("/PLAYLISTS/RENAME", false)
playlists.renamePlaylist(socketUser.identity.id, name, newName)
IOAnswer("/PLAYLISTS/RENAME", true)
})
// CHECKED : 30/04/2025
IORequest("/PLAYLISTS/ADD_SONG", (data) => {
if(!data) return IOAnswer("/PLAYLISTS/ADD_SONG", false)
const {name, song} = data
if(!name || !song) return IOAnswer("/PLAYLISTS/ADD_SONG", false)
const playlist = playlists.getPlaylistOfUser(socketUser.identity.id, name)
if(!playlist) return IOAnswer("/PLAYLISTS/ADD_SONG", false)
playlists.addSong(socketUser.identity.id, name, song)
IOAnswer("/PLAYLISTS/ADD_SONG", true)
})
// CHECKED : 30/04/2025
IORequest("/PLAYLISTS/REMOVE_SONG", (data) => {
if(!data) return IOAnswer("/PLAYLISTS/REMOVE_SONG", false)
const {name, songId} = data
if(!name || !songId) return IOAnswer("/PLAYLISTS/REMOVE_SONG", false)
const playlist = playlists.getPlaylistOfUser(socketUser.identity.id, name)
if(!playlist) return IOAnswer("/PLAYLISTS/REMOVE_SONG", false)
playlists.removeSong(socketUser.identity.id, name, songId)
IOAnswer("/PLAYLISTS/REMOVE_SONG", true)
})
// CHECKED : 05/05/2025
IORequest("/PLAYLISTS/PLAY", async (data) => {
if(!data) return IOAnswer("/PLAYLISTS/PLAY", false)
const {name, now} = data
if(!name) return IOAnswer("/PLAYLISTS/PLAY", false)
if(!checkUserGuild(socketUser, actualGuildId)) return IOAnswer("/PLAYLISTS/PLAY", false)
const playlist = playlists.getPlaylistOfUser(socketUser.identity.id, name)
if(!playlist) return IOAnswer("/PLAYLISTS/PLAY", false)
const player = new Player(actualGuildId)
if(!await connectToPlayer(actualGuildId, player)) return IOAnswer("/PLAYLISTS/PLAY", false)
player.readPlaylist(playlist, now)
IOAnswer("/PLAYLISTS/PLAY", true)
})
// ADMIN
if(socketUser.isAdmin()) {
// CHECKED : 24/04/2025
IORequest("/ADMIN/LOGS", () => {
if(!socketUser.isAdmin()) return IOAnswer("/ADMIN/LOGS", false)
const logs_data = new Array()
const logs_folder = fs.readdirSync(__glob.LOGS)
for(var log of logs_folder) {
logs_data.push({"name":log, "value": fs.readFileSync(__glob.LOGS + path.sep + log).toString()})
}
IOAnswer("/ADMIN/LOGS", logs_data)
})
// CHECKED : 24/04/2025
IORequest("/ADMIN/MAINTENANCE/RESTART", (reason) => {
if(!socketUser.isAdmin()) return IOAnswer("/ADMIN/MAINTENANCE/RESTART", false)
if(!reason) return IOAnswer("/ADMIN/MAINTENANCE/RESTART", false)
restart(reason)
})
// CHECKED : 24/04/2025
IORequest("/ADMIN/USERS/SWITCH_ADMIN", (userId) => {
if(!socketUser.isAdmin()) return IOAnswer("/ADMIN/USERS/SWITCH_ADMIN", false)
if(socketUser.identity.id == userId) return IOAnswer("/ADMIN/USERS/SWITCH_ADMIN", false)
if(!users.getUserById(userId)) return IOAnswer("/ADMIN/USERS/SWITCH_ADMIN", false)
users.setAdmin(userId)
IOAnswer("/ADMIN/USERS/SWITCH_ADMIN", true)
})
// CHECKED : 24/04/2025
IORequest("/ADMIN/USERS/FULL_BAN", (userId) => {
if(!socketUser.isAdmin()) return IOAnswer("/ADMIN/USERS/FULL_BAN", false)
if(socketUser.identity.id == userId) return IOAnswer("/ADMIN/USERS/FULL_BAN", false)
if(!users.getUserById(userId)) return IOAnswer("/ADMIN/USERS/FULL_BAN", false)
if(users.getUserById(userId).isAdmin()) return IOAnswer("/ADMIN/USERS/FULL_BAN", false)
users.setFullBan(userId)
IOAnswer("/ADMIN/USERS/FULL_BAN", true)
})
// CHECKED : 24/04/2025
IORequest("/ADMIN/USERS/DELETE", (userId) => {
if(!socketUser.isAdmin()) return IOAnswer("/ADMIN/USERS/DELETE", false)
if(socketUser.identity.id == userId) return IOAnswer("/ADMIN/USERS/DELETE", false)
if(!users.getUserById(userId)) return IOAnswer("/ADMIN/USERS/DELETE", false)
if(users.getUserById(userId).isAdmin()) return IOAnswer("/ADMIN/USERS/DELETE", false)
users.removeUser(userId)
const userSocket = UsersBySocket.get(userId)
if(userSocket) {
const socket = io.sockets.sockets.get(userSocket)
if(socket) {
socket.emit("AUTH_ERROR", "Votre compte a été supprimé")
socket.disconnect()
}
}
IOAnswer("/ADMIN/USERS/DELETE", true)
})
// CHECKED : 24/04/2025
IORequest("/ADMIN/PLAYER/GETALLSTATE", async () => {
if(!socketUser.isAdmin()) return IOAnswer("/ADMIN/PLAYER/GETTALLSTATE", false)
const allPlayers = players.getAllPlayers()
const states = new Array()
for(var player in allPlayers) {
await states.push(await player.getState())
}
IOAnswer("/ADMIN/PLAYER/GETALLSTATE", states)
})
}
// CHECKED : 24/04/2025
IORequest("/OWNER/USERS/SWITCH_MOD", (userId) => {
if(userId || actualGuildId) return IOAnswer("/OWNER/USERS/SWITCH_MOD", false)
if(socketUser.identity.id == userId) return IOAnswer("/OWNER/USERS/SWITCH_MOD", false)
if(!socketUser.isOwner(actualGuildId)) return IOAnswer("/OWNER/USERS/SWITCH_MOD", false)
users.setGuildMod(userId, actualGuildId)
IOAnswer("/OWNER/USERS/SWITCH_MOD", true)
})
// CHECKED : 24/04/2025
IORequest("/MOD/USERS/BAN", (userId) => {
if(userId || actualGuildId) return IOAnswer("/MOD/USERS/BAN", false)
if(socketUser.identity.id == userId) return IOAnswer("/MOD/USERS/BAN", false)
if(!socketUser.isMod(actualGuildId)) return IOAnswer("/MOD/USERS/BAN", false)
users.setGuildBan(userId, actualGuildId)
IOAnswer("/MOD/USERS/BAN", true)
})
// UTILS
// CHECKED : 24/04/2025
IORequest("/REPORT", (data) => {
if(data.length < 2) return IOAnswer("/REPORT", false)
if(!data["level"] || !data["desc"]) return IOAnswer("/REPORT", false)
const report = new Report(socketUser.identity.username, data["level"], data["desc"]).send()
IOAnswer("/REPORT", true)
})
// Functions
function getUserChannel() {
const membersVoices = discordBot.getMembersVoices()
const member = membersVoices.get(socketUser.identity.id)
if(member) {
const channelId = member.channelId
const guildId = member.guildId
const channel = discordBot.getChannel(guildId, channelId)
if(!channel) {
wlog.warn("Le channel vocal n'existe pas : " + channelId)
return null
}
return channel
} else {
wlog.warn("L'utilisateur '" + socketUser.identity.username + "' n'est pas dans un channel vocal")
return null
}
}
/**
* @param {Player} player
*/
function connectToPlayer(guildId, player) {
if(!checkUserGuild(socketUser, guildId)) return false
if(player.isConnected()) true
const channel = getUserChannel()
if(!channel) return false
player.join(channel)
return true
}
async function verifyPlayerAction(guildId) {
if (!checkUserGuild(socketUser, guildId)) return null;
const player = players.getPlayer(guildId);
if (player) {
return player;
} else {
wlog.warn(`Le player de la guilde : ${guildId} n'existe pas`);
return null;
}
}
function checkUserGuild(socketUser, guildId) {
if(!guildId) {
wlog.warn("Aucun guildId n'est actif pour l'utilisateur : " + socketUser.identity.username)
return false
}
// Check if the guildId is referenced in the bot guilds
if(!discordBot.getGuilds().has(guildId)) {
wlog.warn("La guilde : " + guildId + " n'est pas référencée dans le bot")
return false
}
if(socketUser.isBanned(guildId)) {
wlog.warn("L'utilisateur '" + socketUser.identity.username + "' est banni de la guilde : " + guildId)
return false
}
const allGuilds = discordBot.getGuilds()
if(!allGuilds.get(guildId).allMembers.includes(socketUser.identity.id)) {
wlog.warn("L'utilisateur '" + socketUser.identity.username + "' n'est pas membre de la guilde : " + guildId)
// Si user admin, override
if(!socketUser.isAdmin()) {
return false
}
wlog.log("L'utilisateur '" + socketUser.identity.username + "' est admin donc à le droit d'accéder à la guilde : " + guildId)
}
return true
}
/**
* @param {function(Player)} action - The action to perform on the player.
*/
async function handlePlayerAction(guildId, action, actionName) {
if (!checkUserGuild(socketUser, guildId)) return;
const player = players.getPlayer(guildId);
if (player) {
await action(player);
wlog.log(`L'utilisateur '${socketUser.identity.username}' effectue l'action '${actionName}' sur le player de la guilde : ${guildId}`);
IOAnswer(actionName, true);
} else {
wlog.warn(`Le player de la guilde : ${guildId} n'existe pas`);
IOAnswer(actionName, false);
}
}
}
function handleDisconnect() {
if(socketUser) {
wlog.log("Déconnexion de l'utilisateur : " + socketUser.identity.username + " (" + socketUser.identity.id + ") - Socket : " + socket.id)
allConnectedUsers.splice(allConnectedUsers.indexOf(socketUser.identity), 1)
removeGuildConnectedUser(socketUser.identity)
process.emit("USERS_UPDATE")
// Remove every rooms include admin
socket.rooms.forEach((room) => {
socket.leave(room)
})
UsersBySocket.delete(socketUser.identity.id)
}
}
function sendSession() {
const newSession = session.addSession(socket.id)
socket.emit("NEW_SESSION", newSession)
wlog.log("Envoi d'une nouvelle session : '" + newSession + "' au client : " + socket.id)
socket.disconnect()
}
function IORequest(RequestName, RequestCallback) {
socket.on(RequestName, (value) => {
wlog.log(socketUser.identity.username + " - Socket : " + socket.id + " - " + RequestName + " - [RECIEVED]")
RequestCallback(value)
})
}
function IOAnswer(AnswerName, AnswerValue) {
wlog.log(socketUser.identity.username + " - Socket : " + socket.id + " - " + AnswerName + " - [ANSWERED]")
socket.emit(AnswerName, AnswerValue)
}
})
httpServer.listen(configuration.getPort(), () => {
wlog.log(`Le serveur écoute sur le port ${configuration.getPort()}`)
wlog.step.end("server_init")
})
function addGuildConnectedUser(user, guildId) {
// Check if the user is already connected to the guild
if(!guildConnectedUsers.has(guildId)) {
guildConnectedUsers.set(guildId, new Array())
}
const users = guildConnectedUsers.get(guildId)
if(users.includes(user)) {
wlog.warn("L'utilisateur '" + user.username + "' est déjà connecté à la guilde : " + guildId)
return
}
guildConnectedUsers.get(guildId).push(user)
}
function removeGuildConnectedUser(user) {
for(var guild of guildConnectedUsers.keys()) {
const users = guildConnectedUsers.get(guild)
if(users.includes(user)) {
users.splice(users.indexOf(user), 1)
if(users.length == 0) {
guildConnectedUsers.delete(guild)
}
}
}
}
}
module.exports = {init}

View File

@@ -0,0 +1,126 @@
const { LogType } = require('loguix');
const dlog = new LogType("DiscordAuth");
const { getWebsiteLink, getClientSecret } = require('../../utils/Database/Configuration');
async function getDiscordUser(sessionId, auth_code) {
return new Promise((resolve, reject) => {
const discordBot = require("../../discord/Bot")
const discordBotClient = discordBot.getClient()
dlog.step.init("discord_auth_" + sessionId, "Authentification Discord de la session :" + sessionId);
dlog.log("Récupération de l'autorisation de récupération des informations de l'utilisateur Discord associé à la session : " + sessionId);
const params = new URLSearchParams();
params.append("client_id", discordBotClient.user.id);
params.append("client_secret", getClientSecret());
params.append("grant_type", "authorization_code");
params.append("code", auth_code);
params.append("redirect_uri", getWebsiteLink() + "/redirect");
params.append("scope", "identify guilds");
fetch("https://discord.com/api/oauth2/token", {
method: "POST",
headers: {
"Content-Type": "application/x-www-form-urlencoded",
},
body: params,
}).then(accessTokenResp => accessTokenResp.json()).then(accessToken => {
if (accessToken.error) {
dlog.step.error("discord_auth_" + sessionId, "Erreur lors de la récupération du token d'accès Discord associé à la session : " + sessionId + " : " + accessToken.error + " : " + accessToken.error_description);
resolve("ACCESS_TOKEN_ERROR");
return;
}
dlog.log("Récupération réussi du token d'accès Discord associé à la session : " + sessionId);
fetch("https://discord.com/api/users/@me", {
headers: {
authorization: `${accessToken.token_type} ${accessToken.access_token}`,
},
}).then(userResp => userResp.json()).then(user => {
dlog.log("Récupération réussi des informations de l'utilisateur Discord associé à la session : " + sessionId + " avec le nom d'utilisateur : " + user.username + " (" + user.id + ")");
// Get the guilds of the user
fetch("https://discord.com/api/users/@me/guilds", {
headers: {
authorization: `${accessToken.token_type} ${accessToken.access_token}`,
},
}).then(guildsResp => guildsResp.json()).then(guilds => {
dlog.log("Récupération réussi des guildes de l'utilisateur Discord associé à la session : " + sessionId + " avec le nom d'utilisateur : " + user.username + " (" + user.id + ")");
dlog.step.end("discord_auth_" + sessionId)
const userData = {
auth: accessToken,
identity: user,
guilds: guilds,
}
resolve(userData);
}).catch(err => {
dlog.step.error("discord_auth_" + sessionId, "Erreur lors de la récupération des guildes de l'utilisateur Discord" + " avec le nom d'utilisateur : " + user.username + " (" + user.id + ")" + " associé à la session : " + sessionId + " : " + err);
resolve("GUILDS_ERROR");
});
}).catch(err => {
dlog.step.error("discord_auth_" + sessionId, "Erreur lors de la récupération des informations de l'utilisateur Discord associé à la session : " + sessionId + " : " + err);
resolve( "USER_INFO_ERROR");
})
}).catch(err => {
dlog.step.error("discord_auth_" + sessionId, "Erreur lors de la récupération du token d'accès Discord associé à la session : " + sessionId + " : " + err);
resolve("ACCESS_TOKEN_ERROR");
})
})
}
function refreshToken(refresh_token) {
return new Promise((resolve, reject) => {
const discordBot = require("../../discord/Bot")
const params = new URLSearchParams();
params.append("client_id", discordBot.getClient().user.id);
params.append("client_secret", getClientSecret());
params.append("grant_type", "refresh_token");
params.append("refresh_token", refresh_token);
params.append("scope", "identify guilds");
fetch("https://discord.com/api/oauth2/token", {
method: "POST",
headers: {
"Content-Type": "application/x-www-form-urlencoded",
},
body: params,
}).then(accessTokenResp => accessTokenResp.json()).then(accessToken => {
if (accessToken.error) {
dlog.error("Erreur lors de la récupération du token d'accès Discord : " + accessToken.error + " : " + accessToken.error_description);
resolve(null);
return;
}
resolve(accessToken);
}).catch(err => {
dlog.error("Erreur lors de la récupération du token d'accès Discord : " + err);
resolve(null);
})
})
}
function getUserIdentity(accessToken) {
return new Promise((resolve, reject) => {
fetch("https://discord.com/api/users/@me", {
headers: {
authorization: `${accessToken.token_type} ${accessToken.access_token}`,
},
}).then(userResp => userResp.json()).then(user => {
if (user.error) {
dlog.error("Erreur lors de la récupération des informations de l'utilisateur Discord : " + user.error + " : " + user.error_description);
resolve(null);
return;
}
resolve(user);
}).catch(err => {
dlog.error("Erreur lors de la récupération des informations de l'utilisateur Discord : " + err);
resolve(null);
})
})
}
module.exports = {getDiscordUser, refreshToken, getUserIdentity}

View File

@@ -0,0 +1,33 @@
const { LogType } = require('loguix');
const { generateSessionId } = require('../../utils/TokenManager');
const clog = new LogType("Session");
const sessions = new Array();
function checkSession(sessionId) {
return sessions.includes(sessionId);
}
function addSession() {
const sessionId = generateSessionId();
if (checkSession(sessionId)) {
clog.warn(`Session ${sessionId} non trouvée dans la liste des sessions.`);
return addSession(); // Recursively generate a new session ID if it already exists
}
sessions.push(sessionId);
clog.log(`Nouvelle session ${sessionId} ajoutée.`);
return sessionId;
}
function removeSession(sessionId) {
const index = sessions.indexOf(sessionId);
if (index > -1) {
sessions.splice(index, 1);
clog.log(`Suppression de la session ${sessionId}.`);
} else {
clog.warn(`Session ${sessionId} non trouvée dans la liste des sessions.`);
}
}
module.exports = {checkSession, addSession, removeSession};

526
src/server/auth/User.js Normal file
View File

@@ -0,0 +1,526 @@
const { Database } = require('../../utils/Database/Database');
const { __glob } = require('../../utils/GlobalVars');
const { generateToken } = require('../../utils/TokenManager');
const { LogType } = require('loguix');
const clog = new LogType("User");
const discordAuth = require('./DiscordAuth');
const e = require('cors');
const UserDB = new Database("Users", __glob.USERFILE, []);
var userList = new Array();
class User {
auth;
identity;
tokens;
labels;
constructor(auth, identity, tokens, labels) {
this.auth = auth;
this.identity = identity;
this.tokens = tokens;
this.labels = labels;
}
setAdmin() {
const userInUserList = userList.find(user => user.identity.id === this.identity.id);
if (!userInUserList) {
clog.warn(`Utilisateur ${this.identity.username} non trouvé dans la liste des utilisateurs.`);
return null;
}
if (userInUserList.labels.includes("ADMIN")) {
userInUserList.labels.splice(userInUserList.labels.indexOf("ADMIN"), 1);
clog.log(`L'utilisateur ${this.identity.username} n'est plus admin.`);
} else {
userInUserList.labels.push("ADMIN");
clog.log(`L'utilisateur ${this.identity.username} est maintenant admin.`);
}
saveUsers()
}
setBan(guildId) {
const userInUserList = userList.find(user => user.identity.id === this.identity.id);
if (!userInUserList) {
clog.warn(`Utilisateur ${this.identity.username} non trouvé dans la liste des utilisateurs.`);
return null;
}
const banLabel = `BAN_${guildId}`;
if (userInUserList.labels.includes(banLabel)) {
userInUserList.labels.splice(userInUserList.labels.indexOf(banLabel), 1);
clog.log(`L'utilisateur ${this.identity.username} n'est plus banni du serveur ${guildId}.`);
} else {
userInUserList.labels.push(banLabel);
clog.log(`L'utilisateur ${this.identity.username} est maintenant banni du serveur ${guildId}.`);
}
saveUsers()
}
createToken() {
const token = generateToken(this.identity.id);
const userInUserList = userList.find(user => user.identity.id === this.identity.id);
if (!userInUserList) {
clog.warn(`Utilisateur ${this.identity.username} non trouvé dans la liste des utilisateurs.`);
return null;
}
userInUserList.tokens.push(token);
saveUsers();
clog.log(`Token créé pour l'utilisateur ${this.identity.username}.`);
return token;
}
removeToken(token) {
const userInUserList = userList.find(user => user.identity.id === this.identity.id);
if (!userInUserList) {
clog.warn(`Utilisateur ${this.identity.username} non trouvé dans la liste des utilisateurs.`);
return null;
}
const index = userInUserList.tokens.indexOf(token);
if (index > -1) {
userInUserList.tokens.splice(index, 1);
saveUsers();
clog.log(`Token supprimé pour l'utilisateur ${this.identity.username}.`);
} else {
clog.warn(`Token non trouvé pour l'utilisateur ${this.identity.username}.`);
}
}
clearToken() {
const userInUserList = userList.find(user => user.identity.id === this.identity.id);
if (!userInUserList) {
clog.warn(`Utilisateur ${this.identity.username} non trouvé dans la liste des utilisateurs.`);
return null;
}
userInUserList.tokens = [];
saveUsers();
clog.log(`Tous les tokens supprimés pour l'utilisateur ${this.identity.username}.`);
return userInUserList.tokens;
}
clearAuth() {
const userInUserList = userList.find(user => user.identity.id === this.identity.id);
if (!userInUserList) {
clog.warn(`Utilisateur ${this.identity.username} non trouvé dans la liste des utilisateurs.`);
return null;
}
userInUserList.auth = null;
saveUsers();
clog.log(`Authentification supprimée pour l'utilisateur ${this.identity.username}.`);
}
destroyAuth() {
const userInUserList = userList.find(user => user.identity.id === this.identity.id);
if (!userInUserList) {
clog.warn(`Utilisateur ${this.identity.username} non trouvé dans la liste des utilisateurs.`);
return null;
}
userInUserList.auth = null;
userInUserList.tokens = [];
saveUsers();
clog.log(`Authentification et tokens supprimés pour l'utilisateur ${this.identity.username}.`);
}
setFullBan() {
const userInUserList = userList.find(user => user.identity.id === this.identity.id);
if (!userInUserList) {
clog.warn(`Utilisateur ${this.identity.username} non trouvé dans la liste des utilisateurs.`);
return null;
}
if (userInUserList.labels.find(label => label == "BAN")) {
userInUserList.labels.splice(userInUserList.labels.indexOf("BAN"), 1);
clog.log(`L'utilisateur ${this.identity.username} n'est plus banni.`);
} else {
userInUserList.labels.push("BAN");
clog.log(`L'utilisateur ${this.identity.username} est maintenant banni.`);
}
saveUsers()
}
isBanned(guildId) {
const banLabel = `BAN_${guildId}`;
return this.labels.includes(banLabel);
}
isFullBanned() {
return this.labels.includes("BAN");
}
isAdmin() {
return this.labels.includes("ADMIN");
}
isMod(guildId) {
if(this.isOwner(guildId)) return true;
const modLabel = `MOD_${guildId}`;
return this.labels.includes(modLabel);
}
isOwner(guildId) {
const ownerLabel = `OWNER_${guildId}`;
if(this.isAdmin()) return true;
return this.labels.includes(ownerLabel);
}
justUpdated() {
const userInUserList = userList.find(user => user.identity.id === this.identity.id);
if (!userInUserList) {
clog.warn(`Utilisateur ${this.identity.username} non trouvé dans la liste des utilisateurs.`);
return false;
}
userInUserList.labels = userInUserList.labels.filter(label => !label.startsWith("UPDATED["));
userInUserList.labels.push("UPDATED[" + new Date().toISOString() + "]");
saveUsers();
}
needUpdate() {
const userInUserList = userList.find(user => user.identity.id === this.identity.id);
if (!userInUserList) {
clog.warn(`Utilisateur ${this.identity.username} non trouvé dans la liste des utilisateurs.`);
return false;
}
const lastUpdate = userInUserList.labels.find(label => label.startsWith("UPDATED["));
if (lastUpdate) {
const date = new Date(lastUpdate.replace("UPDATED[", "").replace("]", ""));
const now = new Date();
const diff = now - date;
// Check for 30 seconds
clog.log(`Dernière mise à jour de l'utilisateur ${this.identity.username} : ${date.toISOString()} (${diff} ms) - Besoin de mise à jour : ${diff > 30000}`);
// If the difference is greater than 30 seconds, we need to update
return diff > 30000; // 30 seconds
}
clog.log(`Aucune mise à jour n'a été effectuée pour l'utilisateur ${this.identity.username}.`);
return true;
}
}
//REFRESH USER
async function refreshAllUserInformation() {
await loadUsers();
clog.log("Récupération des informations de tous les utilisateurs...");
for (const user of userList) {
await updateCredientials(user.identity.id);
}
saveUsers();
}
async function updateCredientials(id) {
const user = getUserById(id);
if (!user) {
clog.warn(`Utilisateur ${id} non trouvé.`);
return null;
}
clog.log(`Mise à jour des informations d'authentification Discord de l'utilisateur ${user.identity.username} (${user.identity.id})...`);
if (user.auth) {
// Check if the token is expired
const auth = await discordAuth.refreshToken(user.auth.refresh_token);
if (auth) {
// Check Rate limit by checking if auth.message exists
if(typeof auth.message !== "undefined") {
clog.warn(`Erreur lors de la mise à jour des informations d'authentification de l'utilisateur ${user.identity.username} (${user.identity.id}) : ${auth.message}`);
return null;
}
user.auth = auth;
clog.log(`Mise à jour réussie des informations d'authentification de l'utilisateur ${user.identity.username} (${user.identity.id})`);
} else {
clog.warn(`Erreur lors de la mise à jour des informations d'authentification de l'utilisateur ${user.identity.username} (${user.identity.id})`);
}
// Update the user in the list
const userInUserList = userList.find(u => u.identity.id === user.identity.id);
if (userInUserList) {
userInUserList.auth = user.auth;
}
}
else {
clog.warn(`Aucune authentification trouvée pour l'utilisateur ${user.identity.username} (${user.identity.id})`);
}
saveUsers();
return user.auth;
}
async function updateIdentity(id) {
const user = getUserById(id);
if (!user) {
clog.warn(`Utilisateur ${id} non trouvé.`);
return null;
}
clog.log(`Mise à jour de l'identité de l'utilisateur ${user.identity.username} (${user.identity.id})...`);
if (user.auth) {
const identity = await discordAuth.getUserIdentity(user.auth);
if(identity) {
// Check Rate limit by checking if identity.message exists
if(typeof identity.message !== "undefined") {
clog.warn(`Erreur lors de la mise à jour de l'identité de l'utilisateur ${user.identity.username} (${user.identity.id}) : ${identity.message}`);
return null;
}
user.identity = identity;
clog.log(`Mise à jour réussie de l'identité de l'utilisateur ${user.identity.username} (${user.identity.id})`);
}
else {
clog.warn(`Erreur lors de la mise à jour de l'identité de l'utilisateur ${user.identity.username} (${user.identity.id})`);
return null
}
// Update the user in the list
const userInUserList = userList.find(u => u.identity.id === user.identity.id);
if (userInUserList) {
userInUserList.auth = user.auth;
userInUserList.identity = user.identity;
}
} else {
clog.warn(`Aucune authentification trouvée pour l'utilisateur ${user.identity.username} (${user.identity.id})`);
return null;
}
saveUsers();
return user.identity;
}
// EDIT USER
/**
*
* @param {*} auth
* @param {*} identity
* @returns {User} user
*/
async function addUser(auth, identity) {
// Check if the user already exists
const existingUser = userList.find(user => user.identity.id === identity.id);
if (existingUser) {
clog.warn(`L'utilisateur ${identity.username} existe déjà.`);
// Update the existing user with new information
existingUser.auth = auth;
existingUser.identity = identity;
existingUser.tokens = existingUser.tokens || []; // Ensure tokens array exists
existingUser.labels = existingUser.labels || []; // Ensure labels array exists
saveUsers();
clog.log(`Utilisateur ${identity.username} mis à jour.`);
return existingUser;
}
const newUser = new User(auth, identity, [], []);
userList.push(newUser);
await saveUsers();
return newUser;
}
function removeUser(id) {
const index = userList.findIndex(user => user.identity.id === id);
if (index > -1) {
userList.splice(index, 1);
saveUsers();
clog.log(`Utilisateur ${id} supprimé.`);
} else {
clog.warn(`Utilisateur ${id} non trouvé.`);
}
}
function removeToken(token) {
const user = getUserByToken(token);
if (user) {
const index = user.tokens.indexOf(token);
if (index > -1) {
user.tokens.splice(index, 1);
saveUsers();
clog.log(`Token ${token} supprimé pour l'utilisateur ${user.identity.username}.`);
} else {
clog.warn(`Token ${token} non trouvé pour l'utilisateur ${user.identity.username}.`);
}
} else {
clog.warn(`Utilisateur avec le token "${token}" non trouvé.`);
}
return user;
}
function addToken(id) {
const user = getUserById(id);
if (user) {
const token = generateToken();
user.tokens.push(token);
saveUsers();
clog.log(`Token "${token}" ajouté pour l'utilisateur ${user.identity.username}.`);
return token;
} else {
clog.warn(`Utilisateur ${id} non trouvé.`);
return null;
}
}
// GET
/**
* @param {string} id
* @returns {User} user
*/
function getUserById(id) {
return userList.find(user => user.identity.id === id) || null;
}
/**
*
* @param {string} token
* @returns {User} user
*/
function getUserByToken(token) {
return userList.find(user => user.tokens.includes(token)) || null;
}
function getUsers() {
return userList;
}
function getSimpleUsers() {
return userList.map(user => {
return {
identity: user.identity,
labels: user.labels,
};
});
}
function getSimpleUser(id) {
const user = getUserById(id);
if(user) {
return {
identity: user.identity,
labels: user.labels,
};
} else {
return null;
}
}
// SET LABELS
function setAdmin(id) {
const user = getUserById(id);
if (user) {
user.setAdmin();
saveUsers();
} else {
clog.warn(`Utilisateur ${id} non trouvé.`);
}
}
function setGuildMod(id, guildId) {
const user = getUserById(id);
if (user) {
const modLabel = `MOD_${guildId}`;
if (user.labels.includes(modLabel)) {
user.labels.splice(user.labels.indexOf(modLabel), 1);
clog.log(`L'utilisateur ${user.identity.username} n'est plus modérateur du serveur ${guildId}.`);
} else {
user.labels.push(modLabel);
clog.log(`L'utilisateur ${user.identity.username} est maintenant modérateur du serveur ${guildId}.`);
}
saveUsers();
} else {
clog.warn(`Utilisateur ${id} non trouvé.`);
}
}
function setGuildBan(id, guildId) {
const user = getUserById(id);
if (user) {
if(user.isAdmin()) {
clog.warn(`L'utilisateur ${user.identity.username} est admin, il ne peut pas être banni.`);
return;
}
user.setBan(guildId);
saveUsers();
} else {
clog.warn(`Utilisateur ${id} non trouvé.`);
}
}
function setFullBan(id) {
const user = getUserById(id);
if (user) {
if(user.isAdmin()) {
clog.warn(`L'utilisateur ${user.identity.username} est admin, il ne peut pas être banni.`);
return;
}
user.setFullBan();
saveUsers();
} else {
clog.warn(`Utilisateur ${id} non trouvé.`);
}
}
function setGuildOwner(id, guildId, force) {
const user = getUserById(id);
if (user) {
const ownerLabel = `OWNER_${guildId}`;
if (user.labels.includes(ownerLabel) && !force) {
user.labels.splice(user.labels.indexOf(ownerLabel), 1);
clog.log(`L'utilisateur ${user.identity.username} n'est plus propriétaire du serveur ${guildId}.`);
} else {
if(force && user.labels.includes(ownerLabel)) {
return;
}
user.labels.push(ownerLabel);
clog.log(`L'utilisateur ${user.identity.username} est maintenant propriétaire du serveur ${guildId}.`);
}
saveUsers();
} else {
clog.warn(`Utilisateur ${id} non trouvé.`);
}
}
// USERS DB
function loadUsers() {
UserDB.load()
userList = new Array();
for (const user of UserDB.getData()) {
userList.push(new User(user.auth, user.identity, user.tokens, user.labels));
}
clog.log(`Chargement de ${userList.length} utilisateurs.`);
return userList;
}
function saveUsers() {
UserDB.data = userList.map(user => {
return {
auth: user.auth,
identity: user.identity,
tokens: user.tokens,
labels: user.labels,
};
});
UserDB.save()
clog.log(`Sauvegarde de ${userList.length} utilisateurs.`);
return loadUsers();
}
function clearNeedUpdateForUsers() {
userList.forEach(user => {
user.labels = user.labels.filter(label => !label.startsWith("UPDATED["));
});
saveUsers();
clog.log("Nettoyage des mises à jour nécessaires pour tous les utilisateurs.");
}
module.exports = {User}
module.exports = {
addUser,
setGuildOwner,
setFullBan,
removeUser,
getUserByToken,
getUserById,
getUsers,
setAdmin,
setGuildMod,
setGuildBan,
addToken,
removeToken,
getSimpleUsers,
getSimpleUser,
updateCredientials,
refreshAllUserInformation,
updateIdentity,
clearNeedUpdateForUsers
};

View File

@@ -0,0 +1,31 @@
function isAudioFile(buffer) {
// Ensure the buffer is long enough to contain the magic number
if (!Buffer.isBuffer(buffer)) {
throw new TypeError('Expected a Buffer');
}
if (buffer.length < 2) {
return false;
}
// Convert the first few bytes to a hex string
const signature = buffer.subarray(0, 4).toString('hex').toUpperCase();
// Audio file signatures
const audioSignatures = [
'494433', // ID3 tag for MP3
'FFFB', // Another possible MP3 signature
'FFF3', // Another possible MP3 signature
'FFF2', // Another possible MP3 signature
'52494646', // RIFF header for WAV
'4F676753', // OGG container
'66747970', // MP4 container
// Add more audio file signatures as needed
];
// Check if the signature matches any known audio file type
return audioSignatures.some(magicNumber => signature.startsWith(magicNumber));
}
module.exports = {
isAudioFile
};

View File

@@ -0,0 +1,102 @@
const {Database} = require("./Database")
const {__glob} = require("../GlobalVars")
const {LogType} = require("loguix")
const path = require("path")
const { get } = require("http")
const clog = new LogType("Configuration")
const config = new Database("config", __glob.DATA + path.sep + "config.json", {
token: "",
client_secret: "",
report: {
channel : "",
contact : ""
},
api: {
youtube: {
clientId : "" ,
clientSecret: ""
},
spotify: {
clientId: "",
clientSecret: ""
}
},
website: "",
server_port: 5000,
media: {
guildId: "",
channelId: "",
}
})
function getToken() {
return config.data.token
}
function getReportChannel() {
return config.data.report.channel
}
function getReportContact() {
return config.data.report.contact
}
function getYoutubeApiClientId() {
return config.data.api.youtube.clientId
}
function getYoutubeApiClientSecret() {
return config.data.api.youtube.clientSecret
}
function getSpotifyClientId() {
return config.data.api.spotify.clientId
}
function getSpotifyClientSecret() {
return config.data.api.spotify.clientSecret
}
function getWebsiteLink() {
return config.data.website
}
function getPort() {
return config.data.server_port
}
function getClientSecret() {
return config.data.client_secret
}
function getMediaGuildId() {
return config.data.media.guildId
}
function getMediaChannelId() {
return config.data.media.channelId
}
if(getToken() == "") {
clog.error("Impossible de démarrer sans token valide")
process.exit(1)
}
module.exports = {
getToken,
getClientSecret,
getReportChannel,
getReportContact,
getYoutubeApiClientId,
getYoutubeApiClientSecret,
getSpotifyClientId,
getSpotifyClientSecret,
getWebsiteLink,
getPort,
getMediaGuildId,
getMediaChannelId
}

View File

@@ -61,6 +61,9 @@ class Database {
clog.error(`Erreur lors de la sauvegarde de la base de données '${this.name}'`) clog.error(`Erreur lors de la sauvegarde de la base de données '${this.name}'`)
clog.error(e) clog.error(e)
} }
// Assure that the database is up to date and reloaded
this.update()
} }
@@ -80,6 +83,10 @@ class Database {
} }
getData() {
return this.data
}
} }

View File

@@ -8,7 +8,12 @@ const __glob = {
LOGS: root + path.sep + "logs", LOGS: root + path.sep + "logs",
DATA: root + path.sep + "data", DATA: root + path.sep + "data",
COMMANDS: root + path.sep + "src" + path.sep + "discord" + path.sep + "commands", COMMANDS: root + path.sep + "src" + path.sep + "discord" + path.sep + "commands",
METRIC_FILE: root + path.sep + "data" + path.sep + "metrics.json" METRIC_FILE: root + path.sep + "data" + path.sep + "metrics.json",
PREVIOUSFILE: root + path.sep + "data" + path.sep + "previous.json",
USERFILE: root + path.sep + "data" + path.sep + "users.json",
PLAYLISTFILE: root + path.sep + "data" + path.sep + "playlists.json",
HISTORY_DB: root + path.sep + "data" + path.sep + "history.json",
MEDIA_DB: root + path.sep + "data" + path.sep + "media.json",
} }
module.exports = {__glob} module.exports = {__glob}

79
src/utils/Links.js Normal file
View File

@@ -0,0 +1,79 @@
const YoutubeLinks = [
"youtube.com",
"youtu.be",
"music.youtube.com",
"gaming.youtube.com",
"www.youtube.com",
"m.youtube.com"
]
var youtubePlaylistRegex = new RegExp(/^https?:\/\/(www.)?youtube.com\/playlist\?list=((PL|FL|UU|LL|RD|OL)[a-zA-Z0-9-_]{16,41})$/)
var youtubeVideoURLRegex = new RegExp(/^((?:https?:)?\/\/)?((?:www|m)\.)?((?:youtube\.com|youtu.be))(\/(?:watch\?v=))([\w-]{11})(\S+)?$/)
var youtubeVideoIdRegex = new RegExp(/^[a-zA-Z0-9-_]{11}$/)
const SpotifyLinks = [
"open.spotify.com",
"embed.spotify.com"
]
var spotifySongRegex = new RegExp(/^https?:\/\/(?:embed\.|open\.)(?:spotify\.com\/)(intl-([a-z]|[A-Z])+\/)?(?:track\/|\?uri=spotify:track:)((\w|-){22})(\?si=.+)?$/)
var spotifyPlaylistRegex = new RegExp(/^https?:\/\/(?:embed\.|open\.)(?:spotify\.com\/)(intl-([a-z]|[A-Z])+\/)?(?:playlist\/|\?uri=spotify:playlist:)((\w|-){22})(\?si=.+)?$/)
var spotifyAlbumRegex = new RegExp(/^https?:\/\/(?:embed\.|open\.)(?:spotify\.com\/)(intl-([a-z]|[A-Z])+\/)?(?:album\/|\?uri=spotify:album:)((\w|-){22})(\?si=.+)?$/)
const SoundcloudLinks = [
"soundcloud.com"
]
var soundcloudTrackRegex = new RegExp(/^https?:\/\/(m.|www.)?soundcloud.com\/(\w|-)+\/(\w|-)+(.+)?$/)
var soundcloudPlaylistRegex = new RegExp(/^https?:\/\/(m.|www.)?soundcloud.com\/(\w|-)+\/sets\/(\w|-)+(.+)?$/)
/**
* @typedef {Object} Links
* @property {Object} regex
* @property {Object} regex.youtube
* @property {RegExp} regex.youtube.playlist
* @property {RegExp} regex.youtube.videoURL
* @property {RegExp} regex.youtube.videoId
* @property {Object} regex.spotify
* @property {RegExp} regex.spotify.song
* @property {RegExp} regex.spotify.playlist
* @property {RegExp} regex.spotify.album
* @property {Object} regex.soundcloud
* @property {RegExp} regex.soundcloud.track
* @property {RegExp} regex.soundcloud.playlist
* @property {Object} types
* @property {Array<String>} types.youtube
* @property {Array<String>} types.spotify
* @property {Array<String>} types.soundcloud
*/
const Links = {
regex: {
youtube: {
playlist: youtubePlaylistRegex,
videoURL: youtubeVideoURLRegex,
videoId: youtubeVideoIdRegex
},
spotify: {
song: spotifySongRegex,
playlist: spotifyPlaylistRegex,
album: spotifyAlbumRegex
},
soundcloud: {
track: soundcloudTrackRegex,
playlist: soundcloudPlaylistRegex
}
},
types: {
youtube: YoutubeLinks,
spotify: SpotifyLinks,
soundcloud: SoundcloudLinks
}
}
module.exports = {Links}

11
src/utils/Maintenance.js Normal file
View File

@@ -0,0 +1,11 @@
const pm2 = require("pm2")
const { LogType } = require("loguix")
const clog = new LogType("Maintenance")
function restart(reason) {
clog.warn("Redémarrage du serveur Subsonics")
clog.warn(`Reason: ${reason}`)
pm2.restart("Subsonics")
}
module.exports = {restart}

16
src/utils/QueryType.js Normal file
View File

@@ -0,0 +1,16 @@
/**
* Enum for query types
* @readonly
*/
const QueryType = {
SPOTIFY_PLAYLIST: 'spotify_playlist',
SPOTIFY_ALBUM: 'spotify_album',
SPOTIFY_SONG: 'spotify_song',
YOUTUBE_PLAYLIST: 'youtube_playlist',
YOUTUBE_VIDEO: 'youtube_video',
SOUNDCLOUD_TRACK: 'soundcloud_track',
SOUNDCLOUD_PLAYLIST: 'soundcloud_playlist',
YOUTUBE_SEARCH: 'youtube_search',
}
module.exports = { QueryType };

32
src/utils/Resolver.js Normal file
View File

@@ -0,0 +1,32 @@
const {Links} = require('./Links')
const {QueryType} = require('./QueryType')
function getQueryType(url) {
// Check if it's string
if(typeof url !== "string") return "NOT_STRING"
// Check if it's a Youtube link
if(Links.regex.youtube.playlist.test(url)) return QueryType.YOUTUBE_PLAYLIST
if(Links.regex.youtube.videoURL.test(url)) return QueryType.YOUTUBE_VIDEO
// Check if it's a Spotify link
if(Links.regex.spotify.playlist.test(url)) return QueryType.SPOTIFY_PLAYLIST
if(Links.regex.spotify.album.test(url)) return QueryType.SPOTIFY_ALBUM
if(Links.regex.spotify.song.test(url)) return QueryType.SPOTIFY_SONG
// Check if it's a Soundcloud link
if(Links.regex.soundcloud.playlist.test(url)) return QueryType.SOUNDCLOUD_PLAYLIST
if(Links.regex.soundcloud.track.test(url)) return QueryType.SOUNDCLOUD_TRACK
return QueryType.YOUTUBE_SEARCH
}
module.exports = {getQueryType}

View File

@@ -0,0 +1,46 @@
function getReadableDuration(duration) {
var max = ""
duration *= 1000;
const maxhours = Math.floor(duration / 3600000);
var maxmin = Math.trunc(duration / 60000) - (Math.floor(duration / 60000 / 60) * 60);
var maxsec = Math.floor(duration / 1000) - (Math.floor(duration / 1000 / 60) * 60);
if (maxsec < 10) {
maxsec = `0${maxsec}`;
}
if(maxhours != 0) {
if (maxmin < 10) {
maxmin = `0${maxmin}`;
}
max = maxhours + "h" + maxmin + "m" + maxsec
} else {
max = maxmin + "m" + maxsec + "s"
}
return max
}
function getSecondsDuration(duration) {
// Duration is in format hh:mm:ss and can be just m:ss or mm:ss
var durationArray = duration.split(":");
var seconds = 0;
if(durationArray.length == 3) {
seconds = parseInt(durationArray[0]) * 3600 + parseInt(durationArray[1]) * 60 + parseInt(durationArray[2]);
} else if(durationArray.length == 2) {
seconds = parseInt(durationArray[0]) * 60 + parseInt(durationArray[1]);
} else {
seconds = parseInt(durationArray[0]);
}
return seconds;
}
module.exports = {getReadableDuration, getSecondsDuration}

18
src/utils/TokenManager.js Normal file
View File

@@ -0,0 +1,18 @@
function generateToken(userId) {
// Generate a token using the user ID with 32 random bytes
const crypto = require('crypto');
const token = userId + "_" + crypto.randomBytes(32).toString('hex');
return token;
}
function generateSessionId() {
// Generate a session ID using 32 random bytes
const crypto = require('crypto');
const sessionId = "SESSION" + "_" + crypto.randomBytes(32).toString('hex');
return sessionId;
}
module.exports = {generateToken, generateSessionId}