mirror of
https://github.com/arthur-pbty/LazyBot.git
synced 2026-06-03 15:07:29 +02:00
add statistique command
This commit is contained in:
@@ -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);
|
||||
}
|
||||
});
|
||||
@@ -194,6 +194,25 @@ db.exec(`
|
||||
format TEXT NOT NULL DEFAULT '{stat}',
|
||||
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;
|
||||
|
||||
@@ -7,6 +7,16 @@ module.exports = {
|
||||
if (message.author.bot) return;
|
||||
|
||||
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 =====
|
||||
db.get(
|
||||
|
||||
@@ -10,13 +10,16 @@ module.exports = {
|
||||
async execute(client, oldState, newState) {
|
||||
// ===== PRIVATE ROOM (TEMP VOICE CHANNELS) =====
|
||||
await handlePrivateRoom(client, oldState, newState);
|
||||
|
||||
|
||||
if (newState.member.user.bot) return;
|
||||
|
||||
const guildId = newState.guild.id;
|
||||
const oderId = newState.member.id;
|
||||
const key = `${guildId}_${oderId}`;
|
||||
|
||||
// ===== TRACK VOICE TIME STATS =====
|
||||
await trackVoiceTime(guildId, oderId, oldState, newState);
|
||||
|
||||
// ===== AUTOROLE VOCAL =====
|
||||
db.get(
|
||||
"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
@@ -29,6 +29,7 @@
|
||||
"dotenv": "^17.2.3",
|
||||
"express": "^5.2.1",
|
||||
"express-session": "^1.18.2",
|
||||
"sqlite3": "^5.1.7"
|
||||
"sqlite3": "^5.1.7",
|
||||
"chartjs-node-canvas": "^4.1.6"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -87,6 +87,11 @@
|
||||
<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>
|
||||
</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-icon">⚙️</div>
|
||||
<h3>Dashboard Intuitif</h3>
|
||||
|
||||
Reference in New Issue
Block a user