Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions .vscode/mcp.json
Original file line number Diff line number Diff line change
@@ -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"
]
}
}
}
30 changes: 29 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
10 changes: 6 additions & 4 deletions modules/apiUtils.js
Original file line number Diff line number Diff line change
@@ -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() {
Expand Down Expand Up @@ -76,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` +
Expand All @@ -86,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 => {
Expand All @@ -107,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: {
Expand All @@ -127,7 +127,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) {
Expand Down
20 changes: 20 additions & 0 deletions modules/commandRegistration.js
Original file line number Diff line number Diff line change
Expand Up @@ -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()
];

Expand Down
38 changes: 38 additions & 0 deletions modules/interactionHandler.js
Original file line number Diff line number Diff line change
Expand Up @@ -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}`);
Expand Down Expand Up @@ -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.');
}
Expand Down Expand Up @@ -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 };
49 changes: 47 additions & 2 deletions modules/models/DailySubmission.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
const mongoose = require('mongoose');
const mongoose = require('mongoose');

const dailySubmissionSchema = new mongoose.Schema({
guildId: {
Expand All @@ -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,
Expand All @@ -36,10 +42,49 @@ const dailySubmissionSchema = new mongoose.Schema({
submissionTime: {
type: Date,
required: true
},
completed: {
type: Boolean,
required: true,
default: false
},
streakCount: {
type: Number,
required: true,
default: 0
}
});

// Compound index for efficient querying of user submissions within a guild
dailySubmissionSchema.index({ guildId: 1, userId: 1, date: -1 });

// Pre-save middleware to handle streak calculation
dailySubmissionSchema.pre('save', async function(next) {
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: yesterday
}).sort({ date: -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);
}
});

module.exports = mongoose.model('DailySubmission', dailySubmissionSchema);
5 changes: 4 additions & 1 deletion modules/scheduledTasks.js
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down Expand Up @@ -156,7 +157,9 @@ async function scheduleDailyCheck(client, guildId, channelId, schedule) {
questionTitle: problem.title,
questionSlug: dailySlug,
difficulty: problem.difficulty,
submissionTime
submissionTime,
completed: true, // Explicitly set completed to true
streakCount: await calculateStreak(discordId || username, guildId)
});
}
} else {
Expand Down
93 changes: 93 additions & 0 deletions modules/statsUtils.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
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<Number>} - The current streak count.
*/
async function calculateStreak(userId, guildId) {
const latestSubmission = await DailySubmission.findOne({
userId,
guildId,
completed: true
}).sort({ date: -1 });

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();

// If the latest submission is not from today or yesterday, streak is broken
if (!isToday && !isYesterday) {
return 0;
}

return latestSubmission.streakCount;
}

/**
* 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<Object>} - 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<Array>} - Leaderboard data.
*/
async function generateLeaderboard(guildId) {
// 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);

// Map to leaderboard format
return users.map((submission, index) => ({
rank: index + 1,
userId: submission.userId,
streak: submission.streakCount
}));
}

module.exports = {
calculateStreak,
calculateCompletionRates,
generateLeaderboard
};