From 812d5c72fa830803672ec09f63afdfec28031954 Mon Sep 17 00:00:00 2001 From: Raphix Date: Sun, 20 Apr 2025 23:09:46 +0200 Subject: [PATCH] Version 1.0.0-rc1 - Version initiale (Ajout du serveur, des playlists) --- .gitignore | 3 +- backend/package-lock.json | 49 +- backend/package.json | 7 +- backend/src/discord/Bot.js | 37 +- backend/src/discord/Button.js | 26 + backend/src/discord/Commands/About.js | 5 +- backend/src/discord/Commands/Invite.js | 20 + backend/src/discord/Commands/Media.js | 1 - backend/src/discord/Commands/Pause.js | 2 +- backend/src/discord/Commands/Play.js | 3 +- backend/src/discord/Commands/Previous.js | 1 - backend/src/discord/Commands/Restart.js | 7 + backend/src/discord/Commands/Web.js | 8 +- backend/src/discord/Embed.js | 20 +- backend/src/discord/ReportSender.js | 1 + backend/src/main.js | 2 + backend/src/media/SoundcloudInformation.js | 2 +- backend/src/media/SpotifyInformation.js | 2 +- backend/src/media/YoutubeInformation.js | 17 +- backend/src/player/Finder.js | 7 +- backend/src/player/List.js | 29 +- backend/src/player/Method/Media.js | 19 +- backend/src/player/Method/Soundcloud.js | 24 +- backend/src/player/Method/Youtube.js | 37 +- backend/src/player/Player.js | 227 +++++++- backend/src/player/Playlist.js | 16 - backend/src/playlists/Playlist.js | 30 ++ backend/src/playlists/PlaylistManager.js | 157 ++++++ backend/src/server/Documentation.md | 234 ++++++++ backend/src/server/Server.js | 564 ++++++++++++++++++++ backend/src/server/auth/DiscordAuth.js | 67 +++ backend/src/server/auth/Session.js | 33 ++ backend/src/server/auth/User.js | 301 +++++++++++ backend/src/utils/Database/Configuration.js | 20 +- backend/src/utils/GlobalVars.js | 2 + backend/src/utils/TokenManager.js | 18 + changelog.md | 19 +- 37 files changed, 1860 insertions(+), 157 deletions(-) create mode 100644 backend/src/discord/Button.js create mode 100644 backend/src/discord/Commands/Invite.js delete mode 100644 backend/src/player/Playlist.js create mode 100644 backend/src/playlists/Playlist.js create mode 100644 backend/src/playlists/PlaylistManager.js create mode 100644 backend/src/server/Documentation.md create mode 100644 backend/src/server/Server.js create mode 100644 backend/src/server/auth/DiscordAuth.js create mode 100644 backend/src/server/auth/Session.js create mode 100644 backend/src/server/auth/User.js create mode 100644 backend/src/utils/TokenManager.js diff --git a/.gitignore b/.gitignore index 238b8dd..e8f57ff 100644 --- a/.gitignore +++ b/.gitignore @@ -143,4 +143,5 @@ test/ data/ -__DEBUG.js \ No newline at end of file +__DEBUG.js +__TEST.js \ No newline at end of file diff --git a/backend/package-lock.json b/backend/package-lock.json index fb7330f..4316635 100644 --- a/backend/package-lock.json +++ b/backend/package-lock.json @@ -1,16 +1,16 @@ { "name": "chopin-backend", - "version": "0.3.0", + "version": "0.4.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "chopin-backend", - "version": "0.3.0", + "version": "0.4.0", "license": "ISC", "dependencies": { "@discordjs/voice": "^0.18.0", - "@distube/ytdl-core": "^4.11.5", + "@distube/ytdl-core": "^4.16.8", "@distube/ytsr": "2.0.4", "cors": "^2.8.5", "discord-player": "^7.1.0", @@ -19,6 +19,7 @@ "ffmpeg-static": "^5.2.0", "ffprobe": "^1.1.2", "ffprobe-static": "^3.1.0", + "fluent-ffmpeg": "^2.1.3", "libsodium-wrappers": "^0.7.15", "loguix": "^1.4.2", "nodemon": "^3.1.9", @@ -266,9 +267,9 @@ } }, "node_modules/@distube/ytdl-core": { - "version": "4.16.4", - "resolved": "https://registry.npmjs.org/@distube/ytdl-core/-/ytdl-core-4.16.4.tgz", - "integrity": "sha512-r0ZPMMB5rbUSQSez//dYDWjPSAEOm6eeV+9gyR+1vngGYFUi953Z/CoF4epTBS40X8dR32gyH3ERlh7NbnCaRg==", + "version": "4.16.8", + "resolved": "https://registry.npmjs.org/@distube/ytdl-core/-/ytdl-core-4.16.8.tgz", + "integrity": "sha512-Vl04TCOiSSwCFmOHVfzIX117tpT/eCobp2hwx4lo2EyeE70FMVrpQpKSEdh+EjywmVAHs/ZXIsaDy+wq1fMb+g==", "license": "MIT", "dependencies": { "http-cookie-agent": "^6.0.8", @@ -1573,9 +1574,9 @@ "license": "MIT" }, "node_modules/axios": { - "version": "1.8.1", - "resolved": "https://registry.npmjs.org/axios/-/axios-1.8.1.tgz", - "integrity": "sha512-NN+fvwH/kV01dYUQ3PTOZns4LWtWhOFCAhQ/pHb88WQ1hNe5V/dvFwc4VJcDL11LT9xSX0QtsR8sWUuyOuOq7g==", + "version": "1.8.4", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.8.4.tgz", + "integrity": "sha512-eBSYY4Y68NNlHbHBMdeDmKNtDgXWhQsJcGqzO3iLUM0GraQFSS9cVgPX5I9b3lbdFKyYoAEGAZF1DwhTaljNAw==", "license": "MIT", "dependencies": { "follow-redirects": "^1.15.6", @@ -2854,6 +2855,36 @@ "node": ">= 0.8" } }, + "node_modules/fluent-ffmpeg": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/fluent-ffmpeg/-/fluent-ffmpeg-2.1.3.tgz", + "integrity": "sha512-Be3narBNt2s6bsaqP6Jzq91heDgOEaDCJAXcE3qcma/EJBSy5FB4cvO31XBInuAuKBx8Kptf8dkhjK0IOru39Q==", + "license": "MIT", + "dependencies": { + "async": "^0.2.9", + "which": "^1.1.1" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/fluent-ffmpeg/node_modules/async": { + "version": "0.2.10", + "resolved": "https://registry.npmjs.org/async/-/async-0.2.10.tgz", + "integrity": "sha512-eAkdoKxU6/LkKDBzLpT+t6Ff5EtfSF4wx1WfJiPEEV7WNLnDaRXk0oVysiEPm262roaachGexwUv94WhSgN5TQ==" + }, + "node_modules/fluent-ffmpeg/node_modules/which": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/which/-/which-1.3.1.tgz", + "integrity": "sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==", + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "which": "bin/which" + } + }, "node_modules/follow-redirects": { "version": "1.15.9", "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.9.tgz", diff --git a/backend/package.json b/backend/package.json index bb85ee0..a3a994f 100644 --- a/backend/package.json +++ b/backend/package.json @@ -1,6 +1,6 @@ { "name": "chopin-backend", - "version": "0.4.0", + "version": "1.0.0", "description": "Discord Bot for music - Fetching everywhere !", "main": "src/main.js", "nodemonConfig": { @@ -19,7 +19,7 @@ "license": "ISC", "dependencies": { "@discordjs/voice": "^0.18.0", - "@distube/ytdl-core": "^4.11.5", + "@distube/ytdl-core": "^4.16.8", "@distube/ytsr": "2.0.4", "cors": "^2.8.5", "discord-player": "^7.1.0", @@ -28,13 +28,14 @@ "ffmpeg-static": "^5.2.0", "ffprobe": "^1.1.2", "ffprobe-static": "^3.1.0", + "fluent-ffmpeg": "^2.1.3", "libsodium-wrappers": "^0.7.15", "loguix": "^1.4.2", "nodemon": "^3.1.9", "pm2": "^5.4.3", "socket.io": "^4.8.1", "soundcloud.ts": "^0.6.3", - "spotify-web-api-node": "^5.0.2", + "spotify-web-api-node": "^5.0.2", "uuid": "^11.1.0", "webmetrik": "^0.1.4", "ytfps": "^1.2.0" diff --git a/backend/src/discord/Bot.js b/backend/src/discord/Bot.js index c4b07f2..e6690a9 100644 --- a/backend/src/discord/Bot.js +++ b/backend/src/discord/Bot.js @@ -11,6 +11,7 @@ const dlog = new LogType("Discord") const membersVoices = new Map() const timers = new Map() +const guilds = new Map() const client = new Client({ intents:[GatewayIntentBits.Guilds, GatewayIntentBits.GuildVoiceStates, GatewayIntentBits.GuildMembers], @@ -22,12 +23,32 @@ function getClient() { return client } +function getGuilds() { + return guilds +} + +function getMembersVoices() { + return membersVoices +} + +function getChannel(guildId, channelId) { + return client.guilds.cache.get(guildId).channels.cache.get(channelId) +} function init() { client.once('ready', () => { dlog.log("Connexion au Bot Discord réussi ! Connecté en tant que : " + client.user.tag) + // Add all guilds to the guilds map + client.guilds.cache.forEach(guild => { + guilds.set(guild.id, { + id: guild.id, + name: guild.name, + members: guild.members.cache.map(member => member.user.username), + }) + }) + const Activity = require("./Activity") Activity.idleActivity() @@ -72,6 +93,16 @@ function init() { } }) + // If a new guild is added, we will add it to the guilds map + client.on("guildCreate", (guild) => { + dlog.log("Nouvelle guilde ajoutée : " + guild.name) + guilds.set(guild.id, { + id: guild.id, + name: guild.name, + members: guild.members.cache.map(member => member.user.username), + }) + }) + client.on("voiceStateUpdate", (oldMember, newMember) => { membersVoices.set(newMember.id, { guildId: newMember.guild.id, @@ -84,7 +115,7 @@ function init() { 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 @@ -95,7 +126,7 @@ function init() { dlog.log("[Automatic Task] Guild Id :" + newMember.guild.id + " - Player supprimé : " + channel.name) } - }, 10000)) + }, 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) @@ -115,6 +146,6 @@ function init() { client.login(config.getToken()) } -module.exports = {init, getClient} +module.exports = {init, getClient, getGuilds, getMembersVoices, getChannel} diff --git a/backend/src/discord/Button.js b/backend/src/discord/Button.js new file mode 100644 index 0000000..88850bd --- /dev/null +++ b/backend/src/discord/Button.js @@ -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 }; diff --git a/backend/src/discord/Commands/About.js b/backend/src/discord/Commands/About.js index 85355d5..26093b7 100644 --- a/backend/src/discord/Commands/About.js +++ b/backend/src/discord/Commands/About.js @@ -20,13 +20,16 @@ const command = new Command("about", "Affiche des informations sur le bot", (cli embed.addField("Ping", `${client.ws.ping} ms `, true) embed.addField("Réalisé par", "Raphix - 2025", true) embed.addColumn() - embed.addField('Versions',"") + embed.addField('Versions :',"") embed.addField('Node.js', process.version,true) embed.addField('Discord.js', packageJson.dependencies["discord.js"].replace("^", ""),true) embed.addColumn() embed.addField('Webmetrik', packageJson.dependencies["webmetrik"].replace("^", ""),true) embed.addField('Loguix', packageJson.dependencies["loguix"].replace("^", ""),true) 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) diff --git a/backend/src/discord/Commands/Invite.js b/backend/src/discord/Commands/Invite.js new file mode 100644 index 0000000..9658d31 --- /dev/null +++ b/backend/src/discord/Commands/Invite.js @@ -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() + 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(interaction) + +}) + +module.exports = {command} \ No newline at end of file diff --git a/backend/src/discord/Commands/Media.js b/backend/src/discord/Commands/Media.js index cde51aa..da95bc6 100644 --- a/backend/src/discord/Commands/Media.js +++ b/backend/src/discord/Commands/Media.js @@ -43,7 +43,6 @@ const command = new Command("media", "Lire un média MP3/WAV dans un salon vocal embed.send(interaction) - }, [{type: "FILE", name: "media", description: "Fichier audio à lire", required: true}, {type:"BOOLEAN", name: "now", description: "Lire le média maintenant", required: false}] ) diff --git a/backend/src/discord/Commands/Pause.js b/backend/src/discord/Commands/Pause.js index dac6a82..1d34963 100644 --- a/backend/src/discord/Commands/Pause.js +++ b/backend/src/discord/Commands/Pause.js @@ -34,7 +34,7 @@ const command = new Command("pause", "Mettre en pause / Reprendre la musique en embed.send(interaction) }) - + // Réponse en embed diff --git a/backend/src/discord/Commands/Play.js b/backend/src/discord/Commands/Play.js index 62ba938..b22ba8f 100644 --- a/backend/src/discord/Commands/Play.js +++ b/backend/src/discord/Commands/Play.js @@ -2,7 +2,7 @@ const { Command } = require("../Command"); const { Embed, EmbedError } = require("../Embed"); const { Player } = require("../../player/Player"); const Finder = require("../../player/Finder"); -const { Playlist } = require("../../player/Playlist"); +const { Playlist } = require("../../playlists/Playlist"); const spotify = require("../../media/SpotifyInformation"); const command = new Command("play", "Jouer une musique à partir d'un lien dans un salon vocal", async (client, interaction) => { @@ -44,6 +44,7 @@ const command = new Command("play", "Jouer une musique à partir d'un lien dans 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) diff --git a/backend/src/discord/Commands/Previous.js b/backend/src/discord/Commands/Previous.js index bd1a787..80b88bd 100644 --- a/backend/src/discord/Commands/Previous.js +++ b/backend/src/discord/Commands/Previous.js @@ -4,7 +4,6 @@ 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 !").send(interaction) const channel = interaction.member.voice.channel diff --git a/backend/src/discord/Commands/Restart.js b/backend/src/discord/Commands/Restart.js index 7374f16..1cc2012 100644 --- a/backend/src/discord/Commands/Restart.js +++ b/backend/src/discord/Commands/Restart.js @@ -1,10 +1,17 @@ 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() diff --git a/backend/src/discord/Commands/Web.js b/backend/src/discord/Commands/Web.js index d1d6b42..b365149 100644 --- a/backend/src/discord/Commands/Web.js +++ b/backend/src/discord/Commands/Web.js @@ -1,12 +1,18 @@ 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() embed.setColor(0xffffff) embed.setTitle('Subsonics - Chopin') embed.addBotPicture(client) - embed.addField('Lien',"https://subsonics.raphix.fr/") + + 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(interaction) }) diff --git a/backend/src/discord/Embed.js b/backend/src/discord/Embed.js index 7e6e37b..a2dd7de 100644 --- a/backend/src/discord/Embed.js +++ b/backend/src/discord/Embed.js @@ -1,10 +1,12 @@ -const { EmbedBuilder } = require("discord.js"); +const { EmbedBuilder, ActionRowBuilder } = require("discord.js"); class Embed { fields; + buttons; constructor() { this.embed = new EmbedBuilder().setTimestamp() this.fields = [] + this.buttons = [] } setTitle(title) { @@ -75,18 +77,24 @@ class Embed { return this } + addButton(button) { + this.buttons.push(button) + return this + } + build() { //Add Fields to an object this.embed.addFields(this.fields) + if(this.buttons.length > 0) { + this.actionRow = new ActionRowBuilder() + .addComponents(this.buttons); + } return this.embed } send(interaction, ephemeral) { - if(ephemeral) { - interaction.reply({embeds: [this.build()], ephemeral: true}) - } else { - interaction.reply({embeds: [this.build()]}) - } + if(ephemeral === undefined) ephemeral = false; + interaction.reply({ embeds: [this.build()], ephemeral: ephemeral, components: this.buttons.length > 0 ? [this.actionRow] : [] }) } } diff --git a/backend/src/discord/ReportSender.js b/backend/src/discord/ReportSender.js index 9b86dc0..42036f9 100644 --- a/backend/src/discord/ReportSender.js +++ b/backend/src/discord/ReportSender.js @@ -46,6 +46,7 @@ class Report { } + } module.exports = {Report} \ No newline at end of file diff --git a/backend/src/main.js b/backend/src/main.js index ccc049e..0c531f6 100644 --- a/backend/src/main.js +++ b/backend/src/main.js @@ -21,4 +21,6 @@ setup(); async function setup() { const DiscordBot = require("./discord/Bot") DiscordBot.init() + const Server = require("./server/Server") + Server.init() } \ No newline at end of file diff --git a/backend/src/media/SoundcloudInformation.js b/backend/src/media/SoundcloudInformation.js index bd3cf52..1643005 100644 --- a/backend/src/media/SoundcloudInformation.js +++ b/backend/src/media/SoundcloudInformation.js @@ -1,7 +1,7 @@ const {LogType} = require('loguix'); const clog = new LogType("SoundcloudInformation"); const {Song} = require('../player/Song'); -const {Playlist} = require('../player/Playlist'); +const {Playlist} = require('../playlists/Playlist'); const {Soundcloud} = require('soundcloud.ts') const {getReadableDuration} = require('../utils/TimeConverter'); diff --git a/backend/src/media/SpotifyInformation.js b/backend/src/media/SpotifyInformation.js index d8727eb..c5337e7 100644 --- a/backend/src/media/SpotifyInformation.js +++ b/backend/src/media/SpotifyInformation.js @@ -4,7 +4,7 @@ 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('../player/Playlist'); +const {Playlist} = require('../playlists/Playlist'); const {Song} = require('../player/Song'); const youtube = require("../media/YoutubeInformation"); const {getReadableDuration} = require('../utils/TimeConverter'); diff --git a/backend/src/media/YoutubeInformation.js b/backend/src/media/YoutubeInformation.js index ccb06b9..ce7c198 100644 --- a/backend/src/media/YoutubeInformation.js +++ b/backend/src/media/YoutubeInformation.js @@ -1,28 +1,29 @@ const { LogType } = require('loguix'); const clog = new LogType("YoutubeInformation"); const { Song } = require('../player/Song'); -const { Playlist } = require('../player/Playlist'); +const { Playlist } = require('../playlists/Playlist'); const { getReadableDuration } = require('../utils/TimeConverter'); const ytsr = require('@distube/ytsr'); const ytfps = require('ytfps'); -async function getQuery(query) { - if (query === null || typeof query !== 'string') { +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 searchResults = await ytsr(query, { limit: 1 }); - const video = searchResults.items.find(item => item.type === 'video'); + const limit = multiple ? 25 : 1; + const searchResults = await ytsr(query, { limit }); + const videos = searchResults.items.filter(item => item.type === 'video'); - if (!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 song = await getVideo(video.url); - return song; + 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; diff --git a/backend/src/player/Finder.js b/backend/src/player/Finder.js index 4efed39..eed7b73 100644 --- a/backend/src/player/Finder.js +++ b/backend/src/player/Finder.js @@ -6,12 +6,11 @@ const spotify = require("../media/SpotifyInformation") const soundcloud = require("../media/SoundcloudInformation") -async function search(query) { +async function search(query, multiple) { const type = Resolver.getQueryType(query) if(type == QueryType.YOUTUBE_SEARCH) { - - return await youtube.getQuery(query) - + return await youtube.getQuery(query, multiple) + } if(type == QueryType.YOUTUBE_VIDEO) { diff --git a/backend/src/player/List.js b/backend/src/player/List.js index 9c4508b..bb102a5 100644 --- a/backend/src/player/List.js +++ b/backend/src/player/List.js @@ -41,6 +41,7 @@ class List { } else { return null; } + } nextSong() { @@ -58,30 +59,36 @@ class List { } 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() { @@ -100,6 +107,7 @@ class List { } else { return null; } + } previousSong() { @@ -115,21 +123,25 @@ class List { } 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() { @@ -138,6 +150,7 @@ class List { setCurrent(value) { this.current = value; + process.emit("PLAYERS_UPDATE") } destroy() { @@ -145,11 +158,11 @@ class List { this.current = null this.shuffle = false; AllLists.delete(this.guildId) - + process.emit("PLAYERS_UPDATE") } - setShuffle(value) { - this.shuffle = value; + setShuffle() { + this.shuffle = !this.shuffle; } isShuffle() { @@ -175,6 +188,7 @@ class List { return song } + process.emit("PLAYERS_UPDATE") } addNextPlaylist(playlist, firstAlreadyPlayed) { @@ -185,10 +199,17 @@ class List { for(const song of playlist.songs) { this.addNextSong(song) } + process.emit("PLAYERS_UPDATE") } - + moveNext(fromIndex, toIndex) { + if(fromIndex == toIndex) return; + const song = this.next[fromIndex] + this.next.splice(fromIndex, 1) + this.next.splice(toIndex, 0, song) + process.emit("PLAYERS_UPDATE") + } } diff --git a/backend/src/player/Method/Media.js b/backend/src/player/Method/Media.js index ab26872..d81578b 100644 --- a/backend/src/player/Method/Media.js +++ b/backend/src/player/Method/Media.js @@ -2,22 +2,13 @@ const {createAudioResource, VoiceConnectionStatus, createAudioPlayer, StreamType const {LogType} = require('loguix') const clog = new LogType("Media") const plog = require("loguix").getInstance("Player") +const ffmpeg = require('fluent-ffmpeg') -async function play(instance, song) { +async function getStream(song) { try { - instance.player = createAudioPlayer() - instance.generatePlayerEvents() - const player = instance.player - var resource = await createAudioResource(song.url, { - inputType: StreamType.Arbitrary - }) // Remplace par ton fichier audio - - - instance.setCurrentResource(resource) - player.play(resource); - instance.connection.subscribe(player); - clog.log(`GUILD : ${instance.guildId} - Lecture de la musique (Media): ${song.title} - id : ${song.id}`) + return song.url; + } catch(e) { clog.error("Erreur lors de la lecture de la musique : " + song.title) @@ -27,4 +18,4 @@ async function play(instance, song) { } -module.exports = {play} \ No newline at end of file +module.exports = {getStream} \ No newline at end of file diff --git a/backend/src/player/Method/Soundcloud.js b/backend/src/player/Method/Soundcloud.js index b6031e5..467f42a 100644 --- a/backend/src/player/Method/Soundcloud.js +++ b/backend/src/player/Method/Soundcloud.js @@ -1,31 +1,25 @@ const {createAudioResource, VoiceConnectionStatus, createAudioPlayer, StreamType} = require('@discordjs/voice'); const {LogType} = require('loguix') -const clog = new LogType("Soundcloud") -const plog = require("loguix").getInstance("Player") +const clog = new LogType("Soundcloud-Stream") const {Soundcloud} = require('soundcloud.ts') +const ffmpeg = require('fluent-ffmpeg') const soundcloud = new Soundcloud(); -async function play(instance, song) { +async function getStream(song) { try { - instance.player = createAudioPlayer() - instance.generatePlayerEvents() - const player = instance.player - - const stream = await soundcloud.util.streamTrack(song.url) - var resource = await createAudioResource(stream) - instance.setCurrentResource(resource) - player.play(resource); - instance.connection.subscribe(player); - clog.log(`GUILD : ${instance.guildId} - Lecture de la musique (Soundcloud): ${song.title} - id : ${song.id}`) + var stream = await soundcloud.util.streamTrack(song.url) + return stream + + } catch(e) { - clog.error("Erreur lors de la lecture de la musique : " + song.title) + clog.error("Erreur lors de la récupération du stream : " + song.title) clog.error(e) } } -module.exports = {play} \ No newline at end of file +module.exports = {getStream} \ No newline at end of file diff --git a/backend/src/player/Method/Youtube.js b/backend/src/player/Method/Youtube.js index da73002..8ec8147 100644 --- a/backend/src/player/Method/Youtube.js +++ b/backend/src/player/Method/Youtube.js @@ -1,37 +1,30 @@ const {createAudioResource, VoiceConnectionStatus, createAudioPlayer, StreamType} = require('@discordjs/voice'); const {LogType} = require('loguix') -const clog = new LogType("Youtube") -const plog = require("loguix").getInstance("Player") +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 play(instance, song) { +async function getStream(song) { try { - - instance.player = createAudioPlayer() - instance.generatePlayerEvents() - const player = instance.player - const stream = ytdl(song.url, { + + let stream = ytdl(song.url, { quality: 'highestaudio', highWaterMark: 1 << 30, liveBuffer: 20000, dlChunkSize: 0, bitrate: 128, - + }); - // Add compressor to the audio resource - var resource = createAudioResource(stream); - - instance.setCurrentResource(resource) - - player.play(resource); - instance.connection.subscribe(player); - clog.log(`GUILD : ${instance.guildId} - Lecture de la musique (Youtube): ${song.title} - id : ${song.id}`) + return stream - } catch(e) { - clog.error("Erreur lors de la lecture de la musique : " + song.title) - clog.error(e) - } + } catch(e) { + clog.error("Erreur lors de la récupération du stream : " + song.title) + clog.error(e) + + } } -module.exports = {play} + +module.exports = {getStream} diff --git a/backend/src/player/Player.js b/backend/src/player/Player.js index d73cb5f..2a77e69 100644 --- a/backend/src/player/Player.js +++ b/backend/src/player/Player.js @@ -1,6 +1,9 @@ -const { joinVoiceChannel, getVoiceConnection, entersState, VoiceConnectionStatus, createAudioPlayer, AudioPlayerStatus } = require('@discordjs/voice'); +const { joinVoiceChannel, getVoiceConnection, VoiceConnectionStatus, createAudioPlayer, AudioPlayerStatus, StreamType, createAudioResource } = require('@discordjs/voice'); const {List} = require('./List') const {LogType} = require("loguix"); +const ffmpeg = require('fluent-ffmpeg') +const fs = require('fs') +const { PassThrough } = require('stream'); const plog = new LogType("Player") const clog = new LogType("Signal") @@ -13,11 +16,13 @@ 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") @@ -39,6 +44,20 @@ class Player { 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, @@ -47,11 +66,6 @@ class Player { selfMute: false }); - this.channelId = channel.id - - this.player = createAudioPlayer() - this.generatePlayerEvents() - this.connection.on('stateChange', (oldState, newState) => { clog.log(`GUILD : ${this.guildId} - [STATE] OLD : "${oldState.status}" NEW : "${newState.status}"`); @@ -61,7 +75,8 @@ class Player { this.leave() } }); - + this.connected = true + process.emit("PLAYERS_UPDATE") } generatePlayerEvents() { @@ -71,20 +86,30 @@ class Player { 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, () => { + // 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, () => { 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") }); } @@ -101,6 +126,45 @@ class Player { } } + getState() { + const state = { + current: this.queue.current, + next: this.queue.next, + previous: this.queue.previous, + loop: this.loop, + shuffle: this.queue.shuffle, + paused: this.player.state.status == AudioPlayerStatus.Paused, + playing: this.player.state.status == AudioPlayerStatus.Playing, + duration: this.getDuration(), + playerState: this.player.state.status, + connectionState: this.connection.state.status, + channelId: this.channelId + } + 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(this.checkConnection()) return if(this.queue.current != null) { @@ -108,18 +172,31 @@ class Player { } this.queue.setCurrent(song) + this.stream = await this.getStream(song) - if(song.type == "attachment") { - media.play(this, song) - } - if(song.type == 'youtube') { - youtube.play(this, song) - } - if(song.type == "soundcloud") { - soundcloud.play(this, song) - } + if(this.stream === null) { + plog.error(`GUILD : ${this.guildId} - Impossible de lire la musique : ${song.title} avec le type : ${song.type}`) + return + } - // TODO: Créer une méthode pour les autres types de médias + 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) { @@ -156,6 +233,7 @@ class Player { plog.log(`GUILD : ${this.guildId} - La musique a été mise en pause`) return true } + process.emit("PLAYERS_UPDATE") } async leave() { @@ -170,24 +248,80 @@ class Player { 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) { + + 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.`); } - setDuration(duration) { + 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 - var maxduration = this.queue.current.duration - if(duration > maxduration) return - this.player.stop(); // Arrête la lecture actuelle - this.player.play(this.currentResource, { - startTime: duration * 1000 // Convertit le timecode en millisecondes - }); + return this.currentResource.playbackDuration / 1000 } @@ -195,6 +329,22 @@ class Player { 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" @@ -203,6 +353,7 @@ class Player { } const songSkip = this.queue.nextSong() this.play(songSkip) + process.emit("PLAYERS_UPDATE") return songSkip } @@ -215,11 +366,37 @@ class Player { const songPrevious = this.queue.previousSong() this.play(songPrevious) + process.emit("PLAYERS_UPDATE") return songPrevious } } -module.exports = {Player, AllPlayers} +/** + * + * @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} /* diff --git a/backend/src/player/Playlist.js b/backend/src/player/Playlist.js deleted file mode 100644 index 48838aa..0000000 --- a/backend/src/player/Playlist.js +++ /dev/null @@ -1,16 +0,0 @@ -class Playlist { - title = "Aucun titre"; - id; - url; - author = "Auteur inconnu"; - authorId; - songs = []; - thumbnail = "https://radomisol.fr/wp-content/uploads/2016/08/cropped-note-radomisol-musique.png" ; - duration = 0; - readduration; - description; - type; -} - -module.exports = {Playlist}; - diff --git a/backend/src/playlists/Playlist.js b/backend/src/playlists/Playlist.js new file mode 100644 index 0000000..cb4165c --- /dev/null +++ b/backend/src/playlists/Playlist.js @@ -0,0 +1,30 @@ +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; + 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; + this.thumbnail = thumbnail; + this.duration = duration; + this.readduration = readduration; + this.description = description; + if(!this.url) { + this.type = "playlist"; + } + } +} + +module.exports = {Playlist}; + diff --git a/backend/src/playlists/PlaylistManager.js b/backend/src/playlists/PlaylistManager.js new file mode 100644 index 0000000..0c37961 --- /dev/null +++ b/backend/src/playlists/PlaylistManager.js @@ -0,0 +1,157 @@ +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 playlistDB = new Database("Playlists", __glob.PLAYLISTFILE, {}); + +/** +* @param {string} id +* @param {string} name +* @returns {Array} +* @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.name === 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.name === name)) { + clog.warn(`La playlist ${name} existe déjà pour l'utilisateur ${id}`); + return; + } + if(url) { + await Finder.search(url).then(async (playlistFounded) => { + if(playlistFounded instanceof Playlist) { + playlist = playlistFounded; + } + if(playlist.type === "spotify") { + playlist.songs = await spotify.getTracks(playlist); + } + }) + } + + 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.name === 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.name === 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.name === name); + if (!playlist) { + clog.warn(`La playlist ${name} n'existe pas pour l'utilisateur ${fromId}`); + return null; + } + const toPlaylists = getPlaylistsOfUser(toId); + toPlaylists.push(playlist); + playlistDB.save(); + clog.log(`Copie de la playlist ${name} de l'utilisateur ${fromId} vers l'utilisateur ${toId}`); + + return newPlaylist; +} + +function renamePlaylist(id, oldName, newName) { + const playlists = getPlaylistsOfUser(id); + const playlist = playlists.find(p => p.name === oldName); + if (!playlist) { + clog.warn(`La playlist ${oldName} n'existe pas pour l'utilisateur ${id}`); + return null; + } + playlist.name = newName; + playlistDB.save(); + clog.log(`Renommage de la playlist ${oldName} en ${newName} pour l'utilisateur ${id}`); +} + +function addSong(id, playlistName, song) { + const playlists = getPlaylistsOfUser(id); + const playlist = playlists.find(p => p.name === playlistName); + if (!playlist) { + clog.warn(`La playlist ${playlistName} n'existe pas pour l'utilisateur ${id}`); + 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.name === 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}`); +} + +module.exports = { + getPlaylistsOfUser, + getPlaylistOfUser, + addPlaylist, + removePlaylist, + getPlaylist, + copyPlaylist, + renamePlaylist, + addSong, + removeSong +} \ No newline at end of file diff --git a/backend/src/server/Documentation.md b/backend/src/server/Documentation.md new file mode 100644 index 0000000..ab828e9 --- /dev/null +++ b/backend/src/server/Documentation.md @@ -0,0 +1,234 @@ +# Documentation des Requêtes `socket.io` + +Les requêtes sont du point de vue du serveur. + +Le Client doit être initialisé comme ceci : + +```js +const socket = io("subsonics.raphix.fr:5000", { + auth: { + token: "TOKEN_HERE", + auth_code: "AUTH_FROM_DISCORD_HERE", + session: "SESSION_ID_HERE" + } +}); +``` + +REDIRECT_CALLBACK = `/callback` + +--- + +## Requêtes Envoyées + +### Événement : `NEW_SESSION` + +- **Description** : `/login` et `/` : Envoie un jeton de session utile pour la traçabilité de la connexion par Discord. Dès réception, le client le stocke dans les cookies sous le nom `session`. Si l'utilisateur n'est pas sur `/login`, il doit y être redirigé. Supprime également le cookie `token` s’il existe. +- **Données envoyées** : +```json +"SESSION_ID" +``` + +### Événement : `NEW_TOKEN` + +- **Description** : `/callback` : Lors de la redirection depuis Discord, ce jeton est généré après vérification du code d'autorisation. Il est envoyé au client qui est ensuite redirigé vers `/`. +- **Données envoyées** : +```json +"TOKEN_ID" +``` + +### Événement : `BANNED` + +- **Description** : `/callback` et `/` : Si reçu, le client est redirigé vers la page de connexion avec l'erreur "BANNI". + +### Événement : `AUTH_ERROR` + +- **Description** : `/callback` et `/` : Erreur lors de l’authentification (ex. code Discord invalide ou accès refusé). + +--- + +## Requêtes Reçues + +> Toutes les requêtes commencent par l’événement `socket.on("EVENT_NAME", callback)` côté client, et sont traitées côté serveur par `IORequest("EVENT_NAME", callback)`. + +### Utilisateur + +#### `/USER/INFO` + +- **Description** : Renvoie l’identité Discord, les guildes et les labels de l'utilisateur connecté. +- **Données envoyées** : +```json +{} +``` +- **Réponse** : +```json +{ + "identity": { ... }, + "guilds": [ ... ], + "labels": [ "admin", ... ] +} +``` + +#### `/USERS/LIST` + +- **Description** : Renvoie la liste des utilisateurs connectés à une guilde. +- **Données envoyées** : +```json +"GUILD_ID" +``` +- **Réponse** : +```json +[ { "id": "...", "username": "...", ... }, ... ] +``` + +### Player (Musique) + +#### `/PLAYER/STATE` + +- **Description** : Récupère l'état actuel du player pour une guilde. +- **Données envoyées** : +```json +"GUILD_ID" +``` + +#### `/PLAYER/JOIN` / `/PLAYER/LEAVE` + +- **Description** : Rejoint ou quitte l’écoute du player pour une guilde. +- **Données envoyées** : +```json +"GUILD_ID" +``` + +#### `/PLAYER/PAUSE`, `/PLAYER/BACKWARD`, `/PLAYER/FORWARD`, `/PLAYER/LOOP`, `/PLAYER/SHUFFLE`, `/PLAYER/DISCONNECT` + +- **Description** : Contrôle du player (pause, chanson précédente/suivante, boucle, aléatoire, déconnexion). +- **Données envoyées** : +```json +"GUILD_ID" +``` + +#### `/PLAYER/CHANNEL/CHANGE` + +- **Description** : Change le salon vocal du player vers celui de l’utilisateur. +- **Données envoyées** : +```json +"GUILD_ID" +``` + +#### `/PLAYER/SEEK` + +- **Description** : Change la position de la lecture. +- **Données envoyées** : +```json +["GUILD_ID", TEMPS_EN_SECONDES] +``` + +### Queue + +#### `/QUEUE/PLAY/NOW` + +- **Description** : Joue une chanson de la queue immédiatement. +- **Données envoyées** : +```json +["GUILD_ID", "previous"|"next", INDEX] +``` + +#### `/QUEUE/NEXT/DELETE`, `/QUEUE/NEXT/DELETEALL`, `/QUEUE/NEXT/MOVE` + +- **Description** : Supprime ou déplace une chanson dans la file d’attente. +- **Données envoyées** : +```json +["GUILD_ID", INDEX (ou NEW_INDEX)] +``` + +### Recherche + +#### `/SEARCH` + +- **Description** : Effectue une recherche de musique. +- **Données envoyées** : +```json +"QUERY" +``` + +#### `/SEARCH/PLAY` + +- **Description** : Joue un morceau directement ou l'ajoute à la queue. +- **Données envoyées** : +```json +["GUILD_ID", SONG, now (bool)] +``` + +### Playlists + +#### `/PLAYLISTS/CREATE`, `/DELETE`, `/RENAME`, `/ADD_SONG`, `/REMOVE_SONG`, `/SEND`, `/PLAY` + +- **Description** : Gère les playlists (création, suppression, renommage, ajout, lecture, envoi à un autre utilisateur). +- **Données envoyées** : Variable selon l'action, ex : +```json +["PLAYLIST_NAME", SONG] +``` + +#### `/PLAYLISTS/LIST` + +- **Description** : Renvoie la liste des playlists de l'utilisateur. +- **Données envoyées** : +```json +{} +``` + +### Admin + +> Nécessite le label `"admin"` dans `socketUser.labels`. + +#### `/ADMIN/LOGS` + +- **Description** : Renvoie les logs du serveur. + +#### `/ADMIN/MAINTENANCE/RESTART` + +- **Description** : Redémarre le serveur avec une raison. +- **Données envoyées** : +```json +"RAISON" +``` + +#### `/ADMIN/USERS/SWITCH_ADMIN`, `/FULL_BAN`, `/DELETE` + +- **Description** : Gère les utilisateurs (promotion admin, ban complet, suppression). +- **Données envoyées** : +```json +"USER_ID" +``` + +#### `/ADMIN/PLAYER/GETALLSTATE` + +- **Description** : Renvoie l’état de tous les players. + +### Owner / Modérateur + +#### `/OWNER/USERS/SWITCH_MOD` + +- **Description** : Nomme ou enlève un modérateur. +- **Données envoyées** : +```json +["USER_ID", "GUILD_ID"] +``` + +#### `/MOD/USERS/BAN` + +- **Description** : Bannit un utilisateur d’une guilde. +- **Données envoyées** : +```json +["USER_ID", "GUILD_ID"] +``` + +### Utilitaires + +#### `/REPORT` + +- **Description** : Envoie un rapport avec un niveau et une description. +- **Données envoyées** : +```json +["LEVEL", "DESCRIPTION"] +``` + diff --git a/backend/src/server/Server.js b/backend/src/server/Server.js new file mode 100644 index 0000000..f3165ed --- /dev/null +++ b/backend/src/server/Server.js @@ -0,0 +1,564 @@ +const {LogType} = require('loguix') +const wlog = new LogType("Server") + +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 fs = require("fs") +const {__glob} = require("../utils/GlobalVars") +const playlists = require("../playlists/PlaylistManager") + +const configuration = require("../utils/Database/Configuration") +const { List } = require('../player/List') +const { restart } = require('../utils/Maintenance') + +const allConnectedUsers = new Array() +const guildConnectedUsers = new Map() + +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()) { + const player = players.getPlayer(guild.id) + if(player) { + io.to(guild.id).emit("PLAYER_UPDATE", player.getState()) + wlog.log("Envoi de l'état du player de la guilde : " + guild.id + " à 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()) { + if(guildConnectedUsers.has(guild.id)) { + io.sockets.emit("USERS_UPDATE", guildConnectedUsers.get(guild.id)) + io.to("admin").emit("ALL_USERS_UPDATE", allConnectedUsers) + wlog.log("Envoi de la liste des utilisateurs connectés à la guilde : " + guild.id + " à tous les utilisateurs connectés") + } + } + } + }) + + + + + io.on("connection", async (socket) => { + + wlog.log(`Connexion d'un client : ${socket.id}`) + + 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 + + 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 == "GUILDS_ERROR" || 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") + socket.disconnect() + return + } else { + const loggedUser = users.addUser(discordUser.auth, discordUser.identity, discordUser.guilds) + for(var guild of discordUser.guilds) { + if(guild.owner) { + users.setGuildOwner(loggedUser.identity.id, guild.id) + } + } + const newToken = loggedUser.createToken() + socket.emit("NEW_TOKEN", newToken) + socket.disconnect() + 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) + } + } + } + + if(!token) { + wlog.warn("Token manquant pour le client :" + socket.id) + sendSession() + return + } + + const socketUser = users.getUserByToken(token) + + if(!socketUser) { + wlog.warn("Token invalide pour le client :" + socket.id) + sendSession() + return + } + + if(socketUser) { + if(allConnectedUsers.includes(socketUser.identity.id)) { + wlog.warn("L'utilisateur '" + socketUser.identity.username + "' est déjà connecté sur un autre appareil") + return + } else { + allConnectedUsers.push(socketUser.identity) + addGuildConnectedUser(socketUser.identity, socketUser.guilds) + process.emit("USERS_UPDATE") + } + + 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("BANNED") + socket.disconnect() + } + if(socketUser.labels.includes("admin")) { + socket.join("admin") + wlog.log("Utilisateur admin identifié : " + socketUser.identity.username + " (" + socketUser.identity.id + ")") + } + + // USERS + + IORequest("/USER/INFO", () => { + IOAnswer("/USER/INFO", { + identity: socketUser.identity, + guilds: socketUser.guilds, + labels: socketUser.labels + }) + wlog.log("Envoi des informations Discord de '" + socketUser.identity.id + "' à '" + socket.id + "'" ) + }) + + IORequest("/USERS/LIST", (guildId) => { + if(!checkUserGuild(socketUser, guildId)) return + if(!guildConnectedUsers.has(guildId)) return IOAnswer("/USERS/LIST", false) + IOAnswer("/USERS/LIST", guildConnectedUsers.get(guildId)) + }) + + // PLAYERS + + IORequest("/PLAYER/PREVIOUS/LIST", (guildId) => { + if(!checkUserGuild(socketUser, guildId)) return + const list = new List(guildId) + return list.getPrevious() + }) + + IORequest("/PLAYER/JOIN", (guildId) => { + if(!checkUserGuild(socketUser, guildId)) return + wlog.log("L'utilisateur '" + socketUser.identity.username + "' rejoint la liste d'écoute du player de la guilde : " + guildId) + socket.join(guildId) + IOAnswer("PLAYER_STATE", true) + }) + + IORequest("/PLAYER/LEAVE", (guildId) => { + if(!checkUserGuild(socketUser, guildId)) return + wlog.log("L'utilisateur '" + socketUser.identity.username + "' quitte la liste d'écoute du player de la guilde : " + guildId) + socket.leave(guildId) + IOAnswer("PLAYER_STATE", false) + }) + + IORequest("/PLAYER/STATE", (guildId) => { + handlePlayerAction(guildId, (player) => player.getState(), "/PLAYER/STATE"); + }) + + IORequest("/PLAYER/PAUSE", (guildId) => { + handlePlayerAction(guildId, (player) => player.pause(), "/PLAYER/PAUSE"); + }); + + IORequest("/PLAYER/BACKWARD", (guildId) => { + handlePlayerAction(guildId, (player) => player.previous(), "/PLAYER/BACKWARD"); + }); + + IORequest("/PLAYER/FORWARD", (guildId) => { + handlePlayerAction(guildId, (player) => player.skip(), "/PLAYER/FORWARD"); + }); + + IORequest("/PLAYER/LOOP", (guildId) => { + handlePlayerAction(guildId, (player) => player.setLoop(), "/PLAYER/LOOP"); + }); + + IORequest("/PLAYER/SHUFFLE", (guildId) => { + handlePlayerAction(guildId, (player) => player.setShuffle(), "/PLAYER/SHUFFLE"); + }); + + IORequest("/PLAYER/DISCONNECT", (guildId) => { + handlePlayerAction(guildId, (player) => player.leave(), "/PLAYER/DISCONNECT"); + }); + + IORequest("/PLAYER/CHANNEL/CHANGE", (guildId) => { + handlePlayerAction(guildId, (player) => { + const channel = getUserChannel() + if(!channel) { + IOAnswer("/PLAYER/CHANNEL/CHANGE", false) + return + } + player.changeChannel(channel) + }, "/PLAYER/CHANNEL/CHANGE"); + }); + + IORequest("/PLAYER/SEEK", (guildId, time) => { + handlePlayerAction(guildId, (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"); + }); + + IORequest("/QUEUE/PLAY/NOW", (guildId, listType, index) => { + if(!checkUserGuild(socketUser, guildId)) return + const player = new Player(guildId) + if(!connectToPlayer(guildId, player)) return IOAnswer("/QUEUE/PLAY", false) + 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", false) + player.add(song) + IOAnswer("/QUEUE/PLAY/NOW", true) + }) + + IORequest("/QUEUE/NEXT/DELETE", (guildId, index) => { + handlePlayerAction(guildId, (player) => { + const next = player.queue.getNext() + if(!next[index]) return IOAnswer("/QUEUE/NEXT/DELETE", false); + player.queue.removeNextByIndex(index) + }) + }) + + IORequest("/QUEUE/NEXT/DELETEALL", (guildId) => { + handlePlayerAction(guildId, (player) => player.queue.clearNext()) + }) + + IORequest("/QUEUE/NEXT/MOVE", (guildId, index, newIndex) => { + handlePlayerAction(guildId, (player) => { + const next = player.queue.getNext() + if(!next[index]) return IOAnswer("/QUEUE/NEXT/MOVE", false); + player.queue.moveNext(index, newIndex) + }) + }) + + // SEARCH + + IORequest("/SEARCH", async (query) => { + IOAnswer("/SEARCH", await Finder.search(query)) + }) + + IORequest("/SEARCH/PLAY", async (guildId, song, now) => { + if(!checkUserGuild(socketUser, guildId)) return + const player = new Player(guildId) + if(!connectToPlayer(guildId, player)) return IOAnswer("/SEARCH/PLAY", false) + if(now) { + player.play(song) + } else { + player.add(song) + } + IOAnswer("/SEARCH/PLAY", true) + }) + + + // PLAYLISTS + + IORequest("/PLAYLISTS/CREATE", (name, url) => { + if(!name) return IOAnswer("/PLAYLISTS/CREATE", false) + const playlist = playlists.addPlaylist(socketUser.identity.id, name, url) + if(!playlist) return IOAnswer("/PLAYLISTS/CREATE", false) + IOAnswer("/PLAYLISTS/CREATE", true) + }) + + IORequest("/PLAYLISTS/DELETE", (name) => { + if(!name) return IOAnswer("/PLAYLISTS/DELETE", false) + playlists.removePlaylist(socketUser.identity.id, name) + IOAnswer("/PLAYLISTS/DELETE", true) + }) + + IORequest("/PLAYLISTS/LIST", () => { + const playlist = playlists.getPlaylistsOfUser(socketUser.identity.id) + IOAnswer("/PLAYLISTS/LIST", playlist) + }) + + IORequest("/PLAYLISTS/SEND", (name, toId) => { + // 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) + }) + + IORequest("/PLAYLISTS/RENAME", (name, newName) => { + 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) + }) + + IORequest("/PLAYLISTS/ADD_SONG", (name, song) => { + 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) + }) + + IORequest("/PLAYLISTS/REMOVE_SONG", (name, songId) => { + 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) + }) + + IORequest("/PLAYLISTS/PLAY", (guildId, name, now) => { + const playlist = playlists.getPlaylistOfUser(socketUser.identity.id, name) + if(!playlist) return IOAnswer("/PLAYLISTS/PLAY", false) + const player = new Player(guildId) + if(!connectToPlayer(guildId, player)) return IOAnswer("/PLAYLISTS/PLAY", false) + player.readPlaylist(playlist, now) + IOAnswer("/PLAYLISTS/PLAY", true) + }) + + + // ADMIN + + if(socketUser.labels.includes("admin")) { + IORequest("/ADMIN/LOGS", () => { + 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) + }) + + IORequest("/ADMIN/MAINTENANCE/RESTART", (reason) => { + if(!reason) return IOAnswer("/ADMIN/MAINTENANCE/RESTART", false) + restart(reason) + }) + + IORequest("/ADMIN/USERS/SWITCH_ADMIN", (userId) => { + users.setAdmin(userId) + IOAnswer("/ADMIN/USERS/PROMOTE_ADMIN", true) + }) + + IORequest("/ADMIN/USERS/FULL_BAN", (userId) => { + users.setFullBan(userId) + IOAnswer("/ADMIN/USERS/FULL_BAN", true) + }) + + IORequest("/ADMIN/USERS/DELETE", (userId) => { + users.removeUser(userId) + IOAnswer("/ADMIN/USERS/DELETE", true) + }) + + IORequest("/ADMIN/PLAYER/GETALLSTATE", () => { + + const allPlayers = players.getAllPlayers() + const states = new Array() + for(var player of allPlayers) { + states.push(player.getState()) + } + IOAnswer("/ADMIN/PLAYER/GETALLSTATE", states) + }) + } + + IORequest("/OWNER/USERS/SWITCH_MOD", (userId, guildId) => { + if(!socketUser.isOwner(guildId)) return IOAnswer("/OWNER/USERS/SWITCH_MOD", false) + users.setGuildMod(userId, guildId) + IOAnswer("/OWNER/USERS/SWITCH_MOD", true) + }) + + IORequest("/MOD/USERS/BAN", (userId, guildId) => { + if(!socketUser.isMod(guildId)) return IOAnswer("/MOD/USERS/BAN", false) + users.setGuildBan(userId, guildId) + IOAnswer("/OWNER/USERS/BAN", true) + }) + + // UTILS + + IORequest("/REPORT", (level, desc) => { + const report = new Report(socketUser.identity.username, level, 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 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 + } + + + function checkUserGuild(socketUser, guildId) { + // Vérifie si l'utilisateur n'est pas banni de la guilde + if(socketUser.isBanned(guildId)) { + wlog.warn("L'utilisateur '" + socketUser.identity.username + "' est banni de la guilde : " + guildId) + return false + } + if(!socketUser.guilds.includes(guildId)) { + 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); + } + } + + } + + + socket.on("disconnect", () => { + allConnectedUsers.splice(allConnectedUsers.indexOf(socketUser.identity), 1) + socket.leave("admin") + removeGuildConnectedUser(socketUser.identity) + process.emit("USERS_UPDATE") + clog.log(`Déconnexion du client : ${socket.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(GQname, (value) => { + wlog.log(socketUser.identity.username + " - Socket : " + socket.id + " - POST/" + GQname + " - [RECIEVED]") + GQcallback(value) + }) + + } + function IOAnswer(AnswerName, AnswerValue) { + + wlog.log(socketUser.identity.username + " - Socket : " + socket.id + " - POST/" + GRname + " - [ANSWERED]") + socket.emit("ANSWER/" + GRname, GRvalue) + process.emit("PLAYERS_UPDATE") + } + + }) + + httpServer.listen(configuration.getPort(), () => { + wlog.log(`Le serveur écoute sur le port ${configuration.getPort()}`) + wlog.step.end("server_init") + }) + + function AdminRequest(GRname, GRvalue) { + + io.to("admin").emit("ALWAYS/" + GRname, GRvalue) + + } + + function addGuildConnectedUser(user, guilds) { + for(var guild of guilds) { + if(!guildConnectedUsers.has(guild)) { + guildConnectedUsers.set(guild, new Array()) + } + guildConnectedUsers.get(guild).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} \ No newline at end of file diff --git a/backend/src/server/auth/DiscordAuth.js b/backend/src/server/auth/DiscordAuth.js new file mode 100644 index 0000000..69c5688 --- /dev/null +++ b/backend/src/server/auth/DiscordAuth.js @@ -0,0 +1,67 @@ +const { LogType } = require('loguix'); +const dlog = new LogType("DiscordAuth"); +const { getPort, getToken, getWebsiteLink, getClientSecret } = require('../../utils/Database/Configuration'); +const discordBot = require("../../discord/Bot") + + +async function getDiscordUser(sessionId, auth_code) { + 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() + "/callback"); + 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 => { + 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, + } + return 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); + return "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); + return "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); + return "ACCESS_TOKEN_ERROR"; + }) + +} + +module.exports = {getDiscordUser} \ No newline at end of file diff --git a/backend/src/server/auth/Session.js b/backend/src/server/auth/Session.js new file mode 100644 index 0000000..7341403 --- /dev/null +++ b/backend/src/server/auth/Session.js @@ -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}; \ No newline at end of file diff --git a/backend/src/server/auth/User.js b/backend/src/server/auth/User.js new file mode 100644 index 0000000..87d434c --- /dev/null +++ b/backend/src/server/auth/User.js @@ -0,0 +1,301 @@ +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 UserDB = new Database("Users", __glob.USERFILE, []); +const userList = new Array(); +loadUsers(); + +class User { + auth; + identity; + tokens; + labels; + guilds; + constructor(auth, identity, tokens, labels, guilds) { + this.auth = auth; + this.identity = identity; + this.tokens = tokens; + this.labels = labels; + this.guilds = guilds; + } + + setAdmin() { + if (this.labels.includes("ADMIN")) { + this.labels.splice(this.labels.indexOf("ADMIN"), 1); + clog.log(`L'utilisateur ${this.identity.username} n'est plus admin.`); + } else { + this.labels.push("ADMIN"); + clog.log(`L'utilisateur ${this.identity.username} est maintenant admin.`); + } + } + + setBan(guildId) { + const banLabel = `BAN_${guildId}`; + if (this.labels.includes(banLabel)) { + this.labels.splice(this.labels.indexOf(banLabel), 1); + clog.log(`L'utilisateur ${this.identity.username} n'est plus banni du serveur ${guildId}.`); + } else { + this.labels.push(banLabel); + clog.log(`L'utilisateur ${this.identity.username} est maintenant banni du serveur ${guildId}.`); + } + } + + createToken() { + const token = generateToken(); + this.tokens.push(token); + saveUsers(); + clog.log(`Token créé pour l'utilisateur ${this.identity.username}.`); + return token; + } + + removeToken(token) { + const index = this.tokens.indexOf(token); + if (index > -1) { + this.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}.`); + } + } + + 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}`; + return this.labels.includes(ownerLabel); + } + + +} + +// ADD + +function addUser(auth, identity, guilds) { + // 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.guilds = guilds; + 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, [], [], guilds); + + userList.push(newUser); + 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, + guilds: user.guilds + }; + }); + +} + +function getSimpleUser(id) { + const user = getUserById(id); + if(user) { + return { + identity: user.identity, + labels: user.labels, + guilds: user.guilds + }; + } 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) { + user.setBan(guildId); + saveUsers(); + } else { + clog.warn(`Utilisateur ${id} non trouvé.`); + } +} + +function setFullBan(id) { + const user = getUserById(id); + if (user) { + user.labels.push("BAN"); + saveUsers(); + } else { + clog.warn(`Utilisateur ${id} non trouvé.`); + } +} + +function setGuildOwner(id, guildId) { + const user = getUserById(id); + if (user) { + const ownerLabel = `OWNER_${guildId}`; + if (user.labels.includes(ownerLabel)) { + user.labels.splice(user.labels.indexOf(ownerLabel), 1); + clog.log(`L'utilisateur ${user.identity.username} n'est plus propriétaire du serveur ${guildId}.`); + } else { + 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.length = 0; + for (const user in UserDB.data) { + userList.push(new User(user.auth, user.identity, user.tokens, user.labels, user.guilds)); + } + 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, + guilds: user.guilds + }; + }); + UserDB.save() + clog.log(`Sauvegarde de ${userList.length} utilisateurs.`); + + return loadUsers(); +} + + +module.exports = {User} +module.exports = {addUser, setGuildOwner , setFullBan, removeUser, getUserByToken , getUserById, getUsers, setAdmin, setGuildMod, setGuildBan, addToken, removeToken, getSimpleUsers, getSimpleUser} diff --git a/backend/src/utils/Database/Configuration.js b/backend/src/utils/Database/Configuration.js index 50cd2c0..581b631 100644 --- a/backend/src/utils/Database/Configuration.js +++ b/backend/src/utils/Database/Configuration.js @@ -2,11 +2,13 @@ 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 : "" @@ -17,7 +19,9 @@ const config = new Database("config", __glob.DATA + path.sep + "config.json", { clientId: "", clientSecret: "" } - } + }, + website: "", + server_port: 5000, }) function getToken() { @@ -45,9 +49,21 @@ 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 +} + if(getToken() == "") { clog.error("Impossible de démarrer sans token valide") process.exit(1) } -module.exports = {getToken, getReportChannel, getReportContact, getYoutubeApiKey, getSpotifyClientId, getSpotifyClientSecret} \ No newline at end of file +module.exports = {getToken, getClientSecret, getReportChannel, getReportContact, getYoutubeApiKey, getSpotifyClientId, getSpotifyClientSecret, getWebsiteLink, getPort} \ No newline at end of file diff --git a/backend/src/utils/GlobalVars.js b/backend/src/utils/GlobalVars.js index 48578f6..9fa55de 100644 --- a/backend/src/utils/GlobalVars.js +++ b/backend/src/utils/GlobalVars.js @@ -10,6 +10,8 @@ const __glob = { COMMANDS: root + path.sep + "src" + path.sep + "discord" + path.sep + "commands", 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", } module.exports = {__glob} \ No newline at end of file diff --git a/backend/src/utils/TokenManager.js b/backend/src/utils/TokenManager.js new file mode 100644 index 0000000..e07f9d6 --- /dev/null +++ b/backend/src/utils/TokenManager.js @@ -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} \ No newline at end of file diff --git a/changelog.md b/changelog.md index 2ee268d..0a138f6 100644 --- a/changelog.md +++ b/changelog.md @@ -1,17 +1,2 @@ -# **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 +- Setup serveur +- \ No newline at end of file