Compare commits

..

No commits in common. "main" and "1.0.0" have entirely different histories.
main ... 1.0.0

20 changed files with 2 additions and 3071 deletions

View File

@ -1,60 +0,0 @@
# 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
View File

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

View File

@ -1,97 +1,3 @@
# 🏰 Clash Royale Discord Bot
# clashroyale-bot
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
Code source de notre bot Discord Clash Royale

View File

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

View File

@ -1,172 +0,0 @@
-- =====================================================
-- 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;

View File

@ -1,131 +0,0 @@
-- =====================================================
-- 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;

View File

@ -1,107 +0,0 @@
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();
}
};

View File

@ -1,284 +0,0 @@
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] });
}
}
};

View File

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

@ -1,68 +0,0 @@
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);
}
}
}
}
};

View File

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

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

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

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

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

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

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

@ -1,268 +0,0 @@
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)
};

View File

@ -1,382 +0,0 @@
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
View File

@ -1,181 +0,0 @@
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();