Push structure V1
This commit is contained in:
parent
3d1989ea0f
commit
f31ae7acc2
60
.env.exemple
Normal file
60
.env.exemple
Normal 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
2
.gitignore
vendored
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
node_modules
|
||||||
|
.env
|
||||||
98
README.md
98
README.md
@ -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
29
package.json
Normal 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
172
schema.sql
Normal 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
131
schema_clean.sql
Normal 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
107
src/commands/info.js
Normal 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
284
src/commands/ranking.js
Normal 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
238
src/commands/sync.js
Normal 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] });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
68
src/events/interactionCreate.js
Normal file
68
src/events/interactionCreate.js
Normal 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
52
src/events/ready.js
Normal 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();
|
||||||
|
}
|
||||||
|
};
|
||||||
55
src/handlers/commandHandler.js
Normal file
55
src/handlers/commandHandler.js
Normal 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}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
31
src/handlers/componentHandler.js
Normal file
31
src/handlers/componentHandler.js
Normal 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}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
29
src/handlers/eventHandler.js
Normal file
29
src/handlers/eventHandler.js
Normal 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}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
311
src/services/clanRankingService.js
Normal file
311
src/services/clanRankingService.js
Normal 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)
|
||||||
|
};
|
||||||
315
src/services/clashRoyaleService.js
Normal file
315
src/services/clashRoyaleService.js
Normal 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
|
||||||
|
};
|
||||||
260
src/services/rankingService.js
Normal file
260
src/services/rankingService.js
Normal 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)
|
||||||
|
};
|
||||||
268
src/services/trophyService.js
Normal file
268
src/services/trophyService.js
Normal 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
382
src/utils/database.js
Normal 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
181
start.js
Normal 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();
|
||||||
Loading…
Reference in New Issue
Block a user