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,110 @@
import BotCommand from "../../libs/BotCommand";
import BotClient from "../../libs/BotClient";
import { ChatInputCommandInteraction, MessageFlags, EmbedBuilder, PermissionFlagsBits } from "discord.js";
import User from "../../models/User";
import Team from "../../models/Team";
import Database from "../../libs/Database";
export default class AdminStatsCommand extends BotCommand {
constructor() {
super("serverstats", "View detailed statistics (Admin only)", "/serverstats");
this.data.addStringOption(option =>
option.setName("type")
.setDescription("Statistics type")
.addChoices(
{ name: "Most Active Users", value: "active" },
{ name: "Top Point Earners", value: "points" },
{ name: "Team Activity", value: "teams" },
{ name: "Overall Summary", value: "summary" }
)
.setRequired(true)
);
this.data.setDefaultMemberPermissions(PermissionFlagsBits.ManageGuild);
}
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 type = interaction.options.getString("type", true);
if (type === "active") {
const users = await User.find().sort({ lastActive: -1 }).limit(15).exec();
const embed = new EmbedBuilder()
.setTitle("📊 Most Active Users")
.setColor(0x22c55e)
.setDescription(users.map((u, i) =>
`**${i + 1}.** <@${u.userId}> - Last active <t:${Math.floor(u.lastActive.getTime() / 1000)}:R>\n` +
` Questions: ${u.dailyQuestionsCompleted + u.weeklyQuestionsCompleted} | Points: ${u.points}`
).join("\n\n"))
.setTimestamp();
return interaction.editReply({ embeds: [embed] });
} else if (type === "points") {
const users = await User.find().sort({ points: -1 }).limit(15).exec();
const embed = new EmbedBuilder()
.setTitle("💰 Top Point Earners")
.setColor(0xfacc15)
.setDescription(users.map((u, i) =>
`**${i + 1}.** <@${u.userId}> - **${u.points}** points\n` +
` Daily: ${u.dailyQuestionsCompleted} | Weekly: ${u.weeklyQuestionsCompleted}`
).join("\n\n"))
.setTimestamp();
return interaction.editReply({ embeds: [embed] });
} else if (type === "teams") {
const teams = await Team.find().sort({ memberCount: -1 }).exec();
const embed = new EmbedBuilder()
.setTitle("👥 Team Activity")
.setColor(0x60a5fa)
.setDescription(teams.map((t, i) =>
`**${i + 1}.** ${t.name} (Leader: <@${t.leaderId}>)\n` +
` Members: ${t.memberCount} | Points: ${t.points} (Adjusted: ${t.adjustedPoints})`
).join("\n\n"))
.setTimestamp();
return interaction.editReply({ embeds: [embed] });
} else if (type === "summary") {
const totalUsers = await User.countDocuments();
const totalTeams = await Team.countDocuments();
const totalPoints = await User.aggregate([{ $group: { _id: null, total: { $sum: "$points" } } }]);
const totalDaily = await User.aggregate([{ $group: { _id: null, total: { $sum: "$dailyQuestionsCompleted" } } }]);
const totalWeekly = await User.aggregate([{ $group: { _id: null, total: { $sum: "$weeklyQuestionsCompleted" } } }]);
const usersWithTeams = await User.countDocuments({ teamId: { $ne: null } });
const embed = new EmbedBuilder()
.setTitle("📈 Overall Summary")
.setColor(0x9333ea)
.addFields(
{ name: "Total Users", value: totalUsers.toString(), inline: true },
{ name: "Total Teams", value: totalTeams.toString(), inline: true },
{ name: "Users in Teams", value: `${usersWithTeams}/${totalUsers}`, inline: true },
{ name: "Total Points Earned", value: (totalPoints[0]?.total || 0).toString(), inline: true },
{ name: "Daily Questions Completed", value: (totalDaily[0]?.total || 0).toString(), inline: true },
{ name: "Weekly Questions Completed", value: (totalWeekly[0]?.total || 0).toString(), inline: true }
)
.setFooter({ text: "System Statistics" })
.setTimestamp();
return interaction.editReply({ embeds: [embed] });
}
} catch (error) {
console.error("Error in admin-stats command:", error);
return interaction.editReply({ content: "❌ An error occurred." });
}
}
}

View File

@@ -0,0 +1,245 @@
import BotCommand from "../../libs/BotCommand";
import BotClient from "../../libs/BotClient";
import { ChatInputCommandInteraction, PermissionFlagsBits, MessageFlags } from "discord.js";
import { ChartJSNodeCanvas } from "chartjs-node-canvas";
export default class GuildGrowthCommand extends BotCommand {
constructor() {
super("growth", "Generates a chart of guild member growth over time", "/growth");
this.data.setDefaultMemberPermissions(PermissionFlagsBits.ManageGuild);
this.data.addStringOption(option =>
option.setName("range")
.setDescription("Time range to show")
.addChoices(
{ name: "30 days", value: "30" },
{ name: "90 days", value: "90" },
{ name: "180 days", value: "180" },
{ name: "365 days", value: "365" },
{ name: "All time", value: "all" }
)
.setRequired(false)
);
this.data.addStringOption(option =>
option.setName("granularity")
.setDescription("Bucket granularity for the chart")
.addChoices(
{ name: "Daily", value: "daily" },
{ name: "Weekly", value: "weekly" },
{ name: "Monthly", value: "monthly" }
)
.setRequired(false)
);
this.data.addBooleanOption(option =>
option.setName("refresh")
.setDescription("Force refresh member cache from Discord")
.setRequired(false)
);
}
override async execute(Discord: any, client: BotClient, interaction: ChatInputCommandInteraction): Promise<any> {
const guild = interaction.guild;
if (!guild) {
return interaction.reply({ content: "This command must be run in a guild.", flags: MessageFlags.Ephemeral });
}
try {
await interaction.deferReply({ flags: MessageFlags.Ephemeral });
const refresh = interaction.options.getBoolean("refresh") || false;
const granularity = (interaction.options.getString("granularity") || "monthly") as string;
const members = await client.storage.fetchGuildMembers(client, guild.id, refresh);
const joinCounts = new Map<string, number>();
let earliest: Date | null = null;
let latest: Date | null = null;
members.forEach(member => {
const j = member.joinedAt;
if (!j) return;
const day = j.toISOString().slice(0, 10);
joinCounts.set(day, (joinCounts.get(day) || 0) + 1);
if (!earliest || j < earliest) earliest = j;
if (!latest || j > latest) latest = j;
});
if (!earliest || !latest) {
return interaction.editReply({ content: "No join data available for this guild." });
}
const labels: string[] = [];
const data: number[] = [];
const e = earliest as Date;
const l = latest as Date;
const rangeOpt = interaction.options.getString("range") || "365";
let start: Date;
if (rangeOpt === "all") {
start = new Date(Date.UTC(e.getUTCFullYear(), e.getUTCMonth(), e.getUTCDate()));
} else {
const days = Math.max(1, parseInt(rangeOpt, 10) || 365);
const desired = new Date(Date.now() - days * 24 * 60 * 60 * 1000);
const desiredUTC = new Date(Date.UTC(desired.getUTCFullYear(), desired.getUTCMonth(), desired.getUTCDate()));
const earliestUTC = new Date(Date.UTC(e.getUTCFullYear(), e.getUTCMonth(), e.getUTCDate()));
start = desiredUTC > earliestUTC ? desiredUTC : earliestUTC;
}
const end = new Date(Date.UTC(l.getUTCFullYear(), l.getUTCMonth(), l.getUTCDate()));
const dailyCum = new Map<string, number>();
let running = 0;
for (let cursor = new Date(start); cursor <= end; cursor.setUTCDate(cursor.getUTCDate() + 1)) {
const dayStr = cursor.toISOString().slice(0, 10);
running += (joinCounts.get(dayStr) || 0);
dailyCum.set(dayStr, running);
}
const getCumUpTo = (dateStr: string) => {
if (dailyCum.has(dateStr)) return dailyCum.get(dateStr) || 0;
const keys = Array.from(dailyCum.keys()).sort();
let res = 0;
for (const k of keys) {
if (k > dateStr) break;
res = dailyCum.get(k) || 0;
}
return res;
};
const MS_PER_DAY = 24 * 60 * 60 * 1000;
const MONTHS = ["Jan","Feb","Mar","Apr","May","Jun","Jul","Aug","Sep","Oct","Nov","Dec"];
if (granularity === "daily") {
const totalDays = Math.floor((end.getTime() - start.getTime()) / MS_PER_DAY) + 1;
const step = Math.max(1, Math.ceil(totalDays / 12));
for (let d = 0; d < totalDays; d += step) {
const dt = new Date(Date.UTC(start.getUTCFullYear(), start.getUTCMonth(), start.getUTCDate() + d));
const label = dt.toISOString().slice(0, 10);
const cum = getCumUpTo(label);
labels.push(label);
data.push(cum);
}
} else if (granularity === "weekly") {
const totalDays = Math.floor((end.getTime() - start.getTime()) / MS_PER_DAY) + 1;
const totalWeeks = Math.ceil(totalDays / 7);
const step = Math.max(1, Math.ceil(totalWeeks / 12));
for (let w = 0; w < totalWeeks; w += step) {
const weekStart = new Date(Date.UTC(start.getUTCFullYear(), start.getUTCMonth(), start.getUTCDate() + w * 7));
const weekEnd = new Date(Date.UTC(weekStart.getUTCFullYear(), weekStart.getUTCMonth(), weekStart.getUTCDate() + 6));
const label = `${MONTHS[weekStart.getUTCMonth()]} ${weekStart.getUTCDate()}, ${weekStart.getUTCFullYear()}`;
const monthEndStr = weekEnd.toISOString().slice(0, 10);
const cum = getCumUpTo(monthEndStr);
labels.push(label);
data.push(cum);
}
} else {
const monthDiff = (a: Date, b: Date) => (b.getUTCFullYear() - a.getUTCFullYear()) * 12 + (b.getUTCMonth() - a.getUTCMonth());
const totalMonths = monthDiff(start, end) + 1;
const step = Math.max(1, Math.ceil(totalMonths / 12));
for (let m = 0; m < totalMonths; m += step) {
const dt = new Date(Date.UTC(start.getUTCFullYear(), start.getUTCMonth() + m, 1));
const year = dt.getUTCFullYear();
const month = dt.getUTCMonth();
const label = `${MONTHS[month]} ${year}`;
const monthEnd = new Date(Date.UTC(year, month + 1, 0));
const monthEndStr = monthEnd.toISOString().slice(0, 10);
const cum = getCumUpTo(monthEndStr);
labels.push(label);
data.push(cum);
}
}
const width = 1200;
const height = 600;
const canvasBackground = "#0b1220";
const chartJSNodeCanvas = new ChartJSNodeCanvas({ width, height, backgroundColour: canvasBackground });
let trendData: number[] = [];
if (data.length >= 2) {
const n = data.length;
const xs = data.map((_, i) => i);
const ys = data;
const sumX = xs.reduce((a, b) => a + b, 0);
const sumY = ys.reduce((a, b) => a + b, 0);
const sumXX = xs.reduce((a, b) => a + b * b, 0);
const sumXY = xs.reduce((a, b, i) => a + b * ys[i], 0);
const denom = n * sumXX - sumX * sumX;
if (Math.abs(denom) < 1e-12) {
trendData = ys.slice();
} else {
const slope = (n * sumXY - sumX * sumY) / denom;
const intercept = (sumY - slope * sumX) / n;
trendData = xs.map(x => intercept + slope * x);
}
} else {
trendData = data.slice();
}
const configuration: any = {
type: "line",
data: {
labels,
datasets: [
{
label: "Members",
data,
fill: true,
borderColor: "#60a5fa",
backgroundColor: "rgba(96,165,250,0.12)",
tension: 0.2,
pointRadius: 0,
},
{
label: "Trend",
data: trendData,
fill: false,
borderColor: "#facc15",
borderDash: [6, 6],
tension: 0.1,
pointRadius: 0,
},
],
},
options: {
plugins: {
legend: { labels: { color: "#e6eef8" } },
title: { display: true, text: `Server Growth — ${guild.name}`, color: "#e6eef8", font: { size: 16 } },
},
scales: {
x: {
ticks: { maxRotation: 45, minRotation: 0, color: "#cbd5e1" },
grid: { color: "rgba(255,255,255,0.04)" },
},
y: {
beginAtZero: true,
ticks: { color: "#cbd5e1" },
grid: { color: "rgba(255,255,255,0.04)" },
},
},
backgroundColor: canvasBackground,
responsive: false,
},
};
try {
const buffer = await chartJSNodeCanvas.renderToBuffer(configuration);
const attachment = new Discord.AttachmentBuilder(buffer, { name: "guild-growth.png" });
await interaction.editReply({ content: `Guild growth for **${guild.name}** (members: ${guild.memberCount})`, files: [attachment] });
} catch (err) {
console.error("Error rendering guild growth chart:", err);
await interaction.editReply({ content: "Failed to generate chart. See logs for details." });
}
} catch (err) {
console.error("Error executing growth command:", err);
try {
if (interaction.deferred || interaction.replied) {
await interaction.editReply({ content: "An error occurred while generating the chart. Check logs for details." });
} else {
await interaction.reply({ content: "An error occurred while generating the chart. Check logs for details.", flags: MessageFlags.Ephemeral });
}
} catch (e) {
console.error("Error sending error reply:", e);
}
}
}
}

View File

@@ -0,0 +1,87 @@
import BotCommand from "../../libs/BotCommand";
import BotClient from "../../libs/BotClient";
import { ChatInputCommandInteraction, PermissionFlagsBits, MessageFlags, TextChannel } from "discord.js";
export default class PingHelpCommand extends BotCommand {
private helperChannelID: string;
constructor() {
super("pinghelp", "Pings the helper role", "/pinghelp");
this.helperChannelID = "1310993025958150166";
this.data.addRoleOption(option =>
option.setName("role")
.setDescription("The role to ping")
.setRequired(true)
);
this.data.addUserOption(option =>
option.setName("user")
.setDescription("The user to ping")
.setRequired(true)
);
this.data.addStringOption(option =>
option.setName("messageid")
.setDescription("The message ID to ping")
.setRequired(false)
);
this.data.setDefaultMemberPermissions(PermissionFlagsBits.ManageGuild);
}
override async execute(Discord: any, client: BotClient, interaction: ChatInputCommandInteraction): Promise<any> {
const role = interaction.options.getRole("role");
const user = interaction.options.getUser("user");
const messageId = interaction.options.getString("messageid");
if (!role) {
return interaction.reply({
content: "You must specify a role to ping.",
flags: MessageFlags.Ephemeral
});
}
const helpType = role.name.split(" ")[0];
const helperChannel = await interaction.guild?.channels.fetch(this.helperChannelID) as TextChannel;
if (!helperChannel) {
return interaction.reply({
content: "Helper channel not found.",
flags: MessageFlags.Ephemeral
});
}
if (messageId) {
const message = await interaction.channel?.messages.fetch(messageId).catch(() => null);
if (!message) {
return interaction.reply({
content: "Invalid message ID provided.",
flags: MessageFlags.Ephemeral
});
}
const messageLink = message.url;
helperChannel.send({
content: `${role} **\`${user?.username}\`** needs help => ${messageLink}. Please assist with their ${helpType} question.`,
});
return interaction.reply({
content: "Helper has been pinged!",
flags: MessageFlags.Ephemeral
});
} else {
helperChannel.send({
content: `${role} **\`${user?.username}\`** needs help => ${interaction.channel}. Please assist them.`,
});
return interaction.reply({
content: "Helper has been pinged!",
flags: MessageFlags.Ephemeral
});
}
}
}

View File

@@ -0,0 +1,118 @@
import BotCommand from "../../libs/BotCommand";
import BotClient from "../../libs/BotClient";
import { ChatInputCommandInteraction, MessageFlags, EmbedBuilder, PermissionFlagsBits } from "discord.js";
import QuestionScheduler from "../../libs/QuestionScheduler";
export default class RegenerateQuestionCommand extends BotCommand {
constructor() {
super("regenerate", "Regenerate a question for a specific subject and period (Admin only)", "/regenerate");
this.data.addStringOption(option =>
option.setName("period")
.setDescription("Question period")
.addChoices(
{ name: "Daily", value: "daily" },
{ name: "Weekly", value: "weekly" }
)
.setRequired(true)
);
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)
);
this.data.setDefaultMemberPermissions(PermissionFlagsBits.ManageGuild);
}
override async execute(Discord: any, client: BotClient, interaction: ChatInputCommandInteraction): Promise<any> {
try {
await interaction.deferReply({ flags: MessageFlags.Ephemeral });
const period = interaction.options.getString("period", true) as "daily" | "weekly";
const subject = interaction.options.getString("subject", true);
const scheduler = new QuestionScheduler();
await scheduler.initialize();
const embed = new EmbedBuilder()
.setTitle("🔄 Regenerating Question...")
.setDescription(`Generating new ${period} question for **${subject}**...`)
.setColor(0x60a5fa)
.setTimestamp();
await interaction.editReply({ embeds: [embed] });
// Force regenerate by clearing cache for this period
const regenerated = await scheduler.forceRegenerateQuestion(subject, period);
if (!regenerated) {
const errorEmbed = new EmbedBuilder()
.setTitle("❌ Regeneration Failed")
.setDescription(`Failed to regenerate ${period} question for **${subject}**. Check console logs for details.`)
.setColor(0xef4444)
.setTimestamp();
return interaction.editReply({ embeds: [errorEmbed] });
}
const successEmbed = new EmbedBuilder()
.setTitle("✅ Question Regenerated")
.setDescription(`Successfully regenerated ${period} question for **${subject}**!`)
.addFields(
{ name: "Topic", value: regenerated.topic, inline: true },
{ name: "Difficulty", value: regenerated.difficulty_rating, inline: true },
{ name: "Question ID", value: regenerated.id, inline: false }
)
.setColor(0x22c55e)
.setFooter({ text: `Users can now access the new question via /${period}` })
.setTimestamp();
await interaction.editReply({ embeds: [successEmbed] });
// Log to logs channel
const logsChannelId = process.env.LOGS_CHANNEL_ID;
if (logsChannelId) {
try {
const logsChannel = await client.channels.fetch(logsChannelId);
if (logsChannel?.isTextBased()) {
const logEmbed = new EmbedBuilder()
.setTitle("🔄 Question Regenerated")
.setColor(0x60a5fa)
.addFields(
{ name: "Period", value: period, inline: true },
{ name: "Subject", value: subject, inline: true },
{ name: "Admin", value: `<@${interaction.user.id}>`, inline: true },
{ name: "New Topic", value: regenerated.topic, inline: false },
{ name: "Question ID", value: regenerated.id, inline: false }
)
.setTimestamp();
await (logsChannel as any).send({ embeds: [logEmbed] });
}
} catch (logErr) {
console.error("Failed to send log to logs channel:", logErr);
}
}
} catch (error) {
console.error("Error in regenerate command:", error);
const errorEmbed = new EmbedBuilder()
.setTitle("❌ Error")
.setDescription("An unexpected error occurred while regenerating the question.")
.setColor(0xef4444)
.setTimestamp();
return interaction.editReply({ embeds: [errorEmbed] });
}
}
}

View File

@@ -0,0 +1,210 @@
import BotCommand from "../../libs/BotCommand";
import BotClient from "../../libs/BotClient";
import { ChatInputCommandInteraction, PermissionFlagsBits, MessageFlags, TextChannel } from "discord.js";
export default class SelectRolesCommand extends BotCommand {
constructor() {
super("selectroles", "Post the Embeds for roles", "/selectroles");
this.data.setDefaultMemberPermissions(PermissionFlagsBits.ManageGuild);
}
async languageRolesExecute(Discord: any, client: BotClient, interaction: ChatInputCommandInteraction): Promise<void> {
const selectionMenu = new Discord.StringSelectMenuBuilder()
.setCustomId('select-roles-language')
.setPlaceholder('Select the roles you want to add')
.setMinValues(0)
.setMaxValues(client.config.getLanguageRoles().length);
client.config.getLanguageRoles().forEach((role: any) => {
const option = new Discord.StringSelectMenuOptionBuilder()
.setLabel(role.name)
.setValue(role.id)
.setDescription(`If you can speak ${role.name}`)
.setEmoji(role.emoji);
selectionMenu.addOptions(option);
});
const row = new Discord.ActionRowBuilder()
.addComponents(selectionMenu);
const embed = new Discord.EmbedBuilder()
.setColor("#2b2d31")
.setTitle("Select the language roles that apply to you")
.setDescription(`
:flag_es: - If you can speak Spanish.
:flag_fr: - If you can speak French.
:flag_de: - If you can speak German.
:flag_cn: - If you can speak Chinese.
:flag_ru: - If you can speak Russian.
:flag_il: - If you can speak Hebrew.
:flag_sa: - If you can speak Arabic.
:flag_in: - If you can speak Hindi.
:flag_it: - If you can speak Italian.
:flag_pt: - If you can speak Portuguese.
`)
.setFooter({ text: "You can select multiple roles" })
.setTimestamp();
await (interaction.channel as TextChannel)?.send({ embeds: [embed], components: [row] });
}
async educationLevelRoles(Discord: any, client: BotClient, interaction: ChatInputCommandInteraction): Promise<void> {
const selectionMenu = new Discord.StringSelectMenuBuilder()
.setCustomId('select-roles-education')
.setPlaceholder('Select the roles you want to add')
.setMinValues(0)
.setMaxValues(client.config.getEducationRoles().length);
client.config.getEducationRoles().forEach((role: any) => {
const option = new Discord.StringSelectMenuOptionBuilder()
.setLabel(role.name)
.setValue(role.id)
.setDescription(`If you are a ${role.name}`)
.setEmoji(role.emoji);
selectionMenu.addOptions(option);
});
const row = new Discord.ActionRowBuilder()
.addComponents(selectionMenu);
const embed = new Discord.EmbedBuilder()
.setColor("#2b2d31")
.setTitle("Select the education roles that apply to you")
.setDescription(`
:school: - If you are a High School Student
:mortar_board: - If you are a Undergraduate Student
👨‍🎓 - If you are a Postgraduate
👨‍🔬 - If you are a PhD Student
👨‍🏫 - If you are a Doctorate
`)
.setFooter({ text: "You can select multiple roles" })
.setTimestamp();
await (interaction.channel as TextChannel)?.send({ embeds: [embed], components: [row] });
}
async sendHelperRoles(Discord: any, client: BotClient, interaction: ChatInputCommandInteraction): Promise<void> {
const selectionMenu = new Discord.StringSelectMenuBuilder()
.setCustomId('select-roles-helper')
.setPlaceholder('Select the roles you want to add')
.setMinValues(0)
.setMaxValues(client.config.getHelperRoles().length);
client.config.getHelperRoles().forEach((role: any) => {
const option = new Discord.StringSelectMenuOptionBuilder()
.setLabel(role.name)
.setValue(role.id)
.setDescription(role.description)
.setEmoji(role.emoji);
selectionMenu.addOptions(option);
});
const row = new Discord.ActionRowBuilder()
.addComponents(selectionMenu);
const embed = new Discord.EmbedBuilder()
.setColor("#2b2d31")
.setTitle("Select the roles you want to add")
.setDescription(`
:test_tube: - If you can help with Chemistry.
:atom: - If you can help with Physics.
:infinity: - If you can help with Math.
:dna: - If you can help with Biology.
:man_technologist: - If you can help with Programming.
:student: - I'm just a Student, hoping to learn.
`)
.setFooter({ text: "You can select multiple roles" })
.setTimestamp();
await (interaction.channel as TextChannel)?.send({ embeds: [embed], components: [row] });
}
async sendLocationRoles(Discord: any, client: BotClient, interaction: ChatInputCommandInteraction): Promise<void> {
const selectionMenu = new Discord.StringSelectMenuBuilder()
.setCustomId('select-roles-location')
.setPlaceholder('Select the roles you want to add')
.setMinValues(0)
.setMaxValues(client.config.getLocationRoles().length);
client.config.getLocationRoles().forEach((role: any) => {
const option = new Discord.StringSelectMenuOptionBuilder()
.setLabel(role.name)
.setValue(role.id)
.setDescription(role.description)
.setEmoji(role.emoji);
selectionMenu.addOptions(option);
});
const row = new Discord.ActionRowBuilder()
.addComponents(selectionMenu);
const embed = new Discord.EmbedBuilder()
.setColor("#2b2d31")
.setTitle("Select the location roles that apply to you")
.setDescription(`
:earth_americas: - If you're from North America.
:earth_americas: - If you're from South America.
:earth_africa: - If you're from Europe.
:earth_asia: - If you're from Asia.
:earth_africa: - If you're from Africa.
:earth_asia: - If you're from Oceania.
`)
.setFooter({ text: "You can select multiple roles" })
.setTimestamp();
await (interaction.channel as TextChannel)?.send({ embeds: [embed], components: [row] });
}
async sendPingRoles(Discord: any, client: BotClient, interaction: ChatInputCommandInteraction): Promise<void> {
const selectionMenu = new Discord.StringSelectMenuBuilder()
.setCustomId('select-roles-ping')
.setPlaceholder('Select the roles you want to add')
.setMinValues(0)
.setMaxValues(client.config.getPingRoles().length);
client.config.getPingRoles().forEach((role: any) => {
const option = new Discord.StringSelectMenuOptionBuilder()
.setLabel(role.name)
.setValue(role.id)
.setDescription(role.description)
.setEmoji(role.emoji);
selectionMenu.addOptions(option);
});
const row = new Discord.ActionRowBuilder()
.addComponents(selectionMenu);
let description = "";
client.config.getPingRoles().forEach((role: any) => {
description += `${role.emoji} - ${role.description}\n`;
});
const embed = new Discord.EmbedBuilder()
.setColor("#2b2d31")
.setTitle("Select the ping roles that apply to you")
.setDescription(description)
.setFooter({ text: "You can select multiple roles" })
.setTimestamp();
await (interaction.channel as TextChannel)?.send({ embeds: [embed], components: [row] });
}
override async execute(Discord: any, client: BotClient, interaction: ChatInputCommandInteraction): Promise<any> {
await interaction.reply({ content: `⏳ One Moment Please. . .`, flags: MessageFlags.Ephemeral });
await this.educationLevelRoles(Discord, client, interaction);
await this.sendHelperRoles(Discord, client, interaction);
await this.languageRolesExecute(Discord, client, interaction);
await this.sendLocationRoles(Discord, client, interaction);
await this.sendPingRoles(Discord, client, interaction);
return await interaction.deleteReply();
}
}

View File

@@ -0,0 +1,181 @@
import BotCommand from "../../libs/BotCommand";
import BotClient from "../../libs/BotClient";
import { ChatInputCommandInteraction, MessageFlags, EmbedBuilder, PermissionFlagsBits } from "discord.js";
import Team from "../../models/Team";
import User from "../../models/User";
import Database from "../../libs/Database";
import PointsManager from "../../libs/PointsManager";
export default class TeamManageCommand extends BotCommand {
constructor() {
super("team", "Create, edit, or delete teams (Admin only)", "/team");
this.data.addSubcommand(subcommand =>
subcommand.setName("create")
.setDescription("Create a new team")
.addStringOption(option =>
option.setName("name")
.setDescription("Team name")
.setRequired(true))
.addUserOption(option =>
option.setName("leader")
.setDescription("Team leader")
.setRequired(true))
.addStringOption(option =>
option.setName("description")
.setDescription("Team description")
.setRequired(false))
);
this.data.addSubcommand(subcommand =>
subcommand.setName("delete")
.setDescription("Delete a team")
.addStringOption(option =>
option.setName("name")
.setDescription("Team name")
.setRequired(true))
);
this.data.addSubcommand(subcommand =>
subcommand.setName("change-leader")
.setDescription("Change team leader")
.addStringOption(option =>
option.setName("name")
.setDescription("Team name")
.setRequired(true))
.addUserOption(option =>
option.setName("new-leader")
.setDescription("New team leader")
.setRequired(true))
);
this.data.addSubcommand(subcommand =>
subcommand.setName("list")
.setDescription("List all teams")
);
this.data.addSubcommand(subcommand =>
subcommand.setName("change-user-team")
.setDescription("Change a user's team (only during first week of month)")
.addUserOption(option =>
option.setName("user")
.setDescription("User to move")
.setRequired(true))
.addStringOption(option =>
option.setName("team")
.setDescription("Team name")
.setRequired(true))
);
this.data.setDefaultMemberPermissions(PermissionFlagsBits.ManageGuild);
}
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 });
}
const subcommand = interaction.options.getSubcommand();
try {
if (subcommand === "create") {
const name = interaction.options.getString("name", true);
const leader = interaction.options.getUser("leader", true);
const description = interaction.options.getString("description") || "";
const existing = await Team.findOne({ name });
if (existing) {
return interaction.reply({ content: `❌ Team **${name}** already exists.`, flags: MessageFlags.Ephemeral });
}
const team = new Team({
name,
leaderId: leader.id,
description,
points: 0,
memberCount: 0
});
await team.save();
const embed = new EmbedBuilder()
.setTitle("✅ Team Created")
.setColor(0x22c55e)
.addFields(
{ name: "Team Name", value: name, inline: true },
{ name: "Leader", value: `<@${leader.id}>`, inline: true },
{ name: "Description", value: description || "None", inline: false }
)
.setTimestamp();
return interaction.reply({ embeds: [embed] });
} else if (subcommand === "delete") {
const name = interaction.options.getString("name", true);
const team = await Team.findOne({ name });
if (!team) {
return interaction.reply({ content: `❌ Team **${name}** not found.`, flags: MessageFlags.Ephemeral });
}
await User.updateMany({ teamId: team._id }, { $set: { teamId: null } });
await Team.deleteOne({ _id: team._id });
return interaction.reply({ content: `✅ Team **${name}** has been deleted.`, flags: MessageFlags.Ephemeral });
} else if (subcommand === "change-leader") {
const name = interaction.options.getString("name", true);
const newLeader = interaction.options.getUser("new-leader", true);
const team = await Team.findOne({ name });
if (!team) {
return interaction.reply({ content: `❌ Team **${name}** not found.`, flags: MessageFlags.Ephemeral });
}
team.leaderId = newLeader.id;
await team.save();
return interaction.reply({ content: `✅ Team **${name}** leader changed to <@${newLeader.id}>.`, flags: MessageFlags.Ephemeral });
} else if (subcommand === "list") {
const teams = await Team.find().sort({ points: -1 }).exec();
if (teams.length === 0) {
return interaction.reply({ content: "No teams exist yet.", flags: MessageFlags.Ephemeral });
}
const embed = new EmbedBuilder()
.setTitle("📋 All Teams")
.setColor(0x60a5fa)
.setDescription(teams.map((t, i) =>
`**${i + 1}.** ${t.name} - Leader: <@${t.leaderId}>\n` +
` Members: ${t.memberCount} | Points: ${t.points} (Adjusted: ${t.adjustedPoints})`
).join("\n\n"))
.setTimestamp();
return interaction.reply({ embeds: [embed] });
} else if (subcommand === "change-user-team") {
const user = interaction.options.getUser("user", true);
const teamName = interaction.options.getString("team", true);
const team = await Team.findOne({ name: teamName });
if (!team) {
return interaction.reply({ content: `❌ Team **${teamName}** not found.`, flags: MessageFlags.Ephemeral });
}
const result = await PointsManager.joinTeam(user.id, user.username, team._id.toString(), true);
if (result.success) {
return interaction.reply({ content: `✅ <@${user.id}> has been moved to team **${teamName}**.`, flags: MessageFlags.Ephemeral });
} else {
return interaction.reply({ content: `${result.message}`, flags: MessageFlags.Ephemeral });
}
}
} catch (error) {
console.error("Error in team-manage command:", error);
return interaction.reply({ content: "❌ An error occurred.", flags: MessageFlags.Ephemeral });
}
}
}

View File

@@ -0,0 +1,17 @@
import BotClient from "../../libs/BotClient";
import BotCommand from "../../libs/BotCommand";
export default class StatusCommand extends BotCommand {
constructor() {
super("test", "Test Execute Command", "/test");
}
override async execute(Discord: any, client: BotClient, interaction: any) {
await interaction.deferReply();
await interaction.deleteReply();
// await interaction.reply({ content: "" });
}
}