diff --git a/bot.js b/bot.js index 5ec1b82..d6344b2 100644 --- a/bot.js +++ b/bot.js @@ -1,9 +1,9 @@ -import { Client, GatewayIntentBits, Partials, EmbedBuilder, ActionRowBuilder, ButtonBuilder,ButtonStyle } from 'discord.js'; +import { Client, Collection, GatewayIntentBits, Partials } from 'discord.js'; import { LeetCode } from 'leetcode-query'; -import cron from 'node-cron'; -import keepAlive from './keep_alive.js'; -import axios from 'axios'; + +import KeepAlive from './utility/KeepAlive.js'; import dotenv from 'dotenv'; +import fs from 'fs'; dotenv.config(); @@ -18,309 +18,37 @@ const client = new Client({ ] }); -function chunkArray(array, size) { - const chunked = []; - for (let i = 0; i < array.length; i += size) { - chunked.push(array.slice(i, i + size)); - } - return chunked; -} - -function calculateStreak(submissionCalendar) { - submissionCalendar = JSON.parse(submissionCalendar); - const datesWithSubmissions = Object.entries(submissionCalendar) - .filter(([ts, count]) => count > 0) - .map(([ts]) => new Date(ts * 1000)) - .sort((a, b) => a - b); - - if (datesWithSubmissions.length === 0) return 0; - - let currentStreak = 0; - let hasSolvedToday = false; - const currentDate = new Date(); - const lastSubmissionDate = datesWithSubmissions[datesWithSubmissions.length - 1]; - // Check if the last submission was today - if (lastSubmissionDate.setHours(0, 0, 0, 0) === currentDate.setHours(0, 0, 0, 0)) { - hasSolvedToday = true; - currentDate.setDate(currentDate.getDate() + 1); - } - // Calculate streak - for (let i = datesWithSubmissions.length - 1; i >= 0; i--) { - currentDate.setDate(currentDate.getDate() - 1); - if (datesWithSubmissions[i].setHours(0, 0, 0, 0) === currentDate.setHours(0, 0, 0, 0)) { - currentStreak += 1; - } else { - break; - } - } - return {currentStreak, hasSolvedToday}; -} - -async function fetchLeetCodeProblems() { - try { - const response = await axios.get('https://leetcode.com/api/problems/all/'); - leetcodeProblems = response.data.stat_status_pairs.filter(problem => !problem.paid_only); - } catch (error) { - console.error('Error fetching LeetCode problems:', error); - } -} - - -const lc = new LeetCode(); -let leetcodeProblems = [] +client.lc = new LeetCode() -const topics = [ - 'Array', 'String', 'Hash Table', 'Dynamic Programming', 'Math', - 'Sorting', 'Greedy', 'Depth-First Search', 'Binary Search', 'Database', - 'Breadth-First Search', 'Tree', 'Matrix', 'Two Pointers', 'Bit Manipulation', - 'Stack', 'Design', 'Heap (Priority Queue)', 'Graph', 'Simulation' -]; +client.commands = new Collection() +client.buttons = new Collection() -client.once('ready', () => { - console.log(`Logged in as ${client.user.tag}!`); +/** + * Load all the slash commands + */ - cron.schedule('0 6 * * *', async () => { - try { - const daily = await lc.daily(); - const channel = client.channels.cache.get(process.env.CHANNEL_ID); - if (channel) { - const questionLink = `https://leetcode.com${daily.link}`; - const response = `@everyone **LeetCode Daily Challenge ${daily.date}:**\n**${daily.question.title}** : ${questionLink}`; - channel.send(response); - } else { - console.error('Channel not found'); - } - } catch (error) { - console.error('Error fetching LeetCode daily challenge:', error); - } - }, { - scheduled: true, - timezone: "Asia/Kolkata" - }); +fs.readdirSync('./interactions/commands').filter(file => file.endsWith('.js')).forEach(file => { + import(`./interactions/commands/${file}`).then(({ default: command }) => { + client.commands.set(command.data.name, command); + }).catch(error => console.error(`Failed to load command ${file}:`, error)); }); -client.on('messageCreate', async (message) => { - if (message.author.bot) return; - - const args = message.content.split(' '); - const command = args[0].toLowerCase(); - - if (command === ';potd') { - try { - const daily = await lc.daily(); - const questionLink = `https://leetcode.com${daily.link}`; - - const embed = new EmbedBuilder() - .setTitle(`LeetCode Daily Challenge - ${daily.date}`) - .setURL(questionLink) - .setDescription(`**${daily.question.title}**`) - .setColor(0xD1006C) - .addFields( - { name: 'Difficulty', value: daily.question.difficulty, inline: true }, - { name: 'Link', value: `[Click here](${questionLink})`, inline: true } - ) - .setFooter({ text: 'Good luck solving today\'s problem!' }); - - message.channel.send({ embeds: [embed] }); - } catch (error) { - console.error('Error fetching LeetCode daily challenge:', error); - message.channel.send('Sorry, I could not fetch the LeetCode daily challenge question.'); - } - } else if (command === ';random') { - try { - let difficulty = args[1] ? args[1].toLowerCase() : null; - const difficultyLevel = { 'easy': 1, 'medium': 2, 'hard': 3 }; - const difficultyColors = { 'easy': 0x00FF00, 'medium': 0xFFFF00, 'hard': 0xFF0000 }; // Green, Yellow, Red - - if (leetcodeProblems.length === 0) { - await fetchLeetCodeProblems(); - } - - let filteredProblems = leetcodeProblems; - - if (difficulty && difficultyLevel[difficulty]) { - filteredProblems = leetcodeProblems.filter(problem => problem.difficulty.level === difficultyLevel[difficulty]); - } - - if (filteredProblems.length === 0) { - message.channel.send(`Sorry, I couldn't find any LeetCode problems with the specified difficulty level: ${difficulty}`); - return; - } - - const randomIndex = Math.floor(Math.random() * filteredProblems.length); - const problem = filteredProblems[randomIndex].stat; - const questionLink = `https://leetcode.com/problems/${problem.question__title_slug}/`; - - const embedColor = difficulty ? difficultyColors[difficulty] : 0x7289DA; // Default is Discord's blue - - const embed = new EmbedBuilder() - .setTitle(`${problem.question__title}`) - .setURL(questionLink) - .setColor(embedColor) - .addFields( - { name: 'Difficulty', value: difficulty ? difficulty.charAt(0).toUpperCase() + difficulty.slice(1) : 'N/A', inline: true }, - { name: 'Link', value: `[Solve Problem](${questionLink})`, inline: true }, - { name: 'Acceptance Rate', value: `${problem.total_acs} / ${problem.total_submitted} (${(problem.total_acs / problem.total_submitted * 100).toFixed(2)}%)`, inline: true } - ) - .setFooter({ text: 'Good luck!' }); - - message.channel.send({ embeds: [embed] }); - } catch (error) { - console.error('Error fetching random LeetCode question:', error); - message.channel.send('Sorry, I could not fetch a random LeetCode question.'); - } - } else if (command === ';user' && args.length === 2) { - const username = args[1]; - try { - const [userInfo, contestInfo] = await Promise.all([ - lc.user(username), - lc.user_contest_info(username) - ]); - - if (!userInfo.matchedUser) { - message.channel.send(`User "${username}" not found.`); - return; - } - - const user = userInfo.matchedUser; - const profile = user.profile; - const submitStats = user.submitStats; - - const embed = new EmbedBuilder() - .setColor('#FFD700') // Gold color for the embed - .setTitle(`LeetCode Profile: **${username}**`) - .setThumbnail(profile.userAvatar) - .addFields( - { name: 'šŸ‘¤ Real Name', value: profile.realName || '*Not provided*', inline: true }, - { name: 'šŸ† Ranking', value: profile.ranking ? profile.ranking.toString() : '*Not ranked*', inline: true }, - { name: 'šŸŒ Country', value: profile.countryName || '*Not provided*', inline: true }, - { name: 'šŸ¢ Company', value: profile.company || '*Not provided*', inline: true }, - { name: 'šŸŽ“ School', value: profile.school || '*Not provided*', inline: true }, - { name: '\u200B', value: 'ā¬‡ļø **Problem Solving Stats**', inline: false }, - { name: 'šŸŸ¢ Easy', value: `Solved: ${submitStats.acSubmissionNum[1].count} / ${submitStats.totalSubmissionNum[1].count}`, inline: true }, - { name: 'šŸŸ  Medium', value: `Solved: ${submitStats.acSubmissionNum[2].count} / ${submitStats.totalSubmissionNum[2].count}`, inline: true }, - { name: 'šŸ”“ Hard', value: `Solved: ${submitStats.acSubmissionNum[3].count} / ${submitStats.totalSubmissionNum[3].count}`, inline: true }, - { name: 'šŸ“Š Total', value: `Solved: ${submitStats.acSubmissionNum[0].count} / ${submitStats.totalSubmissionNum[0].count}`, inline: true } - ); - - // Add contest info - if (contestInfo.userContestRanking) { - embed.addFields( - { name: 'šŸš© **Contest Info**', value: `\`\`\`Rating: ${Math.round(contestInfo.userContestRanking.rating)}\nRanking: ${contestInfo.userContestRanking.globalRanking}\nTop: ${contestInfo.userContestRanking.topPercentage.toFixed(2)}%\nAttended: ${contestInfo.userContestRanking.attendedContestsCount}\`\`\`` } - ); - } - - // Add badges if any - if (user.badges && user.badges.length > 0) { - const badgeNames = user.badges.map(badge => badge.displayName).join('\nā€¢'); - embed.addFields({ name: 'šŸ… Badges', value: "ā€¢"+badgeNames, inline: false }); - } - - message.channel.send({ embeds: [embed] }); - - } catch (error) { - console.error('Error fetching user info:', error); - message.channel.send('Sorry, I could not fetch the user info.'); - } - } else if (command === ';streak' && args.length === 2) { - const username = args[1]; - try { - const user = await lc.user(username); - let streakInfo = 0; - let hasSolvedToday = false; - if(user.matchedUser) { - ({ currentStreak: streakInfo, hasSolvedToday } = calculateStreak(user.matchedUser.submissionCalendar)); - } - - let streakMessage; - if (streakInfo > 0) { - if (hasSolvedToday) { - streakMessage = `šŸŽ‰ **${username}** has solved a problem for ${streakInfo} consecutive days! Great work, keep it up! šŸ’Ŗ`; - } else { - streakMessage = `āš ļø **${username}** has solved a problem for ${streakInfo} consecutive days! Solve today's problem to maintain your streak and prevent it from resetting! šŸ”„`; - } - } else { - streakMessage = `āŒ **${username}** does not have a streak yet. Start solving problems today to build your streak! šŸš€`; - } - - message.channel.send(streakMessage); - } catch (error) { - console.error('Error fetching streak info:', error); - message.channel.send('Sorry, I could not fetch the streak info.'); - } - } - else if (command === ';topics') { - const chunkedTopics = chunkArray(topics, 5); // Split topics into groups of 5 - const rows = chunkedTopics.map(chunk => - new ActionRowBuilder().addComponents( - chunk.map(topic => - new ButtonBuilder() - .setCustomId(`topic_${topic.toLowerCase().replace(/\s+/g, '-')}`) - .setLabel(topic) - .setStyle(ButtonStyle.Secondary) - ) - ) - ); - - message.channel.send({ - content: 'Choose a topic to get a random question:', - components: rows - }); - } - else if (command === ';help') { - const helpMessage = `**Available Commands:**\n - \`;potd\` - Shows the LeetCode Daily Challenge\n - \`;random [difficulty]\` - Shows a random LeetCode problem (optional: specify difficulty)\n - \`;user \` - Shows user Info\n - \`;streak \` - Shows user Streak Info\n - \`;topics\` - Shows a list of LeetCode topics to choose from\n - \`;help\` - Shows this help message`; - message.channel.send(helpMessage); - } +fs.readdirSync('./interactions/buttons').filter(file => file.endsWith('.js')).forEach(file => { + import(`./interactions/buttons/${file}`).then(({ default: button }) => { + client.buttons.set(button.name, button); + }).catch(error => console.error(`Failed to load button ${file}:`, error)); }); -client.on('interactionCreate', async interaction => { - if (!interaction.isButton()) return; - - if (interaction.customId.startsWith('topic_')) { - const selectedTopic = interaction.customId.replace('topic_', ''); - await interaction.deferReply(); - - try { - const topicQuestions = await lc.problems({ - categorySlug: '', - skip: 0, - limit: 300000, - filters: { tags: [selectedTopic] } - }); - if (topicQuestions.questions.length === 0) { - await interaction.editReply('No questions found for this topic.'); - return; - } - - const randomQuestion = topicQuestions.questions[Math.floor(Math.random() * topicQuestions.questions.length)]; - - const questionLink = `https://leetcode.com/problems/${randomQuestion.titleSlug}/`; - const embed = new EmbedBuilder() - .setTitle(`Random ${selectedTopic.replace(/-/g, ' ')} Question: ${randomQuestion.title}`) - .setURL(questionLink) - .setColor(0x0099FF) - .addFields( - { name: 'Difficulty', value: randomQuestion.difficulty, inline: true }, - { name: 'Link', value: `[Solve Problem](${questionLink})`, inline: true }, - { name: 'Acceptance Rate', value: `${randomQuestion.acRate.toFixed(2)}%`, inline: true } - ) - .setFooter({ text: 'Good luck solving this problem!' }); - - await interaction.editReply({ embeds: [embed], components: [] }); - } catch (error) { - console.error('Error fetching topic question:', error); - await interaction.editReply('Sorry, I could not fetch a question for this topic.'); - } - } -}); +/** + * Load all events + */ +fs.readdirSync('./events').filter(file => file.endsWith('.js')).forEach((file) => { + import(`./events/${file}`).then(({ default: event }) => { + client.on(file.split(".")[0], (...args) => event(...args)) + }) +}) -keepAlive(); +KeepAlive(); client.login(process.env.TOKEN); diff --git a/cache/problems.json b/cache/problems.json new file mode 100644 index 0000000..0637a08 --- /dev/null +++ b/cache/problems.json @@ -0,0 +1 @@ +[] \ No newline at end of file diff --git a/events/interactionCreate.js b/events/interactionCreate.js new file mode 100644 index 0000000..8ba68f5 --- /dev/null +++ b/events/interactionCreate.js @@ -0,0 +1,19 @@ +export default async (interaction, client=interaction.client) => { + if (interaction.isCommand()) { + let command = client.commands.get(interaction.commandName) + if (!command) return; + + await command.run(interaction).catch((error) => { + console.log(error) + return interaction.channel.send(`āŒ Failed to execute the command due to internal error`) + }) + } else if(interaction.isButton()) { + let button = client.buttons.get(interaction.customId.split("_")[0]) + if(!button) return; + + await button.run(interaction).catch((error) => { + console.log(error) + return interaction.channel.send(`āŒ Failed to handle the interaction due to internal error`) + }) + } +} \ No newline at end of file diff --git a/events/messageCreate.js b/events/messageCreate.js new file mode 100644 index 0000000..1db99e6 --- /dev/null +++ b/events/messageCreate.js @@ -0,0 +1,36 @@ +import { REST, Routes } from 'discord.js' + +export default async (message, client=message.client) => { + if (message.author.bot) return; + + const args = message.content.split(' '); + const command = args[0].toLowerCase(); + + if (command === ';register') { + if (message.author.id !== process.env.DEVELOPER_ID) return; + + let type = args[1] + if (type == 'guild') { + let guildId = args[2] || message.guild.id + + const rest = new REST().setToken(process.env.TOKEN); + await rest.put( + Routes.applicationGuildCommands(client.user.id, guildId), + { body: Array.from(client.commands.values()).map(cmd => cmd.data.toJSON()) } + ) + .then(() => message.channel.send(`āœ… Added (**${client.commands.size}**) commands in guild (\`${guildId}\`)`)) + .catch((error) => message.channel.send(`āŒ Failed to register command due to: \`${error}\``)) + + } else if (type == 'global') { + const rest = new REST().setToken(process.env.TOKEN); + await rest.put( + Routes.applicationCommands(client.user.id), + { body: Array.from(client.commands.values()).map(cmd => cmd.data.toJSON()) } + ) + .then(() => message.channel.send(`āœ… Added (${client.commands.size}) commands to all the guilds, it may take time to show in all guilds.`)) + .catch((error) => message.channel.send(`āŒ Failed to register command due to: \`${error}\``)) + } else { + return message.channel.send(`Invalid Syntax, Use \`;register guild/global (guildId:optinal)\``) + } + } +} \ No newline at end of file diff --git a/events/ready.js b/events/ready.js new file mode 100644 index 0000000..5d67cde --- /dev/null +++ b/events/ready.js @@ -0,0 +1,24 @@ +import cron from 'node-cron' + +export default async (client, lc=client.lc) => { + console.log(`Logged in as ${client.user.tag}!`); + + cron.schedule('0 6 * * *', async () => { + try { + const daily = await lc.daily(); + const channel = client.channels.cache.get(process.env.CHANNEL_ID); + if (channel) { + const questionLink = `https://leetcode.com${daily.link}`; + const response = `@everyone **LeetCode Daily Challenge ${daily.date}:**\n**${daily.question.title}** : ${questionLink}`; + channel.send(response); + } else { + console.error('Channel not found'); + } + } catch (error) { + console.error('Error fetching LeetCode daily challenge:', error); + } + }, { + scheduled: true, + timezone: "Asia/Kolkata" + }); +} \ No newline at end of file diff --git a/example.env b/example.env new file mode 100644 index 0000000..74511d1 --- /dev/null +++ b/example.env @@ -0,0 +1,3 @@ +TOKEN= +CHANNEL_ID= +DEVELOPER_ID= \ No newline at end of file diff --git a/interactions/buttons/topic.js b/interactions/buttons/topic.js new file mode 100644 index 0000000..5f984d9 --- /dev/null +++ b/interactions/buttons/topic.js @@ -0,0 +1,37 @@ +import { EmbedBuilder } from "discord.js"; + +export default { + name: 'topic', + run: async (interaction, lc = interaction.client.lc) => { + const selectedTopic = interaction.customId.replace('topic_', ''); + await interaction.deferReply(); + + const topicQuestions = await lc.problems({ + categorySlug: '', + skip: 0, + limit: 300000, + filters: { tags: [selectedTopic] } + }); + + if (topicQuestions.questions.length === 0) { + await interaction.editReply('No questions found for this topic.'); + return; + } + + const randomQuestion = topicQuestions.questions[Math.floor(Math.random() * topicQuestions.questions.length)]; + + const questionLink = `https://leetcode.com/problems/${randomQuestion.titleSlug}/`; + const embed = new EmbedBuilder() + .setTitle(`Random ${selectedTopic.replace(/-/g, ' ')} Question: ${randomQuestion.title}`) + .setURL(questionLink) + .setColor(0x0099FF) + .addFields( + { name: 'Difficulty', value: randomQuestion.difficulty, inline: true }, + { name: 'Link', value: `[Solve Problem](${questionLink})`, inline: true }, + { name: 'Acceptance Rate', value: `${randomQuestion.acRate.toFixed(2)}%`, inline: true } + ) + .setFooter({ text: 'Good luck solving this problem!' }); + + await interaction.editReply({ embeds: [embed], components: [] }); + } +} \ No newline at end of file diff --git a/interactions/commands/help.js b/interactions/commands/help.js new file mode 100644 index 0000000..60109ca --- /dev/null +++ b/interactions/commands/help.js @@ -0,0 +1,17 @@ +import { SlashCommandBuilder } from "discord.js"; + +export default { + data: new SlashCommandBuilder() + .setName('help') + .setDescription('Get the list of commands available for use'), + run: async (interaction) => { + const helpMessage = `**Available Commands:**\n + \`/potd\` - Shows the LeetCode Daily Challenge\n + \`/random [difficulty]\` - Shows a random LeetCode problem (optional: specify difficulty)\n + \`/user \` - Shows user Info\n + \`/streak \` - Shows user Streak Info\n + \`/topics\` - Shows a list of LeetCode topics to choose from\n + \`/help\` - Shows this help message`; + return interaction.reply({ content: helpMessage}); + } +} \ No newline at end of file diff --git a/interactions/commands/potd.js b/interactions/commands/potd.js new file mode 100644 index 0000000..c7d7745 --- /dev/null +++ b/interactions/commands/potd.js @@ -0,0 +1,24 @@ +import { SlashCommandBuilder, EmbedBuilder } from "discord.js"; + +export default { + data: new SlashCommandBuilder() + .setName('potd') + .setDescription('Shows the LeetCode Daily Challenge'), + run: async (interaction, lc=interaction.client.lc) => { + const daily = await lc.daily(); + const questionLink = `https://leetcode.com${daily.link}`; + + const embed = new EmbedBuilder() + .setTitle(`LeetCode Daily Challenge - ${daily.date}`) + .setURL(questionLink) + .setDescription(`**${daily.question.title}**`) + .setColor(0xD1006C) + .addFields( + { name: 'Difficulty', value: daily.question.difficulty, inline: true }, + { name: 'Link', value: `[Click here](${questionLink})`, inline: true } + ) + .setFooter({ text: 'Good luck solving today\'s problem!' }); + + return interaction.reply({ embeds: [embed] }); + } +} \ No newline at end of file diff --git a/interactions/commands/random.js b/interactions/commands/random.js new file mode 100644 index 0000000..0e2ff94 --- /dev/null +++ b/interactions/commands/random.js @@ -0,0 +1,53 @@ +import { SlashCommandBuilder, EmbedBuilder } from "discord.js"; +import LeetCodeUtility from "../../utility/LeetCode.js"; +import fs from 'fs' + +export default { + data: new SlashCommandBuilder() + .setName('random') + .setDescription('Shows the LeetCode Daily Challenge') + .addStringOption((option) => option + .setName('difficulty') + .setDescription('Mention how hard you want problem to be') + .addChoices({ name: 'easy', value: '1' }, { name: 'medium', value: '2' }, { name: 'hard', value: '3' }) + .setRequired(true) + ), + run: async (interaction) => { + await interaction.deferReply() + const difficulty = parseInt(interaction.options.getString('difficulty')); + + let problems = JSON.parse(fs.readFileSync('./cache/problems.json')) + + if (!problems.length) { /** fetch problems and save them */ + problems = await LeetCodeUtility.fetchLeetCodeProblems(); + fs.writeFileSync('./cache/problems.json', JSON.stringify(problems)) + } + + let filteredProblems = problems.filter(problem => problem.difficulty.level === difficulty); + + if (filteredProblems.length === 0) { + return await interaction + .followUp(`Sorry, I couldn't find any LeetCode problems with the given difficulty level`); + } + + const randomIndex = Math.floor(Math.random() * filteredProblems.length); + const problem = filteredProblems[randomIndex].stat; + const questionLink = `https://leetcode.com/problems/${problem.question__title_slug}/`; + + const embedColor = difficulty == 1 ? 0x00FF00 : difficulty == 2 ? 0xFFFF00 : 0xFF0000 + + const embed = new EmbedBuilder() + .setTitle(`${problem.question__title}`) + .setURL(questionLink) + .setColor(embedColor) + .addFields( + { name: 'Difficulty', value: difficulty === 1 ? 'Easy' : difficulty === 2 ? 'Medium' : 'Hard', inline: true }, + { name: 'Link', value: `[Solve Problem](${questionLink})`, inline: true }, + { name: 'Acceptance Rate', value: `${problem.total_acs} / ${problem.total_submitted} (${(problem.total_acs / problem.total_submitted * 100).toFixed(2)}%)`, inline: true } + ) + .setFooter({ text: 'Good luck!' }); + + + return interaction.followUp({ embeds: [ embed ]}) + } +} \ No newline at end of file diff --git a/interactions/commands/streak.js b/interactions/commands/streak.js new file mode 100644 index 0000000..fde1def --- /dev/null +++ b/interactions/commands/streak.js @@ -0,0 +1,42 @@ +import { SlashCommandBuilder } from "discord.js"; +import LeetCodeUtility from "../../utility/LeetCode.js"; + + +export default { + data: new SlashCommandBuilder() + .setName('streak') + .setDescription('Shows user Streak Info') + .addStringOption( + (option) => option + .setName('username') + .setDescription('Unique username of user') + .setRequired(true) + ), + run: async (interaction, lc=interaction.client.lc) => { + await interaction.deferReply() + + const username = interaction.options.getString('username') + const user = await lc.user(username); + + let streakInfo = 0; + let hasSolvedToday = false; + + if (user.matchedUser) { + ({ currentStreak: streakInfo, hasSolvedToday } = LeetCodeUtility.calculateStreak(user.matchedUser.submissionCalendar)); + } + + let streakMessage; + if (streakInfo > 0) { + if (hasSolvedToday) { + streakMessage = `šŸŽ‰ **${username}** has solved a problem for ${streakInfo} consecutive days! Great work, keep it up! šŸ’Ŗ`; + } else { + streakMessage = `āš ļø **${username}** has solved a problem for ${streakInfo} consecutive days! Solve today's problem to maintain your streak and prevent it from resetting! šŸ”„`; + } + } else { + streakMessage = `āŒ **${username}** does not have a streak yet. Start solving problems today to build your streak! šŸš€`; + } + + return interaction.followUp(streakMessage); + + } +} \ No newline at end of file diff --git a/interactions/commands/topics.js b/interactions/commands/topics.js new file mode 100644 index 0000000..e743e84 --- /dev/null +++ b/interactions/commands/topics.js @@ -0,0 +1,31 @@ +import { SlashCommandBuilder, ActionRowBuilder, ButtonBuilder, ButtonStyle } from "discord.js"; +import LeetCodeUtility from "../../utility/LeetCode.js"; + +const topics = [ + 'Array', 'String', 'Hash Table', 'Dynamic Programming', 'Math', + 'Sorting', 'Greedy', 'Depth-First Search', 'Binary Search', 'Database', + 'Breadth-First Search', 'Tree', 'Matrix', 'Two Pointers', 'Bit Manipulation', + 'Stack', 'Design', 'Heap (Priority Queue)', 'Graph', 'Simulation' +]; + +export default { + data: new SlashCommandBuilder() + .setName('topics') + .setDescription('Shows a list of LeetCode topics to choose from'), + run: async (interaction) => { + const chunkedTopics = LeetCodeUtility.chunkArray(topics, 5); + + const rows = chunkedTopics.map(chunk => + new ActionRowBuilder().addComponents( + chunk.map(topic => + new ButtonBuilder() + .setCustomId(`topic_${topic.toLowerCase().replace(/\s+/g, '-')}`) + .setLabel(topic) + .setStyle(ButtonStyle.Secondary) + ) + ) + ); + + return interaction.reply({ content: 'Choose a topic to get a random question:', components: rows }) + } +} \ No newline at end of file diff --git a/interactions/commands/user.js b/interactions/commands/user.js new file mode 100644 index 0000000..8da4202 --- /dev/null +++ b/interactions/commands/user.js @@ -0,0 +1,60 @@ +import { SlashCommandBuilder, EmbedBuilder } from "discord.js"; + +export default { + data: new SlashCommandBuilder() + .setName('user') + .setDescription(' Shows user Info') + .addStringOption( + (option) => option + .setName('username') + .setDescription('Unique username of user') + .setRequired(true) + ), + run: async (interaction, lc=interaction.client.lc) => { + await interaction.deferReply() + const username = interaction.options.getString('username') + + const [userInfo, contestInfo] = await Promise.all([ + lc.user(username), + lc.user_contest_info(username) + ]); + + if (!userInfo.matchedUser) { + return interaction.followUp({ content: `User "${username}" not found.`}); + } + + const user = userInfo.matchedUser; + const profile = user.profile; + const submitStats = user.submitStats; + + const embed = new EmbedBuilder() + .setColor('#FFD700') // Gold color for the embed + .setTitle(`LeetCode Profile: **${username}**`) + .setThumbnail(profile.userAvatar) + .addFields( + { name: 'šŸ‘¤ Real Name', value: profile.realName || '*Not provided*', inline: true }, + { name: 'šŸ† Ranking', value: profile.ranking ? profile.ranking.toString() : '*Not ranked*', inline: true }, + { name: 'šŸŒ Country', value: profile.countryName || '*Not provided*', inline: true }, + { name: 'šŸ¢ Company', value: profile.company || '*Not provided*', inline: true }, + { name: 'šŸŽ“ School', value: profile.school || '*Not provided*', inline: true }, + { name: '\u200B', value: 'ā¬‡ļø **Problem Solving Stats**', inline: false }, + { name: 'šŸŸ¢ Easy', value: `Solved: ${submitStats.acSubmissionNum[1].count} / ${submitStats.totalSubmissionNum[1].count}`, inline: true }, + { name: 'šŸŸ  Medium', value: `Solved: ${submitStats.acSubmissionNum[2].count} / ${submitStats.totalSubmissionNum[2].count}`, inline: true }, + { name: 'šŸ”“ Hard', value: `Solved: ${submitStats.acSubmissionNum[3].count} / ${submitStats.totalSubmissionNum[3].count}`, inline: true }, + { name: 'šŸ“Š Total', value: `Solved: ${submitStats.acSubmissionNum[0].count} / ${submitStats.totalSubmissionNum[0].count}`, inline: true } + ); + + if (contestInfo.userContestRanking) { + embed.addFields( + { name: 'šŸš© **Contest Info**', value: `\`\`\`Rating: ${Math.round(contestInfo.userContestRanking.rating)}\nRanking: ${contestInfo.userContestRanking.globalRanking}\nTop: ${contestInfo.userContestRanking.topPercentage.toFixed(2)}%\nAttended: ${contestInfo.userContestRanking.attendedContestsCount}\`\`\`` } + ); + } + + if (user.badges && user.badges.length > 0) { + const badgeNames = user.badges.map(badge => badge.displayName).join('\nā€¢'); + embed.addFields({ name: 'šŸ… Badges', value: "ā€¢" + badgeNames, inline: false }); + } + + return interaction.followUp({ embeds: [ embed ]}) + } +} \ No newline at end of file diff --git a/keep_alive.js b/utility/KeepAlive.js similarity index 99% rename from keep_alive.js rename to utility/KeepAlive.js index 5243f4c..e26cba8 100644 --- a/keep_alive.js +++ b/utility/KeepAlive.js @@ -7,4 +7,4 @@ export default function keepAlive() { }).listen(8080); console.log("Server is running on port 8080"); -} +} \ No newline at end of file diff --git a/utility/LeetCode.js b/utility/LeetCode.js new file mode 100644 index 0000000..074d5d2 --- /dev/null +++ b/utility/LeetCode.js @@ -0,0 +1,56 @@ +import axios from 'axios' + +function calculateStreak(submissionCalendar) { + submissionCalendar = JSON.parse(submissionCalendar); + + const datesWithSubmissions = Object.entries(submissionCalendar) + .filter(([ts, count]) => count > 0) + .map(([ts]) => new Date(ts * 1000)) + .sort((a, b) => a - b); + + if (datesWithSubmissions.length === 0) return 0; + + let currentStreak = 0; + let hasSolvedToday = false; + const currentDate = new Date(); + const lastSubmissionDate = datesWithSubmissions[datesWithSubmissions.length - 1]; + // Check if the last submission was today + if (lastSubmissionDate.setHours(0, 0, 0, 0) === currentDate.setHours(0, 0, 0, 0)) { + hasSolvedToday = true; + currentDate.setDate(currentDate.getDate() + 1); + } + // Calculate streak + for (let i = datesWithSubmissions.length - 1; i >= 0; i--) { + currentDate.setDate(currentDate.getDate() - 1); + if (datesWithSubmissions[i].setHours(0, 0, 0, 0) === currentDate.setHours(0, 0, 0, 0)) { + currentStreak += 1; + } else { + break; + } + } + return { currentStreak, hasSolvedToday }; +} + +async function fetchLeetCodeProblems() { + try { + const response = await axios.get('https://leetcode.com/api/problems/all/'); + return response.data.stat_status_pairs.filter(problem => !problem.paid_only); + } catch (error) { + console.error('Error fetching LeetCode problems:', error); + } +} + +function chunkArray(array, size) { + const chunked = []; + for (let i = 0; i < array.length; i += size) { + chunked.push(array.slice(i, i + size)); + } + return chunked; +} + + +export default { + calculateStreak, + fetchLeetCodeProblems, + chunkArray +} \ No newline at end of file