diff --git a/.gitignore b/.gitignore index 37d352b..9dedf63 100644 --- a/.gitignore +++ b/.gitignore @@ -2,6 +2,7 @@ node_modules/ dist/ storage/ +data/ .env bun.lockb diff --git a/src/commands/admin/teamManage.ts b/src/commands/admin/teamManage.ts index 6d94f5e..b693415 100644 --- a/src/commands/admin/teamManage.ts +++ b/src/commands/admin/teamManage.ts @@ -21,6 +21,10 @@ export default class TeamManageCommand extends BotCommand { option.setName("leader") .setDescription("Team leader") .setRequired(true)) + .addRoleOption(option => + option.setName("role") + .setDescription("Team role to assign to members") + .setRequired(true)) .addStringOption(option => option.setName("description") .setDescription("Team description") @@ -82,6 +86,7 @@ export default class TeamManageCommand extends BotCommand { if (subcommand === "create") { const name = interaction.options.getString("name", true); const leader = interaction.options.getUser("leader", true); + const role = interaction.options.getRole("role", true); const description = interaction.options.getString("description") || ""; const existing = await Team.findOne({ name }); @@ -92,6 +97,7 @@ export default class TeamManageCommand extends BotCommand { const team = new Team({ name, leaderId: leader.id, + roleId: role.id, description, points: 0, memberCount: 0 @@ -104,6 +110,7 @@ export default class TeamManageCommand extends BotCommand { .addFields( { name: "Team Name", value: name, inline: true }, { name: "Leader", value: `<@${leader.id}>`, inline: true }, + { name: "Role", value: `<@&${role.id}>`, inline: true }, { name: "Description", value: description || "None", inline: false } ) .setTimestamp(); @@ -118,10 +125,26 @@ export default class TeamManageCommand extends BotCommand { return interaction.reply({ content: `❌ Team **${name}** not found.`, flags: MessageFlags.Ephemeral }); } + // Remove role from all team members + const teamMembers = await User.find({ teamId: team._id }); + const guild = interaction.guild; + if (guild) { + for (const user of teamMembers) { + try { + const member = await guild.members.fetch(user.userId); + if (member && member.roles.cache.has(team.roleId)) { + await member.roles.remove(team.roleId); + } + } catch (err) { + console.error(`Failed to remove role from ${user.userId}:`, err); + } + } + } + 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 }); + return interaction.reply({ content: `✅ Team **${name}** has been deleted and roles removed from members.`, flags: MessageFlags.Ephemeral }); } else if (subcommand === "change-leader") { const name = interaction.options.getString("name", true); @@ -147,8 +170,8 @@ export default class TeamManageCommand extends BotCommand { const embed = new EmbedBuilder() .setTitle("📋 All Teams") .setColor(0x60a5fa) - .setDescription(teams.map((t, i) => - `**${i + 1}.** ${t.name} - Leader: <@${t.leaderId}>\n` + + .setDescription(teams.map((t: any, i: number) => + `**${i + 1}.** ${t.name} - Leader: <@${t.leaderId}> - Role: <@&${t.roleId}>\n` + ` Members: ${t.memberCount} | Points: ${t.points} (Adjusted: ${t.adjustedPoints})` ).join("\n\n")) .setTimestamp(); @@ -164,7 +187,7 @@ export default class TeamManageCommand extends BotCommand { return interaction.reply({ content: `❌ Team **${teamName}** not found.`, flags: MessageFlags.Ephemeral }); } - const result = await PointsManager.joinTeam(user.id, user.username, team._id.toString(), true); + const result = await PointsManager.joinTeam(user.id, user.username, team._id.toString(), true, interaction.guild || undefined); if (result.success) { return interaction.reply({ content: `✅ <@${user.id}> has been moved to team **${teamName}**.`, flags: MessageFlags.Ephemeral }); diff --git a/src/commands/users/joinTeam.ts b/src/commands/users/joinTeam.ts index 247177c..43d4db7 100644 --- a/src/commands/users/joinTeam.ts +++ b/src/commands/users/joinTeam.ts @@ -33,7 +33,7 @@ export default class JoinTeamCommand extends BotCommand { return interaction.editReply({ content: `❌ Team **${teamName}** not found.` }); } - const result = await PointsManager.joinTeam(interaction.user.id, interaction.user.username, team._id.toString()); + const result = await PointsManager.joinTeam(interaction.user.id, interaction.user.username, team._id.toString(), false, interaction.guild || undefined); if (result.success) { const embed = new EmbedBuilder() diff --git a/src/events/bot/client/clientReady.ts b/src/events/bot/client/clientReady.ts index 1c3cd5c..764d5e7 100644 --- a/src/events/bot/client/clientReady.ts +++ b/src/events/bot/client/clientReady.ts @@ -11,6 +11,14 @@ export default async(Discord: any, client: BotClient) => { client.emit("birthdayCheck"); }, ms('30m')); + // Clean up old typst files daily + setInterval(() => { + client.emit("typstCleanup"); + }, ms('24h')); + + // Run cleanup once on startup + client.emit("typstCleanup"); + } catch (err) { console.log(err); } finally { diff --git a/src/events/bot/custom/typstCleanup.ts b/src/events/bot/custom/typstCleanup.ts new file mode 100644 index 0000000..c0cf4ec --- /dev/null +++ b/src/events/bot/custom/typstCleanup.ts @@ -0,0 +1,20 @@ +import BotClient from "../../../libs/BotClient"; + +export default async(Discord: any, client: BotClient) => { + console.log("[TypstCleanup] Running cleanup for old typst files..."); + + // Delete files older than 1 day (after the question day passes) + const result = await client.typst.cleanupOldFiles(1); + + if (result.deleted > 0) { + console.log(`[TypstCleanup] ✅ Successfully cleaned up ${result.deleted} old typst file(s)`); + } + + if (result.errors > 0) { + console.error(`[TypstCleanup] ⚠️ Encountered ${result.errors} error(s) during cleanup`); + } + + if (result.deleted === 0 && result.errors === 0) { + console.log("[TypstCleanup] No old files to clean up"); + } +} diff --git a/src/libs/PointsManager.ts b/src/libs/PointsManager.ts index 1b9f9c6..1ce5b79 100644 --- a/src/libs/PointsManager.ts +++ b/src/libs/PointsManager.ts @@ -1,6 +1,7 @@ import User, { IUser } from "../models/User"; import Team, { ITeam } from "../models/Team"; import Database from "./Database"; +import { Guild } from "discord.js"; export default class PointsManager { private static DAILY_POINTS = 2; @@ -101,7 +102,7 @@ export default class PointsManager { return dayOfMonth <= 7; } - public static async joinTeam(userId: string, username: string, teamId: string, requireTimeCheck: boolean = false): Promise<{ success: boolean; message: string }> { + public static async joinTeam(userId: string, username: string, teamId: string, requireTimeCheck: boolean = false, guild?: Guild): Promise<{ success: boolean; message: string }> { const db = Database.getInstance(); if (!db.isConnected()) return { success: false, message: "Database not connected" }; @@ -125,6 +126,32 @@ export default class PointsManager { return { success: false, message: "You're already in this team" }; } + // Handle Discord role changes if guild is provided + if (guild) { + try { + const member = await guild.members.fetch(userId); + + // Remove old team role if user was in a team + if (oldTeamId) { + const oldTeam = await Team.findById(oldTeamId); + if (oldTeam && member.roles.cache.has(oldTeam.roleId)) { + await member.roles.remove(oldTeam.roleId); + console.log(`[PointsManager] Removed role ${oldTeam.roleId} from ${username}`); + } + } + + // Add new team role + if (!member.roles.cache.has(team.roleId)) { + await member.roles.add(team.roleId); + console.log(`[PointsManager] Added role ${team.roleId} to ${username}`); + } + } catch (roleError) { + console.error(`[PointsManager] Error managing roles for ${username}:`, roleError); + // Continue with team assignment even if role fails + } + } + + // Update old team member count if (oldTeamId) { const oldTeam = await Team.findById(oldTeamId); if (oldTeam) { diff --git a/src/libs/Typst.ts b/src/libs/Typst.ts index abf6a8d..df0bf51 100644 --- a/src/libs/Typst.ts +++ b/src/libs/Typst.ts @@ -112,4 +112,48 @@ ${typstCode} return result.files[0]; } + async cleanupOldFiles(daysOld: number = 1): Promise<{ deleted: number; errors: number }> { + const storageDir = path.resolve('./storage/typst'); + let deleted = 0; + let errors = 0; + + try { + await fs.promises.access(storageDir); + } catch { + console.log('[Typst] Storage directory does not exist, nothing to clean'); + return { deleted: 0, errors: 0 }; + } + + try { + const files = await fs.promises.readdir(storageDir); + const now = Date.now(); + const threshold = daysOld * 24 * 60 * 60 * 1000; // Convert days to milliseconds + + for (const file of files) { + const filePath = path.join(storageDir, file); + + try { + const stats = await fs.promises.stat(filePath); + const fileAge = now - stats.mtimeMs; + + if (fileAge > threshold) { + await fs.promises.unlink(filePath); + deleted++; + console.log(`[Typst] Deleted old file: ${file}`); + } + } catch (err) { + console.error(`[Typst] Error processing file ${file}:`, err); + errors++; + } + } + + console.log(`[Typst] Cleanup complete: ${deleted} files deleted, ${errors} errors`); + } catch (err) { + console.error('[Typst] Error during cleanup:', err); + errors++; + } + + return { deleted, errors }; + } + } \ No newline at end of file diff --git a/src/models/Team.ts b/src/models/Team.ts index 8bbfffe..59dbbac 100644 --- a/src/models/Team.ts +++ b/src/models/Team.ts @@ -4,6 +4,7 @@ export interface ITeam extends Document { name: string; description?: string; leaderId: string; + roleId: string; points: number; memberCount: number; createdAt: Date; @@ -14,6 +15,7 @@ const TeamSchema = new Schema({ name: { type: String, required: true, unique: true, index: true }, description: { type: String, default: "" }, leaderId: { type: String, required: true }, + roleId: { type: String, required: true }, points: { type: Number, default: 0 }, memberCount: { type: Number, default: 0 }, createdAt: { type: Date, default: Date.now },