Initial Code
This commit is contained in:
252
src/commands/users/brithday.ts
Normal file
252
src/commands/users/brithday.ts
Normal file
@@ -0,0 +1,252 @@
|
||||
import BotCommand from "../../libs/BotCommand";
|
||||
import BotClient from "../../libs/BotClient";
|
||||
import { ChatInputCommandInteraction, MessageFlags } from "discord.js";
|
||||
|
||||
export default class BirthdayCommand extends BotCommand {
|
||||
constructor() {
|
||||
super("bday", "Birthday Command", "/bday");
|
||||
|
||||
this.data.addSubcommand(subcommand =>
|
||||
subcommand.setName("set")
|
||||
.setDescription("Set your birthday")
|
||||
.addNumberOption(option =>
|
||||
option.setName("day")
|
||||
.setDescription("Your birthday day (1-31)")
|
||||
.setRequired(true)
|
||||
)
|
||||
.addNumberOption(option =>
|
||||
option.setName("month")
|
||||
.setDescription("Your birthday month (1-12)")
|
||||
.setRequired(true)
|
||||
)
|
||||
.addStringOption(option =>
|
||||
option.setName("timezone")
|
||||
.setDescription("Your timezone (e.g., 'EST', GMT+2, etc.)")
|
||||
.setRequired(false)
|
||||
)
|
||||
);
|
||||
|
||||
this.data.addSubcommand(subcommand =>
|
||||
subcommand.setName("get")
|
||||
.setDescription("Get your birthday")
|
||||
.addUserOption(option =>
|
||||
option.setName("user")
|
||||
.setDescription("The user whose birthday you want to get")
|
||||
.setRequired(false)
|
||||
)
|
||||
);
|
||||
|
||||
this.data.addSubcommand(subcommand =>
|
||||
subcommand.setName("remove")
|
||||
.setDescription("Remove your birthday")
|
||||
);
|
||||
|
||||
this.data.addSubcommand(subcommand =>
|
||||
subcommand.setName("list")
|
||||
.setDescription("List all birthdays")
|
||||
);
|
||||
}
|
||||
|
||||
dateToEpooch(day: number, month: number, timezone?: string): number {
|
||||
const now = new Date();
|
||||
const currentYear = now.getFullYear();
|
||||
|
||||
let birthdayDate = new Date(currentYear, month - 1, day, 0, 0, 0, 0);
|
||||
|
||||
if (birthdayDate < now) {
|
||||
birthdayDate = new Date(currentYear + 1, month - 1, day, 0, 0, 0, 0);
|
||||
}
|
||||
|
||||
if (timezone) {
|
||||
const offsetHours = this.getTimezoneOffset(timezone);
|
||||
birthdayDate.setHours(birthdayDate.getHours() - offsetHours);
|
||||
}
|
||||
|
||||
return Math.floor(birthdayDate.getTime() / 1000);
|
||||
}
|
||||
|
||||
getTimezoneOffset(timezone: string): number {
|
||||
const tz = timezone.toLowerCase();
|
||||
|
||||
// Handle GMT+/-N format
|
||||
const gmtMatch = tz.match(/^gmt([+-])(\d+)$/);
|
||||
if (gmtMatch) {
|
||||
const sign = gmtMatch[1] === '+' ? 1 : -1;
|
||||
const hours = parseInt(gmtMatch[2]);
|
||||
return sign * hours;
|
||||
}
|
||||
|
||||
// Handle UTC+/-N format
|
||||
const utcMatch = tz.match(/^utc([+-])(\d+)$/);
|
||||
if (utcMatch) {
|
||||
const sign = utcMatch[1] === '+' ? 1 : -1;
|
||||
const hours = parseInt(utcMatch[2]);
|
||||
return sign * hours;
|
||||
}
|
||||
|
||||
// Predefined timezone offsets (in hours from UTC)
|
||||
const offsets: { [key: string]: number } = {
|
||||
"est": -5, // Eastern Standard Time
|
||||
"edt": -4, // Eastern Daylight Time
|
||||
"cst": -6, // Central Standard Time
|
||||
"cdt": -5, // Central Daylight Time
|
||||
"mst": -7, // Mountain Standard Time
|
||||
"mdt": -6, // Mountain Daylight Time
|
||||
"pst": -8, // Pacific Standard Time
|
||||
"pdt": -7, // Pacific Daylight Time
|
||||
"gmt": 0, // Greenwich Mean Time
|
||||
"utc": 0 // Coordinated Universal Time
|
||||
};
|
||||
|
||||
return offsets[tz] || 0; // Default to UTC if not found
|
||||
}
|
||||
|
||||
async setCommand(Discord: any, client: BotClient, interaction: ChatInputCommandInteraction): Promise<any> {
|
||||
const day = interaction.options.getNumber("day")!;
|
||||
const month = interaction.options.getNumber("month")!;
|
||||
|
||||
let timezone = interaction.options.getString("timezone");
|
||||
timezone = timezone ? timezone.trim() : null;
|
||||
timezone = timezone ? timezone.toLowerCase() : null;
|
||||
|
||||
// Validate timezone format to lowercase alphanumeric and special characters
|
||||
if (timezone && !/^[a-zA-Z0-9+_-]+$/.test(timezone)) {
|
||||
return interaction.reply({
|
||||
content: "Invalid timezone format. Please use alphanumeric characters, plus (+), underscore (_), or hyphen (-).",
|
||||
flags: MessageFlags.Ephemeral
|
||||
});
|
||||
}
|
||||
|
||||
if (isNaN(day) || isNaN(month)) {
|
||||
return interaction.reply({
|
||||
content: "Invalid date provided. Please ensure the day and month are numbers.",
|
||||
flags: MessageFlags.Ephemeral
|
||||
});
|
||||
}
|
||||
|
||||
if (day < 1 || day > 31 || month < 1 || month > 12) {
|
||||
return interaction.reply({
|
||||
content: "Invalid date provided. Please ensure the day is between 1-31 and the month is between 1-12.",
|
||||
flags: MessageFlags.Ephemeral
|
||||
});
|
||||
}
|
||||
|
||||
client.config.setBday(interaction.user.id, { day, month, timezone: timezone || undefined });
|
||||
|
||||
return interaction.reply({
|
||||
content: `Your birthday has been set to ${day}/${month} ${timezone ? `in timezone ${timezone}` : ''}.\nYour next birthday will be on <t:${this.dateToEpooch(day, month, timezone || undefined)}:D>.`,
|
||||
flags: MessageFlags.Ephemeral
|
||||
});
|
||||
}
|
||||
|
||||
async getCommand(Discord: any, client: BotClient, interaction: ChatInputCommandInteraction): Promise<any> {
|
||||
const user = interaction.options.getUser("user") || interaction.user;
|
||||
const bday = client.config.getBday(user.id);
|
||||
|
||||
if (!bday) {
|
||||
return interaction.reply({
|
||||
content: `\`${user.username}\` has not set a birthday.`,
|
||||
flags: MessageFlags.Ephemeral
|
||||
});
|
||||
}
|
||||
|
||||
const timezone = bday.timezone ? ` in timezone ${bday.timezone}` : '';
|
||||
return interaction.reply({
|
||||
content: `
|
||||
\`${user.username}\`'s birthday is set to ${bday.month}/${bday.day}${timezone}.\nYour next birthday will be on <t:${this.dateToEpooch(bday.day, bday.month, bday.timezone)}:D>.
|
||||
`,
|
||||
flags: MessageFlags.Ephemeral
|
||||
});
|
||||
}
|
||||
|
||||
async removeCommand(Discord: any, client: BotClient, interaction: ChatInputCommandInteraction): Promise<any> {
|
||||
const user = interaction.user;
|
||||
const bday = client.config.getBday(user.id);
|
||||
if (!bday) {
|
||||
return interaction.reply({
|
||||
content: `\`${user.username}\`, you have not set a birthday to remove.`,
|
||||
flags: MessageFlags.Ephemeral
|
||||
});
|
||||
}
|
||||
|
||||
client.config.removeBday(user.id);
|
||||
return interaction.reply({
|
||||
content: `\`${user.username}\`, your birthday has been removed.`,
|
||||
flags: MessageFlags.Ephemeral
|
||||
});
|
||||
}
|
||||
|
||||
async listCommand(Discord: any, client: BotClient, interaction: ChatInputCommandInteraction): Promise<any> {
|
||||
const bdays = client.config.getBdays();
|
||||
if (!bdays || Object.keys(bdays).length === 0) {
|
||||
return interaction.reply({
|
||||
content: "No birthdays have been set yet.",
|
||||
flags: MessageFlags.Ephemeral
|
||||
});
|
||||
}
|
||||
|
||||
// Upcoming birthdays
|
||||
let response = "";
|
||||
const today = new Date();
|
||||
const upcoming = Object.entries(bdays).map(([userId, bday]) => {
|
||||
const nextBirthday = new Date(today.getFullYear(), bday.month - 1, bday.day);
|
||||
if (nextBirthday < today) {
|
||||
nextBirthday.setFullYear(today.getFullYear() + 1); // Move to next year if birthday has passed
|
||||
}
|
||||
return { userId, bday, nextBirthday };
|
||||
}).sort((a, b) => a.nextBirthday.getTime() - b.nextBirthday.getTime());
|
||||
|
||||
// Filter out birthdays that are more than 30 days away
|
||||
const thirtyDaysFromNow = new Date(today);
|
||||
thirtyDaysFromNow.setDate(today.getDate() + 30);
|
||||
const filteredUpcoming = upcoming.filter(b => b.nextBirthday <= thirtyDaysFromNow);
|
||||
if (filteredUpcoming.length === 0) {
|
||||
response = "No upcoming birthdays within the next 30 days.";
|
||||
return interaction.reply({
|
||||
content: response,
|
||||
flags: MessageFlags.Ephemeral
|
||||
});
|
||||
}
|
||||
|
||||
for (const { userId, bday, nextBirthday } of upcoming) {
|
||||
const user = await client.users.fetch(userId);
|
||||
response += `\`${user.username}\`: <t:${Math.floor(nextBirthday.getTime() / 1000)}:D>\n`;
|
||||
}
|
||||
|
||||
if (upcoming.length === 0) {
|
||||
response = "No upcoming birthdays.";
|
||||
}
|
||||
|
||||
const embed = new Discord.EmbedBuilder()
|
||||
.setTitle("Upcoming Birthdays")
|
||||
.setDescription(response)
|
||||
.setColor("Blue")
|
||||
.setTimestamp();
|
||||
|
||||
return interaction.reply({
|
||||
embeds: [embed],
|
||||
flags: MessageFlags.Ephemeral
|
||||
});
|
||||
}
|
||||
|
||||
override async execute(Discord: any, client: BotClient, interaction: ChatInputCommandInteraction): Promise<any> {
|
||||
|
||||
const subcommand = interaction.options.getSubcommand();
|
||||
|
||||
switch (subcommand) {
|
||||
case "set":
|
||||
return this.setCommand(Discord, client, interaction);
|
||||
case "get":
|
||||
return this.getCommand(Discord, client, interaction);
|
||||
case "remove":
|
||||
return this.removeCommand(Discord, client, interaction);
|
||||
case "list":
|
||||
return this.listCommand(Discord, client, interaction);
|
||||
default:
|
||||
return interaction.reply({
|
||||
content: "Unknown subcommand.",
|
||||
flags: MessageFlags.Ephemeral
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
121
src/commands/users/daily.ts
Normal file
121
src/commands/users/daily.ts
Normal file
@@ -0,0 +1,121 @@
|
||||
import BotCommand from "../../libs/BotCommand";
|
||||
import BotClient from "../../libs/BotClient";
|
||||
import { ChatInputCommandInteraction, MessageFlags, EmbedBuilder, AttachmentBuilder, ActionRowBuilder, ButtonBuilder, ButtonStyle, TextChannel } from "discord.js";
|
||||
import QuestionScheduler from "../../libs/QuestionScheduler";
|
||||
import AnswerGrader from "../../libs/AnswerGrader";
|
||||
|
||||
const SUBJECTS = ["Mathematics", "Physics", "Chemistry", "Biology", "Computer Science", "Engineering"];
|
||||
|
||||
export default class DailyCommand extends BotCommand {
|
||||
constructor() {
|
||||
super("daily", "Answer today's daily STEM question", "/daily");
|
||||
this.data.addStringOption(option =>
|
||||
option.setName("subject")
|
||||
.setDescription("Choose a subject")
|
||||
.addChoices(
|
||||
{ name: "Mathematics", value: "mathematics" },
|
||||
{ name: "Physics", value: "physics" },
|
||||
{ name: "Chemistry", value: "chemistry" },
|
||||
{ name: "Organic Chemistry", value: "organic chemistry" },
|
||||
{ name: "Biology", value: "biology" },
|
||||
{ name: "Computer Science", value: "computer science" },
|
||||
{ name: "Engineering", value: "engineering" }
|
||||
)
|
||||
.setRequired(true)
|
||||
);
|
||||
}
|
||||
|
||||
override async execute(Discord: any, client: BotClient, interaction: ChatInputCommandInteraction): Promise<any> {
|
||||
try {
|
||||
await interaction.deferReply({ flags: MessageFlags.Ephemeral });
|
||||
|
||||
const subject = interaction.options.getString("subject", true);
|
||||
|
||||
const scheduler = new QuestionScheduler();
|
||||
await scheduler.initialize();
|
||||
|
||||
const question = await scheduler.getQuestionForPeriod(subject, "daily");
|
||||
|
||||
if (!question) {
|
||||
return interaction.editReply({ content: `Failed to load today's ${subject} question. Please try again later.` });
|
||||
}
|
||||
|
||||
const hasAnswered = await scheduler.hasUserAnswered(interaction.user.id, question.id);
|
||||
if (hasAnswered) {
|
||||
return interaction.editReply({ content: "You've already submitted an answer for today's question! Check back tomorrow." });
|
||||
}
|
||||
|
||||
const imagePath = await client.typst.renderToImage(question.typst_source);
|
||||
|
||||
if (!imagePath) {
|
||||
// Log error to logs channel
|
||||
const logsChannelId = process.env.LOGS_CHANNEL_ID;
|
||||
if (logsChannelId) {
|
||||
try {
|
||||
const logsChannel = await client.channels.fetch(logsChannelId) as TextChannel;
|
||||
if (logsChannel?.isTextBased()) {
|
||||
const errorEmbed = new EmbedBuilder()
|
||||
.setTitle("❌ Typst Render Error - Daily Question")
|
||||
.setColor(0xef4444)
|
||||
.addFields(
|
||||
{ name: "Subject", value: subject, inline: true },
|
||||
{ name: "Question ID", value: question.id, inline: true },
|
||||
{ name: "Topic", value: question.topic, inline: false },
|
||||
{ name: "User", value: `<@${interaction.user.id}>`, inline: true },
|
||||
{ name: "Error", value: "Failed to render Typst image", inline: false }
|
||||
)
|
||||
.setTimestamp();
|
||||
await logsChannel.send({ embeds: [errorEmbed] });
|
||||
}
|
||||
} catch (logErr) {
|
||||
console.error("Failed to send error to logs channel:", logErr);
|
||||
}
|
||||
}
|
||||
return interaction.editReply({
|
||||
content: `❌ An error occurred while generating the question image. This has been reported to administrators.`
|
||||
});
|
||||
}
|
||||
|
||||
const embed = new EmbedBuilder()
|
||||
.setTitle(`📅 Daily ${subject} Question`)
|
||||
.setDescription(`**Topic:** ${question.topic}\n**Difficulty:** ${question.difficulty_rating}`)
|
||||
.setColor(0x60a5fa)
|
||||
.setImage(`attachment://daily_${subject}.png`)
|
||||
.setFooter({ text: `Resets at 12 AM local time` })
|
||||
.setTimestamp();
|
||||
|
||||
const attachment = new AttachmentBuilder(imagePath, { name: `daily_${subject}.png` });
|
||||
|
||||
const row = new ActionRowBuilder<ButtonBuilder>()
|
||||
.addComponents(
|
||||
new ButtonBuilder()
|
||||
.setCustomId(`daily_answer_${question.id}`)
|
||||
.setLabel("Submit Answer")
|
||||
.setStyle(ButtonStyle.Primary)
|
||||
.setEmoji("✍️"),
|
||||
new ButtonBuilder()
|
||||
.setCustomId(`daily_report_${question.id}`)
|
||||
.setLabel("Report Issue")
|
||||
.setStyle(ButtonStyle.Danger)
|
||||
.setEmoji("⚠️")
|
||||
);
|
||||
|
||||
await interaction.editReply({
|
||||
embeds: [embed],
|
||||
files: [attachment],
|
||||
components: [row],
|
||||
});
|
||||
} catch (err) {
|
||||
console.error("Error executing daily command:", err);
|
||||
try {
|
||||
if (interaction.deferred || interaction.replied) {
|
||||
await interaction.editReply({ content: "An error occurred. Please try again later." });
|
||||
} else {
|
||||
await interaction.reply({ content: "An error occurred. Please try again later.", flags: MessageFlags.Ephemeral });
|
||||
}
|
||||
} catch (e) {
|
||||
console.error("Error sending error reply:", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
61
src/commands/users/joinTeam.ts
Normal file
61
src/commands/users/joinTeam.ts
Normal file
@@ -0,0 +1,61 @@
|
||||
import BotCommand from "../../libs/BotCommand";
|
||||
import BotClient from "../../libs/BotClient";
|
||||
import { ChatInputCommandInteraction, MessageFlags, EmbedBuilder } from "discord.js";
|
||||
import Team from "../../models/Team";
|
||||
import PointsManager from "../../libs/PointsManager";
|
||||
import Database from "../../libs/Database";
|
||||
|
||||
export default class JoinTeamCommand extends BotCommand {
|
||||
constructor() {
|
||||
super("jointeam", "Join a team", "/jointeam");
|
||||
|
||||
this.data.addStringOption(option =>
|
||||
option.setName("team")
|
||||
.setDescription("Team name to join")
|
||||
.setRequired(true)
|
||||
.setAutocomplete(true)
|
||||
);
|
||||
}
|
||||
|
||||
override async execute(Discord: any, client: BotClient, interaction: ChatInputCommandInteraction): Promise<any> {
|
||||
const db = Database.getInstance();
|
||||
if (!db.isConnected()) {
|
||||
return interaction.reply({ content: "❌ Database not connected. Points system unavailable.", flags: MessageFlags.Ephemeral });
|
||||
}
|
||||
|
||||
try {
|
||||
await interaction.deferReply({ flags: MessageFlags.Ephemeral });
|
||||
|
||||
const teamName = interaction.options.getString("team", true);
|
||||
const team = await Team.findOne({ name: teamName });
|
||||
|
||||
if (!team) {
|
||||
return interaction.editReply({ content: `❌ Team **${teamName}** not found.` });
|
||||
}
|
||||
|
||||
const result = await PointsManager.joinTeam(interaction.user.id, interaction.user.username, team._id.toString());
|
||||
|
||||
if (result.success) {
|
||||
const embed = new EmbedBuilder()
|
||||
.setTitle("✅ Team Joined!")
|
||||
.setColor(0x22c55e)
|
||||
.setDescription(result.message)
|
||||
.addFields(
|
||||
{ name: "Team", value: team.name, inline: true },
|
||||
{ name: "Members", value: team.memberCount.toString(), inline: true },
|
||||
{ name: "Team Points", value: `${team.adjustedPoints}`, inline: true }
|
||||
)
|
||||
.setFooter({ text: "Earn points for your team by completing questions!" })
|
||||
.setTimestamp();
|
||||
|
||||
return interaction.editReply({ content: null, embeds: [embed] });
|
||||
} else {
|
||||
return interaction.editReply({ content: `❌ ${result.message}` });
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
console.error("Error in join-team command:", error);
|
||||
return interaction.editReply({ content: "❌ An error occurred while joining the team." });
|
||||
}
|
||||
}
|
||||
}
|
||||
95
src/commands/users/leaderboard.ts
Normal file
95
src/commands/users/leaderboard.ts
Normal file
@@ -0,0 +1,95 @@
|
||||
import BotCommand from "../../libs/BotCommand";
|
||||
import BotClient from "../../libs/BotClient";
|
||||
import { ChatInputCommandInteraction, MessageFlags, EmbedBuilder, PermissionFlagsBits } from "discord.js";
|
||||
import PointsManager from "../../libs/PointsManager";
|
||||
import Database from "../../libs/Database";
|
||||
|
||||
export default class LeaderboardCommand extends BotCommand {
|
||||
constructor() {
|
||||
super("leaderboard", "View user or team leaderboards", "/leaderboard");
|
||||
|
||||
this.data.addStringOption(option =>
|
||||
option.setName("type")
|
||||
.setDescription("Leaderboard type")
|
||||
.addChoices(
|
||||
{ name: "Users", value: "users" },
|
||||
{ name: "Teams", value: "teams" }
|
||||
)
|
||||
.setRequired(true)
|
||||
);
|
||||
|
||||
this.data.addIntegerOption(option =>
|
||||
option.setName("limit")
|
||||
.setDescription("Number of entries to show (default: 10)")
|
||||
.setMinValue(5)
|
||||
.setMaxValue(25)
|
||||
.setRequired(false)
|
||||
);
|
||||
}
|
||||
|
||||
override async execute(Discord: any, client: BotClient, interaction: ChatInputCommandInteraction): Promise<any> {
|
||||
const db = Database.getInstance();
|
||||
if (!db.isConnected()) {
|
||||
return interaction.reply({ content: "❌ Database not connected. Points system unavailable.", flags: MessageFlags.Ephemeral });
|
||||
}
|
||||
|
||||
try {
|
||||
await interaction.deferReply();
|
||||
|
||||
const type = interaction.options.getString("type", true);
|
||||
const limit = interaction.options.getInteger("limit") || 10;
|
||||
|
||||
if (type === "users") {
|
||||
const users = await PointsManager.getLeaderboard(limit);
|
||||
|
||||
if (users.length === 0) {
|
||||
return interaction.editReply({ content: "No users found in the leaderboard yet." });
|
||||
}
|
||||
|
||||
const embed = new EmbedBuilder()
|
||||
.setTitle("🏆 User Leaderboard")
|
||||
.setColor(0xfacc15)
|
||||
.setDescription(users.map((u, i) => {
|
||||
const medal = i === 0 ? "🥇" : i === 1 ? "🥈" : i === 2 ? "🥉" : `**${i + 1}.**`;
|
||||
return `${medal} <@${u.userId}> - **${u.points}** points\n` +
|
||||
` 📊 Daily: ${u.dailyQuestionsCompleted} | Weekly: ${u.weeklyQuestionsCompleted}`;
|
||||
}).join("\n\n"))
|
||||
.setFooter({ text: "Complete questions to earn points!" })
|
||||
.setTimestamp();
|
||||
|
||||
return interaction.editReply({ embeds: [embed] });
|
||||
|
||||
} else {
|
||||
const teams = await PointsManager.getTeamLeaderboard(limit);
|
||||
|
||||
if (teams.length === 0) {
|
||||
return interaction.editReply({ content: "No teams found in the leaderboard yet." });
|
||||
}
|
||||
|
||||
const isAdmin = interaction.memberPermissions?.has(PermissionFlagsBits.ManageGuild) || false;
|
||||
|
||||
const embed = new EmbedBuilder()
|
||||
.setTitle("🏆 Team Leaderboard")
|
||||
.setColor(0x60a5fa)
|
||||
.setDescription(teams.map((t, i) => {
|
||||
const medal = i === 0 ? "🥇" : i === 1 ? "🥈" : i === 2 ? "🥉" : `**${i + 1}.**`;
|
||||
if (isAdmin) {
|
||||
return `${medal} **${t.name}** - **${t.adjustedPoints}** points (adjusted)\n` +
|
||||
` Members: ${t.memberCount} | Raw Points: ${t.points}`;
|
||||
} else {
|
||||
return `${medal} **${t.name}** - **${t.adjustedPoints}** points\n` +
|
||||
` Members: ${t.memberCount}`;
|
||||
}
|
||||
}).join("\n\n"))
|
||||
.setFooter({ text: "Smaller teams get bonus multipliers!" })
|
||||
.setTimestamp();
|
||||
|
||||
return interaction.editReply({ embeds: [embed] });
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
console.error("Error in leaderboard command:", error);
|
||||
return interaction.editReply({ content: "❌ An error occurred." });
|
||||
}
|
||||
}
|
||||
}
|
||||
32
src/commands/users/report.ts
Normal file
32
src/commands/users/report.ts
Normal file
@@ -0,0 +1,32 @@
|
||||
import BotCommand from "../../libs/BotCommand";
|
||||
import BotClient from "../../libs/BotClient";
|
||||
import { ChatInputCommandInteraction } from "discord.js";
|
||||
|
||||
export default class ReportCommand extends BotCommand {
|
||||
constructor() {
|
||||
super("report", "Report Command", "/report");
|
||||
this.data.addUserOption(option => option.setName("user").setDescription("User to report").setRequired(true));
|
||||
}
|
||||
|
||||
override async execute(Discord: any, client: BotClient, interaction: ChatInputCommandInteraction): Promise<any> {
|
||||
|
||||
const user = interaction.options.getUser('user');
|
||||
|
||||
const modal = new Discord.ModalBuilder()
|
||||
.setCustomId(`report-${user?.id}`)
|
||||
.setTitle("Report User")
|
||||
|
||||
const reasonInput = new Discord.TextInputBuilder()
|
||||
.setCustomId("reasonInput")
|
||||
.setLabel("Enter a reason")
|
||||
.setStyle(Discord.TextInputStyle.Paragraph)
|
||||
.setPlaceholder('Please be specific and include any evidence.\nCopy and paste any messages or message links.')
|
||||
.setRequired(true);
|
||||
|
||||
const firstActionRow = new Discord.ActionRowBuilder().addComponents(reasonInput);
|
||||
|
||||
modal.addComponents(firstActionRow);
|
||||
|
||||
return await interaction.showModal(modal);
|
||||
}
|
||||
}
|
||||
45
src/commands/users/solved.ts
Normal file
45
src/commands/users/solved.ts
Normal file
@@ -0,0 +1,45 @@
|
||||
import BotCommand from "../../libs/BotCommand";
|
||||
import BotClient from "../../libs/BotClient";
|
||||
import { ChatInputCommandInteraction, GuildMember, ThreadChannel } from "discord.js";
|
||||
|
||||
export default class SolvedCommand extends BotCommand {
|
||||
constructor() {
|
||||
super("solved", "Solved Command", "/solved");
|
||||
}
|
||||
|
||||
override async execute(Discord: any, client: BotClient, interaction: ChatInputCommandInteraction): Promise<any> {
|
||||
|
||||
await interaction.reply({ content: `⏳ One Moment Please. . .` });
|
||||
|
||||
const channel = interaction.channel as ThreadChannel;
|
||||
const isThreadOwner = channel?.ownerId === interaction.user.id;
|
||||
const StaffRoles = ["1310700035842768908", "1311937894306414632"];
|
||||
const member = interaction.member as GuildMember;
|
||||
const isStaff = member?.roles?.cache?.some(role => StaffRoles.includes(role.id));
|
||||
|
||||
if(!isThreadOwner && !isStaff) {
|
||||
return await interaction.editReply({ content: "You do not have permission to use this command." });
|
||||
}
|
||||
|
||||
const availableTags = (channel?.parent as any)?.availableTags;
|
||||
const channelTags = channel?.appliedTags;
|
||||
|
||||
if (!availableTags || !channelTags) {
|
||||
return await interaction.editReply({ content: "This command can only be used in thread channels." });
|
||||
}
|
||||
|
||||
const solvedTag = availableTags.find((tag: any) => tag.name === "Solved");
|
||||
|
||||
if (!solvedTag) {
|
||||
return await interaction.editReply({ content: "No 'Solved' tag found in this forum." });
|
||||
}
|
||||
|
||||
if(!channelTags.includes(solvedTag.id)) {
|
||||
await channel?.setAppliedTags([...channelTags, solvedTag.id]);
|
||||
return await interaction.editReply({ content: "This channel has been marked `Solved`" });
|
||||
} else {
|
||||
await channel?.setAppliedTags(channelTags.filter((tag: string) => tag !== solvedTag.id));
|
||||
return await interaction.editReply({ content: "This channel has been unmarked `Solved`" });
|
||||
}
|
||||
}
|
||||
}
|
||||
60
src/commands/users/stats.ts
Normal file
60
src/commands/users/stats.ts
Normal file
@@ -0,0 +1,60 @@
|
||||
import BotCommand from "../../libs/BotCommand";
|
||||
import BotClient from "../../libs/BotClient";
|
||||
import { ChatInputCommandInteraction, MessageFlags, EmbedBuilder } from "discord.js";
|
||||
import PointsManager from "../../libs/PointsManager";
|
||||
import Database from "../../libs/Database";
|
||||
|
||||
export default class StatsCommand extends BotCommand {
|
||||
constructor() {
|
||||
super("stats", "View your stats or another user's stats", "/stats");
|
||||
|
||||
this.data.addUserOption(option =>
|
||||
option.setName("user")
|
||||
.setDescription("User to view stats for (leave empty for yourself)")
|
||||
.setRequired(false)
|
||||
);
|
||||
}
|
||||
|
||||
override async execute(Discord: any, client: BotClient, interaction: ChatInputCommandInteraction): Promise<any> {
|
||||
const db = Database.getInstance();
|
||||
if (!db.isConnected()) {
|
||||
return interaction.reply({ content: "❌ Database not connected. Points system unavailable.", flags: MessageFlags.Ephemeral });
|
||||
}
|
||||
|
||||
try {
|
||||
await interaction.deferReply({ flags: MessageFlags.Ephemeral });
|
||||
|
||||
const targetUser = interaction.options.getUser("user") || interaction.user;
|
||||
const userStats = await PointsManager.getUserStats(targetUser.id);
|
||||
|
||||
if (!userStats) {
|
||||
return interaction.editReply({
|
||||
content: `${targetUser.id === interaction.user.id ? "You haven't" : `<@${targetUser.id}> hasn't`} completed any questions yet.`
|
||||
});
|
||||
}
|
||||
|
||||
const teamInfo = userStats.teamId ? (userStats.teamId as any).name : "No team";
|
||||
|
||||
const embed = new EmbedBuilder()
|
||||
.setTitle(`📊 Stats for ${targetUser.username}`)
|
||||
.setColor(0x60a5fa)
|
||||
.setThumbnail(targetUser.displayAvatarURL())
|
||||
.addFields(
|
||||
{ name: "Points", value: `**${userStats.points}**`, inline: true },
|
||||
{ name: "Team", value: teamInfo, inline: true },
|
||||
{ name: "Joined", value: `<t:${Math.floor(userStats.joinedAt.getTime() / 1000)}:R>`, inline: true },
|
||||
{ name: "Daily Questions", value: userStats.dailyQuestionsCompleted.toString(), inline: true },
|
||||
{ name: "Weekly Questions", value: userStats.weeklyQuestionsCompleted.toString(), inline: true },
|
||||
{ name: "Last Active", value: `<t:${Math.floor(userStats.lastActive.getTime() / 1000)}:R>`, inline: true }
|
||||
)
|
||||
.setFooter({ text: "Keep solving to climb the leaderboard!" })
|
||||
.setTimestamp();
|
||||
|
||||
return interaction.editReply({ embeds: [embed] });
|
||||
|
||||
} catch (error) {
|
||||
console.error("Error in stats command:", error);
|
||||
return interaction.editReply({ content: "❌ An error occurred." });
|
||||
}
|
||||
}
|
||||
}
|
||||
121
src/commands/users/weekly.ts
Normal file
121
src/commands/users/weekly.ts
Normal file
@@ -0,0 +1,121 @@
|
||||
import BotCommand from "../../libs/BotCommand";
|
||||
import BotClient from "../../libs/BotClient";
|
||||
import { ChatInputCommandInteraction, MessageFlags, EmbedBuilder, AttachmentBuilder, ActionRowBuilder, ButtonBuilder, ButtonStyle, TextChannel } from "discord.js";
|
||||
import QuestionScheduler from "../../libs/QuestionScheduler";
|
||||
import AnswerGrader from "../../libs/AnswerGrader";
|
||||
|
||||
const SUBJECTS = ["Mathematics", "Physics", "Chemistry", "Biology", "Computer Science", "Engineering"];
|
||||
|
||||
export default class WeeklyCommand extends BotCommand {
|
||||
constructor() {
|
||||
super("weekly", "Answer this week's weekly STEM question", "/weekly");
|
||||
this.data.addStringOption(option =>
|
||||
option.setName("subject")
|
||||
.setDescription("Choose a subject")
|
||||
.addChoices(
|
||||
{ name: "Mathematics", value: "mathematics" },
|
||||
{ name: "Physics", value: "physics" },
|
||||
{ name: "Chemistry", value: "chemistry" },
|
||||
{ name: "Organic Chemistry", value: "organic chemistry" },
|
||||
{ name: "Biology", value: "biology" },
|
||||
{ name: "Computer Science", value: "computer science" },
|
||||
{ name: "Engineering", value: "engineering" }
|
||||
)
|
||||
.setRequired(true)
|
||||
);
|
||||
}
|
||||
|
||||
override async execute(Discord: any, client: BotClient, interaction: ChatInputCommandInteraction): Promise<any> {
|
||||
try {
|
||||
await interaction.deferReply({ flags: MessageFlags.Ephemeral });
|
||||
|
||||
const subject = interaction.options.getString("subject", true);
|
||||
|
||||
const scheduler = new QuestionScheduler();
|
||||
await scheduler.initialize();
|
||||
|
||||
const question = await scheduler.getQuestionForPeriod(subject, "weekly");
|
||||
|
||||
if (!question) {
|
||||
return interaction.editReply({ content: `Failed to load this week's ${subject} question. Please try again later.` });
|
||||
}
|
||||
|
||||
const hasAnswered = await scheduler.hasUserAnswered(interaction.user.id, question.id);
|
||||
if (hasAnswered) {
|
||||
return interaction.editReply({ content: "You've already submitted an answer for this week's question! Check back next Sunday." });
|
||||
}
|
||||
|
||||
const imagePath = await client.typst.renderToImage(question.typst_source);
|
||||
|
||||
if (!imagePath) {
|
||||
// Log error to logs channel
|
||||
const logsChannelId = process.env.LOGS_CHANNEL_ID;
|
||||
if (logsChannelId) {
|
||||
try {
|
||||
const logsChannel = await client.channels.fetch(logsChannelId) as TextChannel;
|
||||
if (logsChannel?.isTextBased()) {
|
||||
const errorEmbed = new EmbedBuilder()
|
||||
.setTitle("❌ Typst Render Error - Weekly Question")
|
||||
.setColor(0xef4444)
|
||||
.addFields(
|
||||
{ name: "Subject", value: subject, inline: true },
|
||||
{ name: "Question ID", value: question.id, inline: true },
|
||||
{ name: "Topic", value: question.topic, inline: false },
|
||||
{ name: "User", value: `<@${interaction.user.id}>`, inline: true },
|
||||
{ name: "Error", value: "Failed to render Typst image", inline: false }
|
||||
)
|
||||
.setTimestamp();
|
||||
await logsChannel.send({ embeds: [errorEmbed] });
|
||||
}
|
||||
} catch (logErr) {
|
||||
console.error("Failed to send error to logs channel:", logErr);
|
||||
}
|
||||
}
|
||||
return interaction.editReply({
|
||||
content: `❌ An error occurred while generating the question image. This has been reported to administrators.`
|
||||
});
|
||||
}
|
||||
|
||||
const embed = new EmbedBuilder()
|
||||
.setTitle(`🎓 Weekly ${subject} Question (PhD Level)`)
|
||||
.setDescription(`**Topic:** ${question.topic}\n**Difficulty:** ${question.difficulty_rating}`)
|
||||
.setColor(0xfacc15)
|
||||
.setImage(`attachment://weekly_${subject}.png`)
|
||||
.setFooter({ text: `Resets at 12 AM Sunday` })
|
||||
.setTimestamp();
|
||||
|
||||
const attachment = new AttachmentBuilder(imagePath, { name: `weekly_${subject}.png` });
|
||||
|
||||
const row = new ActionRowBuilder<ButtonBuilder>()
|
||||
.addComponents(
|
||||
new ButtonBuilder()
|
||||
.setCustomId(`weekly_answer_${question.id}`)
|
||||
.setLabel("Submit Answer")
|
||||
.setStyle(ButtonStyle.Primary)
|
||||
.setEmoji("✍️"),
|
||||
new ButtonBuilder()
|
||||
.setCustomId(`weekly_report_${question.id}`)
|
||||
.setLabel("Report Issue")
|
||||
.setStyle(ButtonStyle.Danger)
|
||||
.setEmoji("⚠️")
|
||||
);
|
||||
|
||||
await interaction.editReply({
|
||||
embeds: [embed],
|
||||
files: [attachment],
|
||||
components: [row],
|
||||
});
|
||||
} catch (err) {
|
||||
console.error("Error executing weekly command:", err);
|
||||
try {
|
||||
if (interaction.deferred || interaction.replied) {
|
||||
await interaction.editReply({ content: "An error occurred. Please try again later." });
|
||||
} else {
|
||||
await interaction.reply({ content: "An error occurred. Please try again later.", flags: MessageFlags.Ephemeral });
|
||||
}
|
||||
} catch (e) {
|
||||
console.error("Error sending error reply:", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user