From b3d0edb701530a8189a15c7abf542c66f02fc9f7 Mon Sep 17 00:00:00 2001 From: Suraj S <72451747+mochiron-desu@users.noreply.github.com> Date: Sun, 4 May 2025 07:48:28 +0000 Subject: [PATCH 1/4] Add commands for streak tracking, leaderboard, and stats; implement related handlers and calculations --- modules/commandRegistration.js | 20 ++++++++ modules/interactionHandler.js | 38 ++++++++++++++ modules/models/DailySubmission.js | 12 ++++- modules/statsUtils.js | 85 +++++++++++++++++++++++++++++++ 4 files changed, 154 insertions(+), 1 deletion(-) create mode 100644 modules/statsUtils.js diff --git a/modules/commandRegistration.js b/modules/commandRegistration.js index 1b9b538..16997af 100644 --- a/modules/commandRegistration.js +++ b/modules/commandRegistration.js @@ -82,6 +82,26 @@ const commands = [ new SlashCommandBuilder() .setName('botinfo') .setDescription('Display information about the bot and its GitHub repository') + .toJSON(), + new SlashCommandBuilder() + .setName('streak') + .setDescription('Check your current streak for completing LeetCode Daily Challenges') + .toJSON(), + new SlashCommandBuilder() + .setName('leaderboard') + .setDescription('View the leaderboard for LeetCode Daily Challenge streaks in this server') + .toJSON(), + new SlashCommandBuilder() + .setName('stats') + .setDescription('View your weekly or monthly completion stats for LeetCode Daily Challenges') + .addStringOption(option => + option.setName('period') + .setDescription('Choose the period: weekly or monthly') + .setRequired(true) + .addChoices( + { name: 'Weekly', value: 'weekly' }, + { name: 'Monthly', value: 'monthly' } + )) .toJSON() ]; diff --git a/modules/interactionHandler.js b/modules/interactionHandler.js index c89d807..1b88a7e 100644 --- a/modules/interactionHandler.js +++ b/modules/interactionHandler.js @@ -2,6 +2,7 @@ const { addUser, removeUser, getGuildUsers, initializeGuildConfig, updateGuildCh const { enhancedCheck } = require('./apiUtils'); const { updateGuildCronJobs } = require('./scheduledTasks'); const logger = require('./logger'); +const { calculateStreak, calculateCompletionRates, generateLeaderboard } = require('./statsUtils'); async function handleInteraction(interaction) { logger.info(`Interaction received: ${interaction.commandName}`); @@ -40,6 +41,15 @@ async function handleInteraction(interaction) { case 'botinfo': await handleBotInfo(interaction); break; + case 'streak': + await handleStreak(interaction); + break; + case 'leaderboard': + await handleLeaderboard(interaction); + break; + case 'stats': + await handleStats(interaction); + break; default: await interaction.reply('Unknown command.'); } @@ -239,4 +249,32 @@ async function handleBotInfo(interaction) { await interaction.reply({ embeds: [botInfoEmbed] }); } +async function handleStreak(interaction) { + await interaction.deferReply(); + const streak = await calculateStreak(interaction.user.id, interaction.guildId); + await interaction.editReply(`Your current streak is **${streak}** days! Keep it up!`); +} + +async function handleLeaderboard(interaction) { + await interaction.deferReply(); + const leaderboard = await generateLeaderboard(interaction.guildId); + if (leaderboard.length === 0) { + await interaction.editReply('No leaderboard data available yet. Encourage your server members to participate!'); + return; + } + + const leaderboardMessage = leaderboard + .map(entry => `**#${entry.rank}** <@${entry.userId}> - **${entry.streak}** days`) + .join('\n'); + + await interaction.editReply(`πŸ† **Leaderboard** πŸ†\n${leaderboardMessage}`); +} + +async function handleStats(interaction) { + await interaction.deferReply(); + const period = interaction.options.getString('period'); + const stats = await calculateCompletionRates(interaction.user.id, interaction.guildId, period); + await interaction.editReply(`You have completed **${stats.total}** challenges in the past ${stats.period}. Great job!`); +} + module.exports = { handleInteraction }; \ No newline at end of file diff --git a/modules/models/DailySubmission.js b/modules/models/DailySubmission.js index 7e72145..cf5747e 100644 --- a/modules/models/DailySubmission.js +++ b/modules/models/DailySubmission.js @@ -1,4 +1,4 @@ - const mongoose = require('mongoose'); +const mongoose = require('mongoose'); const dailySubmissionSchema = new mongoose.Schema({ guildId: { @@ -36,6 +36,16 @@ const dailySubmissionSchema = new mongoose.Schema({ submissionTime: { type: Date, required: true + }, + completed: { + type: Boolean, + required: true, + default: false + }, + streakCount: { + type: Number, + required: false, + default: 0 } }); diff --git a/modules/statsUtils.js b/modules/statsUtils.js new file mode 100644 index 0000000..6e5c9a1 --- /dev/null +++ b/modules/statsUtils.js @@ -0,0 +1,85 @@ +const DailySubmission = require('./models/DailySubmission'); + +/** + * Calculate streaks for a user based on their daily submissions. + * @param {String} userId - The ID of the user. + * @param {String} guildId - The ID of the guild. + * @returns {Promise} - The current streak count. + */ +async function calculateStreak(userId, guildId) { + const submissions = await DailySubmission.find({ + userId, + guildId, + completed: true + }).sort({ date: -1 }); + + let streak = 0; + let currentDate = new Date(); + + for (const submission of submissions) { + const submissionDate = new Date(submission.date); + if ( + currentDate.toDateString() === submissionDate.toDateString() || + currentDate.toDateString() === new Date(submissionDate.setDate(submissionDate.getDate() + 1)).toDateString() + ) { + streak++; + currentDate = submission.date; + } else { + break; + } + } + + return streak; +} + +/** + * Calculate weekly or monthly completion rates for a user. + * @param {String} userId - The ID of the user. + * @param {String} guildId - The ID of the guild. + * @param {String} period - 'weekly' or 'monthly'. + * @returns {Promise} - Completion rates. + */ +async function calculateCompletionRates(userId, guildId, period) { + const now = new Date(); + const startDate = new Date( + period === 'weekly' ? now.setDate(now.getDate() - 7) : now.setMonth(now.getMonth() - 1) + ); + + const submissions = await DailySubmission.find({ + userId, + guildId, + date: { $gte: startDate }, + completed: true + }); + + return { + total: submissions.length, + period + }; +} + +/** + * Generate a leaderboard for a guild based on streaks. + * @param {String} guildId - The ID of the guild. + * @returns {Promise} - Leaderboard data. + */ +async function generateLeaderboard(guildId) { + const users = await DailySubmission.aggregate([ + { $match: { guildId, completed: true } }, + { $group: { _id: '$userId', streak: { $sum: 1 } } }, + { $sort: { streak: -1 } }, + { $limit: 10 } + ]); + + return users.map((user, index) => ({ + rank: index + 1, + userId: user._id, + streak: user.streak + })); +} + +module.exports = { + calculateStreak, + calculateCompletionRates, + generateLeaderboard +}; \ No newline at end of file From 7a44112c0becff6bb854a72a57ef1986c5d114d3 Mon Sep 17 00:00:00 2001 From: Suraj S <72451747+mochiron-desu@users.noreply.github.com> Date: Sun, 4 May 2025 08:04:04 +0000 Subject: [PATCH 2/4] Implement streak tracking: add streakCount to submissions, enhance calculateStreak logic, and add pre-save middleware for streak management --- modules/apiUtils.js | 5 ++- modules/models/DailySubmission.js | 24 +++++++++++++ modules/scheduledTasks.js | 5 ++- modules/statsUtils.js | 56 ++++++++++++++++++------------- 4 files changed, 64 insertions(+), 26 deletions(-) diff --git a/modules/apiUtils.js b/modules/apiUtils.js index 6150090..a483d0c 100644 --- a/modules/apiUtils.js +++ b/modules/apiUtils.js @@ -1,6 +1,7 @@ const axios = require('axios'); const logger = require('./logger'); const DailySubmission = require('./models/DailySubmission'); +const { calculateStreak } = require('./statsUtils'); // Fetch today’s daily challenge slug async function getDailySlug() { @@ -127,7 +128,9 @@ async function enhancedCheck(users, client, channelId) { questionTitle: problem.title, questionSlug: dailyData, difficulty: problem.difficulty, - submissionTime + submissionTime, + completed: true, + streakCount: await calculateStreak(userId, guild.id) }); } } catch (error) { diff --git a/modules/models/DailySubmission.js b/modules/models/DailySubmission.js index cf5747e..6d283f9 100644 --- a/modules/models/DailySubmission.js +++ b/modules/models/DailySubmission.js @@ -52,4 +52,28 @@ const dailySubmissionSchema = new mongoose.Schema({ // Compound index for efficient querying of user submissions within a guild dailySubmissionSchema.index({ guildId: 1, userId: 1, date: -1 }); +// Add pre-save middleware after the schema definition +dailySubmissionSchema.pre('save', async function(next) { + if (this.isNew && this.completed) { + const yesterday = new Date(this.date); + yesterday.setDate(yesterday.getDate() - 1); + + // Find yesterday's submission + const prevSubmission = await this.constructor.findOne({ + userId: this.userId, + guildId: this.guildId, + completed: true, + date: { + $gte: new Date(yesterday.setHours(0, 0, 0, 0)), + $lt: new Date(yesterday.setHours(23, 59, 59, 999)) + } + }); + + // If there was a submission yesterday, increment that streak + // Otherwise start a new streak at 1 + this.streakCount = prevSubmission ? prevSubmission.streakCount + 1 : 1; + } + next(); +}); + module.exports = mongoose.model('DailySubmission', dailySubmissionSchema); \ No newline at end of file diff --git a/modules/scheduledTasks.js b/modules/scheduledTasks.js index b782d0d..a9b3449 100644 --- a/modules/scheduledTasks.js +++ b/modules/scheduledTasks.js @@ -5,6 +5,7 @@ const axios = require('axios'); const logger = require('./logger'); const Guild = require('./models/Guild'); const DailySubmission = require('./models/DailySubmission'); +const { calculateStreak } = require('./statsUtils'); // Helper function to safely parse submission timestamp function parseSubmissionTime(submission) { @@ -156,7 +157,9 @@ async function scheduleDailyCheck(client, guildId, channelId, schedule) { questionTitle: problem.title, questionSlug: dailySlug, difficulty: problem.difficulty, - submissionTime + submissionTime, + completed: true, + streakCount: await calculateStreak(discordId || username, guildId) }); } } else { diff --git a/modules/statsUtils.js b/modules/statsUtils.js index 6e5c9a1..a8e5fa1 100644 --- a/modules/statsUtils.js +++ b/modules/statsUtils.js @@ -7,29 +7,28 @@ const DailySubmission = require('./models/DailySubmission'); * @returns {Promise} - The current streak count. */ async function calculateStreak(userId, guildId) { - const submissions = await DailySubmission.find({ + const latestSubmission = await DailySubmission.findOne({ userId, guildId, completed: true }).sort({ date: -1 }); - let streak = 0; - let currentDate = new Date(); + if (!latestSubmission) { + return 0; + } + + // Check if the latest submission is from today or yesterday + const now = new Date(); + const submissionDate = new Date(latestSubmission.date); + const isToday = submissionDate.toDateString() === now.toDateString(); + const isYesterday = submissionDate.toDateString() === new Date(now.setDate(now.getDate() - 1)).toDateString(); - for (const submission of submissions) { - const submissionDate = new Date(submission.date); - if ( - currentDate.toDateString() === submissionDate.toDateString() || - currentDate.toDateString() === new Date(submissionDate.setDate(submissionDate.getDate() + 1)).toDateString() - ) { - streak++; - currentDate = submission.date; - } else { - break; - } + // If the latest submission is not from today or yesterday, streak is broken + if (!isToday && !isYesterday) { + return 0; } - return streak; + return latestSubmission.streakCount; } /** @@ -64,17 +63,26 @@ async function calculateCompletionRates(userId, guildId, period) { * @returns {Promise} - Leaderboard data. */ async function generateLeaderboard(guildId) { - const users = await DailySubmission.aggregate([ - { $match: { guildId, completed: true } }, - { $group: { _id: '$userId', streak: { $sum: 1 } } }, - { $sort: { streak: -1 } }, - { $limit: 10 } - ]); + // Get latest submission for each user to check their current streak + const now = new Date(); + const yesterday = new Date(now); + yesterday.setDate(yesterday.getDate() - 1); + + // Find latest submissions within today or yesterday that have a streak + const users = await DailySubmission.find({ + guildId, + completed: true, + date: { + $gte: new Date(yesterday.setHours(0, 0, 0, 0)) + }, + streakCount: { $gt: 0 } + }).sort({ streakCount: -1, date: -1 }).limit(10); - return users.map((user, index) => ({ + // Map to leaderboard format + return users.map((submission, index) => ({ rank: index + 1, - userId: user._id, - streak: user.streak + userId: submission.userId, + streak: submission.streakCount })); } From 3c76cf6c6b901da8c40e887078afc53c9920a85b Mon Sep 17 00:00:00 2001 From: Suraj S Date: Sun, 4 May 2025 13:44:47 +0530 Subject: [PATCH 3/4] Enhance DailySubmission model: normalize date to midnight UTC, require streakCount, and update pre-save middleware for streak calculation --- .vscode/mcp.json | 12 ++++++++ modules/apiUtils.js | 5 ++- modules/models/DailySubmission.js | 51 +++++++++++++++++++------------ modules/scheduledTasks.js | 2 +- 4 files changed, 46 insertions(+), 24 deletions(-) create mode 100644 .vscode/mcp.json diff --git a/.vscode/mcp.json b/.vscode/mcp.json new file mode 100644 index 0000000..9bf925c --- /dev/null +++ b/.vscode/mcp.json @@ -0,0 +1,12 @@ +{ + "servers": { + "mongodbLeetCode": { + "command": "npx", + "args": [ + "-y", + "mcp-mongo-server", + "mongodb+srv://new-admin:OJhhcbw3LFrZwbxC@leetcode.vco1osy.mongodb.net/?retryWrites=true&w=majority&appName=leetCode" + ] + } + } +} \ No newline at end of file diff --git a/modules/apiUtils.js b/modules/apiUtils.js index a483d0c..edcf74d 100644 --- a/modules/apiUtils.js +++ b/modules/apiUtils.js @@ -77,7 +77,6 @@ async function enhancedCheck(users, client, channelId) { const topicTags = problem.topicTags ? problem.topicTags.map(tag => tag.name).join(', ') : 'N/A'; const stats = problem.stats ? JSON.parse(problem.stats) : { acRate: 'N/A' }; - // Create problem info field const problemField = { name: 'Problem Info', value: `**${problem.title || 'Unknown Problem'}** (${problem.difficulty || 'N/A'})\n` + @@ -87,7 +86,7 @@ async function enhancedCheck(users, client, channelId) { }; const today = new Date(); - today.setHours(0, 0, 0, 0); + today.setUTCHours(0, 0, 0, 0); // Create individual fields for each user status const userStatusFields = await Promise.all(users.map(async username => { @@ -108,7 +107,7 @@ async function enhancedCheck(users, client, channelId) { // Check if we already have a submission record for today const existingSubmission = await DailySubmission.findOne({ guildId: guild.id, - userId: userId, + userId, leetcodeUsername: username, questionSlug: dailyData, date: { diff --git a/modules/models/DailySubmission.js b/modules/models/DailySubmission.js index 6d283f9..38a3993 100644 --- a/modules/models/DailySubmission.js +++ b/modules/models/DailySubmission.js @@ -18,7 +18,13 @@ const dailySubmissionSchema = new mongoose.Schema({ date: { type: Date, required: true, - index: true + index: true, + set: function(val) { + // Normalize date to midnight UTC + const date = new Date(val); + date.setUTCHours(0, 0, 0, 0); + return date; + } }, questionTitle: { type: String, @@ -44,7 +50,7 @@ const dailySubmissionSchema = new mongoose.Schema({ }, streakCount: { type: Number, - required: false, + required: true, default: 0 } }); @@ -52,28 +58,33 @@ const dailySubmissionSchema = new mongoose.Schema({ // Compound index for efficient querying of user submissions within a guild dailySubmissionSchema.index({ guildId: 1, userId: 1, date: -1 }); -// Add pre-save middleware after the schema definition +// Pre-save middleware to handle streak calculation dailySubmissionSchema.pre('save', async function(next) { - if (this.isNew && this.completed) { - const yesterday = new Date(this.date); - yesterday.setDate(yesterday.getDate() - 1); + try { + if (this.isNew && this.completed) { + const yesterday = new Date(this.date); + yesterday.setDate(yesterday.getDate() - 1); + yesterday.setUTCHours(0, 0, 0, 0); - // Find yesterday's submission - const prevSubmission = await this.constructor.findOne({ - userId: this.userId, - guildId: this.guildId, - completed: true, - date: { - $gte: new Date(yesterday.setHours(0, 0, 0, 0)), - $lt: new Date(yesterday.setHours(23, 59, 59, 999)) - } - }); + // Find yesterday's submission + const prevSubmission = await this.constructor.findOne({ + userId: this.userId, + guildId: this.guildId, + completed: true, + date: yesterday + }).sort({ date: -1 }); - // If there was a submission yesterday, increment that streak - // Otherwise start a new streak at 1 - this.streakCount = prevSubmission ? prevSubmission.streakCount + 1 : 1; + // If there was a submission yesterday, increment streak + // Otherwise start a new streak at 1 + this.streakCount = prevSubmission ? prevSubmission.streakCount + 1 : 1; + } else if (!this.completed) { + // Reset streak if submission is marked as incomplete + this.streakCount = 0; + } + next(); + } catch (error) { + next(error); } - next(); }); module.exports = mongoose.model('DailySubmission', dailySubmissionSchema); \ No newline at end of file diff --git a/modules/scheduledTasks.js b/modules/scheduledTasks.js index a9b3449..d7bba9c 100644 --- a/modules/scheduledTasks.js +++ b/modules/scheduledTasks.js @@ -158,7 +158,7 @@ async function scheduleDailyCheck(client, guildId, channelId, schedule) { questionSlug: dailySlug, difficulty: problem.difficulty, submissionTime, - completed: true, + completed: true, // Explicitly set completed to true streakCount: await calculateStreak(discordId || username, guildId) }); } From 279816454b4fddc45481bfbe80cd9d19b9c497ef Mon Sep 17 00:00:00 2001 From: Suraj S Date: Sun, 4 May 2025 13:52:05 +0530 Subject: [PATCH 4/4] Update README: add v2.1.1 changelog and enhance streak tracking system details --- README.md | 30 +++++++++++++++++++++++++++++- 1 file changed, 29 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 661afbd..d11a76c 100644 --- a/README.md +++ b/README.md @@ -35,6 +35,7 @@ This Discord bot tracks LeetCode activity for specified users and posts updates - [Running Specific Tests](#running-specific-tests) - [License](#license) - [Changelog](#changelog) + - [v2.1.1 (2025-05-04)](#v211-2025-05-04) - [v2.1.0 (2025-05-04)](#v210-2025-05-04) - [v2.0.0 (2025-05-02)](#v200-2025-05-02) - [v1.0.0](#v100) @@ -95,11 +96,27 @@ When the bot joins a new server: - MongoDB Atlas integration for reliable data storage - Per-server announcement channels - Automated welcome message with setup instructions +- Advanced Streak Tracking System: + - Daily streak counting + - Automatic streak maintenance + - Streak preservation across timezone boundaries + - Streak reset on missed days + - Per-guild streak leaderboards +- Submission Tracking and Validation: + - Normalized UTC timestamp handling + - Duplicate submission prevention + - Accurate streak counting with date normalization + - Complete submission history +- User Progress Features: + - Daily challenge completion tracking + - Individual streak statistics + - Weekly and monthly completion rates + - Server-wide leaderboards - Permission-based command system: - Users can add/remove themselves - Admins can manage all users - Channel management requires "Manage Channels" permission -- Optional Discord user mentions when reporting challenge status + - Optional Discord user mentions when reporting challenge status - Flexible cron job management for check schedules - Detailed problem information in status updates: - Problem difficulty @@ -223,6 +240,17 @@ This project is licensed under the MIT License - see the [LICENSE](LICENSE) file ## Changelog +### v2.1.1 (2025-05-04) +- ✨ Enhanced streak tracking system + - Improved date handling with UTC normalization + - Fixed streak counting across timezone boundaries + - Added streak preservation logic + - Enhanced duplicate submission detection +- πŸ”„ Improved submission validation +- πŸ“Š Added per-guild leaderboards +- ⚑️ Optimized database queries +- πŸ› Fixed streak reset issues + ### v2.1.0 (2025-05-04) - ✨ Added submission tracking with MongoDB - πŸŽ‰ Added welcome message when bot joins a server