Version 0.1.0 - Mise en place de Discord.js

This commit is contained in:
2025-02-23 13:54:07 +01:00
parent cb206ffa22
commit b054c8a316
19 changed files with 2465 additions and 2 deletions

1717
backend/package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

30
backend/package.json Normal file
View File

@@ -0,0 +1,30 @@
{
"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

@@ -0,0 +1,34 @@
const bot = require('./Bot')
const dlog = require('loguix').getInstance('Discord')
const {ActivityType} = require('discord.js')
function getActivity() {
const client = bot.getClient()
return client.user.presence.activities[0]
}
// Set All type of activities
function setMusicActivity(songName, artistName, imageUrl) {
const client = bot.getClient()
client.user.setActivity(`${songName} - ${artistName}`,{
type: ActivityType.Listening,
/*assets: {
largeImage: imageUrl,
largeText: songName
}*/
});
dlog.log(`Activité mise à jour : ${songName} - ${artistName}`)
}
function idleActivity() {
const client = bot.getClient()
client.user.setActivity("le silence absolu", {
type: ActivityType.Listening
});
dlog.log(`Activité mise à jour : rien`)
}
module.exports = {getActivity, setMusicActivity, idleActivity}

View File

@@ -0,0 +1,78 @@
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

@@ -0,0 +1,73 @@
const { SlashCommandBuilder} = require("discord.js");
class Command {
name;
description;
callback;
data;
constructor(name, description, callback, options) {
this.name = name
this.description = description
this.callback = callback
this.options = options
const SlashCommand = new SlashCommandBuilder()
.setName(name)
.setDescription(description)
// Options is an array with the following structure: [{name: "name", description: "description", type: "type", required: true/false, choices: [{name: "name", value: "value"}]}]
if (options) {
options.forEach(SelOption => {
if(SelOption.type === "STRING") {
SlashCommand.addStringOption(option => option.setName(SelOption.name).setDescription(SelOption.description).setRequired(SelOption.required))
}
if(SelOption.type === "INTEGER") {
SlashCommand.addIntegerOption(option => option.setName(SelOption.name).setDescription(SelOption.description).setRequired(SelOption.required))
}
if(SelOption.type === "BOOLEAN") {
SlashCommand.addBooleanOption(option => option.setName(SelOption.name).setDescription(SelOption.description).setRequired(SelOption.required))
}
if(SelOption.type === "USER") {
SlashCommand.addUserOption(option => option.setName(SelOption.name).setDescription(SelOption.description).setRequired(SelOption.required))
}
if(SelOption.type === "CHANNEL") {
SlashCommand.addChannelOption(option => option.setName(SelOption.name).setDescription(SelOption.description).setRequired(SelOption.required))
}
if(SelOption.type === "ROLE") {
SlashCommand.addRoleOption(option => option.setName(SelOption.name).setDescription(SelOption.description).setRequired(SelOption.required))
}
if(SelOption.type === "NUMBER") {
SlashCommand.addNumberOption(option => option.setName(SelOption.name).setDescription(SelOption.description).setRequired(SelOption.required))
}
if(SelOption.type === "SUB_COMMAND") {
SlashCommand.addSubcommand(option => option.setName(SelOption.name).setDescription(SelOption.description).setRequired(SelOption.required))
}
if(SelOption.type === "SUB_COMMAND_GROUP") {
SlashCommand.addSubcommandGroup(option => option.setName(SelOption.name).setDescription(SelOption.description).setRequired(SelOption.required))
}
if(SelOption.type === "MENTIONABLE") {
SlashCommand.addMentionableOption(option => option.setName(SelOption.name).setDescription(SelOption.description).setRequired(SelOption.required))
}
if(SelOption.type === "CHOICES") {
let choices = []
SelOption.choices.forEach(SelChoice => {
choices.push({name: SelChoice.name, value: SelChoice.value})
})
SlashCommand.addStringOption(option => option.setName(SelOption.name).setDescription(SelOption.description).setRequired(SelOption.required).addChoices(choices))
}
})
}
this.data = {data: SlashCommand, async execute(client, interaction) {callback(client, interaction)}}
}
getData() {
return this.data
}
getName() {
return this.name
}
}
module.exports = {Command}

View File

@@ -0,0 +1,78 @@
function init() {
const bot = require("./Bot")
const client = bot.getClient()
const { Collection, REST, Routes } = require("discord.js")
const fs = require("node:fs")
const path = require("path")
const {__glob} = require("../utils/GlobalVars")
const dlog = require("loguix").getInstance("Discord")
const config = require("../utils/Database/Configuration")
client.commands = new Collection()
dlog.step.init("d_init", "Initialisation du Bot Discord")
dlog.step.init("d_get_commands", "Récupération des commandes en local depuis : " + __glob.COMMANDS)
const commands = [];
// Grab all the command files from the commands directory you created earlier
const commandsPath = __glob.COMMANDS
const commandFiles = fs.readdirSync(commandsPath).filter(file => file.endsWith('.js'));
for (const file of commandFiles) {
const cmd = require(commandsPath + path.sep + file);
client.commands.set(cmd.command.getName(), cmd.command.getData())
commands.push(cmd.command.getData().data.toJSON());
}
dlog.step.end("d_get_commands")
const rest = new REST().setToken(config.getToken());
if(commands.length != 0) {
(async () => {
try {
const guilds = client.guilds.cache.map(guild => guild.id);
dlog.step.init("d_commands_refresh", `Refreshing ${guilds.length} guilds (/) commands.`);
for (const guildId of guilds) {
const data = await rest.put(
Routes.applicationGuildCommands(client.user.id, guildId),
{ body: commands },
);
// Log the guilds where the commands have been refreshed with number of commands
dlog.log(`Refreshed "${data.length}" commands in guild "${guildId} - ${client.guilds.cache.get(guildId).name}"`);
}
dlog.step.end("d_commands_refresh")
} catch (error) {
// And of course, make sure you catch and log any errors!
dlog.error(error)
}
})();
} else {
dlog.warn("Aucune commande à envoyer à Discord !")
}
rest.on("rateLimited", (datawarn) => {
dlog.warn("REST - Limite de requête atteinte ! TimeToReset : " + datawarn.timeToReset);
})
}
module.exports = {init}

View File

@@ -0,0 +1,36 @@
const { Command } = require('../Command');
const { Embed } = require('../Embed');
const { __glob } = require("../../utils/GlobalVars");
const packageJson = require(__glob.PACKAGEINFO);
const command = new Command("about", "Affiche des informations sur le bot", (client, interaction) => {
const uptime = process.uptime();
const hours = Math.floor(uptime / 3600);
const minutes = Math.floor((uptime % 3600) / 60);
const seconds = Math.floor(uptime % 60);
const embed = new Embed()
embed.setColor(0xb0f542)
embed.setThumbnail("https://cdn.discordapp.com/avatars/" + client.user.id + "/" + client.user.avatar + ".png")
embed.setTitle('Subsonics - Chopin')
embed.addField('Informations',"")
embed.addField('Version', packageJson.version + " ", true)
embed.addField('Uptime', `${hours}h ${minutes}m ${seconds}s `, true)
embed.addField("Ping", `${client.ws.ping} ms `, true)
embed.addField("Réalisé par", "Raphix - 2025", true)
embed.addColumn()
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.send(interaction)
})
module.exports = {command}

View File

@@ -0,0 +1,34 @@
const { Command } = require('../Command');
const { Embed } = require('../Embed');
const command = new Command("help", "Affiche la liste des commandes", (client, interaction) => {
const embed = new Embed()
embed.setColor(0x03ff2d)
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.addField('**Liste des commandes :**',"")
client.commands.forEach(command => {
let CommandName = command.data.name
if (command.data.options) {
command.data.options.forEach(option => {
if (option.choices) {
let choices = []
option.choices.forEach(choice => {
choices.push(choice.name)
})
CommandName += " " + choices.join(" | ")
}
})
}
embed.addField("/" + CommandName, command.data.description)
})
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.send(interaction)
})
module.exports = {command}

View File

@@ -0,0 +1,36 @@
const { Command } = require('../Command');
const { Embed } = require('../Embed');
const { Report } = require('../ReportSender');
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 result = report.send()
const embed = new Embed()
result.then((res) => {
if(!res) {
embed.setColor(0xc20f02)
embed.setTitle('Erreur')
embed.setDescription("Une erreur est survenue lors de l'envoi du rapport")
} else {
embed.setColor(0x00ff66)
embed.setTitle('Rapport envoyé')
embed.setDescription("Votre rapport a bien été envoyé !")
}
embed.send(interaction)
})
},
[{type: "CHOICES", name: "type", description: "Type", required: true, choices:
[{name: "Bug", value: "bug"},
{name: "Suggestion", value: "sugguestion"}],
},
{type: "STRING", name: "description", description: "Description du problème", required: true}]
)
module.exports = {command}

View File

@@ -0,0 +1,14 @@
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

@@ -0,0 +1,89 @@
const { EmbedBuilder } = require("discord.js");
class Embed {
fields;
constructor() {
this.embed = new EmbedBuilder().setTimestamp()
this.fields = []
}
setTitle(title) {
this.embed.setTitle(title)
return this
}
setDescription(description) {
this.embed.setDescription(description)
return this
}
setAuthor(author) {
this.embed.setAuthor(author)
return this
}
setColor(r, g, b) {
// if only R is provided, set color with R as hex else set color with RGB
if (g === undefined && b === undefined) {
this.embed.setColor(r)
return this
} else {
// Transforme r, g, b in one 0xRRGGBB value
r = Math.max(0, Math.min(255, r));
g = Math.max(0, Math.min(255, g));
b = Math.max(0, Math.min(255, b));
// Shift the values to their respective positions in a 24-bit number
const hexNumber = (r << 16) + (g << 8) + b;
this.embed.setColor(hexNumber)
}
}
setFooter(footer) {
this.embed.setFooter(footer)
return this
}
setImage(imageUrl) {
this.embed.setImage(imageUrl)
return this
}
setThumbnail(thumbnailUrl) {
this.embed.setThumbnail(thumbnailUrl)
return this
}
addBotPicture(client) {
this.embed.setThumbnail("https://cdn.discordapp.com/avatars/" + client.user.id + "/" + client.user.avatar + ".png")
}
addField(name, value, inline) {
if(!inline) inline = false;
this.fields.push({name: name, value: value, inline: inline})
return this
}
addColumn() {
this.fields.push({name: '\u200B', value: '\u200B', inline: true})
return this
}
build() {
//Add Fields to an object
this.embed.addFields(this.fields)
return this.embed
}
send(interaction) {
interaction.reply({embeds: [this.build()]})
}
}
module.exports = {Embed}

View File

@@ -0,0 +1,51 @@
const config = require("../utils/Database/Configuration");
const { __glob } = require("../utils/GlobalVars");
const discord = require("./Bot")
const {Embed} = require("./Embed")
const log = require("loguix")
const packageJson = require(__glob.PACKAGEINFO)
class Report {
client = discord.getClient();
report_channel = config.getReportChannel();
report_contact = config.getReportContact();
embed;
constructor(provider, level, desc) {
const embed = new Embed()
embed.setDescription('**Version : **' + packageJson.version)
embed.setTitle("Rapport de : " + provider)
var levelString = null
if(level == "bug") {
levelString = "Bug"
embed.setColor(0xc20f02)
} else {
levelString = "Suggestion"
embed.setColor(20, 50, 200) //
}
embed.addField("Type", levelString)
embed.addField("Description", desc)
this.embed = embed
}
async send() {
if(!this.report_channel || this.report_channel == "") {
log.getInstance("Discord").error("Pas de channel de rapport configuré")
return false
} else {
const channel = await this.client.channels.fetch(this.report_channel)
channel.send({embeds: [this.embed.build()]})
if(this.report_contact && this.report_contact != "") channel.send({content: "<@" + this.report_contact + ">"})
return true
}
}
}
module.exports = {Report}

24
backend/src/main.js Normal file
View File

@@ -0,0 +1,24 @@
/**
* [Subsonics Chopin - Backend] - Raphix - 02/2025
* File: main.js
* Description: Main entry point for the backend application
*/
const { LogType } = require('loguix');
const { __glob } = require("./utils/GlobalVars")
require("loguix").setup(__glob.LOGS, __glob.PACKAGEINFO)
const config = require("./utils/Database/Configuration")
const metric = require("webmetrik")
metric.setMetricFile(__glob.METRIC_FILE)
metric.publishMetrics("8001", "raphraph")
// SETUP
setup();
function setup() {
const DiscordBot = require("./discord/Bot")
DiscordBot.init()
}

View File

@@ -0,0 +1,33 @@
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

@@ -0,0 +1,87 @@
const fs = require('fs');
const path = require('path');
const { LogType } = require('loguix');
const { __glob } = require("../GlobalVars")
const AllDatabases = {}
const clog = new LogType("DataBase")
function getDatabase(name) {
return AllDatabases[name]
}
class Database {
data;
path;
name;
empty;
constructor(name, path, empty) {
if(name == undefined || path == undefined) throw clog.error("Impossible de créer une base de données sans nom ou sans chemin")
clog.log(`Enregistrement de la base de données '${name}' contenu dans ${path}`)
this.name = name;
this.path = path;
this.data = {};
if(empty) this.empty = empty; else this.empty = {}
this.load()
AllDatabases[name] = this
}
load() {
clog.log(`Chargement de la base de données '${this.name}'`)
try {
this.create()
this.update()
} catch(e) {
clog.error(`Erreur lors du chargement de la base de données '${this.name}'`)
clog.error(e)
}
}
create() {
try {
if(!fs.existsSync(this.path)) {
clog.warn(`Le fichier de la base de données '${this.name}' n'existe pas, création du fichier`)
fs.writeFileSync(this.path, JSON.stringify(this.empty, null, 2))
}
} catch(e) {
clog.error(`Erreur lors de la création de la base de données '${this.name}'`)
clog.error(e)
}
}
save() {
try {
clog.log(`Sauvegarde de la base de données '${this.name}'`)
fs.writeFileSync(this.path, JSON.stringify(this.data, null, 2))
} catch(e) {
clog.error(`Erreur lors de la sauvegarde de la base de données '${this.name}'`)
clog.error(e)
}
}
update() {
try{
clog.log(`Mise à jour de la base de données '${this.name}'`)
let rawdata = fs.readFileSync(this.path);
this.data = JSON.parse(rawdata);
clog.log(`Base de donnée '${this.name}' chargée avec succès`)
} catch(e) {
clog.error(`Erreur lors de la mise à jour de la base de données '${this.name}'`)
clog.error(e)
}
}
}
module.exports = {Database, getDatabase}

View File

@@ -0,0 +1,14 @@
const path = require("path")
const root = path.resolve(__dirname, '../../')
const __glob = {
PACKAGEINFO: root + path.sep + "package.json",
ROOT: root + + path.sep,
SRC: root + path.sep + "src",
LOGS: root + path.sep + "logs",
DATA: root + path.sep + "data",
COMMANDS: root + path.sep + "src" + path.sep + "discord" + path.sep + "commands",
METRIC_FILE: root + path.sep + "data" + path.sep + "metrics.json"
}
module.exports = {__glob}