Initial Code

This commit is contained in:
2025-11-23 13:22:13 -05:00
parent b16d4adfd2
commit c3e52d6a03
96 changed files with 7088 additions and 135 deletions

View 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
View 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);
}
}
}
}

View 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." });
}
}
}

View 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." });
}
}
}

View 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);
}
}

View 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`" });
}
}
}

View 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." });
}
}
}

View 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);
}
}
}
}