add statistique command

This commit is contained in:
Arthur Puechberty
2026-01-18 01:36:20 +01:00
parent fcffa00ec8
commit c3ee18c7d9
6 changed files with 470 additions and 2 deletions
+383
View File
@@ -0,0 +1,383 @@
const { EmbedBuilder, AttachmentBuilder } = require("discord.js");
const addCommand = require("../fonctions/addCommand");
const db = require("../db");
const { ChartJSNodeCanvas } = require('chartjs-node-canvas');
// Créer le renderer de graphique
const chartRenderer = new ChartJSNodeCanvas({
width: 800,
height: 400,
backgroundColour: '#2f3136'
});
// Fonction pour générer le buffer du graphique
async function generateChartBuffer(labels, messagesData, voiceData, title) {
const configuration = {
type: 'bar',
data: {
labels: labels,
datasets: [
{
label: 'Messages',
data: messagesData,
backgroundColor: '#5865F2',
borderColor: '#5865F2',
yAxisID: 'y'
},
{
label: 'Heures vocal',
data: voiceData,
backgroundColor: '#57F287',
borderColor: '#57F287',
yAxisID: 'y1'
}
]
},
options: {
responsive: false,
plugins: {
title: {
display: true,
text: title,
color: '#ffffff',
font: { size: 18 }
},
legend: {
labels: { color: '#ffffff', font: { size: 12 } }
}
},
scales: {
x: {
ticks: { color: '#ffffff' },
grid: { color: 'rgba(255,255,255,0.1)' }
},
y: {
position: 'left',
title: { display: true, text: 'Messages', color: '#5865F2' },
ticks: { color: '#5865F2' },
grid: { color: 'rgba(255,255,255,0.1)' },
beginAtZero: true
},
y1: {
position: 'right',
title: { display: true, text: 'Heures', color: '#57F287' },
ticks: { color: '#57F287' },
grid: { drawOnChartArea: false },
beginAtZero: true
}
}
}
};
try {
const buffer = await chartRenderer.renderToBuffer(configuration);
return buffer;
} catch (err) {
console.error('Erreur génération graphique:', err);
return null;
}
}
// Fonction pour obtenir les dates selon la période
function getDateRange(period) {
const now = new Date();
let startDate;
let labels = [];
let groupBy = 'day';
switch (period) {
case 'week':
startDate = new Date(now);
startDate.setDate(now.getDate() - 6);
// Labels pour les 7 derniers jours
for (let i = 6; i >= 0; i--) {
const d = new Date(now);
d.setDate(now.getDate() - i);
labels.push(d.toLocaleDateString('fr-FR', { weekday: 'short', day: 'numeric' }));
}
break;
case 'month':
startDate = new Date(now);
startDate.setDate(now.getDate() - 29);
groupBy = 'week';
labels = ['Sem. 4', 'Sem. 3', 'Sem. 2', 'Sem. 1'];
break;
case 'year':
startDate = new Date(now);
startDate.setFullYear(now.getFullYear() - 1);
groupBy = 'month';
for (let i = 11; i >= 0; i--) {
const d = new Date(now);
d.setMonth(now.getMonth() - i);
labels.push(d.toLocaleDateString('fr-FR', { month: 'short' }));
}
break;
case 'all':
default:
startDate = new Date(2020, 0, 1); // Depuis le début
groupBy = 'month';
// On génère les labels dynamiquement pour "all"
labels = [];
break;
}
return {
startDate: startDate.toISOString().split('T')[0],
labels,
groupBy,
period
};
}
// Fonction pour formater le temps en heures et minutes
function formatVoiceTime(seconds) {
const hours = Math.floor(seconds / 3600);
const minutes = Math.floor((seconds % 3600) / 60);
if (hours > 0) {
return `${hours}h ${minutes}m`;
}
return `${minutes}m`;
}
async function executeStats(context, isSlash) {
const guild = context.guild;
const targetUser = isSlash
? (context.options.getUser('utilisateur') || context.user)
: (context.mentions.users.first() || context.author);
const period = isSlash
? (context.options.getString('periode') || 'week')
: 'week';
const { startDate, labels, groupBy, period: selectedPeriod } = getDateRange(period);
// Récupérer les stats de l'utilisateur
const stats = await db.allAsync(
`SELECT stat_type, value, date FROM user_activity_stats
WHERE guild_id = ? AND user_id = ? AND date >= ?
ORDER BY date ASC`,
[guild.id, targetUser.id, startDate]
);
// Calculer les totaux
let totalMessages = 0;
let totalVoiceSeconds = 0;
const messagesByDate = {};
const voiceByDate = {};
stats.forEach(stat => {
if (stat.stat_type === 'messages') {
totalMessages += stat.value;
messagesByDate[stat.date] = (messagesByDate[stat.date] || 0) + stat.value;
} else if (stat.stat_type === 'voice_time') {
totalVoiceSeconds += stat.value;
voiceByDate[stat.date] = (voiceByDate[stat.date] || 0) + stat.value;
}
});
// Préparer les données pour le graphique
let chartLabels = labels;
let messagesData = [];
let voiceData = [];
if (selectedPeriod === 'week') {
// Données journalières pour la semaine
const now = new Date();
for (let i = 6; i >= 0; i--) {
const d = new Date(now);
d.setDate(now.getDate() - i);
const dateStr = d.toISOString().split('T')[0];
messagesData.push(messagesByDate[dateStr] || 0);
voiceData.push(Math.round(((voiceByDate[dateStr] || 0) / 3600) * 10) / 10); // En heures
}
} else if (selectedPeriod === 'month') {
// Données par semaine pour le mois
const now = new Date();
for (let week = 3; week >= 0; week--) {
let weekMessages = 0;
let weekVoice = 0;
for (let day = 0; day < 7; day++) {
const d = new Date(now);
d.setDate(now.getDate() - (week * 7 + day));
const dateStr = d.toISOString().split('T')[0];
weekMessages += messagesByDate[dateStr] || 0;
weekVoice += voiceByDate[dateStr] || 0;
}
messagesData.push(weekMessages);
voiceData.push(Math.round((weekVoice / 3600) * 10) / 10);
}
} else if (selectedPeriod === 'year') {
// Données par mois pour l'année
const now = new Date();
for (let i = 11; i >= 0; i--) {
const targetMonth = new Date(now);
targetMonth.setMonth(now.getMonth() - i);
const year = targetMonth.getFullYear();
const month = targetMonth.getMonth();
let monthMessages = 0;
let monthVoice = 0;
Object.entries(messagesByDate).forEach(([date, value]) => {
const d = new Date(date);
if (d.getFullYear() === year && d.getMonth() === month) {
monthMessages += value;
}
});
Object.entries(voiceByDate).forEach(([date, value]) => {
const d = new Date(date);
if (d.getFullYear() === year && d.getMonth() === month) {
monthVoice += value;
}
});
messagesData.push(monthMessages);
voiceData.push(Math.round((monthVoice / 3600) * 10) / 10);
}
} else {
// "all" - Tout depuis le début, groupé par mois
const allDates = [...new Set([...Object.keys(messagesByDate), ...Object.keys(voiceByDate)])].sort();
if (allDates.length > 0) {
const firstDate = new Date(allDates[0]);
const lastDate = new Date();
chartLabels = [];
const current = new Date(firstDate.getFullYear(), firstDate.getMonth(), 1);
while (current <= lastDate) {
const year = current.getFullYear();
const month = current.getMonth();
chartLabels.push(current.toLocaleDateString('fr-FR', { month: 'short', year: '2-digit' }));
let monthMessages = 0;
let monthVoice = 0;
Object.entries(messagesByDate).forEach(([date, value]) => {
const d = new Date(date);
if (d.getFullYear() === year && d.getMonth() === month) {
monthMessages += value;
}
});
Object.entries(voiceByDate).forEach(([date, value]) => {
const d = new Date(date);
if (d.getFullYear() === year && d.getMonth() === month) {
monthVoice += value;
}
});
messagesData.push(monthMessages);
voiceData.push(Math.round((monthVoice / 3600) * 10) / 10);
current.setMonth(current.getMonth() + 1);
}
} else {
chartLabels = ['Aucune donnée'];
messagesData = [0];
voiceData = [0];
}
}
// Limiter à 12 labels max pour le graphique
if (chartLabels.length > 12) {
const step = Math.ceil(chartLabels.length / 12);
chartLabels = chartLabels.filter((_, i) => i % step === 0);
messagesData = messagesData.filter((_, i) => i % step === 0);
voiceData = voiceData.filter((_, i) => i % step === 0);
}
const periodNames = {
'week': 'cette semaine',
'month': 'ce mois',
'year': 'cette année',
'all': 'depuis le début'
};
const chartTitle = `Statistiques de ${targetUser.username} - ${periodNames[selectedPeriod]}`;
const chartBuffer = await generateChartBuffer(chartLabels, messagesData, voiceData, chartTitle);
// Calculer le rang sur le serveur
const allUsersMessages = await db.allAsync(
`SELECT user_id, SUM(value) as total FROM user_activity_stats
WHERE guild_id = ? AND stat_type = 'messages' AND date >= ?
GROUP BY user_id ORDER BY total DESC`,
[guild.id, startDate]
);
const allUsersVoice = await db.allAsync(
`SELECT user_id, SUM(value) as total FROM user_activity_stats
WHERE guild_id = ? AND stat_type = 'voice_time' AND date >= ?
GROUP BY user_id ORDER BY total DESC`,
[guild.id, startDate]
);
const messageRank = allUsersMessages.findIndex(u => u.user_id === targetUser.id) + 1 || '-';
const voiceRank = allUsersVoice.findIndex(u => u.user_id === targetUser.id) + 1 || '-';
const embed = new EmbedBuilder()
.setColor(0x5865F2)
.setAuthor({
name: `📊 Statistiques de ${targetUser.username}`,
iconURL: targetUser.displayAvatarURL({ dynamic: true })
})
.setDescription(`Statistiques d'activité **${periodNames[selectedPeriod]}**`)
.addFields(
{
name: '💬 Messages',
value: `**${totalMessages.toLocaleString('fr-FR')}** messages\n🏆 Rang: #${messageRank}`,
inline: true
},
{
name: '🎤 Temps vocal',
value: `**${formatVoiceTime(totalVoiceSeconds)}**\n🏆 Rang: #${voiceRank}`,
inline: true
}
)
.setFooter({ text: `Serveur: ${guild.name}` })
.setTimestamp();
// Créer l'attachment et ajouter l'image à l'embed
const files = [];
if (chartBuffer) {
const attachment = new AttachmentBuilder(chartBuffer, { name: 'stats.png' });
files.push(attachment);
embed.setImage('attachment://stats.png');
}
if (isSlash) {
await context.reply({ embeds: [embed], files });
} else {
await context.channel.send({ embeds: [embed], files });
}
}
module.exports = addCommand({
name: "stats",
description: "Affiche les statistiques d'activité d'un utilisateur",
slashOptions: [
{
name: "utilisateur",
description: "L'utilisateur dont vous voulez voir les stats",
type: "USER",
required: false
},
{
name: "periode",
description: "La période à afficher",
type: "STRING",
required: false,
choices: [
{ name: "📅 Cette semaine", value: "week" },
{ name: "📆 Ce mois", value: "month" },
{ name: "🗓️ Cette année", value: "year" },
{ name: "♾️ Depuis le début", value: "all" }
]
}
],
executeSlash: async (client, interaction) => {
await executeStats(interaction, true);
},
executePrefix: async (client, message, args) => {
await executeStats(message, false);
}
});
+19
View File
@@ -194,6 +194,25 @@ db.exec(`
format TEXT NOT NULL DEFAULT '{stat}', format TEXT NOT NULL DEFAULT '{stat}',
UNIQUE(guild_id, channel_id) UNIQUE(guild_id, channel_id)
); );
CREATE TABLE IF NOT EXISTS user_activity_stats (
id INTEGER PRIMARY KEY AUTOINCREMENT,
guild_id TEXT NOT NULL,
user_id TEXT NOT NULL,
stat_type TEXT NOT NULL,
value INTEGER NOT NULL DEFAULT 0,
date TEXT NOT NULL,
UNIQUE(guild_id, user_id, stat_type, date)
);
CREATE TABLE IF NOT EXISTS voice_sessions (
guild_id TEXT NOT NULL,
user_id TEXT NOT NULL,
join_timestamp INTEGER NOT NULL,
PRIMARY KEY(guild_id, user_id)
);
CREATE INDEX IF NOT EXISTS idx_user_activity_stats_date ON user_activity_stats(guild_id, user_id, date);
`); `);
module.exports = db; module.exports = db;
+10
View File
@@ -7,6 +7,16 @@ module.exports = {
if (message.author.bot) return; if (message.author.bot) return;
const guildId = message.guild.id; const guildId = message.guild.id;
const userId = message.author.id;
// ===== TRACK MESSAGE STATS =====
const today = new Date().toISOString().split('T')[0]; // YYYY-MM-DD
db.run(
`INSERT INTO user_activity_stats (guild_id, user_id, stat_type, value, date)
VALUES (?, ?, 'messages', 1, ?)
ON CONFLICT(guild_id, user_id, stat_type, date) DO UPDATE SET value = value + 1`,
[guildId, userId, today]
);
// ===== XP SYSTEM ===== // ===== XP SYSTEM =====
db.get( db.get(
+51 -1
View File
@@ -10,13 +10,16 @@ module.exports = {
async execute(client, oldState, newState) { async execute(client, oldState, newState) {
// ===== PRIVATE ROOM (TEMP VOICE CHANNELS) ===== // ===== PRIVATE ROOM (TEMP VOICE CHANNELS) =====
await handlePrivateRoom(client, oldState, newState); await handlePrivateRoom(client, oldState, newState);
if (newState.member.user.bot) return; if (newState.member.user.bot) return;
const guildId = newState.guild.id; const guildId = newState.guild.id;
const oderId = newState.member.id; const oderId = newState.member.id;
const key = `${guildId}_${oderId}`; const key = `${guildId}_${oderId}`;
// ===== TRACK VOICE TIME STATS =====
await trackVoiceTime(guildId, oderId, oldState, newState);
// ===== AUTOROLE VOCAL ===== // ===== AUTOROLE VOCAL =====
db.get( db.get(
"SELECT enabled, role_id, exclude_channel_ids FROM autorole_vocal_config WHERE guild_id = ?", "SELECT enabled, role_id, exclude_channel_ids FROM autorole_vocal_config WHERE guild_id = ?",
@@ -222,3 +225,50 @@ async function checkAndDeleteEmptyTempChannel(oldState) {
} }
} }
} }
// ===== VOICE TIME TRACKING =====
async function trackVoiceTime(guildId, userId, oldState, newState) {
const today = new Date().toISOString().split('T')[0]; // YYYY-MM-DD
// User joined voice channel
if (newState.channelId && !oldState.channelId) {
// Save join timestamp
db.run(
`INSERT OR REPLACE INTO voice_sessions (guild_id, user_id, join_timestamp)
VALUES (?, ?, ?)`,
[guildId, userId, Date.now()]
);
}
// User left voice channel
else if (!newState.channelId && oldState.channelId) {
// Get join timestamp and calculate duration
const session = await db.getAsync(
"SELECT join_timestamp FROM voice_sessions WHERE guild_id = ? AND user_id = ?",
[guildId, userId]
);
if (session) {
const durationMs = Date.now() - session.join_timestamp;
const durationSeconds = Math.floor(durationMs / 1000);
// Add voice time to stats (in seconds)
db.run(
`INSERT INTO user_activity_stats (guild_id, user_id, stat_type, value, date)
VALUES (?, ?, 'voice_time', ?, ?)
ON CONFLICT(guild_id, user_id, stat_type, date) DO UPDATE SET value = value + ?`,
[guildId, userId, durationSeconds, today, durationSeconds]
);
// Delete session
db.run(
"DELETE FROM voice_sessions WHERE guild_id = ? AND user_id = ?",
[guildId, userId]
);
}
}
// User switched channels (still in voice)
else if (newState.channelId && oldState.channelId && newState.channelId !== oldState.channelId) {
// Update session timestamp (continue tracking)
// No action needed, session continues
}
}
+2 -1
View File
@@ -29,6 +29,7 @@
"dotenv": "^17.2.3", "dotenv": "^17.2.3",
"express": "^5.2.1", "express": "^5.2.1",
"express-session": "^1.18.2", "express-session": "^1.18.2",
"sqlite3": "^5.1.7" "sqlite3": "^5.1.7",
"chartjs-node-canvas": "^4.1.6"
} }
} }
+5
View File
@@ -87,6 +87,11 @@
<h3>Salons de Statistiques</h3> <h3>Salons de Statistiques</h3>
<p>Affichez les stats de votre serveur en temps réel dans des salons vocaux : membres, bots, en ligne, boosts, rôles et plus encore.</p> <p>Affichez les stats de votre serveur en temps réel dans des salons vocaux : membres, bots, en ligne, boosts, rôles et plus encore.</p>
</div> </div>
<div class="feature-card">
<div class="feature-icon">📈</div>
<h3>Stats d'Activité</h3>
<p>Suivez l'activité de vos membres avec des graphiques détaillés : messages envoyés, temps en vocal, par semaine, mois ou année.</p>
</div>
<div class="feature-card"> <div class="feature-card">
<div class="feature-icon">⚙️</div> <div class="feature-icon">⚙️</div>
<h3>Dashboard Intuitif</h3> <h3>Dashboard Intuitif</h3>