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