Push structure V1

This commit is contained in:
Loïc 2025-10-18 08:14:08 +02:00
parent 3d1989ea0f
commit f31ae7acc2
20 changed files with 3071 additions and 2 deletions

60
.env.exemple Normal file
View File

@ -0,0 +1,60 @@
# Configuration du Bot Discord - Clash Royale
# Renommer ce fichier en .env après avoir rempli les valeurs
# ===========================================
# Configuration Discord
# ===========================================
DISCORD_TOKEN=DISCORD_BOT_TOKEN_HERE
DISCORD_CLIENT_ID=DISCORD_CLIENT_ID_HERE
GUILD_ID=GUILD_ID_HERE
# ===========================================
# Configuration Clash Royale API
# ===========================================
CLASH_ROYALE_TOKEN=TOKEN_CLASH_ROYALE_HERE
# Tag du clan principal pour le classement
CLAN_TAG=CLAN_TAG_HERE # Sans le # !
# ===========================================
# Configuration des Salons Discord
# ===========================================
# Salon pour afficher le classement top 10
RANKING_CHANNEL_ID=RANKING_CHANNEL_ID_HERE
# Salon pour les félicitations de trophées
CONGRATULATIONS_CHANNEL_ID=CONGRATULATIONS_CHANNEL_ID_HERE
# ===========================================
# Configuration des Rôles Discord
# ===========================================
# Rôle requis pour utiliser les commandes Clash Royale
CLASH_ROYALE_ROLE_ID=CLASH_ROYALE_ROLE_ID_HERE
# Rôles de la hiérarchie du clan
CHEF_ROLE_ID=CHEF_ROLE_ID_HERE
AINE_ROLE_ID=AINE_ROLE_ID_HERE
MEMBRE_ROLE_ID=MEMBRE_ROLE_ID_HERE
# ===========================================
# Configuration Base de Données
# ===========================================
DATABASE_PATH=./database/bot.db
# ===========================================
# Configuration Bot
# ===========================================
# Intervalle de vérification des trophées (en minutes)
TROPHY_CHECK_INTERVAL=30
# Intervalle de mise à jour du classement (en minutes)
RANKING_UPDATE_INTERVAL=60
# Seuil de trophées pour félicitations
TROPHY_MILESTONE=1000
# Mode de développement (true/false)
DEVELOPMENT_MODE=false
# Niveau de logs (error, warn, info, debug)
LOG_LEVEL=info

2
.gitignore vendored Normal file
View File

@ -0,0 +1,2 @@
node_modules
.env

View File

@ -1,3 +1,97 @@
# clashroyale-bot # 🏰 Clash Royale Discord Bot
Code source de notre bot Discord Clash Royale Un bot Discord avancé intégrant l'API officielle Clash Royale pour gérer automatiquement les classements, félicitations et synchronisations de profils.
## ✨ Fonctionnalités Principales
### 🎯 Synchronisation de Profils
- **Commande `/sync`** : Associe automatiquement un profil Clash Royale à un utilisateur Discord
- Vérification des rôles requis avant synchronisation
- Validation en temps réel via l'API officielle Clash Royale
- Protection contre les doublons de profils
### 🏆 Système de Classement
- **Classement Top 10** automatiquement mis à jour dans le salon dédié
- Mise à jour programmée toutes les heures
- Interface visuelle avec médailles et icônes de trophées
- Statistiques du serveur (moyenne, total des trophées)
- **Commande `/ranking`** avec sous-commandes :
- `top` : Affiche le top 10
- `me` : Votre position personnelle
- `update` : Mise à jour forcée (admin)
### 🎉 Félicitations Automatiques
- Détection automatique des paliers de 1000 trophées
- Messages personnalisés selon le niveau atteint
- Réactions automatiques sur les messages
- Envoi dans le salon de félicitations configuré
- Gestion des doublons (pas de spam)
## 🚀 Installation Rapide
### 1. Prérequis
```bash
Node.js 16+ (recommandé : 18+)
npm ou yarn
```
### 2. Installation
```bash
git clone <repository-url>
cd clashroyale-bot
npm install
```
### 3. Configuration
```bash
# Copier le fichier de configuration
cp .env.exemple .env
# Éditer le fichier .env avec vos tokens et IDs
nano .env
```
### 4. Lancement
```bash
npm start
```
## ⚙️ Configuration
Copiez `.env.exemple` vers `.env` et remplissez toutes les valeurs selon votre serveur Discord et votre token API Clash Royale.
### Variables principales :
- `DISCORD_TOKEN` : Token de votre bot Discord
- `CLASH_ROYALE_TOKEN` : Token API Clash Royale (Supercell)
- `GUILD_ID` : ID de votre serveur Discord
- `RANKING_CHANNEL_ID` : Salon pour le classement automatique
- `CONGRATULATIONS_CHANNEL_ID` : Salon pour les félicitations
- `CLASH_ROYALE_ROLE_ID` : Rôle requis pour utiliser les commandes
## 🗂️ Structure
```
src/
├── handlers/ # Chargement automatique des modules
├── commands/ # Commandes slash Discord
├── events/ # Événements Discord
├── services/ # Logique métier (API, classements, etc.)
└── utils/ # Base de données et utilitaires
```
## 📋 Commandes
- `/sync id:TAG` - Synchroniser son profil Clash Royale
- `/ranking top` - Top 10 du serveur
- `/ranking me` - Votre position
- `/ranking update` - Mise à jour forcée (admin)
## 🔄 Automatisations
- **Classement** : Mis à jour automatiquement toutes les heures
- **Félicitations** : Détection des paliers de 1000 trophées toutes les 30 minutes
- **Logs colorés** : Monitoring en temps réel avec système de couleurs
---
**Développé par l'équipe Neptunia** • Bot opérationnel 24/7

29
package.json Normal file
View File

@ -0,0 +1,29 @@
{
"name": "clashroyale-discord-bot",
"version": "1.0.0",
"description": "Bot Discord avec intégration Clash Royale pour classements et félicitations",
"main": "start.js",
"scripts": {
"start": "node start.js",
"dev": "nodemon start.js"
},
"dependencies": {
"discord.js": "^14.14.1",
"axios": "^1.6.2",
"sqlite3": "^5.1.6",
"dotenv": "^16.3.1",
"chalk": "^4.1.2",
"node-cron": "^3.0.3"
},
"devDependencies": {
"nodemon": "^3.0.2"
},
"keywords": [
"discord",
"bot",
"clash-royale",
"gaming"
],
"author": "Neptunia Team",
"license": "MIT"
}

172
schema.sql Normal file
View File

@ -0,0 +1,172 @@
-- =====================================================
-- SCHEMA BASE DE DONNÉES - CLASH ROYALE DISCORD BOT
-- =====================================================
-- Créé pour gérer les associations utilisateurs Discord <-> Clash Royale
-- et le suivi des trophées pour les félicitations automatiques
-- Suppression des tables existantes (si elles existent)
DROP TABLE IF EXISTS trophy_history;
DROP TABLE IF EXISTS user_profiles;
DROP TABLE IF EXISTS bot_settings;
-- =====================================================
-- TABLE: user_profiles
-- =====================================================
-- Stocke les associations entre utilisateurs Discord et profils Clash Royale
CREATE TABLE user_profiles (
id INTEGER PRIMARY KEY AUTOINCREMENT,
discord_id TEXT UNIQUE NOT NULL, -- ID Discord de l'utilisateur
clash_tag TEXT UNIQUE NOT NULL, -- Tag Clash Royale (ex: #ABC123DEF)
clash_name TEXT NOT NULL, -- Nom du joueur Clash Royale
current_trophies INTEGER DEFAULT 0, -- Trophées actuels
highest_trophies INTEGER DEFAULT 0, -- Record de trophées
level INTEGER DEFAULT 1, -- Niveau du joueur
clan_name TEXT, -- Nom du clan (si dans un clan)
clan_tag TEXT, -- Tag du clan (si dans un clan)
clan_role TEXT, -- Rôle dans le clan
verified BOOLEAN DEFAULT FALSE, -- Profil vérifié
sync_date DATETIME DEFAULT CURRENT_TIMESTAMP, -- Date de synchronisation
last_update DATETIME DEFAULT CURRENT_TIMESTAMP, -- Dernière mise à jour
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
);
-- =====================================================
-- TABLE: trophy_history
-- =====================================================
-- Historique des trophées pour détecter les gains et féliciter
CREATE TABLE trophy_history (
id INTEGER PRIMARY KEY AUTOINCREMENT,
user_profile_id INTEGER NOT NULL, -- Référence vers user_profiles
old_trophies INTEGER NOT NULL, -- Ancien nombre de trophées
new_trophies INTEGER NOT NULL, -- Nouveau nombre de trophées
trophy_gain INTEGER NOT NULL, -- Gain de trophées
milestone_reached INTEGER, -- Palier atteint (1000, 2000, etc.)
congratulated BOOLEAN DEFAULT FALSE, -- Si félicitations envoyées
recorded_at DATETIME DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (user_profile_id) REFERENCES user_profiles(id) ON DELETE CASCADE
);
-- =====================================================
-- TABLE: bot_settings
-- =====================================================
-- Paramètres et configuration du bot
CREATE TABLE bot_settings (
id INTEGER PRIMARY KEY AUTOINCREMENT,
setting_key TEXT UNIQUE NOT NULL, -- Clé du paramètre
setting_value TEXT NOT NULL, -- Valeur du paramètre
description TEXT, -- Description du paramètre
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP
);
-- =====================================================
-- INDEX pour optimiser les performances
-- =====================================================
CREATE INDEX idx_user_profiles_discord_id ON user_profiles(discord_id);
CREATE INDEX idx_user_profiles_clash_tag ON user_profiles(clash_tag);
CREATE INDEX idx_trophy_history_user_profile ON trophy_history(user_profile_id);
CREATE INDEX idx_trophy_history_recorded_at ON trophy_history(recorded_at);
CREATE INDEX idx_trophy_history_milestone ON trophy_history(milestone_reached);
CREATE INDEX idx_bot_settings_key ON bot_settings(setting_key);
-- =====================================================
-- DONNÉES INITIALES
-- =====================================================
-- Paramètres par défaut du bot
INSERT INTO bot_settings (setting_key, setting_value, description) VALUES
('last_ranking_update', '0', 'Timestamp de la derniere mise a jour du classement'),
('total_users_synced', '0', 'Nombre total utilisateurs synchronises'),
('trophy_check_enabled', 'true', 'Active/desactive la verification des trophees'),
('ranking_update_enabled', 'true', 'Active/desactive les mises a jour du classement'),
('congratulations_enabled', 'true', 'Active/desactive les felicitations automatiques'),
('bot_version', '1.0.0', 'Version actuelle du bot'),
('maintenance_mode', 'false', 'Mode maintenance du bot');
-- =====================================================
-- VUES UTILES
-- =====================================================
-- Vue pour le classement des utilisateurs
CREATE VIEW ranking_view AS
SELECT
up.discord_id,
up.clash_name,
up.current_trophies,
up.highest_trophies,
up.level,
up.clan_name,
up.clan_role,
ROW_NUMBER() OVER (ORDER BY up.current_trophies DESC) as rank_position
FROM user_profiles up
WHERE up.verified = TRUE
ORDER BY up.current_trophies DESC;
-- Vue pour les gains de trophées récents (24h)
CREATE VIEW recent_trophy_gains AS
SELECT
up.discord_id,
up.clash_name,
th.old_trophies,
th.new_trophies,
th.trophy_gain,
th.milestone_reached,
th.congratulated,
th.recorded_at
FROM trophy_history th
JOIN user_profiles up ON th.user_profile_id = up.id
WHERE th.recorded_at >= datetime('now', '-1 day')
AND th.trophy_gain > 0
ORDER BY th.recorded_at DESC;
-- =====================================================
-- TRIGGERS
-- =====================================================
-- Trigger pour mettre à jour last_update automatiquement
CREATE TRIGGER update_user_profiles_timestamp
AFTER UPDATE ON user_profiles
BEGIN
UPDATE user_profiles
SET last_update = CURRENT_TIMESTAMP
WHERE id = NEW.id;
END;
-- Trigger pour mettre à jour updated_at dans bot_settings
CREATE TRIGGER update_bot_settings_timestamp
AFTER UPDATE ON bot_settings
BEGIN
UPDATE bot_settings
SET updated_at = CURRENT_TIMESTAMP
WHERE id = NEW.id;
END;
-- =====================================================
-- FONCTIONS UTILES (commentées pour référence)
-- =====================================================
/*
-- Exemple de requête pour obtenir le top 10
SELECT * FROM ranking_view LIMIT 10;
-- Exemple pour obtenir les utilisateurs ayant gagné plus de 1000 trophées
SELECT * FROM recent_trophy_gains WHERE trophy_gain >= 1000;
-- Exemple pour obtenir les statistiques d'un utilisateur
SELECT
up.*,
COUNT(th.id) as total_records,
SUM(CASE WHEN th.trophy_gain > 0 THEN th.trophy_gain ELSE 0 END) as total_gains,
SUM(CASE WHEN th.trophy_gain < 0 THEN ABS(th.trophy_gain) ELSE 0 END) as total_losses
FROM user_profiles up
LEFT JOIN trophy_history th ON up.id = th.user_profile_id
WHERE up.discord_id = 'DISCORD_USER_ID'
GROUP BY up.id;
*/
-- =====================================================
-- VALIDATION DU SCHÉMA
-- =====================================================
-- Vérification que toutes les tables sont créées correctement
SELECT
name as table_name,
type
FROM sqlite_master
WHERE type IN ('table', 'view', 'index', 'trigger')
ORDER BY type, name;

131
schema_clean.sql Normal file
View File

@ -0,0 +1,131 @@
-- =====================================================
-- CLASH ROYALE DISCORD BOT - DATABASE SCHEMA
-- =====================================================
-- Clean schema without special characters
-- =====================================================
-- Drop existing tables if they exist
DROP TABLE IF EXISTS trophy_history;
DROP TABLE IF EXISTS user_profiles;
DROP TABLE IF EXISTS bot_settings;
-- =====================================================
-- TABLE: user_profiles
-- =====================================================
CREATE TABLE user_profiles (
id INTEGER PRIMARY KEY AUTOINCREMENT,
discord_id TEXT UNIQUE NOT NULL,
clash_tag TEXT UNIQUE NOT NULL,
clash_name TEXT NOT NULL,
current_trophies INTEGER DEFAULT 0,
highest_trophies INTEGER DEFAULT 0,
level INTEGER DEFAULT 1,
clan_name TEXT,
clan_tag TEXT,
clan_role TEXT,
verified BOOLEAN DEFAULT FALSE,
sync_date DATETIME DEFAULT CURRENT_TIMESTAMP,
last_update DATETIME DEFAULT CURRENT_TIMESTAMP,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
);
-- =====================================================
-- TABLE: trophy_history
-- =====================================================
CREATE TABLE trophy_history (
id INTEGER PRIMARY KEY AUTOINCREMENT,
user_profile_id INTEGER NOT NULL,
old_trophies INTEGER NOT NULL,
new_trophies INTEGER NOT NULL,
trophy_gain INTEGER NOT NULL,
milestone_reached INTEGER,
congratulated BOOLEAN DEFAULT FALSE,
recorded_at DATETIME DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (user_profile_id) REFERENCES user_profiles(id) ON DELETE CASCADE
);
-- =====================================================
-- TABLE: bot_settings
-- =====================================================
CREATE TABLE bot_settings (
id INTEGER PRIMARY KEY AUTOINCREMENT,
setting_key TEXT UNIQUE NOT NULL,
setting_value TEXT NOT NULL,
description TEXT,
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP
);
-- =====================================================
-- INDEXES FOR PERFORMANCE
-- =====================================================
CREATE INDEX idx_user_profiles_discord_id ON user_profiles(discord_id);
CREATE INDEX idx_user_profiles_clash_tag ON user_profiles(clash_tag);
CREATE INDEX idx_trophy_history_user_profile ON trophy_history(user_profile_id);
CREATE INDEX idx_trophy_history_recorded_at ON trophy_history(recorded_at);
CREATE INDEX idx_trophy_history_milestone ON trophy_history(milestone_reached);
CREATE INDEX idx_bot_settings_key ON bot_settings(setting_key);
-- =====================================================
-- DEFAULT BOT SETTINGS
-- =====================================================
INSERT INTO bot_settings (setting_key, setting_value, description) VALUES
('last_ranking_update', '0', 'Last ranking update timestamp'),
('total_users_synced', '0', 'Total number of synced users'),
('trophy_check_enabled', 'true', 'Enable trophy milestone checking'),
('ranking_update_enabled', 'true', 'Enable automatic ranking updates'),
('congratulations_enabled', 'true', 'Enable automatic congratulations'),
('bot_version', '1.0.0', 'Current bot version'),
('maintenance_mode', 'false', 'Bot maintenance mode');
-- =====================================================
-- USEFUL VIEWS
-- =====================================================
CREATE VIEW ranking_view AS
SELECT
up.discord_id,
up.clash_name,
up.current_trophies,
up.highest_trophies,
up.level,
up.clan_name,
up.clan_role,
ROW_NUMBER() OVER (ORDER BY up.current_trophies DESC) as rank_position
FROM user_profiles up
WHERE up.verified = 1
ORDER BY up.current_trophies DESC;
CREATE VIEW recent_trophy_gains AS
SELECT
up.discord_id,
up.clash_name,
th.old_trophies,
th.new_trophies,
th.trophy_gain,
th.milestone_reached,
th.congratulated,
th.recorded_at
FROM trophy_history th
JOIN user_profiles up ON th.user_profile_id = up.id
WHERE th.recorded_at >= datetime('now', '-1 day')
AND th.trophy_gain > 0
ORDER BY th.recorded_at DESC;
-- =====================================================
-- TRIGGERS
-- =====================================================
CREATE TRIGGER update_user_profiles_timestamp
AFTER UPDATE ON user_profiles
BEGIN
UPDATE user_profiles
SET last_update = CURRENT_TIMESTAMP
WHERE id = NEW.id;
END;
CREATE TRIGGER update_bot_settings_timestamp
AFTER UPDATE ON bot_settings
BEGIN
UPDATE bot_settings
SET updated_at = CURRENT_TIMESTAMP
WHERE id = NEW.id;
END;

107
src/commands/info.js Normal file
View File

@ -0,0 +1,107 @@
const { SlashCommandBuilder, EmbedBuilder } = require('discord.js');
module.exports = {
data: new SlashCommandBuilder()
.setName('info')
.setDescription('Affiche les informations du bot'),
async execute(interaction, client) {
await interaction.deferReply();
try {
const guild = interaction.guild;
const uptime = process.uptime();
const uptimeFormatted = this.formatUptime(uptime);
const embed = new EmbedBuilder()
.setColor('#FFD700')
.setTitle('🏰 Clash Royale Discord Bot')
.setDescription('Bot Discord intégrant l\'API Clash Royale pour classements automatiques et félicitations')
.setThumbnail(client.user.displayAvatarURL({ dynamic: true, size: 256 }))
.addFields([
{
name: '⚙️ Informations Techniques',
value: `📡 **Ping:** ${Math.round(client.ws.ping)}ms\n⏱️ **Uptime:** ${uptimeFormatted}\n🟢 **Statut:** Opérationnel\n📦 **Version:** 1.0.0`,
inline: true
},
{
name: '🏰 Clan Configuré',
value: `🏷️ **Tag:** \`${"#" + process.env.CLAN_TAG || 'Non configuré'}\`\n🔄 **Mise à jour:** Toutes les 5 minutes\n📊 **Classement:** Automatique`,
inline: true
},
{
name: '📈 Statistiques',
value: `🎮 **Serveurs:** ${client.guilds.cache.size}\n👥 **Utilisateurs:** ${client.users.cache.size}\n💬 **Commandes:** ${client.commands.size}`,
inline: true
}
])
.addFields([
{
name: '🎯 Fonctionnalités',
value: '• **`/sync`** - Synchroniser profil Clash Royale\n• **`/ranking`** - Classement du clan\n• **Félicitations automatiques** - Paliers de trophées\n• **Mise à jour live** - Toutes les 5 minutes',
inline: false
}
])
.addFields([
{
name: '🔗 Code Source',
value: '[📂 Repository Git](https://git.valloic.dev/Neptunia/clashroyale-bot)\n*Code open source disponible*',
inline: true
},
{
name: '👨‍💻 Développé par',
value: '**cut0x**\n[valloic.dev](https://valloic.dev)',
inline: true
},
{
name: '📞 Support',
value: 'Contactez les administrateurs\npour toute assistance',
inline: true
}
])
.setFooter({
text: `Demandé par ${interaction.user.tag} • Bot opérationnel`,
iconURL: interaction.user.displayAvatarURL({ dynamic: true })
})
.setTimestamp();
// Ajouter des informations spécifiques au serveur si disponible
if (guild) {
embed.addFields([{
name: '🏢 Serveur Actuel',
value: `**${guild.name}**\n👥 ${guild.memberCount} membres\n📅 Créé le ${guild.createdAt.toLocaleDateString('fr-FR')}`,
inline: true
}]);
}
await interaction.editReply({ embeds: [embed] });
} catch (error) {
client.logger.error(`Erreur lors de l'affichage des informations: ${error.message}`);
const errorEmbed = new EmbedBuilder()
.setColor('#FF0000')
.setTitle('❌ Erreur')
.setDescription('Impossible d\'afficher les informations du bot.')
.setTimestamp();
await interaction.editReply({ embeds: [errorEmbed] });
}
},
// Fonction pour formater l'uptime
formatUptime(seconds) {
const days = Math.floor(seconds / 86400);
const hours = Math.floor((seconds % 86400) / 3600);
const minutes = Math.floor((seconds % 3600) / 60);
const secs = Math.floor(seconds % 60);
let result = '';
if (days > 0) result += `${days}j `;
if (hours > 0) result += `${hours}h `;
if (minutes > 0) result += `${minutes}m `;
if (secs > 0 || result === '') result += `${secs}s`;
return result.trim();
}
};

284
src/commands/ranking.js Normal file
View File

@ -0,0 +1,284 @@
const { SlashCommandBuilder, EmbedBuilder } = require('discord.js');
const { clanRankingService } = require('../services/clanRankingService');
module.exports = {
data: new SlashCommandBuilder()
.setName('ranking')
.setDescription('Affiche le classement du clan')
.addSubcommand(subcommand =>
subcommand
.setName('top')
.setDescription('Affiche le top 10 du clan')
)
.addSubcommand(subcommand =>
subcommand
.setName('me')
.setDescription('Affiche votre position dans le clan')
)
.addSubcommand(subcommand =>
subcommand
.setName('update')
.setDescription('Force la mise à jour du classement (Admin uniquement)')
),
async execute(interaction, client) {
const subcommand = interaction.options.getSubcommand();
switch (subcommand) {
case 'top':
await this.handleTopRanking(interaction, client);
break;
case 'me':
await this.handleMyRanking(interaction, client);
break;
case 'update':
await this.handleUpdateRanking(interaction, client);
break;
}
},
async handleTopRanking(interaction, client) {
await interaction.deferReply();
try {
const clanRanking = await clanRankingService.getFullClanRanking();
if (!clanRanking.success) {
const embed = new EmbedBuilder()
.setColor('#FF0000')
.setTitle('❌ Erreur')
.setDescription(`Impossible de récupérer le classement du clan: ${clanRanking.error}`)
.setTimestamp();
await interaction.editReply({ embeds: [embed] });
return;
}
const topPlayers = clanRanking.data.slice(0, 10);
if (topPlayers.length === 0) {
const embed = new EmbedBuilder()
.setColor('#FFA500')
.setTitle('🏰 Classement du Clan')
.setDescription(`Aucun membre trouvé dans le clan \`${"#" + process.env.CLAN_TAG}\``)
.setTimestamp();
await interaction.editReply({ embeds: [embed] });
return;
}
const embed = new EmbedBuilder()
.setColor('#FFD700')
.setTitle('🏰 Top 10 du Clan');
let description = '';
const medals = ['🥇', '🥈', '🥉'];
for (let i = 0; i < topPlayers.length; i++) {
const player = topPlayers[i];
const medal = i < 3 ? medals[i] : `**${i + 1}.**`;
const trophyIcon = clanRankingService.getTrophyIcon(player.trophies);
const roleEmoji = clanRankingService.getRoleEmoji(player.role);
description += `${medal} ${trophyIcon} **${player.name}** ${roleEmoji}${player.trophies.toLocaleString()}\n`;
}
embed.setDescription(`Clan: \`${"#" + process.env.CLAN_TAG}\`\n\n${description}`);
embed.setFooter({
text: `${clanRanking.data.length} membres dans le clan • Mise à jour toutes les 5min`,
iconURL: client.user.displayAvatarURL()
});
embed.setTimestamp();
await interaction.editReply({ embeds: [embed] });
} catch (error) {
client.logger.error(`Erreur lors de l'affichage du classement: ${error.message}`);
const errorEmbed = new EmbedBuilder()
.setColor('#FF0000')
.setTitle('❌ Erreur')
.setDescription('Impossible d\'afficher le classement.')
.setTimestamp();
await interaction.editReply({ embeds: [errorEmbed] });
}
},
async handleMyRanking(interaction, client) {
await interaction.deferReply({ ephemeral: true });
try {
// D'abord vérifier si l'utilisateur a un profil synchronisé
const { UserProfileService } = require('../utils/database');
const userProfile = await UserProfileService.getByDiscordId(interaction.user.id);
if (!userProfile) {
const embed = new EmbedBuilder()
.setColor('#FFA500')
.setTitle('❌ Profil non synchronisé')
.setDescription('Vous devez d\'abord synchroniser votre profil Clash Royale.')
.addFields([{
name: '💡 Comment faire ?',
value: 'Utilisez la commande `/sync id:VOTRE_TAG` pour associer votre profil.',
inline: false
}])
.setTimestamp();
await interaction.editReply({ embeds: [embed] });
return;
}
// Obtenir la position dans le clan
const memberRank = await clanRankingService.getMemberRank(userProfile.clash_tag);
if (!memberRank) {
const embed = new EmbedBuilder()
.setColor('#FFA500')
.setTitle('❌ Non trouvé dans le clan')
.setDescription(`Vous n'êtes pas membre du clan configuré \`${"#" + process.env.CLAN_TAG}\``)
.addFields([{
name: '💡 Solution',
value: 'Rejoignez le clan ou contactez un administrateur si c\'est une erreur.',
inline: false
}])
.setTimestamp();
await interaction.editReply({ embeds: [embed] });
return;
}
const { rank, total, member, above, below } = memberRank;
const trophyIcon = clanRankingService.getTrophyIcon(member.trophies);
const roleEmoji = clanRankingService.getRoleEmoji(member.role);
const embed = new EmbedBuilder()
.setColor('#00BFFF')
.setTitle(`${trophyIcon} Votre Position dans le Clan`)
.setDescription(`Vous êtes **#${rank}** sur **${total}** membres`)
.setThumbnail(interaction.user.displayAvatarURL({ dynamic: true }))
.addFields([
{
name: '👤 Votre Profil',
value: `**${member.name}**\n${roleEmoji} **${member.role}**\n🏆 ${member.trophies.toLocaleString()} trophées\n💎 Niveau ${member.expLevel}`,
inline: true
}
]);
if (above) {
embed.addFields([{
name: '⬆️ Membre au-dessus',
value: `**${above.name}**\n${clanRankingService.getRoleEmoji(above.role)} ${above.role}\n🏆 ${above.trophies.toLocaleString()} trophées\n📈 +${(above.trophies - member.trophies).toLocaleString()} pour le rattraper`,
inline: true
}]);
}
if (below) {
embed.addFields([{
name: '⬇️ Membre en-dessous',
value: `**${below.name}**\n${clanRankingService.getRoleEmoji(below.role)} ${below.role}\n🏆 ${below.trophies.toLocaleString()} trophées\n📉 Avance: +${(member.trophies - below.trophies).toLocaleString()}`,
inline: true
}]);
}
// Ajouter des encouragements selon la position
let encouragement = '';
if (rank === 1) {
encouragement = '👑 **LEADER DU CLAN !** Performance exceptionnelle !';
} else if (rank <= 3) {
encouragement = `${rank === 2 ? '🥈' : '🥉'} **Podium du clan !** Excellente performance !`;
} else if (rank <= 10) {
encouragement = '🏆 **Top 10 du clan !** Vous faites partie de l\'élite !';
} else {
const percentile = Math.round(((total - rank + 1) / total) * 100);
encouragement = `📊 Top ${percentile}% du clan ! Continuez à progresser !`;
}
embed.addFields([{
name: '💪 Motivation',
value: encouragement,
inline: false
}]);
embed.setFooter({
text: `Classement clan • Mise à jour toutes les 5min`,
iconURL: client.user.displayAvatarURL()
});
embed.setTimestamp();
await interaction.editReply({ embeds: [embed] });
} catch (error) {
client.logger.error(`Erreur lors de l'affichage du rang personnel: ${error.message}`);
const errorEmbed = new EmbedBuilder()
.setColor('#FF0000')
.setTitle('❌ Erreur')
.setDescription('Impossible d\'afficher votre position.')
.setTimestamp();
await interaction.editReply({ embeds: [errorEmbed] });
}
},
async handleUpdateRanking(interaction, client) {
// Vérifier les permissions d'administrateur
if (!interaction.member.permissions.has('Administrator') &&
!interaction.member.roles.cache.has(process.env.CHEF_ROLE_ID) &&
!interaction.member.roles.cache.has(process.env.AINE_ROLE_ID)) {
const embed = new EmbedBuilder()
.setColor('#FF0000')
.setTitle('❌ Permission refusée')
.setDescription('Seuls les administrateurs et les responsables peuvent forcer la mise à jour du classement.')
.setTimestamp();
await interaction.reply({ embeds: [embed], ephemeral: true });
return;
}
await interaction.deferReply();
try {
const embed = new EmbedBuilder()
.setColor('#FFD700')
.setTitle('🔄 Mise à jour en cours...')
.setDescription('Mise à jour du classement du clan en cours, veuillez patienter...')
.setTimestamp();
await interaction.editReply({ embeds: [embed] });
// Forcer la mise à jour
await clanRankingService.updateClanRanking(client);
const successEmbed = new EmbedBuilder()
.setColor('#00FF00')
.setTitle('✅ Mise à jour terminée')
.setDescription('Le classement du clan a été mis à jour avec succès !')
.addFields([{
name: '📍 Où voir le classement ?',
value: `Consultez <#${process.env.RANKING_CHANNEL_ID}> pour voir le classement actualisé.`,
inline: false
}])
.setTimestamp();
await interaction.editReply({ embeds: [successEmbed] });
} catch (error) {
client.logger.error(`Erreur lors de la mise à jour forcée: ${error.message}`);
const errorEmbed = new EmbedBuilder()
.setColor('#FF0000')
.setTitle('❌ Erreur')
.setDescription('Impossible de mettre à jour le classement.')
.addFields([{
name: 'Détails',
value: error.message,
inline: false
}])
.setTimestamp();
await interaction.editReply({ embeds: [errorEmbed] });
}
}
};

238
src/commands/sync.js Normal file
View File

@ -0,0 +1,238 @@
const { SlashCommandBuilder, EmbedBuilder, PermissionFlagsBits } = require('discord.js');
const { clashRoyaleAPI } = require('../services/clashRoyaleService');
const { UserProfileService } = require('../utils/database');
module.exports = {
data: new SlashCommandBuilder()
.setName('sync')
.setDescription('Associer votre profil Clash Royale à votre compte Discord')
.addStringOption(option =>
option
.setName('id')
.setDescription('Votre tag Clash Royale (ex: #ABC123DEF)')
.setRequired(true)
),
async execute(interaction, client) {
await interaction.deferReply({ ephemeral: true });
try {
// Vérification du rôle requis
const requiredRoleId = process.env.CLASH_ROYALE_ROLE_ID;
if (!interaction.member.roles.cache.has(requiredRoleId)) {
const errorEmbed = new EmbedBuilder()
.setColor('#FF0000')
.setTitle('❌ Accès refusé')
.setDescription('Vous devez avoir le rôle requis pour utiliser cette commande.')
.addFields([
{
name: 'Rôle requis',
value: `<@&${requiredRoleId}>`,
inline: true
}
])
.setTimestamp();
await interaction.editReply({ embeds: [errorEmbed] });
return;
}
const clashTag = interaction.options.getString('id');
const discordId = interaction.user.id;
client.logger.clash(`Tentative de synchronisation: ${interaction.user.tag} -> ${clashTag}`);
// Vérifier si l'utilisateur a déjà un profil synchronisé
const existingProfile = await UserProfileService.getByDiscordId(discordId);
// Vérifier si le tag Clash Royale est déjà utilisé par quelqu'un d'autre
const existingClashProfile = await UserProfileService.getByClashTag(clashTag);
if (existingClashProfile && existingClashProfile.discord_id !== discordId) {
const conflictEmbed = new EmbedBuilder()
.setColor('#FF6B00')
.setTitle('⚠️ Tag déjà utilisé')
.setDescription('Ce tag Clash Royale est déjà associé à un autre utilisateur Discord.')
.addFields([
{
name: 'Tag Clash Royale',
value: `\`${clashTag}\``,
inline: true
},
{
name: 'Solution',
value: 'Vérifiez que vous avez entré le bon tag ou contactez un administrateur.',
inline: false
}
])
.setTimestamp();
await interaction.editReply({ embeds: [conflictEmbed] });
return;
}
// Valider le profil Clash Royale via l'API
const validationResult = await clashRoyaleAPI.validatePlayer(clashTag);
if (!validationResult.valid) {
let errorMessage = 'Tag Clash Royale invalide ou joueur non trouvé.';
let errorTitle = '❌ Profil non trouvé';
switch (validationResult.error.type) {
case 'NOT_FOUND':
errorMessage = 'Aucun joueur trouvé avec ce tag. Vérifiez que vous avez entré le bon tag.';
break;
case 'BAD_REQUEST':
errorMessage = 'Format de tag invalide. Utilisez le format #ABC123DEF.';
break;
case 'RATE_LIMITED':
errorMessage = 'Trop de requêtes à l\'API. Réessayez dans quelques minutes.';
errorTitle = '⏳ Limite atteinte';
break;
case 'SERVICE_UNAVAILABLE':
errorMessage = 'API Clash Royale temporairement indisponible. Réessayez plus tard.';
errorTitle = '🛠️ Maintenance';
break;
}
const errorEmbed = new EmbedBuilder()
.setColor('#FF0000')
.setTitle(errorTitle)
.setDescription(errorMessage)
.addFields([
{
name: 'Tag fourni',
value: `\`${clashTag}\``,
inline: true
},
{
name: 'Aide',
value: 'Le tag se trouve dans votre profil Clash Royale, sous votre nom.',
inline: false
}
])
.setTimestamp();
await interaction.editReply({ embeds: [errorEmbed] });
return;
}
const playerData = validationResult.data;
// Créer ou mettre à jour le profil dans la base de données
const profile = await UserProfileService.createOrUpdate(discordId, playerData);
client.logger.success(`Profil synchronisé: ${playerData.name} (${playerData.tag})`);
// Déterminer si c'est une nouvelle synchronisation ou une mise à jour
const isNewSync = !existingProfile;
const actionText = isNewSync ? 'synchronisé' : 'mis à jour';
const actionEmoji = isNewSync ? '🎉' : '🔄';
// Créer l'embed de confirmation
const successEmbed = new EmbedBuilder()
.setColor('#00FF00')
.setTitle(`${actionEmoji} Profil ${actionText} avec succès !`)
.setDescription(`Votre compte Discord est maintenant associé à votre profil Clash Royale.`)
.setThumbnail(interaction.user.displayAvatarURL({ dynamic: true }))
.addFields([
{
name: '👤 Nom du joueur',
value: playerData.name,
inline: true
},
{
name: '🏷️ Tag Clash Royale',
value: `\`${playerData.tag}\``,
inline: true
},
{
name: '🏆 Trophées actuels',
value: `${playerData.trophies.toLocaleString()}`,
inline: true
},
{
name: '🥇 Meilleur score',
value: `${playerData.bestTrophies.toLocaleString()}`,
inline: true
},
{
name: '⭐ Niveau',
value: `${playerData.expLevel}`,
inline: true
},
{
name: '🏰 Clan',
value: playerData.clan ?
`${playerData.clan.name}\n\`${playerData.clan.tag}\`\n*${playerData.clan.role}*` :
'Aucun clan',
inline: true
}
])
.addFields([
{
name: '✨ Fonctionnalités disponibles',
value: '• Classement automatique du serveur\n• Félicitations pour les gains de trophées\n• Statistiques personnalisées\n• Comparaisons avec les autres membres',
inline: false
}
])
.setFooter({
text: `${isNewSync ? 'Première synchronisation' : 'Profile mis à jour'} • Clash Royale Bot`,
iconURL: client.user.displayAvatarURL()
})
.setTimestamp();
await interaction.editReply({ embeds: [successEmbed] });
// Log dans le canal de logs si configuré
try {
const logChannel = client.channels.cache.get(process.env.LOGS_CHANNEL_ID);
if (logChannel) {
const logEmbed = new EmbedBuilder()
.setColor('#00BFFF')
.setTitle('📊 Nouvelle synchronisation')
.setDescription(`Un utilisateur a ${actionText} son profil Clash Royale`)
.addFields([
{
name: 'Utilisateur Discord',
value: `${interaction.user.tag} (<@${interaction.user.id}>)`,
inline: true
},
{
name: 'Joueur Clash Royale',
value: `${playerData.name} (\`${playerData.tag}\`)`,
inline: true
},
{
name: 'Trophées',
value: `${playerData.trophies.toLocaleString()}`,
inline: true
}
])
.setTimestamp();
await logChannel.send({ embeds: [logEmbed] });
}
} catch (error) {
client.logger.warning('Impossible d\'envoyer le log de synchronisation');
}
} catch (error) {
client.logger.error(`Erreur lors de la synchronisation: ${error.message}`);
const errorEmbed = new EmbedBuilder()
.setColor('#FF0000')
.setTitle('❌ Erreur interne')
.setDescription('Une erreur s\'est produite lors de la synchronisation. Veuillez réessayer.')
.addFields([
{
name: 'Code d\'erreur',
value: `\`${error.message}\``,
inline: false
}
])
.setTimestamp();
await interaction.editReply({ embeds: [errorEmbed] });
}
}
};

View File

@ -0,0 +1,68 @@
const { EmbedBuilder } = require('discord.js');
module.exports = {
name: 'interactionCreate',
async execute(interaction, client) {
// Gestion des commandes slash
if (interaction.isChatInputCommand()) {
const command = client.commands.get(interaction.commandName);
if (!command) {
client.logger.warning(`Commande inexistante: ${interaction.commandName}`);
return;
}
try {
client.logger.command(`Commande exécutée: /${interaction.commandName} par ${interaction.user.tag}`);
await command.execute(interaction, client);
} catch (error) {
client.logger.error(`Erreur lors de l'exécution de /${interaction.commandName}: ${error.message}`);
const errorEmbed = new EmbedBuilder()
.setColor('#FF0000')
.setTitle('❌ Erreur')
.setDescription('Une erreur s\'est produite lors de l\'exécution de cette commande.')
.setTimestamp();
const replyOptions = { embeds: [errorEmbed], ephemeral: true };
if (interaction.replied || interaction.deferred) {
await interaction.followUp(replyOptions);
} else {
await interaction.reply(replyOptions);
}
}
}
// Gestion des composants (boutons, menus)
else if (interaction.isButton() || interaction.isStringSelectMenu()) {
const component = client.components.get(interaction.customId);
if (!component) {
client.logger.warning(`Composant inexistant: ${interaction.customId}`);
return;
}
try {
client.logger.system(`Composant utilisé: ${interaction.customId} par ${interaction.user.tag}`);
await component.execute(interaction, client);
} catch (error) {
client.logger.error(`Erreur lors de l'exécution du composant ${interaction.customId}: ${error.message}`);
const errorEmbed = new EmbedBuilder()
.setColor('#FF0000')
.setTitle('❌ Erreur')
.setDescription('Une erreur s\'est produite lors du traitement de votre interaction.')
.setTimestamp();
const replyOptions = { embeds: [errorEmbed], ephemeral: true };
if (interaction.replied || interaction.deferred) {
await interaction.followUp(replyOptions);
} else {
await interaction.reply(replyOptions);
}
}
}
}
};

52
src/events/ready.js Normal file
View File

@ -0,0 +1,52 @@
module.exports = {
name: 'ready',
once: true,
async execute(client) {
client.logger.discord(`Bot connecté en tant que ${client.user.tag}`);
const guild = client.guilds.cache.get(process.env.GUILD_ID);
if (guild) {
client.logger.discord(`Serveur principal: ${guild.name} (${guild.memberCount} membres)`);
}
// Configuration du statut du bot
client.user.setActivity('⚔️ Clash Royale Rankings', { type: 'Watching' });
// Démarrer le système de classement automatique du clan
try {
const { startAutoUpdate } = require('../services/clanRankingService');
startAutoUpdate(client);
client.logger.success('Système de classement clan démarré (toutes les 5min)');
} catch (error) {
client.logger.error(`Erreur lors du démarrage du classement clan: ${error.message}`);
}
// Afficher les informations du clan configuré
if (process.env.CLAN_TAG) {
try {
const { clashRoyaleAPI } = require('../services/clashRoyaleService');
const clanResult = await clashRoyaleAPI.getClan("#" + process.env.CLAN_TAG);
if (clanResult.success) {
const clan = clanResult.data;
client.logger.clash(`Clan configuré: ${clan.name} (${clan.tag}) - ${clan.members}/50 membres`);
} else {
client.logger.warning(`Impossible de récupérer les infos du clan ${"#" + process.env.CLAN_TAG}: ${clanResult.error.message}`);
}
} catch (error) {
client.logger.error(`Erreur lors de la vérification du clan: ${error.message}`);
}
} else {
client.logger.warning('CLAN_TAG non configuré - le classement clan ne fonctionnera pas');
}
client.logger.success('🎉 Bot entièrement opérationnel !');
console.log();
client.logger.info('Fonctionnalités actives:');
client.logger.info(' • Classement automatique du clan toutes les 5min');
client.logger.info(' • Commandes slash Clash Royale');
client.logger.info(' • Félicitations pour les trophées');
client.logger.info(' • Synchronisation des profils');
console.log();
}
};

View File

@ -0,0 +1,55 @@
const { REST, Routes } = require('discord.js');
const { readdirSync } = require('fs');
const { join } = require('path');
module.exports = async (client) => {
const commandFiles = readdirSync(join(__dirname, '..', 'commands')).filter(file => file.endsWith('.js'));
const commands = [];
client.logger.command('Chargement des commandes slash...');
for (const file of commandFiles) {
try {
const command = require(join(__dirname, '..', 'commands', file));
if (!command.data || !command.execute) {
client.logger.warning(`Commande ${file} invalide - propriétés data/execute manquantes`);
continue;
}
client.commands.set(command.data.name, command);
commands.push(command.data.toJSON());
client.logger.success(`Commande chargée: /${command.data.name}`);
} catch (error) {
client.logger.error(`Erreur lors du chargement de ${file}: ${error.message}`);
}
}
// Enregistrement des commandes slash auprès de Discord
if (commands.length > 0) {
try {
const rest = new REST({ version: '10' }).setToken(process.env.DISCORD_TOKEN);
client.logger.system('Enregistrement des commandes slash auprès de Discord...');
if (process.env.DEVELOPMENT_MODE === 'true') {
// Mode développement - commandes pour le serveur spécifique uniquement
await rest.put(
Routes.applicationGuildCommands(process.env.DISCORD_CLIENT_ID, process.env.GUILD_ID),
{ body: commands }
);
client.logger.success(`${commands.length} commandes enregistrées en mode développement`);
} else {
// Mode production - commandes globales
await rest.put(
Routes.applicationCommands(process.env.DISCORD_CLIENT_ID),
{ body: commands }
);
client.logger.success(`${commands.length} commandes enregistrées globalement`);
}
} catch (error) {
client.logger.error(`Erreur lors de l'enregistrement des commandes: ${error.message}`);
}
}
};

View File

@ -0,0 +1,31 @@
const { readdirSync } = require('fs');
const { join } = require('path');
module.exports = async (client) => {
const componentDirs = readdirSync(join(__dirname, '..', 'components'), { withFileTypes: true })
.filter(dirent => dirent.isDirectory())
.map(dirent => dirent.name);
client.logger.system('Chargement des composants...');
for (const dir of componentDirs) {
const componentFiles = readdirSync(join(__dirname, '..', 'components', dir))
.filter(file => file.endsWith('.js'));
for (const file of componentFiles) {
try {
const component = require(join(__dirname, '..', 'components', dir, file));
if (!component.customId || !component.execute) {
client.logger.warning(`Composant ${file} invalide - propriétés customId/execute manquantes`);
continue;
}
client.components.set(component.customId, component);
client.logger.success(`Composant ${dir} chargé: ${component.customId}`);
} catch (error) {
client.logger.error(`Erreur lors du chargement du composant ${file}: ${error.message}`);
}
}
}
};

View File

@ -0,0 +1,29 @@
const { readdirSync } = require('fs');
const { join } = require('path');
module.exports = async (client) => {
const eventFiles = readdirSync(join(__dirname, '..', 'events')).filter(file => file.endsWith('.js'));
client.logger.event('Chargement des événements...');
for (const file of eventFiles) {
try {
const event = require(join(__dirname, '..', 'events', file));
if (!event.name || !event.execute) {
client.logger.warning(`Événement ${file} invalide - propriétés name/execute manquantes`);
continue;
}
if (event.once) {
client.once(event.name, (...args) => event.execute(...args, client));
} else {
client.on(event.name, (...args) => event.execute(...args, client));
}
client.logger.success(`Événement chargé: ${event.name}`);
} catch (error) {
client.logger.error(`Erreur lors du chargement de l'événement ${file}: ${error.message}`);
}
}
};

View File

@ -0,0 +1,311 @@
const { EmbedBuilder } = require('discord.js');
const { clashRoyaleAPI } = require('./clashRoyaleService');
const { SettingsService } = require('../utils/database');
class ClanRankingService {
constructor() {
this.isUpdating = false;
this.lastUpdate = null;
this.clanTag = '#' + process.env.CLAN_TAG;
this.updateInterval = null;
}
// Démarrer les mises à jour automatiques toutes les 5 minutes
startAutoUpdate(client) {
if (this.updateInterval) {
clearInterval(this.updateInterval);
}
// Première mise à jour immédiate
this.updateClanRanking(client);
// Puis toutes les 5 minutes
this.updateInterval = setInterval(async () => {
await this.updateClanRanking(client);
}, 5 * 60 * 1000); // 5 minutes
client.logger.system('Mise à jour automatique du classement clan démarrée (5min)');
}
// Arrêter les mises à jour automatiques
stopAutoUpdate() {
if (this.updateInterval) {
clearInterval(this.updateInterval);
this.updateInterval = null;
}
}
// Mettre à jour le classement du clan dans le salon dédié
async updateClanRanking(client) {
if (this.isUpdating) {
client.logger.warning('Mise à jour du classement clan déjà en cours');
return;
}
if (!this.clanTag) {
client.logger.error('CLAN_TAG non configuré dans .env');
return;
}
this.isUpdating = true;
try {
client.logger.clash(`Mise à jour du classement clan ${this.clanTag}...`);
const rankingChannelId = process.env.RANKING_CHANNEL_ID;
if (!rankingChannelId) {
throw new Error('RANKING_CHANNEL_ID non configuré');
}
const channel = await client.channels.fetch(rankingChannelId);
if (!channel) {
throw new Error('Canal de classement non trouvé');
}
// Récupérer les membres du clan via l'API
const clanResult = await clashRoyaleAPI.getClanMembers(this.clanTag);
if (!clanResult.success) {
client.logger.error(`Erreur API clan: ${clanResult.error.message}`);
return;
}
const clanMembers = clanResult.data.items || [];
if (clanMembers.length === 0) {
const noMembersEmbed = new EmbedBuilder()
.setColor('#FFA500')
.setTitle('🏰 Classement du Clan')
.setDescription(`Aucun membre trouvé dans le clan \`${this.clanTag}\``)
.setTimestamp();
await this.sendOrUpdateRanking(channel, noMembersEmbed, client);
return;
}
// Récupérer les infos du clan
const clanInfoResult = await clashRoyaleAPI.getClan(this.clanTag);
const clanInfo = clanInfoResult.success ? clanInfoResult.data : null;
client.logger.success(`${clanMembers.length} membres trouvés dans le clan`);
// Trier les membres par trophées (décroissant)
clanMembers.sort((a, b) => b.trophies - a.trophies);
// Prendre les 10 premiers
const topMembers = clanMembers.slice(0, 10);
// Créer l'embed du classement
const rankingEmbed = await this.createClanRankingEmbed(topMembers, clanInfo, clanMembers.length, client);
// Envoyer ou mettre à jour le message
await this.sendOrUpdateRanking(channel, rankingEmbed, client);
// Mettre à jour les statistiques
await SettingsService.set('last_clan_ranking_update', Date.now().toString());
this.lastUpdate = new Date();
client.logger.success('Classement clan mis à jour avec succès');
} catch (error) {
client.logger.error(`Erreur lors de la mise à jour du classement clan: ${error.message}`);
} finally {
this.isUpdating = false;
}
}
// Créer l'embed du classement du clan
async createClanRankingEmbed(topMembers, clanInfo, totalMembers, client) {
const embed = new EmbedBuilder()
.setColor('#FFD700')
.setTitle('🏰 Classement du Clan')
.setThumbnail(clanInfo?.badgeUrls?.large || null);
// Description avec infos du clan
let description = '';
if (clanInfo) {
description = `**${clanInfo.name}** \`${clanInfo.tag}\`\n`;
description += `👥 **${totalMembers}/50** membres • `;
description += `🏆 **${clanInfo.clanScore.toLocaleString()}** score clan\n`;
description += `🎯 **${clanInfo.requiredTrophies.toLocaleString()}** trophées requis\n\n`;
}
embed.setDescription(description + '**🏆 Top 10 du Clan**');
if (topMembers.length === 0) {
embed.addFields([{
name: 'Aucun membre',
value: 'Le clan semble vide.',
inline: false
}]);
return embed;
}
// Ajouter les 3 premiers avec des médailles spéciales
const medals = ['🥇', '🥈', '🥉'];
for (let i = 0; i < Math.min(3, topMembers.length); i++) {
const member = topMembers[i];
const roleEmoji = this.getRoleEmoji(member.role);
embed.addFields([{
name: `${medals[i]} ${i + 1}. ${member.name}`,
value: `${roleEmoji} **${member.role}**\n🏆 **${member.trophies.toLocaleString()}** trophées\n💎 Niveau ${member.expLevel}`,
inline: i < 2 ? true : false
}]);
}
// Ajouter le reste du top 10 dans un seul champ
if (topMembers.length > 3) {
let rankingText = '';
for (let i = 3; i < topMembers.length; i++) {
const member = topMembers[i];
const roleEmoji = this.getRoleEmoji(member.role);
const trophyIcon = this.getTrophyIcon(member.trophies);
rankingText += `**${i + 1}.** ${trophyIcon} ${member.name} ${roleEmoji} • **${member.trophies.toLocaleString()}**\n`;
}
embed.addFields([{
name: '🏅 Reste du classement',
value: rankingText || 'Aucun autre membre',
inline: false
}]);
}
// Statistiques du clan
const totalTrophies = topMembers.reduce((sum, m) => sum + m.trophies, 0);
const averageTrophies = Math.round(totalTrophies / topMembers.length);
const highestTrophies = Math.max(...topMembers.map(m => m.trophies));
embed.addFields([{
name: '📊 Statistiques Top 10',
value: `🏆 **${totalTrophies.toLocaleString()}** trophées total\n📈 **${averageTrophies.toLocaleString()}** moyenne\n🎯 **${highestTrophies.toLocaleString()}** meilleur score`,
inline: false
}]);
embed.setFooter({
text: `Mise à jour automatique toutes les 5min • ${new Date().toLocaleTimeString('fr-FR')}`,
iconURL: client.user.displayAvatarURL()
});
embed.setTimestamp();
return embed;
}
// Obtenir l'emoji correspondant au rôle dans le clan
getRoleEmoji(role) {
switch (role.toLowerCase()) {
case 'leader': return '👑';
case 'coleader': return '🔱';
case 'elder': return '⭐';
case 'member': return '🛡️';
default: return '👤';
}
}
// Obtenir l'icône correspondant au niveau de trophées
getTrophyIcon(trophies) {
if (trophies >= 8000) return '👑';
if (trophies >= 7000) return '💎';
if (trophies >= 6000) return '🏆';
if (trophies >= 5000) return '🥇';
if (trophies >= 4000) return '🥈';
if (trophies >= 3000) return '🥉';
if (trophies >= 2000) return '🏅';
if (trophies >= 1000) return '⭐';
return '🔰';
}
// Envoyer ou mettre à jour le message de classement
async sendOrUpdateRanking(channel, embed, client) {
try {
// Chercher un message existant du bot dans les 50 derniers messages
const messages = await channel.messages.fetch({ limit: 50 });
const botMessage = messages.find(msg =>
msg.author.id === client.user.id &&
msg.embeds.length > 0 &&
(msg.embeds[0].title?.includes('Classement') || msg.embeds[0].title?.includes('Clan'))
);
if (botMessage) {
// Mettre à jour le message existant
await botMessage.edit({ embeds: [embed] });
client.logger.system('Message de classement mis à jour');
} else {
// Envoyer un nouveau message
await channel.send({ embeds: [embed] });
client.logger.system('Nouveau message de classement envoyé');
}
} catch (error) {
client.logger.error(`Erreur lors de l'envoi du classement: ${error.message}`);
// Si la mise à jour échoue, essayer d'envoyer un nouveau message
try {
await channel.send({ embeds: [embed] });
} catch (retryError) {
client.logger.error(`Erreur lors du retry: ${retryError.message}`);
}
}
}
// Obtenir le classement complet du clan
async getFullClanRanking() {
if (!this.clanTag) {
return { success: false, error: 'Clan tag not configured' };
}
const result = await clashRoyaleAPI.getClanMembers(this.clanTag);
if (!result.success) {
return result;
}
const members = result.data.items || [];
members.sort((a, b) => b.trophies - a.trophies);
return {
success: true,
data: members.map((member, index) => ({
...member,
rank: index + 1
}))
};
}
// Obtenir la position d'un membre spécifique
async getMemberRank(playerTag) {
const fullRanking = await this.getFullClanRanking();
if (!fullRanking.success) {
return null;
}
const memberIndex = fullRanking.data.findIndex(member =>
member.tag.toUpperCase() === playerTag.toUpperCase()
);
if (memberIndex === -1) {
return null;
}
return {
rank: memberIndex + 1,
total: fullRanking.data.length,
member: fullRanking.data[memberIndex],
above: fullRanking.data[memberIndex - 1] || null,
below: fullRanking.data[memberIndex + 1] || null
};
}
}
// Instance singleton
const clanRankingService = new ClanRankingService();
module.exports = {
ClanRankingService,
clanRankingService,
updateClanRanking: (client) => clanRankingService.updateClanRanking(client),
startAutoUpdate: (client) => clanRankingService.startAutoUpdate(client)
};

View File

@ -0,0 +1,315 @@
const axios = require('axios');
const chalk = require('chalk');
class ClashRoyaleAPI {
constructor() {
this.baseURL = 'https://api.clashroyale.com/v1';
this.token = process.env.CLASH_ROYALE_TOKEN;
if (!this.token) {
throw new Error('CLASH_ROYALE_TOKEN manquant dans les variables d\'environnement');
}
this.client = axios.create({
baseURL: this.baseURL,
headers: {
'Authorization': `Bearer ${this.token}`,
'Accept': 'application/json'
},
timeout: 10000
});
// Intercepteur pour logger les requêtes
this.client.interceptors.request.use(
(config) => {
console.log(chalk.hex('#FFD700')('🌐 API'), `Requête vers: ${config.url}`);
return config;
},
(error) => {
console.error(chalk.red('❌'), 'Erreur requête API:', error.message);
return Promise.reject(error);
}
);
this.client.interceptors.response.use(
(response) => {
console.log(chalk.hex('#FFD700')('✅ API'), `Réponse reçue: ${response.status}`);
return response;
},
(error) => {
const status = error.response?.status;
const message = error.response?.data?.message || error.message;
console.error(chalk.red('❌ API'), `Erreur ${status}: ${message}`);
return Promise.reject(error);
}
);
}
// Normaliser un tag Clash Royale (ajouter # si nécessaire)
normalizeTag(tag) {
if (!tag) return null;
tag = tag.trim().toUpperCase();
return tag.startsWith('#') ? tag : `#${tag}`;
}
// Encoder un tag pour l'URL
encodeTag(tag) {
return encodeURIComponent(tag);
}
// Obtenir les informations d'un joueur
async getPlayer(playerTag) {
try {
const normalizedTag = this.normalizeTag(playerTag);
const encodedTag = this.encodeTag(normalizedTag);
const response = await this.client.get(`/players/${encodedTag}`);
return {
success: true,
data: response.data
};
} catch (error) {
return {
success: false,
error: this.handleError(error),
data: null
};
}
}
// Obtenir les informations d'un clan
async getClan(clanTag) {
try {
const normalizedTag = this.normalizeTag(clanTag);
const encodedTag = this.encodeTag(normalizedTag);
const response = await this.client.get(`/clans/${encodedTag}`);
return {
success: true,
data: response.data
};
} catch (error) {
return {
success: false,
error: this.handleError(error),
data: null
};
}
}
// Obtenir les membres d'un clan
async getClanMembers(clanTag) {
try {
const normalizedTag = this.normalizeTag(clanTag);
const encodedTag = this.encodeTag(normalizedTag);
const response = await this.client.get(`/clans/${encodedTag}/members`);
return {
success: true,
data: response.data
};
} catch (error) {
return {
success: false,
error: this.handleError(error),
data: null
};
}
}
// Rechercher des clans par nom
async searchClans(name, limit = 10) {
try {
const response = await this.client.get('/clans', {
params: {
name: name,
limit: limit
}
});
return {
success: true,
data: response.data
};
} catch (error) {
return {
success: false,
error: this.handleError(error),
data: null
};
}
}
// Obtenir le classement global des joueurs
async getGlobalPlayerRanking(limit = 200) {
try {
const response = await this.client.get('/locations/global/rankings/players', {
params: { limit }
});
return {
success: true,
data: response.data
};
} catch (error) {
return {
success: false,
error: this.handleError(error),
data: null
};
}
}
// Obtenir le classement global des clans
async getGlobalClanRanking(limit = 200) {
try {
const response = await this.client.get('/locations/global/rankings/clans', {
params: { limit }
});
return {
success: true,
data: response.data
};
} catch (error) {
return {
success: false,
error: this.handleError(error),
data: null
};
}
}
// Vérifier si un joueur existe et obtenir ses informations de base
async validatePlayer(playerTag) {
const result = await this.getPlayer(playerTag);
if (!result.success) {
return {
valid: false,
error: result.error,
data: null
};
}
const player = result.data;
return {
valid: true,
error: null,
data: {
tag: player.tag,
name: player.name,
trophies: player.trophies,
bestTrophies: player.bestTrophies,
expLevel: player.expLevel,
clan: player.clan ? {
tag: player.clan.tag,
name: player.clan.name,
role: player.role
} : null,
arena: player.arena,
wins: player.wins,
losses: player.losses,
battleCount: player.battleCount,
threeCrownWins: player.threeCrownWins
}
};
}
// Obtenir les statistiques détaillées d'un joueur
async getPlayerStats(playerTag) {
const result = await this.getPlayer(playerTag);
if (!result.success) {
return result;
}
const player = result.data;
return {
success: true,
data: {
basic: {
tag: player.tag,
name: player.name,
trophies: player.trophies,
bestTrophies: player.bestTrophies,
level: player.expLevel
},
clan: player.clan ? {
tag: player.clan.tag,
name: player.clan.name,
role: player.role,
donations: player.donations,
donationsReceived: player.donationsReceived
} : null,
stats: {
wins: player.wins,
losses: player.losses,
battleCount: player.battleCount,
threeCrownWins: player.threeCrownWins,
winRate: player.battleCount > 0 ? ((player.wins / player.battleCount) * 100).toFixed(2) : 0
},
arena: player.arena,
leagueStatistics: player.leagueStatistics || null
}
};
}
// Gérer les erreurs de l'API
handleError(error) {
if (!error.response) {
return {
type: 'NETWORK_ERROR',
message: 'Erreur de connexion à l\'API Clash Royale',
details: error.message
};
}
const status = error.response.status;
const data = error.response.data;
switch (status) {
case 400:
return {
type: 'BAD_REQUEST',
message: 'Paramètres de requête invalides',
details: data?.message || 'Vérifiez le format du tag'
};
case 403:
return {
type: 'FORBIDDEN',
message: 'Token API invalide ou accès refusé',
details: data?.message || 'Vérifiez votre token API'
};
case 404:
return {
type: 'NOT_FOUND',
message: 'Joueur ou clan non trouvé',
details: 'Vérifiez que le tag est correct'
};
case 429:
return {
type: 'RATE_LIMITED',
message: 'Limite de requêtes atteinte',
details: 'Réessayez dans quelques minutes'
};
case 503:
return {
type: 'SERVICE_UNAVAILABLE',
message: 'API Clash Royale temporairement indisponible',
details: 'Maintenance en cours'
};
default:
return {
type: 'UNKNOWN_ERROR',
message: `Erreur API inconnue (${status})`,
details: data?.message || error.message
};
}
}
}
// Instance singleton
const clashRoyaleAPI = new ClashRoyaleAPI();
module.exports = {
ClashRoyaleAPI,
clashRoyaleAPI
};

View File

@ -0,0 +1,260 @@
const { EmbedBuilder, AttachmentBuilder } = require('discord.js');
const { UserProfileService, SettingsService } = require('../utils/database');
const { clashRoyaleAPI } = require('./clashRoyaleService');
class RankingService {
constructor() {
this.isUpdating = false;
this.lastUpdate = null;
}
// Mettre à jour le classement dans le salon dédié
async updateRanking(client) {
if (this.isUpdating) {
client.logger.warning('Mise à jour du classement déjà en cours');
return;
}
this.isUpdating = true;
try {
client.logger.system('Début de la mise à jour du classement...');
const rankingChannelId = process.env.RANKING_CHANNEL_ID;
if (!rankingChannelId) {
throw new Error('RANKING_CHANNEL_ID non configuré');
}
const channel = await client.channels.fetch(rankingChannelId);
if (!channel) {
throw new Error('Canal de classement non trouvé');
}
// Récupérer tous les profils synchronisés
const profiles = await UserProfileService.getAllVerified();
if (profiles.length === 0) {
const noPlayersEmbed = new EmbedBuilder()
.setColor('#FFA500')
.setTitle('🏆 Classement du Serveur')
.setDescription('Aucun joueur synchronisé pour le moment.\n\nUtilisez `/sync` pour associer votre profil Clash Royale !')
.setTimestamp();
await this.sendOrUpdateRanking(channel, noPlayersEmbed);
return;
}
client.logger.info(`Mise à jour de ${profiles.length} profils...`);
// Mettre à jour les trophées de tous les joueurs
const updatedProfiles = [];
let updateCount = 0;
for (const profile of profiles) {
try {
const playerResult = await clashRoyaleAPI.getPlayer(profile.clash_tag);
if (playerResult.success) {
const currentTrophies = playerResult.data.trophies;
const oldTrophies = profile.current_trophies;
// Mettre à jour si les trophées ont changé
if (currentTrophies !== oldTrophies) {
await UserProfileService.updateTrophies(profile.discord_id, currentTrophies);
updateCount++;
client.logger.info(`Trophées mis à jour: ${profile.clash_name} ${oldTrophies} -> ${currentTrophies}`);
}
updatedProfiles.push({
...profile,
current_trophies: currentTrophies,
clash_name: playerResult.data.name,
level: playerResult.data.expLevel,
clan_name: playerResult.data.clan?.name || null,
clan_role: playerResult.data.clan?.role || null
});
} else {
// Si l'API échoue, garder les données existantes
updatedProfiles.push(profile);
client.logger.warning(`Impossible de mettre à jour ${profile.clash_name}: ${playerResult.error.message}`);
}
// Petit délai pour éviter les rate limits
await new Promise(resolve => setTimeout(resolve, 100));
} catch (error) {
updatedProfiles.push(profile);
client.logger.error(`Erreur lors de la mise à jour de ${profile.clash_name}: ${error.message}`);
}
}
client.logger.success(`${updateCount} profils mis à jour`);
// Trier par trophées décroissant
updatedProfiles.sort((a, b) => b.current_trophies - a.current_trophies);
// Créer l'embed du classement
const rankingEmbed = await this.createRankingEmbed(updatedProfiles, client);
// Envoyer ou mettre à jour le message
await this.sendOrUpdateRanking(channel, rankingEmbed);
// Mettre à jour les statistiques
await SettingsService.set('last_ranking_update', Date.now().toString());
await SettingsService.set('total_users_synced', updatedProfiles.length.toString());
this.lastUpdate = new Date();
client.logger.success('Classement mis à jour avec succès');
} catch (error) {
client.logger.error(`Erreur lors de la mise à jour du classement: ${error.message}`);
throw error;
} finally {
this.isUpdating = false;
}
}
// Créer l'embed du classement
async createRankingEmbed(profiles, client) {
const topProfiles = profiles.slice(0, 10);
const guildId = process.env.GUILD_ID;
const guild = await client.guilds.fetch(guildId);
const embed = new EmbedBuilder()
.setColor('#FFD700')
.setTitle('🏆 Classement Top 10 du Serveur')
.setDescription(`**${guild.name}** • Dernière mise à jour`)
.setThumbnail(guild.iconURL({ dynamic: true }));
if (topProfiles.length === 0) {
embed.setDescription('Aucun joueur dans le classement');
return embed;
}
// Ajouter les 3 premiers avec des médailles spéciales
const podiumFields = [];
const medals = ['🥇', '🥈', '🥉'];
for (let i = 0; i < Math.min(3, topProfiles.length); i++) {
const profile = topProfiles[i];
const member = guild.members.cache.get(profile.discord_id);
const displayName = member?.displayName || 'Utilisateur introuvable';
podiumFields.push({
name: `${medals[i]} ${i + 1}. ${profile.clash_name}`,
value: `👤 ${displayName}\n🏆 **${profile.current_trophies.toLocaleString()}** trophées\n${profile.clan_name ? `🏰 ${profile.clan_name}` : ''}`,
inline: i === 2 ? false : true
});
}
embed.addFields(podiumFields);
// Ajouter le reste du top 10 dans un seul champ
if (topProfiles.length > 3) {
let rankingText = '';
for (let i = 3; i < topProfiles.length; i++) {
const profile = topProfiles[i];
const member = guild.members.cache.get(profile.discord_id);
const displayName = member?.displayName || 'Utilisateur introuvable';
const trophyIcon = this.getTrophyIcon(profile.current_trophies);
rankingText += `**${i + 1}.** ${trophyIcon} ${profile.clash_name} • **${profile.current_trophies.toLocaleString()}**\n`;
}
embed.addFields([{
name: '🏅 Reste du classement',
value: rankingText || 'Aucun autre joueur',
inline: false
}]);
}
// Statistiques globales
const totalTrophies = profiles.reduce((sum, p) => sum + p.current_trophies, 0);
const averageTrophies = Math.round(totalTrophies / profiles.length);
embed.addFields([{
name: '📊 Statistiques du serveur',
value: `👥 **${profiles.length}** joueurs synchronisés\n🏆 **${totalTrophies.toLocaleString()}** trophées au total\n📈 **${averageTrophies.toLocaleString()}** trophées en moyenne`,
inline: false
}]);
embed.setFooter({
text: `Mise à jour automatique • Prochain update dans 1h • /sync pour rejoindre`,
iconURL: client.user.displayAvatarURL()
});
embed.setTimestamp();
return embed;
}
// Obtenir l'icône correspondant au niveau de trophées
getTrophyIcon(trophies) {
if (trophies >= 8000) return '👑';
if (trophies >= 7000) return '💎';
if (trophies >= 6000) return '🏆';
if (trophies >= 5000) return '🥇';
if (trophies >= 4000) return '🥈';
if (trophies >= 3000) return '🥉';
if (trophies >= 2000) return '🏅';
if (trophies >= 1000) return '⭐';
return '🔰';
}
// Envoyer ou mettre à jour le message de classement
async sendOrUpdateRanking(channel, embed) {
try {
// Chercher un message existant du bot dans les 50 derniers messages
const messages = await channel.messages.fetch({ limit: 50 });
const botMessage = messages.find(msg =>
msg.author.id === channel.client.user.id &&
msg.embeds.length > 0 &&
msg.embeds[0].title?.includes('Classement')
);
if (botMessage) {
// Mettre à jour le message existant
await botMessage.edit({ embeds: [embed] });
} else {
// Envoyer un nouveau message
await channel.send({ embeds: [embed] });
}
} catch (error) {
// Si la mise à jour échoue, envoyer un nouveau message
await channel.send({ embeds: [embed] });
}
}
// Obtenir le classement d'un utilisateur spécifique
async getUserRank(discordId) {
const profiles = await UserProfileService.getAllVerified();
profiles.sort((a, b) => b.current_trophies - a.current_trophies);
const userIndex = profiles.findIndex(p => p.discord_id === discordId);
if (userIndex === -1) return null;
return {
rank: userIndex + 1,
total: profiles.length,
profile: profiles[userIndex],
above: profiles[userIndex - 1] || null,
below: profiles[userIndex + 1] || null
};
}
// Obtenir le top N
async getTopPlayers(limit = 10) {
const profiles = await UserProfileService.getAllVerified();
profiles.sort((a, b) => b.current_trophies - a.current_trophies);
return profiles.slice(0, limit);
}
}
// Instance singleton
const rankingService = new RankingService();
module.exports = {
RankingService,
rankingService,
updateRanking: (client) => rankingService.updateRanking(client)
};

View File

@ -0,0 +1,268 @@
const { EmbedBuilder } = require('discord.js');
const { TrophyHistoryService, UserProfileService } = require('../utils/database');
class TrophyService {
constructor() {
this.isChecking = false;
}
// Vérifier les paliers de trophées et envoyer des félicitations
async checkTrophyMilestones(client) {
if (this.isChecking) {
client.logger.warning('Vérification des trophées déjà en cours');
return;
}
this.isChecking = true;
try {
client.logger.trophy('Vérification des paliers de trophées...');
// Récupérer les paliers non félicités
const milestones = await TrophyHistoryService.getUncongratedMilestones();
if (milestones.length === 0) {
client.logger.trophy('Aucun nouveau palier à féliciter');
return;
}
const congratsChannelId = process.env.CONGRATULATIONS_CHANNEL_ID;
if (!congratsChannelId) {
client.logger.warning('Canal de félicitations non configuré');
return;
}
const channel = await client.channels.fetch(congratsChannelId);
if (!channel) {
client.logger.error('Canal de félicitations non trouvé');
return;
}
client.logger.trophy(`${milestones.length} paliers à féliciter`);
for (const milestone of milestones) {
try {
await this.sendCongratulations(channel, milestone, client);
await TrophyHistoryService.markAsCongratulated(milestone.id);
client.logger.success(`Félicitations envoyées pour ${milestone.clash_name} (${milestone.milestone_reached} trophées)`);
// Petit délai entre les messages
await new Promise(resolve => setTimeout(resolve, 1000));
} catch (error) {
client.logger.error(`Erreur lors de l'envoi des félicitations pour ${milestone.clash_name}: ${error.message}`);
}
}
} catch (error) {
client.logger.error(`Erreur lors de la vérification des trophées: ${error.message}`);
throw error;
} finally {
this.isChecking = false;
}
}
// Envoyer un message de félicitations
async sendCongratulations(channel, milestone, client) {
const { discord_id, clash_name, milestone_reached, trophy_gain, new_trophies } = milestone;
const guild = channel.guild;
const member = await guild.members.fetch(discord_id).catch(() => null);
if (!member) {
client.logger.warning(`Membre Discord non trouvé pour ${clash_name}`);
return;
}
// Déterminer le type de félicitations selon le palier
const congratsData = this.getCongratulationsData(milestone_reached, new_trophies);
const embed = new EmbedBuilder()
.setColor(congratsData.color)
.setTitle(congratsData.title)
.setDescription(congratsData.description)
.setThumbnail(member.displayAvatarURL({ dynamic: true, size: 128 }))
.addFields([
{
name: '👤 Joueur',
value: `**${clash_name}**\n<@${discord_id}>`,
inline: true
},
{
name: '🏆 Nouveau palier',
value: `**${milestone_reached.toLocaleString()}** trophées`,
inline: true
},
{
name: '📈 Gain récent',
value: `+${trophy_gain.toLocaleString()} trophées`,
inline: true
}
])
.addFields([
{
name: `${congratsData.emoji} Message`,
value: congratsData.message,
inline: false
}
])
.setFooter({
text: `Continuez comme ça ! • Clash Royale Bot`,
iconURL: client.user.displayAvatarURL()
})
.setTimestamp();
// Ajouter des réactions selon le niveau
const message = await channel.send({
content: `🎉 <@${discord_id}>`,
embeds: [embed]
});
// Ajouter des réactions automatiques
const reactions = this.getReactionsForMilestone(milestone_reached);
for (const reaction of reactions) {
try {
await message.react(reaction);
await new Promise(resolve => setTimeout(resolve, 300));
} catch (error) {
// Ignorer les erreurs de réactions
}
}
}
// Obtenir les données de félicitations selon le palier
getCongratulationsData(milestone, currentTrophies) {
const data = {
1000: {
title: '🎉 Premier Palier Atteint !',
description: '**Félicitations pour vos 1000 trophées !**',
message: 'Excellente progression ! Vous entrez dans la cour des grands. Continuez sur cette lancée !',
color: '#00FF00',
emoji: '🔰'
},
2000: {
title: '⭐ 2000 Trophées !',
description: '**Bravo, vous montez en grade !**',
message: 'Votre détermination paie ! Vous progressez rapidement vers les ligues supérieures.',
color: '#FFD700',
emoji: '⭐'
},
3000: {
title: '🥉 3000 Trophées !',
description: '**Niveau bronze atteint !**',
message: 'Impressionnant ! Vous développez vraiment votre jeu et ça se voit !',
color: '#CD7F32',
emoji: '🥉'
},
4000: {
title: '🥈 4000 Trophées !',
description: '**Niveau argent débloqué !**',
message: 'Fantastique ! Vous faites partie des joueurs sérieux maintenant. Respect ! 💪',
color: '#C0C0C0',
emoji: '🥈'
},
5000: {
title: '🥇 5000 Trophées !',
description: '**Niveau or atteint !**',
message: 'INCROYABLE ! 🔥 Vous êtes maintenant dans l\'élite du serveur ! Quel talent !',
color: '#FFD700',
emoji: '🥇'
},
6000: {
title: '🏆 6000 Trophées !',
description: '**Maître confirmé !**',
message: 'EXTRAORDINAIRE ! 🚀 Peu de joueurs atteignent ce niveau. Vous êtes une légende !',
color: '#FF6B35',
emoji: '🏆'
},
7000: {
title: '💎 7000 Trophées !',
description: '**Rang de diamant !**',
message: 'PHÉNOMÉNAL ! 💎✨ Vous brillez comme un diamant ! Performance absolument époustouflante !',
color: '#00CED1',
emoji: '💎'
},
8000: {
title: '👑 8000 Trophées !',
description: '**CHAMPION SUPRÊME !**',
message: '👑 LÉGENDAIRE ! 👑 Vous régner sur l\'arène ! Une performance digne des plus grands champions ! 🔥🎯',
color: '#9932CC',
emoji: '👑'
}
};
// Si le palier n'est pas défini, créer un message générique
if (!data[milestone]) {
const isHighLevel = milestone >= 9000;
return {
title: `${isHighLevel ? '🌟' : '🎯'} ${milestone.toLocaleString()} Trophées !`,
description: `**${isHighLevel ? 'Niveau cosmique atteint !' : 'Nouveau palier débloqué !'}**`,
message: isHighLevel
? `🌟 COSMIQUE ! 🌟 Vous transcendez le jeu lui-même ! Performance légendaire de ${milestone.toLocaleString()} trophées ! 🚀✨`
: `Bravo pour ce nouveau palier de ${milestone.toLocaleString()} trophées ! Votre progression est remarquable ! 🎯`,
color: isHighLevel ? '#FF1493' : '#32CD32',
emoji: isHighLevel ? '🌟' : '🎯'
};
}
return data[milestone];
}
// Obtenir les réactions à ajouter selon le palier
getReactionsForMilestone(milestone) {
if (milestone >= 8000) return ['👑', '🔥', '⚡', '🌟', '💎'];
if (milestone >= 7000) return ['💎', '✨', '🔥', '⚡'];
if (milestone >= 6000) return ['🏆', '🔥', '💪', '⚡'];
if (milestone >= 5000) return ['🥇', '🔥', '💪'];
if (milestone >= 4000) return ['🥈', '💪', '👏'];
if (milestone >= 3000) return ['🥉', '👏', '🎯'];
if (milestone >= 2000) return ['⭐', '👏'];
return ['🎉', '👏'];
}
// Analyser les gains de trophées récents
async getRecentGainsAnalysis(hours = 24) {
const gains = await TrophyHistoryService.getRecentGains(hours);
const analysis = {
totalPlayers: gains.length,
totalGains: gains.reduce((sum, g) => sum + (g.trophy_gain > 0 ? g.trophy_gain : 0), 0),
totalLosses: Math.abs(gains.reduce((sum, g) => sum + (g.trophy_gain < 0 ? g.trophy_gain : 0), 0)),
biggestGain: gains.length > 0 ? Math.max(...gains.map(g => g.trophy_gain)) : 0,
biggestLoss: gains.length > 0 ? Math.min(...gains.map(g => g.trophy_gain)) : 0,
milestones: gains.filter(g => g.milestone_reached !== null).length
};
return analysis;
}
// Obtenir les stats d'un joueur spécifique
async getPlayerTrophyStats(discordId) {
const profile = await UserProfileService.getByDiscordId(discordId);
if (!profile) return null;
// Obtenir l'historique récent
const recentGains = await TrophyHistoryService.getRecentGains(168); // 7 jours
const playerGains = recentGains.filter(g => g.discord_id === discordId);
const stats = {
currentTrophies: profile.current_trophies,
bestTrophies: profile.highest_trophies,
recentChanges: playerGains.length,
weeklyGain: playerGains.reduce((sum, g) => sum + g.trophy_gain, 0),
biggestDailyGain: playerGains.length > 0 ? Math.max(...playerGains.map(g => g.trophy_gain)) : 0,
milestonesThisWeek: playerGains.filter(g => g.milestone_reached !== null).length
};
return stats;
}
}
// Instance singleton
const trophyService = new TrophyService();
module.exports = {
TrophyService,
trophyService,
checkTrophyMilestones: (client) => trophyService.checkTrophyMilestones(client)
};

382
src/utils/database.js Normal file
View File

@ -0,0 +1,382 @@
const sqlite3 = require('sqlite3').verbose();
const { readFileSync } = require('fs');
const { join } = require('path');
const chalk = require('chalk');
class Database {
constructor() {
this.db = null;
this.isInitialized = false;
}
async init() {
return new Promise((resolve, reject) => {
const dbPath = process.env.DATABASE_PATH || './database/bot.db';
this.db = new sqlite3.Database(dbPath, (err) => {
if (err) {
console.error(chalk.red('❌ Erreur connexion base de données:'), err.message);
reject(err);
return;
}
console.log(chalk.green('✅ Base de données connectée:'), chalk.cyan(dbPath));
this.isInitialized = true;
resolve();
});
// Activer les clés étrangères
this.db.run('PRAGMA foreign_keys = ON');
});
}
async executeSchema() {
if (!this.isInitialized) {
throw new Error('Base de données non initialisée');
}
return new Promise((resolve, reject) => {
try {
// Inline schema to avoid file reading issues - with safe creation
const schema = `
-- Drop views first (they depend on tables)
DROP VIEW IF EXISTS recent_trophy_gains;
DROP VIEW IF EXISTS ranking_view;
-- Drop tables
DROP TABLE IF EXISTS trophy_history;
DROP TABLE IF EXISTS user_profiles;
DROP TABLE IF EXISTS bot_settings;
-- Create tables
CREATE TABLE IF NOT EXISTS user_profiles (
id INTEGER PRIMARY KEY AUTOINCREMENT,
discord_id TEXT UNIQUE NOT NULL,
clash_tag TEXT UNIQUE NOT NULL,
clash_name TEXT NOT NULL,
current_trophies INTEGER DEFAULT 0,
highest_trophies INTEGER DEFAULT 0,
level INTEGER DEFAULT 1,
clan_name TEXT,
clan_tag TEXT,
clan_role TEXT,
verified BOOLEAN DEFAULT 0,
sync_date DATETIME DEFAULT CURRENT_TIMESTAMP,
last_update DATETIME DEFAULT CURRENT_TIMESTAMP,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
);
CREATE TABLE IF NOT EXISTS trophy_history (
id INTEGER PRIMARY KEY AUTOINCREMENT,
user_profile_id INTEGER NOT NULL,
old_trophies INTEGER NOT NULL,
new_trophies INTEGER NOT NULL,
trophy_gain INTEGER NOT NULL,
milestone_reached INTEGER,
congratulated BOOLEAN DEFAULT 0,
recorded_at DATETIME DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (user_profile_id) REFERENCES user_profiles(id) ON DELETE CASCADE
);
CREATE TABLE IF NOT EXISTS bot_settings (
id INTEGER PRIMARY KEY AUTOINCREMENT,
setting_key TEXT UNIQUE NOT NULL,
setting_value TEXT NOT NULL,
description TEXT,
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP
);
-- Create indexes if they don't exist
CREATE INDEX IF NOT EXISTS idx_user_profiles_discord_id ON user_profiles(discord_id);
CREATE INDEX IF NOT EXISTS idx_user_profiles_clash_tag ON user_profiles(clash_tag);
CREATE INDEX IF NOT EXISTS idx_trophy_history_user_profile ON trophy_history(user_profile_id);
CREATE INDEX IF NOT EXISTS idx_trophy_history_recorded_at ON trophy_history(recorded_at);
CREATE INDEX IF NOT EXISTS idx_trophy_history_milestone ON trophy_history(milestone_reached);
CREATE INDEX IF NOT EXISTS idx_bot_settings_key ON bot_settings(setting_key);
-- Insert default settings (ignore if they already exist)
INSERT OR IGNORE INTO bot_settings (setting_key, setting_value, description) VALUES
('last_ranking_update', '0', 'Last ranking update timestamp'),
('total_users_synced', '0', 'Total number of synced users'),
('trophy_check_enabled', 'true', 'Enable trophy milestone checking'),
('ranking_update_enabled', 'true', 'Enable automatic ranking updates'),
('congratulations_enabled', 'true', 'Enable automatic congratulations'),
('bot_version', '1.0.0', 'Current bot version'),
('maintenance_mode', 'false', 'Bot maintenance mode');
-- Create views (will be recreated if dropped above)
CREATE VIEW IF NOT EXISTS ranking_view AS
SELECT
up.discord_id,
up.clash_name,
up.current_trophies,
up.highest_trophies,
up.level,
up.clan_name,
up.clan_role,
ROW_NUMBER() OVER (ORDER BY up.current_trophies DESC) as rank_position
FROM user_profiles up
WHERE up.verified = 1
ORDER BY up.current_trophies DESC;
CREATE VIEW IF NOT EXISTS recent_trophy_gains AS
SELECT
up.discord_id,
up.clash_name,
th.old_trophies,
th.new_trophies,
th.trophy_gain,
th.milestone_reached,
th.congratulated,
th.recorded_at
FROM trophy_history th
JOIN user_profiles up ON th.user_profile_id = up.id
WHERE th.recorded_at >= datetime('now', '-1 day')
AND th.trophy_gain > 0
ORDER BY th.recorded_at DESC;
`;
this.db.exec(schema, (err) => {
if (err) {
console.error(chalk.red('❌ Erreur exécution schéma:'), err.message);
reject(err);
return;
}
console.log(chalk.green('✅ Schéma de base de données appliqué'));
resolve();
});
} catch (error) {
reject(error);
}
});
}
// Méthodes utilitaires pour les requêtes
run(sql, params = []) {
return new Promise((resolve, reject) => {
this.db.run(sql, params, function (err) {
if (err) {
reject(err);
return;
}
resolve({ id: this.lastID, changes: this.changes });
});
});
}
get(sql, params = []) {
return new Promise((resolve, reject) => {
this.db.get(sql, params, (err, row) => {
if (err) {
reject(err);
return;
}
resolve(row);
});
});
}
all(sql, params = []) {
return new Promise((resolve, reject) => {
this.db.all(sql, params, (err, rows) => {
if (err) {
reject(err);
return;
}
resolve(rows);
});
});
}
close() {
return new Promise((resolve, reject) => {
if (!this.db) {
resolve();
return;
}
this.db.close((err) => {
if (err) {
reject(err);
return;
}
console.log(chalk.yellow('⚠️ Connexion base de données fermée'));
resolve();
});
});
}
}
// Instance singleton
const database = new Database();
// Fonction d'initialisation
async function initDB() {
try {
await database.init();
await database.executeSchema();
console.log(chalk.green('🗄️ Base de données entièrement configurée'));
return database;
} catch (error) {
console.error(chalk.red('❌ Erreur initialisation base de données:'), error.message);
throw error;
}
}
// Méthodes spécifiques au bot
const UserProfileService = {
async createOrUpdate(discordId, clashData) {
const existingUser = await database.get(
'SELECT * FROM user_profiles WHERE discord_id = ?',
[discordId]
);
if (existingUser) {
// Mettre à jour l'utilisateur existant
await database.run(`
UPDATE user_profiles SET
clash_tag = ?, clash_name = ?, current_trophies = ?,
highest_trophies = ?, level = ?, clan_name = ?,
clan_tag = ?, clan_role = ?, verified = 1,
last_update = CURRENT_TIMESTAMP
WHERE discord_id = ?
`, [
clashData.tag, clashData.name, clashData.trophies,
clashData.bestTrophies, clashData.expLevel,
clashData.clan?.name || null, clashData.clan?.tag || null,
clashData.clan?.role || null, discordId
]);
return { ...existingUser, ...clashData };
} else {
// Créer un nouveau profil
const result = await database.run(`
INSERT INTO user_profiles
(discord_id, clash_tag, clash_name, current_trophies, highest_trophies,
level, clan_name, clan_tag, clan_role, verified)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, 1)
`, [
discordId, clashData.tag, clashData.name, clashData.trophies,
clashData.bestTrophies, clashData.expLevel,
clashData.clan?.name || null, clashData.clan?.tag || null,
clashData.clan?.role || null
]);
return { id: result.id, discord_id: discordId, ...clashData };
}
},
async getByDiscordId(discordId) {
return await database.get(
'SELECT * FROM user_profiles WHERE discord_id = ?',
[discordId]
);
},
async getByClashTag(clashTag) {
return await database.get(
'SELECT * FROM user_profiles WHERE clash_tag = ?',
[clashTag]
);
},
async getAllVerified() {
return await database.all(
'SELECT * FROM user_profiles WHERE verified = 1 ORDER BY current_trophies DESC'
);
},
async getTopPlayers(limit = 10) {
return await database.all(
'SELECT * FROM ranking_view LIMIT ?',
[limit]
);
},
async updateTrophies(discordId, newTrophies) {
const user = await this.getByDiscordId(discordId);
if (!user) return null;
const oldTrophies = user.current_trophies;
const trophyGain = newTrophies - oldTrophies;
// Mettre à jour les trophées
await database.run(
'UPDATE user_profiles SET current_trophies = ?, last_update = CURRENT_TIMESTAMP WHERE discord_id = ?',
[newTrophies, discordId]
);
// Enregistrer l'historique si il y a un changement
if (trophyGain !== 0) {
const milestone = Math.floor(newTrophies / 1000) * 1000;
const oldMilestone = Math.floor(oldTrophies / 1000) * 1000;
const milestoneReached = milestone > oldMilestone ? milestone : null;
await database.run(`
INSERT INTO trophy_history
(user_profile_id, old_trophies, new_trophies, trophy_gain, milestone_reached)
VALUES (?, ?, ?, ?, ?)
`, [user.id, oldTrophies, newTrophies, trophyGain, milestoneReached]);
}
return { oldTrophies, newTrophies, trophyGain };
}
};
const TrophyHistoryService = {
async getUncongratedMilestones() {
return await database.all(`
SELECT th.*, up.discord_id, up.clash_name
FROM trophy_history th
JOIN user_profiles up ON th.user_profile_id = up.id
WHERE th.milestone_reached IS NOT NULL
AND th.congratulated = FALSE
AND th.trophy_gain > 0
`);
},
async markAsCongratulated(historyId) {
await database.run(
'UPDATE trophy_history SET congratulated = TRUE WHERE id = ?',
[historyId]
);
},
async getRecentGains(hours = 24) {
return await database.all(
'SELECT * FROM recent_trophy_gains WHERE recorded_at >= datetime("now", "-" || ? || " hours")',
[hours]
);
}
};
const SettingsService = {
async get(key) {
const result = await database.get(
'SELECT setting_value FROM bot_settings WHERE setting_key = ?',
[key]
);
return result ? result.setting_value : null;
},
async set(key, value) {
await database.run(`
INSERT OR REPLACE INTO bot_settings (setting_key, setting_value)
VALUES (?, ?)
`, [key, value]);
},
async increment(key, amount = 1) {
const currentValue = parseInt(await this.get(key)) || 0;
await this.set(key, currentValue + amount);
}
};
module.exports = {
database,
initDB,
UserProfileService,
TrophyHistoryService,
SettingsService
};

181
start.js Normal file
View File

@ -0,0 +1,181 @@
const { Client, GatewayIntentBits, Collection, ActivityType } = require('discord.js');
const { readdirSync } = require('fs');
const { join } = require('path');
const chalk = require('chalk');
const cron = require('node-cron');
require('dotenv').config();
// Configuration des couleurs pour les logs
const colors = {
success: chalk.green.bold,
error: chalk.red.bold,
warning: chalk.yellow.bold,
info: chalk.blue.bold,
debug: chalk.magenta.bold,
system: chalk.cyan.bold,
command: chalk.white.bold,
event: chalk.greenBright.bold,
database: chalk.blueBright.bold,
api: chalk.yellowBright.bold,
discord: chalk.hex('#5865F2').bold,
clash: chalk.hex('#FFD700').bold,
trophy: chalk.hex('#FF6B35').bold
};
// Fonction de logging avec couleurs
const logger = {
success: (msg) => console.log(`${colors.success('✅ SUCCESS')} ${chalk.white(msg)}`),
error: (msg) => console.log(`${colors.error('❌ ERROR')} ${chalk.white(msg)}`),
warning: (msg) => console.log(`${colors.warning('⚠️ WARNING')} ${chalk.white(msg)}`),
info: (msg) => console.log(`${colors.info(' INFO')} ${chalk.white(msg)}`),
debug: (msg) => console.log(`${colors.debug('🔍 DEBUG')} ${chalk.white(msg)}`),
system: (msg) => console.log(`${colors.system('⚙️ SYSTEM')} ${chalk.white(msg)}`),
command: (msg) => console.log(`${colors.command('🔧 COMMAND')} ${chalk.white(msg)}`),
event: (msg) => console.log(`${colors.event('📡 EVENT')} ${chalk.white(msg)}`),
database: (msg) => console.log(`${colors.database('🗄️ DATABASE')} ${chalk.white(msg)}`),
api: (msg) => console.log(`${colors.api('🌐 API')} ${chalk.white(msg)}`),
discord: (msg) => console.log(`${colors.discord('🎮 DISCORD')} ${chalk.white(msg)}`),
clash: (msg) => console.log(`${colors.clash('⚔️ CLASH')} ${chalk.white(msg)}`),
trophy: (msg) => console.log(`${colors.trophy('🏆 TROPHY')} ${chalk.white(msg)}`)
};
// Banner d'initialisation
function displayBanner() {
console.clear();
console.log(colors.clash('╔══════════════════════════════════════════════════════════════════╗'));
console.log(colors.clash('║ 🏰 CLASH ROYALE DISCORD BOT 🏰 ║'));
console.log(colors.clash('║ By Neptunia Team ║'));
console.log(colors.clash('╠══════════════════════════════════════════════════════════════════╣'));
console.log(colors.clash('║ 🎮 Discord Integration | ⚔️ Clash Royale API | 🏆 Rankings ║'));
console.log(colors.clash('╚══════════════════════════════════════════════════════════════════╝'));
console.log();
}
// Initialisation du client Discord
const client = new Client({
intents: [
GatewayIntentBits.Guilds,
GatewayIntentBits.GuildMembers,
GatewayIntentBits.GuildMessages,
GatewayIntentBits.MessageContent,
GatewayIntentBits.GuildMessageReactions
]
});
// Collections pour stocker les handlers
client.commands = new Collection();
client.events = new Collection();
client.components = new Collection();
client.logger = logger;
// Fonction pour charger les handlers
async function loadHandlers() {
const handlerFiles = readdirSync(join(__dirname, 'src', 'handlers')).filter(file => file.endsWith('.js'));
logger.system('Chargement des handlers...');
for (const file of handlerFiles) {
try {
const handler = require(join(__dirname, 'src', 'handlers', file));
await handler(client);
logger.success(`Handler chargé: ${colors.command(file)}`);
} catch (error) {
logger.error(`Erreur lors du chargement du handler ${file}: ${error.message}`);
}
}
}
// Fonction pour initialiser la base de données
async function initializeDatabase() {
try {
const { initDB } = require('./src/utils/database');
await initDB();
logger.database('Base de données initialisée avec succès');
} catch (error) {
logger.error(`Erreur lors de l'initialisation de la base de données: ${error.message}`);
process.exit(1);
}
}
// Fonction pour programmer les tâches automatiques
function scheduleTasks() {
// Vérification des trophées toutes les 30 minutes
cron.schedule('*/30 * * * *', async () => {
try {
const { checkTrophyMilestones } = require('./src/services/trophyService');
await checkTrophyMilestones(client);
logger.system('Vérification des trophées effectuée');
} catch (error) {
logger.error(`Erreur lors de la vérification des trophées: ${error.message}`);
}
});
logger.system('Tâches automatiques programmées (trophées uniquement)');
}
// Fonction principale d'initialisation
async function initialize() {
displayBanner();
logger.system('🚀 Initialisation du bot en cours...');
try {
// Vérification des variables d'environnement
if (!process.env.DISCORD_TOKEN) {
throw new Error('DISCORD_TOKEN manquant dans le fichier .env');
}
if (!process.env.CLASH_ROYALE_TOKEN) {
throw new Error('CLASH_ROYALE_TOKEN manquant dans le fichier .env');
}
logger.success('Variables d\'environnement validées');
// Initialisation de la base de données
await initializeDatabase();
// Chargement des handlers
await loadHandlers();
// Connexion à Discord
logger.discord('Connexion à Discord en cours...');
await client.login(process.env.DISCORD_TOKEN);
} catch (error) {
logger.error(`Erreur fatale lors de l'initialisation: ${error.message}`);
process.exit(1);
}
}
// L'événement ready est géré par src/events/ready.js
// Programmation des tâches automatiques (trophées uniquement)
client.once('ready', () => {
scheduleTasks();
});
// Gestion des erreurs globales
process.on('unhandledRejection', (reason, promise) => {
logger.error(`Promesse rejetée non gérée: ${reason}`);
console.error(promise);
});
process.on('uncaughtException', (error) => {
logger.error(`Exception non capturée: ${error.message}`);
console.error(error);
});
// Gestion de l'arrêt propre
process.on('SIGINT', () => {
logger.warning('Signal SIGINT reçu, arrêt du bot...');
client.destroy();
process.exit(0);
});
process.on('SIGTERM', () => {
logger.warning('Signal SIGTERM reçu, arrêt du bot...');
client.destroy();
process.exit(0);
});
// Démarrage du bot
initialize();