Initial Code
This commit is contained in:
110
src/commands/admin/adminStats.ts
Normal file
110
src/commands/admin/adminStats.ts
Normal 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." });
|
||||
}
|
||||
}
|
||||
}
|
||||
245
src/commands/admin/growth.ts
Normal file
245
src/commands/admin/growth.ts
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
87
src/commands/admin/pinghelp.ts
Normal file
87
src/commands/admin/pinghelp.ts
Normal 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
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
118
src/commands/admin/regenerate.ts
Normal file
118
src/commands/admin/regenerate.ts
Normal 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] });
|
||||
}
|
||||
}
|
||||
}
|
||||
210
src/commands/admin/selectroles.ts
Normal file
210
src/commands/admin/selectroles.ts
Normal 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();
|
||||
}
|
||||
}
|
||||
181
src/commands/admin/teamManage.ts
Normal file
181
src/commands/admin/teamManage.ts
Normal 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 });
|
||||
}
|
||||
}
|
||||
}
|
||||
17
src/commands/admin/test.ts
Normal file
17
src/commands/admin/test.ts
Normal 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: "" });
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user