feat: standardize command error handling (Sprint 4)
- Create withCommandErrorHandling utility in bot/lib/commandUtils.ts - Migrate economy commands: daily, exam, pay, trivia - Migrate inventory command: use - Migrate admin/moderation commands: warn, case, cases, clearwarning, warnings, note, notes, create_color, listing, webhook, refresh, terminal, featureflags, settings, prune - Add 9 unit tests for the utility - Update AGENTS.md with new recommended error handling pattern
This commit is contained in:
43
AGENTS.md
43
AGENTS.md
@@ -141,22 +141,36 @@ throw new UserError("You don't have enough coins!");
|
|||||||
throw new SystemError("Database connection failed");
|
throw new SystemError("Database connection failed");
|
||||||
```
|
```
|
||||||
|
|
||||||
### Standard Error Pattern
|
### Recommended: `withCommandErrorHandling`
|
||||||
|
|
||||||
|
Use the `withCommandErrorHandling` utility from `@lib/commandUtils` to standardize
|
||||||
|
error handling across all commands. It handles `deferReply`, `UserError` display,
|
||||||
|
and unexpected error logging automatically.
|
||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
try {
|
import { withCommandErrorHandling } from "@lib/commandUtils";
|
||||||
const result = await service.method();
|
|
||||||
await interaction.editReply({ embeds: [createSuccessEmbed(result)] });
|
export const myCommand = createCommand({
|
||||||
} catch (error) {
|
data: new SlashCommandBuilder()
|
||||||
if (error instanceof UserError) {
|
.setName("mycommand")
|
||||||
await interaction.editReply({ embeds: [createErrorEmbed(error.message)] });
|
.setDescription("Does something"),
|
||||||
} else {
|
execute: async (interaction) => {
|
||||||
console.error("Unexpected error:", error);
|
await withCommandErrorHandling(
|
||||||
await interaction.editReply({
|
interaction,
|
||||||
embeds: [createErrorEmbed("An unexpected error occurred.")],
|
async () => {
|
||||||
});
|
const result = await service.method();
|
||||||
}
|
await interaction.editReply({ embeds: [createSuccessEmbed(result)] });
|
||||||
}
|
},
|
||||||
|
{ ephemeral: true } // optional: makes the deferred reply ephemeral
|
||||||
|
);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
Options:
|
||||||
|
- `ephemeral` — whether `deferReply` should be ephemeral
|
||||||
|
- `successMessage` — a simple string to send on success
|
||||||
|
- `onSuccess` — a callback invoked with the operation result
|
||||||
```
|
```
|
||||||
|
|
||||||
## Database Patterns
|
## Database Patterns
|
||||||
@@ -240,3 +254,4 @@ describe("serviceName", () => {
|
|||||||
| Environment | `shared/lib/env.ts` |
|
| Environment | `shared/lib/env.ts` |
|
||||||
| Embed helpers | `bot/lib/embeds.ts` |
|
| Embed helpers | `bot/lib/embeds.ts` |
|
||||||
| Command utils | `shared/lib/utils.ts` |
|
| Command utils | `shared/lib/utils.ts` |
|
||||||
|
| Error handler | `bot/lib/commandUtils.ts` |
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import { createCommand } from "@shared/lib/utils";
|
|||||||
import { SlashCommandBuilder, PermissionFlagsBits, MessageFlags } from "discord.js";
|
import { SlashCommandBuilder, PermissionFlagsBits, MessageFlags } from "discord.js";
|
||||||
import { moderationService } from "@shared/modules/moderation/moderation.service";
|
import { moderationService } from "@shared/modules/moderation/moderation.service";
|
||||||
import { getCaseEmbed, getModerationErrorEmbed } from "@/modules/moderation/moderation.view";
|
import { getCaseEmbed, getModerationErrorEmbed } from "@/modules/moderation/moderation.view";
|
||||||
|
import { withCommandErrorHandling } from "@lib/commandUtils";
|
||||||
|
|
||||||
export const moderationCase = createCommand({
|
export const moderationCase = createCommand({
|
||||||
data: new SlashCommandBuilder()
|
data: new SlashCommandBuilder()
|
||||||
@@ -16,39 +17,35 @@ export const moderationCase = createCommand({
|
|||||||
.setDefaultMemberPermissions(PermissionFlagsBits.Administrator),
|
.setDefaultMemberPermissions(PermissionFlagsBits.Administrator),
|
||||||
|
|
||||||
execute: async (interaction) => {
|
execute: async (interaction) => {
|
||||||
await interaction.deferReply({ flags: MessageFlags.Ephemeral });
|
await withCommandErrorHandling(
|
||||||
|
interaction,
|
||||||
|
async () => {
|
||||||
|
const caseId = interaction.options.getString("case_id", true).toUpperCase();
|
||||||
|
|
||||||
try {
|
// Validate case ID format
|
||||||
const caseId = interaction.options.getString("case_id", true).toUpperCase();
|
if (!caseId.match(/^CASE-\d+$/)) {
|
||||||
|
await interaction.editReply({
|
||||||
|
embeds: [getModerationErrorEmbed("Invalid case ID format. Expected format: CASE-0001")]
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
// Validate case ID format
|
// Get the case
|
||||||
if (!caseId.match(/^CASE-\d+$/)) {
|
const moderationCase = await moderationService.getCaseById(caseId);
|
||||||
|
|
||||||
|
if (!moderationCase) {
|
||||||
|
await interaction.editReply({
|
||||||
|
embeds: [getModerationErrorEmbed(`Case **${caseId}** not found.`)]
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Display the case
|
||||||
await interaction.editReply({
|
await interaction.editReply({
|
||||||
embeds: [getModerationErrorEmbed("Invalid case ID format. Expected format: CASE-0001")]
|
embeds: [getCaseEmbed(moderationCase)]
|
||||||
});
|
});
|
||||||
return;
|
},
|
||||||
}
|
{ ephemeral: true }
|
||||||
|
);
|
||||||
// Get the case
|
|
||||||
const moderationCase = await moderationService.getCaseById(caseId);
|
|
||||||
|
|
||||||
if (!moderationCase) {
|
|
||||||
await interaction.editReply({
|
|
||||||
embeds: [getModerationErrorEmbed(`Case **${caseId}** not found.`)]
|
|
||||||
});
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Display the case
|
|
||||||
await interaction.editReply({
|
|
||||||
embeds: [getCaseEmbed(moderationCase)]
|
|
||||||
});
|
|
||||||
|
|
||||||
} catch (error) {
|
|
||||||
console.error("Case command error:", error);
|
|
||||||
await interaction.editReply({
|
|
||||||
embeds: [getModerationErrorEmbed("An error occurred while fetching the case.")]
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,7 +1,8 @@
|
|||||||
import { createCommand } from "@shared/lib/utils";
|
import { createCommand } from "@shared/lib/utils";
|
||||||
import { SlashCommandBuilder, PermissionFlagsBits, MessageFlags } from "discord.js";
|
import { SlashCommandBuilder, PermissionFlagsBits } from "discord.js";
|
||||||
import { moderationService } from "@shared/modules/moderation/moderation.service";
|
import { moderationService } from "@shared/modules/moderation/moderation.service";
|
||||||
import { getCasesListEmbed, getModerationErrorEmbed } from "@/modules/moderation/moderation.view";
|
import { getCasesListEmbed } from "@/modules/moderation/moderation.view";
|
||||||
|
import { withCommandErrorHandling } from "@lib/commandUtils";
|
||||||
|
|
||||||
export const cases = createCommand({
|
export const cases = createCommand({
|
||||||
data: new SlashCommandBuilder()
|
data: new SlashCommandBuilder()
|
||||||
@@ -22,33 +23,29 @@ export const cases = createCommand({
|
|||||||
.setDefaultMemberPermissions(PermissionFlagsBits.Administrator),
|
.setDefaultMemberPermissions(PermissionFlagsBits.Administrator),
|
||||||
|
|
||||||
execute: async (interaction) => {
|
execute: async (interaction) => {
|
||||||
await interaction.deferReply({ flags: MessageFlags.Ephemeral });
|
await withCommandErrorHandling(
|
||||||
|
interaction,
|
||||||
|
async () => {
|
||||||
|
const targetUser = interaction.options.getUser("user", true);
|
||||||
|
const activeOnly = interaction.options.getBoolean("active_only") || false;
|
||||||
|
|
||||||
try {
|
// Get cases for the user
|
||||||
const targetUser = interaction.options.getUser("user", true);
|
const userCases = await moderationService.getUserCases(targetUser.id, activeOnly);
|
||||||
const activeOnly = interaction.options.getBoolean("active_only") || false;
|
|
||||||
|
|
||||||
// Get cases for the user
|
const title = activeOnly
|
||||||
const userCases = await moderationService.getUserCases(targetUser.id, activeOnly);
|
? `⚠️ Active Cases for ${targetUser.username}`
|
||||||
|
: `📋 All Cases for ${targetUser.username}`;
|
||||||
|
|
||||||
const title = activeOnly
|
const description = userCases.length === 0
|
||||||
? `⚠️ Active Cases for ${targetUser.username}`
|
? undefined
|
||||||
: `📋 All Cases for ${targetUser.username}`;
|
: `Total cases: **${userCases.length}**`;
|
||||||
|
|
||||||
const description = userCases.length === 0
|
// Display the cases
|
||||||
? undefined
|
await interaction.editReply({
|
||||||
: `Total cases: **${userCases.length}**`;
|
embeds: [getCasesListEmbed(userCases, title, description)]
|
||||||
|
});
|
||||||
// Display the cases
|
},
|
||||||
await interaction.editReply({
|
{ ephemeral: true }
|
||||||
embeds: [getCasesListEmbed(userCases, title, description)]
|
);
|
||||||
});
|
|
||||||
|
|
||||||
} catch (error) {
|
|
||||||
console.error("Cases command error:", error);
|
|
||||||
await interaction.editReply({
|
|
||||||
embeds: [getModerationErrorEmbed("An error occurred while fetching cases.")]
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,7 +1,8 @@
|
|||||||
import { createCommand } from "@shared/lib/utils";
|
import { createCommand } from "@shared/lib/utils";
|
||||||
import { SlashCommandBuilder, PermissionFlagsBits, MessageFlags } from "discord.js";
|
import { SlashCommandBuilder, PermissionFlagsBits } from "discord.js";
|
||||||
import { moderationService } from "@shared/modules/moderation/moderation.service";
|
import { moderationService } from "@shared/modules/moderation/moderation.service";
|
||||||
import { getClearSuccessEmbed, getModerationErrorEmbed } from "@/modules/moderation/moderation.view";
|
import { getClearSuccessEmbed, getModerationErrorEmbed } from "@/modules/moderation/moderation.view";
|
||||||
|
import { withCommandErrorHandling } from "@lib/commandUtils";
|
||||||
|
|
||||||
export const clearwarning = createCommand({
|
export const clearwarning = createCommand({
|
||||||
data: new SlashCommandBuilder()
|
data: new SlashCommandBuilder()
|
||||||
@@ -23,62 +24,58 @@ export const clearwarning = createCommand({
|
|||||||
.setDefaultMemberPermissions(PermissionFlagsBits.Administrator),
|
.setDefaultMemberPermissions(PermissionFlagsBits.Administrator),
|
||||||
|
|
||||||
execute: async (interaction) => {
|
execute: async (interaction) => {
|
||||||
await interaction.deferReply({ flags: MessageFlags.Ephemeral });
|
await withCommandErrorHandling(
|
||||||
|
interaction,
|
||||||
|
async () => {
|
||||||
|
const caseId = interaction.options.getString("case_id", true).toUpperCase();
|
||||||
|
const reason = interaction.options.getString("reason") || "Cleared by moderator";
|
||||||
|
|
||||||
try {
|
// Validate case ID format
|
||||||
const caseId = interaction.options.getString("case_id", true).toUpperCase();
|
if (!caseId.match(/^CASE-\d+$/)) {
|
||||||
const reason = interaction.options.getString("reason") || "Cleared by moderator";
|
await interaction.editReply({
|
||||||
|
embeds: [getModerationErrorEmbed("Invalid case ID format. Expected format: CASE-0001")]
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
// Validate case ID format
|
// Check if case exists and is active
|
||||||
if (!caseId.match(/^CASE-\d+$/)) {
|
const existingCase = await moderationService.getCaseById(caseId);
|
||||||
await interaction.editReply({
|
|
||||||
embeds: [getModerationErrorEmbed("Invalid case ID format. Expected format: CASE-0001")]
|
if (!existingCase) {
|
||||||
|
await interaction.editReply({
|
||||||
|
embeds: [getModerationErrorEmbed(`Case **${caseId}** not found.`)]
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!existingCase.active) {
|
||||||
|
await interaction.editReply({
|
||||||
|
embeds: [getModerationErrorEmbed(`Case **${caseId}** is already resolved.`)]
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (existingCase.type !== 'warn') {
|
||||||
|
await interaction.editReply({
|
||||||
|
embeds: [getModerationErrorEmbed(`Case **${caseId}** is not a warning. Only warnings can be cleared.`)]
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Clear the warning
|
||||||
|
await moderationService.clearCase({
|
||||||
|
caseId,
|
||||||
|
clearedBy: interaction.user.id,
|
||||||
|
clearedByName: interaction.user.username,
|
||||||
|
reason
|
||||||
});
|
});
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check if case exists and is active
|
// Send success message
|
||||||
const existingCase = await moderationService.getCaseById(caseId);
|
|
||||||
|
|
||||||
if (!existingCase) {
|
|
||||||
await interaction.editReply({
|
await interaction.editReply({
|
||||||
embeds: [getModerationErrorEmbed(`Case **${caseId}** not found.`)]
|
embeds: [getClearSuccessEmbed(caseId)]
|
||||||
});
|
});
|
||||||
return;
|
},
|
||||||
}
|
{ ephemeral: true }
|
||||||
|
);
|
||||||
if (!existingCase.active) {
|
|
||||||
await interaction.editReply({
|
|
||||||
embeds: [getModerationErrorEmbed(`Case **${caseId}** is already resolved.`)]
|
|
||||||
});
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (existingCase.type !== 'warn') {
|
|
||||||
await interaction.editReply({
|
|
||||||
embeds: [getModerationErrorEmbed(`Case **${caseId}** is not a warning. Only warnings can be cleared.`)]
|
|
||||||
});
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Clear the warning
|
|
||||||
await moderationService.clearCase({
|
|
||||||
caseId,
|
|
||||||
clearedBy: interaction.user.id,
|
|
||||||
clearedByName: interaction.user.username,
|
|
||||||
reason
|
|
||||||
});
|
|
||||||
|
|
||||||
// Send success message
|
|
||||||
await interaction.editReply({
|
|
||||||
embeds: [getClearSuccessEmbed(caseId)]
|
|
||||||
});
|
|
||||||
|
|
||||||
} catch (error) {
|
|
||||||
console.error("Clear warning command error:", error);
|
|
||||||
await interaction.editReply({
|
|
||||||
embeds: [getModerationErrorEmbed("An error occurred while clearing the warning.")]
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,10 +1,11 @@
|
|||||||
import { createCommand } from "@shared/lib/utils";
|
import { createCommand } from "@shared/lib/utils";
|
||||||
import { SlashCommandBuilder, PermissionFlagsBits, AttachmentBuilder } from "discord.js";
|
import { SlashCommandBuilder, PermissionFlagsBits } from "discord.js";
|
||||||
import { getGuildConfig, invalidateGuildConfigCache } from "@shared/lib/config";
|
import { getGuildConfig, invalidateGuildConfigCache } from "@shared/lib/config";
|
||||||
import { guildSettingsService } from "@shared/modules/guild-settings/guild-settings.service";
|
import { guildSettingsService } from "@shared/modules/guild-settings/guild-settings.service";
|
||||||
import { DrizzleClient } from "@shared/db/DrizzleClient";
|
import { DrizzleClient } from "@shared/db/DrizzleClient";
|
||||||
import { items } from "@db/schema";
|
import { items } from "@db/schema";
|
||||||
import { createSuccessEmbed, createErrorEmbed } from "@lib/embeds";
|
import { createSuccessEmbed, createErrorEmbed } from "@lib/embeds";
|
||||||
|
import { withCommandErrorHandling } from "@lib/commandUtils";
|
||||||
|
|
||||||
export const createColor = createCommand({
|
export const createColor = createCommand({
|
||||||
data: new SlashCommandBuilder()
|
data: new SlashCommandBuilder()
|
||||||
@@ -32,62 +33,60 @@ export const createColor = createCommand({
|
|||||||
)
|
)
|
||||||
.setDefaultMemberPermissions(PermissionFlagsBits.Administrator),
|
.setDefaultMemberPermissions(PermissionFlagsBits.Administrator),
|
||||||
execute: async (interaction) => {
|
execute: async (interaction) => {
|
||||||
await interaction.deferReply();
|
await withCommandErrorHandling(
|
||||||
|
interaction,
|
||||||
|
async () => {
|
||||||
|
const name = interaction.options.getString("name", true);
|
||||||
|
const colorInput = interaction.options.getString("color", true);
|
||||||
|
const price = interaction.options.getNumber("price") || 500;
|
||||||
|
const imageUrl = interaction.options.getString("image") || "https://cdn.discordapp.com/attachments/1450061247365124199/1453122950822760559/Main_Chip_1.png";
|
||||||
|
|
||||||
const name = interaction.options.getString("name", true);
|
// 1. Validate Color
|
||||||
const colorInput = interaction.options.getString("color", true);
|
const colorRegex = /^#([0-9A-F]{3}){1,2}$/i;
|
||||||
const price = interaction.options.getNumber("price") || 500;
|
if (!colorRegex.test(colorInput)) {
|
||||||
const imageUrl = interaction.options.getString("image") || "https://cdn.discordapp.com/attachments/1450061247365124199/1453122950822760559/Main_Chip_1.png";
|
await interaction.editReply({ embeds: [createErrorEmbed("Invalid Hex Color code. Format: #RRGGBB")] });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
// 1. Validate Color
|
// 2. Create Role
|
||||||
const colorRegex = /^#([0-9A-F]{3}){1,2}$/i;
|
const role = await interaction.guild?.roles.create({
|
||||||
if (!colorRegex.test(colorInput)) {
|
name: name,
|
||||||
await interaction.editReply({ embeds: [createErrorEmbed("Invalid Hex Color code. Format: #RRGGBB")] });
|
color: colorInput as any,
|
||||||
return;
|
reason: `Created via /createcolor by ${interaction.user.tag}`
|
||||||
}
|
});
|
||||||
|
|
||||||
try {
|
if (!role) {
|
||||||
// 2. Create Role
|
throw new Error("Failed to create role.");
|
||||||
const role = await interaction.guild?.roles.create({
|
}
|
||||||
name: name,
|
|
||||||
color: colorInput as any,
|
|
||||||
reason: `Created via /createcolor by ${interaction.user.tag}`
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!role) {
|
// 3. Add to guild settings
|
||||||
throw new Error("Failed to create role.");
|
await guildSettingsService.addColorRole(interaction.guildId!, role.id);
|
||||||
}
|
invalidateGuildConfigCache(interaction.guildId!);
|
||||||
|
|
||||||
// 3. Add to guild settings
|
// 4. Create Item
|
||||||
await guildSettingsService.addColorRole(interaction.guildId!, role.id);
|
await DrizzleClient.insert(items).values({
|
||||||
invalidateGuildConfigCache(interaction.guildId!);
|
name: `Color Role - ${name}`,
|
||||||
|
description: `Use this item to apply the ${name} color to your name.`,
|
||||||
|
type: "CONSUMABLE",
|
||||||
|
rarity: "Common",
|
||||||
|
price: BigInt(price),
|
||||||
|
iconUrl: "",
|
||||||
|
imageUrl: imageUrl,
|
||||||
|
usageData: {
|
||||||
|
consume: false,
|
||||||
|
effects: [{ type: "COLOR_ROLE", roleId: role.id }]
|
||||||
|
} as any
|
||||||
|
});
|
||||||
|
|
||||||
// 4. Create Item
|
// 5. Success
|
||||||
await DrizzleClient.insert(items).values({
|
await interaction.editReply({
|
||||||
name: `Color Role - ${name}`,
|
embeds: [createSuccessEmbed(
|
||||||
description: `Use this item to apply the ${name} color to your name.`,
|
`**Role:** <@&${role.id}> (${colorInput})\n**Item:** Color Role - ${name}\n**Price:** ${price} 🪙`,
|
||||||
type: "CONSUMABLE",
|
"✅ Color Role & Item Created"
|
||||||
rarity: "Common",
|
)]
|
||||||
price: BigInt(price),
|
});
|
||||||
iconUrl: "",
|
},
|
||||||
imageUrl: imageUrl,
|
{ ephemeral: true }
|
||||||
usageData: {
|
);
|
||||||
consume: false,
|
|
||||||
effects: [{ type: "COLOR_ROLE", roleId: role.id }]
|
|
||||||
} as any
|
|
||||||
});
|
|
||||||
|
|
||||||
// 5. Success
|
|
||||||
await interaction.editReply({
|
|
||||||
embeds: [createSuccessEmbed(
|
|
||||||
`**Role:** <@&${role.id}> (${colorInput})\n**Item:** Color Role - ${name}\n**Price:** ${price} 🪙`,
|
|
||||||
"✅ Color Role & Item Created"
|
|
||||||
)]
|
|
||||||
});
|
|
||||||
|
|
||||||
} catch (error: any) {
|
|
||||||
console.error("Error in createcolor:", error);
|
|
||||||
await interaction.editReply({ embeds: [createErrorEmbed(`Failed to create color role: ${error.message}`)] });
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import { createCommand } from "@shared/lib/utils";
|
|||||||
import { SlashCommandBuilder, PermissionFlagsBits, Colors, userMention, roleMention, ChatInputCommandInteraction } from "discord.js";
|
import { SlashCommandBuilder, PermissionFlagsBits, Colors, userMention, roleMention, ChatInputCommandInteraction } from "discord.js";
|
||||||
import { featureFlagsService } from "@shared/modules/feature-flags/feature-flags.service";
|
import { featureFlagsService } from "@shared/modules/feature-flags/feature-flags.service";
|
||||||
import { createBaseEmbed, createErrorEmbed, createSuccessEmbed } from "@lib/embeds";
|
import { createBaseEmbed, createErrorEmbed, createSuccessEmbed } from "@lib/embeds";
|
||||||
import { UserError } from "@shared/lib/errors";
|
import { withCommandErrorHandling } from "@lib/commandUtils";
|
||||||
|
|
||||||
export const featureflags = createCommand({
|
export const featureflags = createCommand({
|
||||||
data: new SlashCommandBuilder()
|
data: new SlashCommandBuilder()
|
||||||
@@ -111,44 +111,40 @@ export const featureflags = createCommand({
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
execute: async (interaction) => {
|
execute: async (interaction) => {
|
||||||
await interaction.deferReply();
|
await withCommandErrorHandling(
|
||||||
|
interaction,
|
||||||
|
async () => {
|
||||||
|
const subcommand = interaction.options.getSubcommand();
|
||||||
|
|
||||||
const subcommand = interaction.options.getSubcommand();
|
switch (subcommand) {
|
||||||
|
case "list":
|
||||||
try {
|
await handleList(interaction);
|
||||||
switch (subcommand) {
|
break;
|
||||||
case "list":
|
case "create":
|
||||||
await handleList(interaction);
|
await handleCreate(interaction);
|
||||||
break;
|
break;
|
||||||
case "create":
|
case "delete":
|
||||||
await handleCreate(interaction);
|
await handleDelete(interaction);
|
||||||
break;
|
break;
|
||||||
case "delete":
|
case "enable":
|
||||||
await handleDelete(interaction);
|
await handleEnable(interaction);
|
||||||
break;
|
break;
|
||||||
case "enable":
|
case "disable":
|
||||||
await handleEnable(interaction);
|
await handleDisable(interaction);
|
||||||
break;
|
break;
|
||||||
case "disable":
|
case "grant":
|
||||||
await handleDisable(interaction);
|
await handleGrant(interaction);
|
||||||
break;
|
break;
|
||||||
case "grant":
|
case "revoke":
|
||||||
await handleGrant(interaction);
|
await handleRevoke(interaction);
|
||||||
break;
|
break;
|
||||||
case "revoke":
|
case "access":
|
||||||
await handleRevoke(interaction);
|
await handleAccess(interaction);
|
||||||
break;
|
break;
|
||||||
case "access":
|
}
|
||||||
await handleAccess(interaction);
|
},
|
||||||
break;
|
{ ephemeral: true }
|
||||||
}
|
);
|
||||||
} catch (error) {
|
|
||||||
if (error instanceof UserError) {
|
|
||||||
await interaction.editReply({ embeds: [createErrorEmbed(error.message)] });
|
|
||||||
} else {
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -7,12 +7,12 @@ import {
|
|||||||
} from "discord.js";
|
} from "discord.js";
|
||||||
import { inventoryService } from "@shared/modules/inventory/inventory.service";
|
import { inventoryService } from "@shared/modules/inventory/inventory.service";
|
||||||
import { createErrorEmbed } from "@lib/embeds";
|
import { createErrorEmbed } from "@lib/embeds";
|
||||||
import { UserError } from "@shared/lib/errors";
|
|
||||||
import { items } from "@db/schema";
|
import { items } from "@db/schema";
|
||||||
import { ilike, isNotNull, and, inArray } from "drizzle-orm";
|
import { ilike, isNotNull, and, inArray } from "drizzle-orm";
|
||||||
import { DrizzleClient } from "@shared/db/DrizzleClient";
|
import { DrizzleClient } from "@shared/db/DrizzleClient";
|
||||||
import { getShopListingMessage } from "@/modules/economy/shop.view";
|
import { getShopListingMessage } from "@/modules/economy/shop.view";
|
||||||
import { EffectType, LootType } from "@shared/lib/constants";
|
import { EffectType, LootType } from "@shared/lib/constants";
|
||||||
|
import { withCommandErrorHandling } from "@lib/commandUtils";
|
||||||
|
|
||||||
export const listing = createCommand({
|
export const listing = createCommand({
|
||||||
data: new SlashCommandBuilder()
|
data: new SlashCommandBuilder()
|
||||||
@@ -31,72 +31,67 @@ export const listing = createCommand({
|
|||||||
)
|
)
|
||||||
.setDefaultMemberPermissions(PermissionFlagsBits.Administrator),
|
.setDefaultMemberPermissions(PermissionFlagsBits.Administrator),
|
||||||
execute: async (interaction) => {
|
execute: async (interaction) => {
|
||||||
await interaction.deferReply({ flags: MessageFlags.Ephemeral });
|
await withCommandErrorHandling(
|
||||||
|
interaction,
|
||||||
|
async () => {
|
||||||
|
const itemId = interaction.options.getNumber("item", true);
|
||||||
|
const targetChannel = (interaction.options.getChannel("channel") as BaseGuildTextChannel) || interaction.channel as BaseGuildTextChannel;
|
||||||
|
|
||||||
const itemId = interaction.options.getNumber("item", true);
|
if (!targetChannel || !targetChannel.isSendable()) {
|
||||||
const targetChannel = (interaction.options.getChannel("channel") as BaseGuildTextChannel) || interaction.channel as BaseGuildTextChannel;
|
await interaction.editReply({ content: "", embeds: [createErrorEmbed("Target channel is invalid or not sendable.")] });
|
||||||
|
return;
|
||||||
if (!targetChannel || !targetChannel.isSendable()) {
|
|
||||||
await interaction.editReply({ content: "", embeds: [createErrorEmbed("Target channel is invalid or not sendable.")] });
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const item = await inventoryService.getItem(itemId);
|
|
||||||
if (!item) {
|
|
||||||
await interaction.editReply({ content: "", embeds: [createErrorEmbed(`Item with ID ${itemId} not found.`)] });
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!item.price) {
|
|
||||||
await interaction.editReply({ content: "", embeds: [createErrorEmbed(`Item "${item.name}" is not for sale (no price set).`)] });
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Prepare context for lootboxes
|
|
||||||
const context: { referencedItems: Map<number, { name: string; rarity: string }> } = { referencedItems: new Map() };
|
|
||||||
|
|
||||||
const usageData = item.usageData as any;
|
|
||||||
const lootboxEffect = usageData?.effects?.find((e: any) => e.type === EffectType.LOOTBOX);
|
|
||||||
|
|
||||||
if (lootboxEffect && lootboxEffect.pool) {
|
|
||||||
const itemIds = lootboxEffect.pool
|
|
||||||
.filter((drop: any) => drop.type === LootType.ITEM && drop.itemId)
|
|
||||||
.map((drop: any) => drop.itemId);
|
|
||||||
|
|
||||||
if (itemIds.length > 0) {
|
|
||||||
// Remove duplicates
|
|
||||||
const uniqueIds = [...new Set(itemIds)] as number[];
|
|
||||||
|
|
||||||
const referencedItems = await DrizzleClient.select({
|
|
||||||
id: items.id,
|
|
||||||
name: items.name,
|
|
||||||
rarity: items.rarity
|
|
||||||
}).from(items).where(inArray(items.id, uniqueIds));
|
|
||||||
|
|
||||||
for (const ref of referencedItems) {
|
|
||||||
context.referencedItems.set(ref.id, { name: ref.name, rarity: ref.rarity || 'C' });
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const listingMessage = getShopListingMessage({
|
const item = await inventoryService.getItem(itemId);
|
||||||
...item,
|
if (!item) {
|
||||||
rarity: item.rarity || undefined,
|
await interaction.editReply({ content: "", embeds: [createErrorEmbed(`Item with ID ${itemId} not found.`)] });
|
||||||
formattedPrice: `${item.price} 🪙`,
|
return;
|
||||||
price: item.price
|
}
|
||||||
}, context);
|
|
||||||
|
|
||||||
try {
|
if (!item.price) {
|
||||||
await targetChannel.send(listingMessage as any);
|
await interaction.editReply({ content: "", embeds: [createErrorEmbed(`Item "${item.name}" is not for sale (no price set).`)] });
|
||||||
await interaction.editReply({ content: `✅ Listing for **${item.name}** posted in ${targetChannel}.` });
|
return;
|
||||||
} catch (error: any) {
|
}
|
||||||
if (error instanceof UserError) {
|
|
||||||
await interaction.editReply({ embeds: [createErrorEmbed(error.message)] });
|
// Prepare context for lootboxes
|
||||||
} else {
|
const context: { referencedItems: Map<number, { name: string; rarity: string }> } = { referencedItems: new Map() };
|
||||||
console.error("Error creating listing:", error);
|
|
||||||
await interaction.editReply({ embeds: [createErrorEmbed("An unexpected error occurred.")] });
|
const usageData = item.usageData as any;
|
||||||
}
|
const lootboxEffect = usageData?.effects?.find((e: any) => e.type === EffectType.LOOTBOX);
|
||||||
}
|
|
||||||
|
if (lootboxEffect && lootboxEffect.pool) {
|
||||||
|
const itemIds = lootboxEffect.pool
|
||||||
|
.filter((drop: any) => drop.type === LootType.ITEM && drop.itemId)
|
||||||
|
.map((drop: any) => drop.itemId);
|
||||||
|
|
||||||
|
if (itemIds.length > 0) {
|
||||||
|
// Remove duplicates
|
||||||
|
const uniqueIds = [...new Set(itemIds)] as number[];
|
||||||
|
|
||||||
|
const referencedItems = await DrizzleClient.select({
|
||||||
|
id: items.id,
|
||||||
|
name: items.name,
|
||||||
|
rarity: items.rarity
|
||||||
|
}).from(items).where(inArray(items.id, uniqueIds));
|
||||||
|
|
||||||
|
for (const ref of referencedItems) {
|
||||||
|
context.referencedItems.set(ref.id, { name: ref.name, rarity: ref.rarity || 'C' });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const listingMessage = getShopListingMessage({
|
||||||
|
...item,
|
||||||
|
rarity: item.rarity || undefined,
|
||||||
|
formattedPrice: `${item.price} 🪙`,
|
||||||
|
price: item.price
|
||||||
|
}, context);
|
||||||
|
|
||||||
|
await targetChannel.send(listingMessage as any);
|
||||||
|
await interaction.editReply({ content: `✅ Listing for **${item.name}** posted in ${targetChannel}.` });
|
||||||
|
},
|
||||||
|
{ ephemeral: true }
|
||||||
|
);
|
||||||
},
|
},
|
||||||
autocomplete: async (interaction) => {
|
autocomplete: async (interaction) => {
|
||||||
const focusedValue = interaction.options.getFocused();
|
const focusedValue = interaction.options.getFocused();
|
||||||
|
|||||||
@@ -1,8 +1,9 @@
|
|||||||
import { createCommand } from "@shared/lib/utils";
|
import { createCommand } from "@shared/lib/utils";
|
||||||
import { SlashCommandBuilder, PermissionFlagsBits, MessageFlags } from "discord.js";
|
import { SlashCommandBuilder, PermissionFlagsBits } from "discord.js";
|
||||||
import { moderationService } from "@shared/modules/moderation/moderation.service";
|
import { moderationService } from "@shared/modules/moderation/moderation.service";
|
||||||
import { CaseType } from "@shared/lib/constants";
|
import { CaseType } from "@shared/lib/constants";
|
||||||
import { getNoteSuccessEmbed, getModerationErrorEmbed } from "@/modules/moderation/moderation.view";
|
import { getNoteSuccessEmbed, getModerationErrorEmbed } from "@/modules/moderation/moderation.view";
|
||||||
|
import { withCommandErrorHandling } from "@lib/commandUtils";
|
||||||
|
|
||||||
export const note = createCommand({
|
export const note = createCommand({
|
||||||
data: new SlashCommandBuilder()
|
data: new SlashCommandBuilder()
|
||||||
@@ -24,39 +25,35 @@ export const note = createCommand({
|
|||||||
.setDefaultMemberPermissions(PermissionFlagsBits.Administrator),
|
.setDefaultMemberPermissions(PermissionFlagsBits.Administrator),
|
||||||
|
|
||||||
execute: async (interaction) => {
|
execute: async (interaction) => {
|
||||||
await interaction.deferReply({ flags: MessageFlags.Ephemeral });
|
await withCommandErrorHandling(
|
||||||
|
interaction,
|
||||||
|
async () => {
|
||||||
|
const targetUser = interaction.options.getUser("user", true);
|
||||||
|
const noteText = interaction.options.getString("note", true);
|
||||||
|
|
||||||
try {
|
// Create the note case
|
||||||
const targetUser = interaction.options.getUser("user", true);
|
const moderationCase = await moderationService.createCase({
|
||||||
const noteText = interaction.options.getString("note", true);
|
type: CaseType.NOTE,
|
||||||
|
userId: targetUser.id,
|
||||||
// Create the note case
|
username: targetUser.username,
|
||||||
const moderationCase = await moderationService.createCase({
|
moderatorId: interaction.user.id,
|
||||||
type: CaseType.NOTE,
|
moderatorName: interaction.user.username,
|
||||||
userId: targetUser.id,
|
reason: noteText,
|
||||||
username: targetUser.username,
|
|
||||||
moderatorId: interaction.user.id,
|
|
||||||
moderatorName: interaction.user.username,
|
|
||||||
reason: noteText,
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!moderationCase) {
|
|
||||||
await interaction.editReply({
|
|
||||||
embeds: [getModerationErrorEmbed("Failed to create note.")]
|
|
||||||
});
|
});
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Send success message
|
if (!moderationCase) {
|
||||||
await interaction.editReply({
|
await interaction.editReply({
|
||||||
embeds: [getNoteSuccessEmbed(moderationCase.caseId, targetUser.username)]
|
embeds: [getModerationErrorEmbed("Failed to create note.")]
|
||||||
});
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
} catch (error) {
|
// Send success message
|
||||||
console.error("Note command error:", error);
|
await interaction.editReply({
|
||||||
await interaction.editReply({
|
embeds: [getNoteSuccessEmbed(moderationCase.caseId, targetUser.username)]
|
||||||
embeds: [getModerationErrorEmbed("An error occurred while adding the note.")]
|
});
|
||||||
});
|
},
|
||||||
}
|
{ ephemeral: true }
|
||||||
|
);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,7 +1,8 @@
|
|||||||
import { createCommand } from "@shared/lib/utils";
|
import { createCommand } from "@shared/lib/utils";
|
||||||
import { SlashCommandBuilder, PermissionFlagsBits, MessageFlags } from "discord.js";
|
import { SlashCommandBuilder, PermissionFlagsBits } from "discord.js";
|
||||||
import { moderationService } from "@shared/modules/moderation/moderation.service";
|
import { moderationService } from "@shared/modules/moderation/moderation.service";
|
||||||
import { getCasesListEmbed, getModerationErrorEmbed } from "@/modules/moderation/moderation.view";
|
import { getCasesListEmbed } from "@/modules/moderation/moderation.view";
|
||||||
|
import { withCommandErrorHandling } from "@lib/commandUtils";
|
||||||
|
|
||||||
export const notes = createCommand({
|
export const notes = createCommand({
|
||||||
data: new SlashCommandBuilder()
|
data: new SlashCommandBuilder()
|
||||||
@@ -16,28 +17,24 @@ export const notes = createCommand({
|
|||||||
.setDefaultMemberPermissions(PermissionFlagsBits.Administrator),
|
.setDefaultMemberPermissions(PermissionFlagsBits.Administrator),
|
||||||
|
|
||||||
execute: async (interaction) => {
|
execute: async (interaction) => {
|
||||||
await interaction.deferReply({ flags: MessageFlags.Ephemeral });
|
await withCommandErrorHandling(
|
||||||
|
interaction,
|
||||||
|
async () => {
|
||||||
|
const targetUser = interaction.options.getUser("user", true);
|
||||||
|
|
||||||
try {
|
// Get all notes for the user
|
||||||
const targetUser = interaction.options.getUser("user", true);
|
const userNotes = await moderationService.getUserNotes(targetUser.id);
|
||||||
|
|
||||||
// Get all notes for the user
|
// Display the notes
|
||||||
const userNotes = await moderationService.getUserNotes(targetUser.id);
|
await interaction.editReply({
|
||||||
|
embeds: [getCasesListEmbed(
|
||||||
// Display the notes
|
userNotes,
|
||||||
await interaction.editReply({
|
`📝 Staff Notes for ${targetUser.username}`,
|
||||||
embeds: [getCasesListEmbed(
|
userNotes.length === 0 ? undefined : `Total notes: **${userNotes.length}**`
|
||||||
userNotes,
|
)]
|
||||||
`📝 Staff Notes for ${targetUser.username}`,
|
});
|
||||||
userNotes.length === 0 ? undefined : `Total notes: **${userNotes.length}**`
|
},
|
||||||
)]
|
{ ephemeral: true }
|
||||||
});
|
);
|
||||||
|
|
||||||
} catch (error) {
|
|
||||||
console.error("Notes command error:", error);
|
|
||||||
await interaction.editReply({
|
|
||||||
embeds: [getModerationErrorEmbed("An error occurred while fetching notes.")]
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import {
|
|||||||
getPruneWarningEmbed,
|
getPruneWarningEmbed,
|
||||||
getCancelledEmbed
|
getCancelledEmbed
|
||||||
} from "@/modules/moderation/prune.view";
|
} from "@/modules/moderation/prune.view";
|
||||||
|
import { withCommandErrorHandling } from "@lib/commandUtils";
|
||||||
|
|
||||||
export const prune = createCommand({
|
export const prune = createCommand({
|
||||||
data: new SlashCommandBuilder()
|
data: new SlashCommandBuilder()
|
||||||
@@ -38,142 +39,126 @@ export const prune = createCommand({
|
|||||||
.setDefaultMemberPermissions(PermissionFlagsBits.Administrator),
|
.setDefaultMemberPermissions(PermissionFlagsBits.Administrator),
|
||||||
|
|
||||||
execute: async (interaction) => {
|
execute: async (interaction) => {
|
||||||
await interaction.deferReply({ flags: MessageFlags.Ephemeral });
|
await withCommandErrorHandling(
|
||||||
|
interaction,
|
||||||
|
async () => {
|
||||||
|
const amount = interaction.options.getInteger("amount");
|
||||||
|
const user = interaction.options.getUser("user");
|
||||||
|
const all = interaction.options.getBoolean("all") || false;
|
||||||
|
|
||||||
try {
|
// Validate inputs
|
||||||
const amount = interaction.options.getInteger("amount");
|
if (!amount && !all) {
|
||||||
const user = interaction.options.getUser("user");
|
// Default to 10 messages
|
||||||
const all = interaction.options.getBoolean("all") || false;
|
} else if (amount && all) {
|
||||||
|
|
||||||
// Validate inputs
|
|
||||||
if (!amount && !all) {
|
|
||||||
// Default to 10 messages
|
|
||||||
} else if (amount && all) {
|
|
||||||
await interaction.editReply({
|
|
||||||
embeds: [getPruneErrorEmbed("Cannot specify both `amount` and `all`. Please use one or the other.")]
|
|
||||||
});
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const finalAmount = all ? 'all' : (amount || 10);
|
|
||||||
const confirmThreshold = config.moderation.prune.confirmThreshold;
|
|
||||||
|
|
||||||
// Check if confirmation is needed
|
|
||||||
const needsConfirmation = all || (typeof finalAmount === 'number' && finalAmount > confirmThreshold);
|
|
||||||
|
|
||||||
if (needsConfirmation) {
|
|
||||||
// Estimate message count for confirmation
|
|
||||||
let estimatedCount: number | undefined;
|
|
||||||
if (all) {
|
|
||||||
try {
|
|
||||||
estimatedCount = await pruneService.estimateMessageCount(interaction.channel!);
|
|
||||||
} catch {
|
|
||||||
estimatedCount = undefined;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const { embeds, components } = getConfirmationMessage(finalAmount, estimatedCount);
|
|
||||||
const response = await interaction.editReply({ embeds, components });
|
|
||||||
|
|
||||||
try {
|
|
||||||
const confirmation = await response.awaitMessageComponent({
|
|
||||||
filter: (i) => i.user.id === interaction.user.id,
|
|
||||||
componentType: ComponentType.Button,
|
|
||||||
time: 30000
|
|
||||||
});
|
|
||||||
|
|
||||||
if (confirmation.customId === "cancel_prune") {
|
|
||||||
await confirmation.update({
|
|
||||||
embeds: [getCancelledEmbed()],
|
|
||||||
components: []
|
|
||||||
});
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// User confirmed, proceed with deletion
|
|
||||||
await confirmation.update({
|
|
||||||
embeds: [getProgressEmbed({ current: 0, total: estimatedCount || finalAmount as number })],
|
|
||||||
components: []
|
|
||||||
});
|
|
||||||
|
|
||||||
// Execute deletion with progress callback for 'all' mode
|
|
||||||
const result = await pruneService.deleteMessages(
|
|
||||||
interaction.channel!,
|
|
||||||
{
|
|
||||||
amount: typeof finalAmount === 'number' ? finalAmount : undefined,
|
|
||||||
userId: user?.id,
|
|
||||||
all
|
|
||||||
},
|
|
||||||
all ? async (progress) => {
|
|
||||||
await interaction.editReply({
|
|
||||||
embeds: [getProgressEmbed(progress)]
|
|
||||||
});
|
|
||||||
} : undefined
|
|
||||||
);
|
|
||||||
|
|
||||||
// Show success
|
|
||||||
await interaction.editReply({
|
await interaction.editReply({
|
||||||
embeds: [getSuccessEmbed(result)],
|
embeds: [getPruneErrorEmbed("Cannot specify both `amount` and `all`. Please use one or the other.")]
|
||||||
components: []
|
|
||||||
});
|
});
|
||||||
|
|
||||||
} catch (error) {
|
|
||||||
if (error instanceof Error && error.message.includes("time")) {
|
|
||||||
await interaction.editReply({
|
|
||||||
embeds: [getPruneWarningEmbed("Confirmation timed out. Please try again.")],
|
|
||||||
components: []
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// No confirmation needed, proceed directly
|
|
||||||
const result = await pruneService.deleteMessages(
|
|
||||||
interaction.channel!,
|
|
||||||
{
|
|
||||||
amount: finalAmount as number,
|
|
||||||
userId: user?.id,
|
|
||||||
all: false
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
// Check if no messages were found
|
|
||||||
if (result.deletedCount === 0) {
|
|
||||||
if (user) {
|
|
||||||
await interaction.editReply({
|
|
||||||
embeds: [getPruneWarningEmbed(`No messages found from **${user.username}** in the last ${finalAmount} messages.`)]
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
await interaction.editReply({
|
|
||||||
embeds: [getPruneWarningEmbed("No messages found to delete.")]
|
|
||||||
});
|
|
||||||
}
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
await interaction.editReply({
|
const finalAmount = all ? 'all' : (amount || 10);
|
||||||
embeds: [getSuccessEmbed(result)]
|
const confirmThreshold = config.moderation.prune.confirmThreshold;
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
} catch (error) {
|
// Check if confirmation is needed
|
||||||
console.error("Prune command error:", error);
|
const needsConfirmation = all || (typeof finalAmount === 'number' && finalAmount > confirmThreshold);
|
||||||
|
|
||||||
let errorMessage = "An unexpected error occurred while trying to delete messages.";
|
if (needsConfirmation) {
|
||||||
if (error instanceof Error) {
|
// Estimate message count for confirmation
|
||||||
if (error.message.includes("permission")) {
|
let estimatedCount: number | undefined;
|
||||||
errorMessage = "I don't have permission to delete messages in this channel.";
|
if (all) {
|
||||||
} else if (error.message.includes("channel type")) {
|
try {
|
||||||
errorMessage = "This command cannot be used in this type of channel.";
|
estimatedCount = await pruneService.estimateMessageCount(interaction.channel!);
|
||||||
|
} catch {
|
||||||
|
estimatedCount = undefined;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const { embeds, components } = getConfirmationMessage(finalAmount, estimatedCount);
|
||||||
|
const response = await interaction.editReply({ embeds, components });
|
||||||
|
|
||||||
|
try {
|
||||||
|
const confirmation = await response.awaitMessageComponent({
|
||||||
|
filter: (i) => i.user.id === interaction.user.id,
|
||||||
|
componentType: ComponentType.Button,
|
||||||
|
time: 30000
|
||||||
|
});
|
||||||
|
|
||||||
|
if (confirmation.customId === "cancel_prune") {
|
||||||
|
await confirmation.update({
|
||||||
|
embeds: [getCancelledEmbed()],
|
||||||
|
components: []
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// User confirmed, proceed with deletion
|
||||||
|
await confirmation.update({
|
||||||
|
embeds: [getProgressEmbed({ current: 0, total: estimatedCount || finalAmount as number })],
|
||||||
|
components: []
|
||||||
|
});
|
||||||
|
|
||||||
|
// Execute deletion with progress callback for 'all' mode
|
||||||
|
const result = await pruneService.deleteMessages(
|
||||||
|
interaction.channel!,
|
||||||
|
{
|
||||||
|
amount: typeof finalAmount === 'number' ? finalAmount : undefined,
|
||||||
|
userId: user?.id,
|
||||||
|
all
|
||||||
|
},
|
||||||
|
all ? async (progress) => {
|
||||||
|
await interaction.editReply({
|
||||||
|
embeds: [getProgressEmbed(progress)]
|
||||||
|
});
|
||||||
|
} : undefined
|
||||||
|
);
|
||||||
|
|
||||||
|
// Show success
|
||||||
|
await interaction.editReply({
|
||||||
|
embeds: [getSuccessEmbed(result)],
|
||||||
|
components: []
|
||||||
|
});
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
if (error instanceof Error && error.message.includes("time")) {
|
||||||
|
await interaction.editReply({
|
||||||
|
embeds: [getPruneWarningEmbed("Confirmation timed out. Please try again.")],
|
||||||
|
components: []
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
errorMessage = error.message;
|
// No confirmation needed, proceed directly
|
||||||
}
|
const result = await pruneService.deleteMessages(
|
||||||
}
|
interaction.channel!,
|
||||||
|
{
|
||||||
|
amount: finalAmount as number,
|
||||||
|
userId: user?.id,
|
||||||
|
all: false
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
await interaction.editReply({
|
// Check if no messages were found
|
||||||
embeds: [getPruneErrorEmbed(errorMessage)]
|
if (result.deletedCount === 0) {
|
||||||
});
|
if (user) {
|
||||||
}
|
await interaction.editReply({
|
||||||
|
embeds: [getPruneWarningEmbed(`No messages found from **${user.username}** in the last ${finalAmount} messages.`)]
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
await interaction.editReply({
|
||||||
|
embeds: [getPruneWarningEmbed("No messages found to delete.")]
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
await interaction.editReply({
|
||||||
|
embeds: [getSuccessEmbed(result)]
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{ ephemeral: true }
|
||||||
|
);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,7 +1,8 @@
|
|||||||
import { createCommand } from "@shared/lib/utils";
|
import { createCommand } from "@shared/lib/utils";
|
||||||
import { AuroraClient } from "@/lib/BotClient";
|
import { AuroraClient } from "@/lib/BotClient";
|
||||||
import { SlashCommandBuilder, PermissionFlagsBits, MessageFlags } from "discord.js";
|
import { SlashCommandBuilder, PermissionFlagsBits } from "discord.js";
|
||||||
import { createErrorEmbed, createSuccessEmbed } from "@lib/embeds";
|
import { createSuccessEmbed } from "@lib/embeds";
|
||||||
|
import { withCommandErrorHandling } from "@lib/commandUtils";
|
||||||
|
|
||||||
export const refresh = createCommand({
|
export const refresh = createCommand({
|
||||||
data: new SlashCommandBuilder()
|
data: new SlashCommandBuilder()
|
||||||
@@ -9,25 +10,24 @@ export const refresh = createCommand({
|
|||||||
.setDescription("Reloads all commands and config without restarting")
|
.setDescription("Reloads all commands and config without restarting")
|
||||||
.setDefaultMemberPermissions(PermissionFlagsBits.Administrator),
|
.setDefaultMemberPermissions(PermissionFlagsBits.Administrator),
|
||||||
execute: async (interaction) => {
|
execute: async (interaction) => {
|
||||||
await interaction.deferReply({ flags: MessageFlags.Ephemeral });
|
await withCommandErrorHandling(
|
||||||
|
interaction,
|
||||||
|
async () => {
|
||||||
|
const start = Date.now();
|
||||||
|
await AuroraClient.loadCommands(true);
|
||||||
|
const duration = Date.now() - start;
|
||||||
|
|
||||||
try {
|
// Deploy commands
|
||||||
const start = Date.now();
|
await AuroraClient.deployCommands();
|
||||||
await AuroraClient.loadCommands(true);
|
|
||||||
const duration = Date.now() - start;
|
|
||||||
|
|
||||||
// Deploy commands
|
const embed = createSuccessEmbed(
|
||||||
await AuroraClient.deployCommands();
|
`Successfully reloaded ${AuroraClient.commands.size} commands in ${duration}ms.`,
|
||||||
|
"System Refreshed"
|
||||||
|
);
|
||||||
|
|
||||||
const embed = createSuccessEmbed(
|
await interaction.editReply({ embeds: [embed] });
|
||||||
`Successfully reloaded ${AuroraClient.commands.size} commands in ${duration}ms.`,
|
},
|
||||||
"System Refreshed"
|
{ ephemeral: true }
|
||||||
);
|
);
|
||||||
|
|
||||||
await interaction.editReply({ embeds: [embed] });
|
|
||||||
} catch (error) {
|
|
||||||
console.error(error);
|
|
||||||
await interaction.editReply({ embeds: [createErrorEmbed("An error occurred while refreshing commands. Check console for details.", "Refresh Failed")] });
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -3,7 +3,7 @@ import { SlashCommandBuilder, PermissionFlagsBits, Colors, ChatInputCommandInter
|
|||||||
import { guildSettingsService } from "@shared/modules/guild-settings/guild-settings.service";
|
import { guildSettingsService } from "@shared/modules/guild-settings/guild-settings.service";
|
||||||
import { createBaseEmbed, createErrorEmbed, createSuccessEmbed } from "@lib/embeds";
|
import { createBaseEmbed, createErrorEmbed, createSuccessEmbed } from "@lib/embeds";
|
||||||
import { getGuildConfig, invalidateGuildConfigCache } from "@shared/lib/config";
|
import { getGuildConfig, invalidateGuildConfigCache } from "@shared/lib/config";
|
||||||
import { UserError } from "@shared/lib/errors";
|
import { withCommandErrorHandling } from "@lib/commandUtils";
|
||||||
|
|
||||||
export const settings = createCommand({
|
export const settings = createCommand({
|
||||||
data: new SlashCommandBuilder()
|
data: new SlashCommandBuilder()
|
||||||
@@ -84,33 +84,29 @@ export const settings = createCommand({
|
|||||||
.setRequired(false))),
|
.setRequired(false))),
|
||||||
|
|
||||||
execute: async (interaction) => {
|
execute: async (interaction) => {
|
||||||
await interaction.deferReply({ ephemeral: true });
|
await withCommandErrorHandling(
|
||||||
|
interaction,
|
||||||
|
async () => {
|
||||||
|
const subcommand = interaction.options.getSubcommand();
|
||||||
|
const guildId = interaction.guildId!;
|
||||||
|
|
||||||
const subcommand = interaction.options.getSubcommand();
|
switch (subcommand) {
|
||||||
const guildId = interaction.guildId!;
|
case "show":
|
||||||
|
await handleShow(interaction, guildId);
|
||||||
try {
|
break;
|
||||||
switch (subcommand) {
|
case "set":
|
||||||
case "show":
|
await handleSet(interaction, guildId);
|
||||||
await handleShow(interaction, guildId);
|
break;
|
||||||
break;
|
case "reset":
|
||||||
case "set":
|
await handleReset(interaction, guildId);
|
||||||
await handleSet(interaction, guildId);
|
break;
|
||||||
break;
|
case "colors":
|
||||||
case "reset":
|
await handleColors(interaction, guildId);
|
||||||
await handleReset(interaction, guildId);
|
break;
|
||||||
break;
|
}
|
||||||
case "colors":
|
},
|
||||||
await handleColors(interaction, guildId);
|
{ ephemeral: true }
|
||||||
break;
|
);
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
if (error instanceof UserError) {
|
|
||||||
await interaction.editReply({ embeds: [createErrorEmbed(error.message)] });
|
|
||||||
} else {
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -2,7 +2,8 @@
|
|||||||
import { createCommand } from "@shared/lib/utils";
|
import { createCommand } from "@shared/lib/utils";
|
||||||
import { SlashCommandBuilder, PermissionFlagsBits, ChannelType, TextChannel } from "discord.js";
|
import { SlashCommandBuilder, PermissionFlagsBits, ChannelType, TextChannel } from "discord.js";
|
||||||
import { terminalService } from "@shared/modules/terminal/terminal.service";
|
import { terminalService } from "@shared/modules/terminal/terminal.service";
|
||||||
import { createBaseEmbed, createErrorEmbed } from "@/lib/embeds";
|
import { createErrorEmbed } from "@/lib/embeds";
|
||||||
|
import { withCommandErrorHandling } from "@lib/commandUtils";
|
||||||
|
|
||||||
export const terminal = createCommand({
|
export const terminal = createCommand({
|
||||||
data: new SlashCommandBuilder()
|
data: new SlashCommandBuilder()
|
||||||
@@ -23,15 +24,14 @@ export const terminal = createCommand({
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
await interaction.reply({ ephemeral: true, content: "Initializing terminal..." });
|
await withCommandErrorHandling(
|
||||||
|
interaction,
|
||||||
try {
|
async () => {
|
||||||
await terminalService.init(channel as TextChannel);
|
await terminalService.init(channel as TextChannel);
|
||||||
await interaction.editReply({ content: "✅ Terminal initialized!" });
|
await interaction.editReply({ content: "✅ Terminal initialized!" });
|
||||||
} catch (error) {
|
},
|
||||||
console.error(error);
|
{ ephemeral: true }
|
||||||
await interaction.editReply({ content: "❌ Failed to initialize terminal." });
|
);
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -4,9 +4,9 @@ import { moderationService } from "@shared/modules/moderation/moderation.service
|
|||||||
import {
|
import {
|
||||||
getWarnSuccessEmbed,
|
getWarnSuccessEmbed,
|
||||||
getModerationErrorEmbed,
|
getModerationErrorEmbed,
|
||||||
getUserWarningEmbed
|
|
||||||
} from "@/modules/moderation/moderation.view";
|
} from "@/modules/moderation/moderation.view";
|
||||||
import { getGuildConfig } from "@shared/lib/config";
|
import { getGuildConfig } from "@shared/lib/config";
|
||||||
|
import { withCommandErrorHandling } from "@lib/commandUtils";
|
||||||
|
|
||||||
export const warn = createCommand({
|
export const warn = createCommand({
|
||||||
data: new SlashCommandBuilder()
|
data: new SlashCommandBuilder()
|
||||||
@@ -28,67 +28,63 @@ export const warn = createCommand({
|
|||||||
.setDefaultMemberPermissions(PermissionFlagsBits.Administrator),
|
.setDefaultMemberPermissions(PermissionFlagsBits.Administrator),
|
||||||
|
|
||||||
execute: async (interaction) => {
|
execute: async (interaction) => {
|
||||||
await interaction.deferReply({ flags: MessageFlags.Ephemeral });
|
await withCommandErrorHandling(
|
||||||
|
interaction,
|
||||||
|
async () => {
|
||||||
|
const targetUser = interaction.options.getUser("user", true);
|
||||||
|
const reason = interaction.options.getString("reason", true);
|
||||||
|
|
||||||
try {
|
// Don't allow warning bots
|
||||||
const targetUser = interaction.options.getUser("user", true);
|
if (targetUser.bot) {
|
||||||
const reason = interaction.options.getString("reason", true);
|
await interaction.editReply({
|
||||||
|
embeds: [getModerationErrorEmbed("You cannot warn bots.")]
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
// Don't allow warning bots
|
// Don't allow self-warnings
|
||||||
if (targetUser.bot) {
|
if (targetUser.id === interaction.user.id) {
|
||||||
|
await interaction.editReply({
|
||||||
|
embeds: [getModerationErrorEmbed("You cannot warn yourself.")]
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fetch guild config for moderation settings
|
||||||
|
const guildConfig = await getGuildConfig(interaction.guildId!);
|
||||||
|
|
||||||
|
// Issue the warning via service
|
||||||
|
const { moderationCase, warningCount, autoTimeoutIssued } = await moderationService.issueWarning({
|
||||||
|
userId: targetUser.id,
|
||||||
|
username: targetUser.username,
|
||||||
|
moderatorId: interaction.user.id,
|
||||||
|
moderatorName: interaction.user.username,
|
||||||
|
reason,
|
||||||
|
guildName: interaction.guild?.name || undefined,
|
||||||
|
dmTarget: targetUser,
|
||||||
|
timeoutTarget: await interaction.guild?.members.fetch(targetUser.id),
|
||||||
|
config: {
|
||||||
|
dmOnWarn: guildConfig.moderation?.cases?.dmOnWarn,
|
||||||
|
autoTimeoutThreshold: guildConfig.moderation?.cases?.autoTimeoutThreshold,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
// Send success message to moderator
|
||||||
await interaction.editReply({
|
await interaction.editReply({
|
||||||
embeds: [getModerationErrorEmbed("You cannot warn bots.")]
|
embeds: [getWarnSuccessEmbed(moderationCase.caseId, targetUser.username, reason)]
|
||||||
});
|
});
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Don't allow self-warnings
|
// Follow up if auto-timeout was issued
|
||||||
if (targetUser.id === interaction.user.id) {
|
if (autoTimeoutIssued) {
|
||||||
await interaction.editReply({
|
await interaction.followUp({
|
||||||
embeds: [getModerationErrorEmbed("You cannot warn yourself.")]
|
embeds: [getModerationErrorEmbed(
|
||||||
});
|
`⚠️ User has reached ${warningCount} warnings and has been automatically timed out for 24 hours.`
|
||||||
return;
|
)],
|
||||||
}
|
flags: MessageFlags.Ephemeral
|
||||||
|
});
|
||||||
// Fetch guild config for moderation settings
|
}
|
||||||
const guildConfig = await getGuildConfig(interaction.guildId!);
|
},
|
||||||
|
{ ephemeral: true }
|
||||||
// Issue the warning via service
|
);
|
||||||
const { moderationCase, warningCount, autoTimeoutIssued } = await moderationService.issueWarning({
|
|
||||||
userId: targetUser.id,
|
|
||||||
username: targetUser.username,
|
|
||||||
moderatorId: interaction.user.id,
|
|
||||||
moderatorName: interaction.user.username,
|
|
||||||
reason,
|
|
||||||
guildName: interaction.guild?.name || undefined,
|
|
||||||
dmTarget: targetUser,
|
|
||||||
timeoutTarget: await interaction.guild?.members.fetch(targetUser.id),
|
|
||||||
config: {
|
|
||||||
dmOnWarn: guildConfig.moderation?.cases?.dmOnWarn,
|
|
||||||
autoTimeoutThreshold: guildConfig.moderation?.cases?.autoTimeoutThreshold,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
// Send success message to moderator
|
|
||||||
await interaction.editReply({
|
|
||||||
embeds: [getWarnSuccessEmbed(moderationCase.caseId, targetUser.username, reason)]
|
|
||||||
});
|
|
||||||
|
|
||||||
// Follow up if auto-timeout was issued
|
|
||||||
if (autoTimeoutIssued) {
|
|
||||||
await interaction.followUp({
|
|
||||||
embeds: [getModerationErrorEmbed(
|
|
||||||
`⚠️ User has reached ${warningCount} warnings and has been automatically timed out for 24 hours.`
|
|
||||||
)],
|
|
||||||
flags: MessageFlags.Ephemeral
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
} catch (error) {
|
|
||||||
console.error("Warn command error:", error);
|
|
||||||
await interaction.editReply({
|
|
||||||
embeds: [getModerationErrorEmbed("An error occurred while issuing the warning.")]
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,7 +1,8 @@
|
|||||||
import { createCommand } from "@shared/lib/utils";
|
import { createCommand } from "@shared/lib/utils";
|
||||||
import { SlashCommandBuilder, PermissionFlagsBits, MessageFlags } from "discord.js";
|
import { SlashCommandBuilder, PermissionFlagsBits } from "discord.js";
|
||||||
import { moderationService } from "@shared/modules/moderation/moderation.service";
|
import { moderationService } from "@shared/modules/moderation/moderation.service";
|
||||||
import { getWarningsEmbed, getModerationErrorEmbed } from "@/modules/moderation/moderation.view";
|
import { getWarningsEmbed } from "@/modules/moderation/moderation.view";
|
||||||
|
import { withCommandErrorHandling } from "@lib/commandUtils";
|
||||||
|
|
||||||
export const warnings = createCommand({
|
export const warnings = createCommand({
|
||||||
data: new SlashCommandBuilder()
|
data: new SlashCommandBuilder()
|
||||||
@@ -16,24 +17,20 @@ export const warnings = createCommand({
|
|||||||
.setDefaultMemberPermissions(PermissionFlagsBits.Administrator),
|
.setDefaultMemberPermissions(PermissionFlagsBits.Administrator),
|
||||||
|
|
||||||
execute: async (interaction) => {
|
execute: async (interaction) => {
|
||||||
await interaction.deferReply({ flags: MessageFlags.Ephemeral });
|
await withCommandErrorHandling(
|
||||||
|
interaction,
|
||||||
|
async () => {
|
||||||
|
const targetUser = interaction.options.getUser("user", true);
|
||||||
|
|
||||||
try {
|
// Get active warnings for the user
|
||||||
const targetUser = interaction.options.getUser("user", true);
|
const activeWarnings = await moderationService.getUserWarnings(targetUser.id);
|
||||||
|
|
||||||
// Get active warnings for the user
|
// Display the warnings
|
||||||
const activeWarnings = await moderationService.getUserWarnings(targetUser.id);
|
await interaction.editReply({
|
||||||
|
embeds: [getWarningsEmbed(activeWarnings, targetUser.username)]
|
||||||
// Display the warnings
|
});
|
||||||
await interaction.editReply({
|
},
|
||||||
embeds: [getWarningsEmbed(activeWarnings, targetUser.username)]
|
{ ephemeral: true }
|
||||||
});
|
);
|
||||||
|
|
||||||
} catch (error) {
|
|
||||||
console.error("Warnings command error:", error);
|
|
||||||
await interaction.editReply({
|
|
||||||
embeds: [getModerationErrorEmbed("An error occurred while fetching warnings.")]
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import { createCommand } from "@shared/lib/utils";
|
|||||||
import { SlashCommandBuilder, PermissionFlagsBits, MessageFlags } from "discord.js";
|
import { SlashCommandBuilder, PermissionFlagsBits, MessageFlags } from "discord.js";
|
||||||
import { createErrorEmbed } from "@/lib/embeds";
|
import { createErrorEmbed } from "@/lib/embeds";
|
||||||
import { sendWebhookMessage } from "@/lib/webhookUtils";
|
import { sendWebhookMessage } from "@/lib/webhookUtils";
|
||||||
|
import { withCommandErrorHandling } from "@lib/commandUtils";
|
||||||
|
|
||||||
export const webhook = createCommand({
|
export const webhook = createCommand({
|
||||||
data: new SlashCommandBuilder()
|
data: new SlashCommandBuilder()
|
||||||
@@ -14,43 +15,40 @@ export const webhook = createCommand({
|
|||||||
.setRequired(true)
|
.setRequired(true)
|
||||||
),
|
),
|
||||||
execute: async (interaction) => {
|
execute: async (interaction) => {
|
||||||
await interaction.deferReply({ flags: MessageFlags.Ephemeral });
|
await withCommandErrorHandling(
|
||||||
|
interaction,
|
||||||
|
async () => {
|
||||||
|
const payloadString = interaction.options.getString("payload", true);
|
||||||
|
let payload;
|
||||||
|
|
||||||
const payloadString = interaction.options.getString("payload", true);
|
try {
|
||||||
let payload;
|
payload = JSON.parse(payloadString);
|
||||||
|
} catch (error) {
|
||||||
|
await interaction.editReply({
|
||||||
|
embeds: [createErrorEmbed("The provided payload is not valid JSON.", "Invalid JSON")]
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
const channel = interaction.channel;
|
||||||
payload = JSON.parse(payloadString);
|
|
||||||
} catch (error) {
|
|
||||||
await interaction.editReply({
|
|
||||||
embeds: [createErrorEmbed("The provided payload is not valid JSON.", "Invalid JSON")]
|
|
||||||
});
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const channel = interaction.channel;
|
if (!channel || !('createWebhook' in channel)) {
|
||||||
|
await interaction.editReply({
|
||||||
|
embeds: [createErrorEmbed("This channel does not support webhooks.", "Unsupported Channel")]
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (!channel || !('createWebhook' in channel)) {
|
await sendWebhookMessage(
|
||||||
await interaction.editReply({
|
channel,
|
||||||
embeds: [createErrorEmbed("This channel does not support webhooks.", "Unsupported Channel")]
|
payload,
|
||||||
});
|
interaction.client.user,
|
||||||
return;
|
`Proxy message requested by ${interaction.user.tag}`
|
||||||
}
|
);
|
||||||
|
|
||||||
try {
|
await interaction.editReply({ content: "Message sent successfully!" });
|
||||||
await sendWebhookMessage(
|
},
|
||||||
channel,
|
{ ephemeral: true }
|
||||||
payload,
|
);
|
||||||
interaction.client.user,
|
|
||||||
`Proxy message requested by ${interaction.user.tag}`
|
|
||||||
);
|
|
||||||
|
|
||||||
await interaction.editReply({ content: "Message sent successfully!" });
|
|
||||||
} catch (error) {
|
|
||||||
console.error("Webhook error:", error);
|
|
||||||
await interaction.editReply({
|
|
||||||
embeds: [createErrorEmbed("Failed to send message via webhook. Ensure the bot has 'Manage Webhooks' permission and the payload is valid.", "Delivery Failed")]
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -2,35 +2,29 @@
|
|||||||
import { createCommand } from "@shared/lib/utils";
|
import { createCommand } from "@shared/lib/utils";
|
||||||
import { SlashCommandBuilder } from "discord.js";
|
import { SlashCommandBuilder } from "discord.js";
|
||||||
import { economyService } from "@shared/modules/economy/economy.service";
|
import { economyService } from "@shared/modules/economy/economy.service";
|
||||||
import { createErrorEmbed, createSuccessEmbed } from "@lib/embeds";
|
import { createSuccessEmbed } from "@lib/embeds";
|
||||||
import { UserError } from "@shared/lib/errors";
|
import { withCommandErrorHandling } from "@lib/commandUtils";
|
||||||
|
|
||||||
export const daily = createCommand({
|
export const daily = createCommand({
|
||||||
data: new SlashCommandBuilder()
|
data: new SlashCommandBuilder()
|
||||||
.setName("daily")
|
.setName("daily")
|
||||||
.setDescription("Claim your daily reward"),
|
.setDescription("Claim your daily reward"),
|
||||||
execute: async (interaction) => {
|
execute: async (interaction) => {
|
||||||
await interaction.deferReply();
|
await withCommandErrorHandling(
|
||||||
try {
|
interaction,
|
||||||
const result = await economyService.claimDaily(interaction.user.id);
|
async () => {
|
||||||
|
const result = await economyService.claimDaily(interaction.user.id);
|
||||||
|
|
||||||
const embed = createSuccessEmbed(`You claimed ** ${result.amount}** Astral Units!${result.isWeekly ? `\n🎉 **Weekly Bonus!** +${result.weeklyBonus} extra!` : ''}`, "💰 Daily Reward Claimed!")
|
const embed = createSuccessEmbed(`You claimed ** ${result.amount}** Astral Units!${result.isWeekly ? `\n🎉 **Weekly Bonus!** +${result.weeklyBonus} extra!` : ''}`, "💰 Daily Reward Claimed!")
|
||||||
.addFields(
|
.addFields(
|
||||||
{ name: "Streak", value: `🔥 ${result.streak} days`, inline: true },
|
{ name: "Streak", value: `🔥 ${result.streak} days`, inline: true },
|
||||||
{ name: "Weekly Progress", value: `${"🟩".repeat(result.streak % 7 || 7)}${"⬜".repeat(7 - (result.streak % 7 || 7))} (${result.streak % 7 || 7}/7)`, inline: true },
|
{ name: "Weekly Progress", value: `${"🟩".repeat(result.streak % 7 || 7)}${"⬜".repeat(7 - (result.streak % 7 || 7))} (${result.streak % 7 || 7}/7)`, inline: true },
|
||||||
{ name: "Next Reward", value: `<t:${Math.floor(result.nextReadyAt.getTime() / 1000)}:R> `, inline: true }
|
{ name: "Next Reward", value: `<t:${Math.floor(result.nextReadyAt.getTime() / 1000)}:R> `, inline: true }
|
||||||
)
|
)
|
||||||
.setColor("Gold");
|
.setColor("Gold");
|
||||||
|
|
||||||
await interaction.editReply({ embeds: [embed] });
|
await interaction.editReply({ embeds: [embed] });
|
||||||
|
|
||||||
} catch (error: any) {
|
|
||||||
if (error instanceof UserError) {
|
|
||||||
await interaction.editReply({ embeds: [createErrorEmbed(error.message)] });
|
|
||||||
} else {
|
|
||||||
console.error("Error claiming daily:", error);
|
|
||||||
await interaction.editReply({ embeds: [createErrorEmbed("An unexpected error occurred.")] });
|
|
||||||
}
|
}
|
||||||
}
|
);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -2,6 +2,7 @@ import { createCommand } from "@shared/lib/utils";
|
|||||||
import { SlashCommandBuilder } from "discord.js";
|
import { SlashCommandBuilder } from "discord.js";
|
||||||
import { createErrorEmbed, createSuccessEmbed } from "@lib/embeds";
|
import { createErrorEmbed, createSuccessEmbed } from "@lib/embeds";
|
||||||
import { examService, ExamStatus } from "@shared/modules/economy/exam.service";
|
import { examService, ExamStatus } from "@shared/modules/economy/exam.service";
|
||||||
|
import { withCommandErrorHandling } from "@lib/commandUtils";
|
||||||
|
|
||||||
const DAYS = ["Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"];
|
const DAYS = ["Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"];
|
||||||
|
|
||||||
@@ -10,66 +11,62 @@ export const exam = createCommand({
|
|||||||
.setName("exam")
|
.setName("exam")
|
||||||
.setDescription("Take your weekly exam to earn rewards based on your XP progress."),
|
.setDescription("Take your weekly exam to earn rewards based on your XP progress."),
|
||||||
execute: async (interaction) => {
|
execute: async (interaction) => {
|
||||||
await interaction.deferReply();
|
await withCommandErrorHandling(
|
||||||
|
interaction,
|
||||||
|
async () => {
|
||||||
|
// First, try to take the exam or check status
|
||||||
|
const result = await examService.takeExam(interaction.user.id);
|
||||||
|
|
||||||
try {
|
if (result.status === ExamStatus.NOT_REGISTERED) {
|
||||||
// First, try to take the exam or check status
|
// Register the user
|
||||||
const result = await examService.takeExam(interaction.user.id);
|
const regResult = await examService.registerForExam(interaction.user.id, interaction.user.username);
|
||||||
|
const nextRegTimestamp = Math.floor(regResult.nextExamAt!.getTime() / 1000);
|
||||||
|
|
||||||
if (result.status === ExamStatus.NOT_REGISTERED) {
|
await interaction.editReply({
|
||||||
// Register the user
|
embeds: [createSuccessEmbed(
|
||||||
const regResult = await examService.registerForExam(interaction.user.id, interaction.user.username);
|
`You have registered for the exam! Your exam day is **${DAYS[regResult.examDay!]}** (Server Time).\n` +
|
||||||
const nextRegTimestamp = Math.floor(regResult.nextExamAt!.getTime() / 1000);
|
`Come back on <t:${nextRegTimestamp}:D> (<t:${nextRegTimestamp}:R>) to take your first exam!`,
|
||||||
|
"Exam Registration Successful"
|
||||||
|
)]
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const nextExamTimestamp = Math.floor(result.nextExamAt!.getTime() / 1000);
|
||||||
|
|
||||||
|
if (result.status === ExamStatus.COOLDOWN) {
|
||||||
|
await interaction.editReply({
|
||||||
|
embeds: [createErrorEmbed(
|
||||||
|
`You have already taken your exam for this week (or are waiting for your first week to pass).\n` +
|
||||||
|
`Next exam available: <t:${nextExamTimestamp}:D> (<t:${nextExamTimestamp}:R>)`
|
||||||
|
)]
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (result.status === ExamStatus.MISSED) {
|
||||||
|
await interaction.editReply({
|
||||||
|
embeds: [createErrorEmbed(
|
||||||
|
`You missed your exam day! Your exam day is **${DAYS[result.examDay!]}** (Server Time).\n` +
|
||||||
|
`You verify your attendance but score a **0**.\n` +
|
||||||
|
`Your next exam opportunity is: <t:${nextExamTimestamp}:D> (<t:${nextExamTimestamp}:R>)`,
|
||||||
|
"Exam Failed"
|
||||||
|
)]
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// If it reached here with AVAILABLE, it means they passed
|
||||||
await interaction.editReply({
|
await interaction.editReply({
|
||||||
embeds: [createSuccessEmbed(
|
embeds: [createSuccessEmbed(
|
||||||
`You have registered for the exam! Your exam day is **${DAYS[regResult.examDay!]}** (Server Time).\n` +
|
`**XP Gained:** ${result.xpDiff?.toString()}\n` +
|
||||||
`Come back on <t:${nextRegTimestamp}:D> (<t:${nextRegTimestamp}:R>) to take your first exam!`,
|
`**Multiplier:** x${result.multiplier?.toFixed(2)}\n` +
|
||||||
"Exam Registration Successful"
|
`**Reward:** ${result.reward?.toString()} Currency\n\n` +
|
||||||
|
`See you next week: <t:${nextExamTimestamp}:D>`,
|
||||||
|
"Exam Passed!"
|
||||||
)]
|
)]
|
||||||
});
|
});
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
|
);
|
||||||
const nextExamTimestamp = Math.floor(result.nextExamAt!.getTime() / 1000);
|
|
||||||
|
|
||||||
if (result.status === ExamStatus.COOLDOWN) {
|
|
||||||
await interaction.editReply({
|
|
||||||
embeds: [createErrorEmbed(
|
|
||||||
`You have already taken your exam for this week (or are waiting for your first week to pass).\n` +
|
|
||||||
`Next exam available: <t:${nextExamTimestamp}:D> (<t:${nextExamTimestamp}:R>)`
|
|
||||||
)]
|
|
||||||
});
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (result.status === ExamStatus.MISSED) {
|
|
||||||
await interaction.editReply({
|
|
||||||
embeds: [createErrorEmbed(
|
|
||||||
`You missed your exam day! Your exam day is **${DAYS[result.examDay!]}** (Server Time).\n` +
|
|
||||||
`You verify your attendance but score a **0**.\n` +
|
|
||||||
`Your next exam opportunity is: <t:${nextExamTimestamp}:D> (<t:${nextExamTimestamp}:R>)`,
|
|
||||||
"Exam Failed"
|
|
||||||
)]
|
|
||||||
});
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// If it reached here with AVAILABLE, it means they passed
|
|
||||||
await interaction.editReply({
|
|
||||||
embeds: [createSuccessEmbed(
|
|
||||||
`**XP Gained:** ${result.xpDiff?.toString()}\n` +
|
|
||||||
`**Multiplier:** x${result.multiplier?.toFixed(2)}\n` +
|
|
||||||
`**Reward:** ${result.reward?.toString()} Currency\n\n` +
|
|
||||||
`See you next week: <t:${nextExamTimestamp}:D>`,
|
|
||||||
"Exam Passed!"
|
|
||||||
)]
|
|
||||||
});
|
|
||||||
|
|
||||||
} catch (error: any) {
|
|
||||||
console.error("Error in exam command:", error);
|
|
||||||
await interaction.editReply({ embeds: [createErrorEmbed(error.message || "An unexpected error occurred.")] });
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import { economyService } from "@shared/modules/economy/economy.service";
|
|||||||
import { userService } from "@shared/modules/user/user.service";
|
import { userService } from "@shared/modules/user/user.service";
|
||||||
import { config } from "@shared/lib/config";
|
import { config } from "@shared/lib/config";
|
||||||
import { createErrorEmbed, createSuccessEmbed } from "@lib/embeds";
|
import { createErrorEmbed, createSuccessEmbed } from "@lib/embeds";
|
||||||
import { UserError } from "@shared/lib/errors";
|
import { withCommandErrorHandling } from "@lib/commandUtils";
|
||||||
|
|
||||||
export const pay = createCommand({
|
export const pay = createCommand({
|
||||||
data: new SlashCommandBuilder()
|
data: new SlashCommandBuilder()
|
||||||
@@ -50,20 +50,14 @@ export const pay = createCommand({
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
await withCommandErrorHandling(
|
||||||
await interaction.deferReply();
|
interaction,
|
||||||
await economyService.transfer(senderId, receiverId.toString(), amount);
|
async () => {
|
||||||
|
await economyService.transfer(senderId, receiverId.toString(), amount);
|
||||||
|
|
||||||
const embed = createSuccessEmbed(`Successfully sent ** ${amount}** Astral Units to <@${targetUser.id}>.`, "💸 Transfer Successful");
|
const embed = createSuccessEmbed(`Successfully sent ** ${amount}** Astral Units to <@${targetUser.id}>.`, "💸 Transfer Successful");
|
||||||
await interaction.editReply({ embeds: [embed], content: `<@${receiverId}>` });
|
await interaction.editReply({ embeds: [embed], content: `<@${receiverId}>` });
|
||||||
|
|
||||||
} catch (error: any) {
|
|
||||||
if (error instanceof UserError) {
|
|
||||||
await interaction.editReply({ embeds: [createErrorEmbed(error.message)] });
|
|
||||||
} else {
|
|
||||||
console.error("Error sending payment:", error);
|
|
||||||
await interaction.editReply({ embeds: [createErrorEmbed("An unexpected error occurred.")] });
|
|
||||||
}
|
}
|
||||||
}
|
);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import { createErrorEmbed } from "@lib/embeds";
|
|||||||
import { UserError } from "@shared/lib/errors";
|
import { UserError } from "@shared/lib/errors";
|
||||||
import { config } from "@shared/lib/config";
|
import { config } from "@shared/lib/config";
|
||||||
import { TriviaCategory } from "@shared/lib/constants";
|
import { TriviaCategory } from "@shared/lib/constants";
|
||||||
|
import { withCommandErrorHandling } from "@lib/commandUtils";
|
||||||
|
|
||||||
export const trivia = createCommand({
|
export const trivia = createCommand({
|
||||||
data: new SlashCommandBuilder()
|
data: new SlashCommandBuilder()
|
||||||
@@ -53,64 +54,54 @@ export const trivia = createCommand({
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// User can play - defer publicly for trivia question
|
// User can play - use standardized error handling for the main operation
|
||||||
await interaction.deferReply();
|
await withCommandErrorHandling(
|
||||||
|
interaction,
|
||||||
|
async () => {
|
||||||
|
// Start trivia session (deducts entry fee)
|
||||||
|
const session = await triviaService.startTrivia(
|
||||||
|
interaction.user.id,
|
||||||
|
interaction.user.username,
|
||||||
|
categoryId ? parseInt(categoryId) : undefined
|
||||||
|
);
|
||||||
|
|
||||||
// Start trivia session (deducts entry fee)
|
// Generate Components v2 message
|
||||||
const session = await triviaService.startTrivia(
|
const { components, flags } = getTriviaQuestionView(session, interaction.user.username);
|
||||||
interaction.user.id,
|
|
||||||
interaction.user.username,
|
// Reply with Components v2 question
|
||||||
categoryId ? parseInt(categoryId) : undefined
|
await interaction.editReply({
|
||||||
|
components,
|
||||||
|
flags
|
||||||
|
});
|
||||||
|
|
||||||
|
// Set up automatic timeout cleanup
|
||||||
|
setTimeout(async () => {
|
||||||
|
const stillActive = triviaService.getSession(session.sessionId);
|
||||||
|
if (stillActive) {
|
||||||
|
// User didn't answer - clean up session with no reward
|
||||||
|
try {
|
||||||
|
await triviaService.submitAnswer(session.sessionId, interaction.user.id, false);
|
||||||
|
} catch (error) {
|
||||||
|
// Session already cleaned up, ignore
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, config.trivia.timeoutSeconds * 1000 + 5000); // 5 seconds grace period
|
||||||
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
// Generate Components v2 message
|
|
||||||
const { components, flags } = getTriviaQuestionView(session, interaction.user.username);
|
|
||||||
|
|
||||||
// Reply with Components v2 question
|
|
||||||
await interaction.editReply({
|
|
||||||
components,
|
|
||||||
flags
|
|
||||||
});
|
|
||||||
|
|
||||||
// Set up automatic timeout cleanup
|
|
||||||
setTimeout(async () => {
|
|
||||||
const stillActive = triviaService.getSession(session.sessionId);
|
|
||||||
if (stillActive) {
|
|
||||||
// User didn't answer - clean up session with no reward
|
|
||||||
try {
|
|
||||||
await triviaService.submitAnswer(session.sessionId, interaction.user.id, false);
|
|
||||||
} catch (error) {
|
|
||||||
// Session already cleaned up, ignore
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}, config.trivia.timeoutSeconds * 1000 + 5000); // 5 seconds grace period
|
|
||||||
|
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
|
// Handle errors from the pre-defer canPlayTrivia check
|
||||||
if (error instanceof UserError) {
|
if (error instanceof UserError) {
|
||||||
// Check if we've already deferred
|
await interaction.reply({
|
||||||
if (interaction.deferred) {
|
embeds: [createErrorEmbed(error.message)],
|
||||||
await interaction.editReply({
|
ephemeral: true
|
||||||
embeds: [createErrorEmbed(error.message)]
|
});
|
||||||
});
|
|
||||||
} else {
|
|
||||||
await interaction.reply({
|
|
||||||
embeds: [createErrorEmbed(error.message)],
|
|
||||||
ephemeral: true
|
|
||||||
});
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
console.error("Error in trivia command:", error);
|
console.error("Error in trivia command:", error);
|
||||||
// Check if we've already deferred
|
await interaction.reply({
|
||||||
if (interaction.deferred) {
|
embeds: [createErrorEmbed("An unexpected error occurred. Please try again later.")],
|
||||||
await interaction.editReply({
|
ephemeral: true
|
||||||
embeds: [createErrorEmbed("An unexpected error occurred. Please try again later.")]
|
});
|
||||||
});
|
|
||||||
} else {
|
|
||||||
await interaction.reply({
|
|
||||||
embeds: [createErrorEmbed("An unexpected error occurred. Please try again later.")],
|
|
||||||
ephemeral: true
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,8 +4,7 @@ import { inventoryService } from "@shared/modules/inventory/inventory.service";
|
|||||||
import { userService } from "@shared/modules/user/user.service";
|
import { userService } from "@shared/modules/user/user.service";
|
||||||
import { createErrorEmbed } from "@lib/embeds";
|
import { createErrorEmbed } from "@lib/embeds";
|
||||||
import { getItemUseResultEmbed } from "@/modules/inventory/inventory.view";
|
import { getItemUseResultEmbed } from "@/modules/inventory/inventory.view";
|
||||||
import type { ItemUsageData } from "@shared/lib/types";
|
import { withCommandErrorHandling } from "@lib/commandUtils";
|
||||||
import { UserError } from "@shared/lib/errors";
|
|
||||||
import { getGuildConfig } from "@shared/lib/config";
|
import { getGuildConfig } from "@shared/lib/config";
|
||||||
|
|
||||||
export const use = createCommand({
|
export const use = createCommand({
|
||||||
@@ -19,57 +18,50 @@ export const use = createCommand({
|
|||||||
.setAutocomplete(true)
|
.setAutocomplete(true)
|
||||||
),
|
),
|
||||||
execute: async (interaction) => {
|
execute: async (interaction) => {
|
||||||
await interaction.deferReply();
|
await withCommandErrorHandling(
|
||||||
|
interaction,
|
||||||
|
async () => {
|
||||||
|
const guildConfig = await getGuildConfig(interaction.guildId!);
|
||||||
|
const colorRoles = guildConfig.colorRoles ?? [];
|
||||||
|
|
||||||
const guildConfig = await getGuildConfig(interaction.guildId!);
|
const itemId = interaction.options.getNumber("item", true);
|
||||||
const colorRoles = guildConfig.colorRoles ?? [];
|
const user = await userService.getOrCreateUser(interaction.user.id, interaction.user.username);
|
||||||
|
if (!user) {
|
||||||
|
await interaction.editReply({ embeds: [createErrorEmbed("Failed to load user data.")] });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const itemId = interaction.options.getNumber("item", true);
|
const result = await inventoryService.useItem(user.id.toString(), itemId);
|
||||||
const user = await userService.getOrCreateUser(interaction.user.id, interaction.user.username);
|
|
||||||
if (!user) {
|
|
||||||
await interaction.editReply({ embeds: [createErrorEmbed("Failed to load user data.")] });
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
const usageData = result.usageData;
|
||||||
const result = await inventoryService.useItem(user.id.toString(), itemId);
|
if (usageData) {
|
||||||
|
for (const effect of usageData.effects) {
|
||||||
const usageData = result.usageData;
|
if (effect.type === 'TEMP_ROLE' || effect.type === 'COLOR_ROLE') {
|
||||||
if (usageData) {
|
try {
|
||||||
for (const effect of usageData.effects) {
|
const member = await interaction.guild?.members.fetch(user.id.toString());
|
||||||
if (effect.type === 'TEMP_ROLE' || effect.type === 'COLOR_ROLE') {
|
if (member) {
|
||||||
try {
|
if (effect.type === 'TEMP_ROLE') {
|
||||||
const member = await interaction.guild?.members.fetch(user.id.toString());
|
await member.roles.add(effect.roleId);
|
||||||
if (member) {
|
} else if (effect.type === 'COLOR_ROLE') {
|
||||||
if (effect.type === 'TEMP_ROLE') {
|
// Remove existing color roles
|
||||||
await member.roles.add(effect.roleId);
|
const rolesToRemove = colorRoles.filter(r => member.roles.cache.has(r));
|
||||||
} else if (effect.type === 'COLOR_ROLE') {
|
if (rolesToRemove.length > 0) await member.roles.remove(rolesToRemove);
|
||||||
// Remove existing color roles
|
await member.roles.add(effect.roleId);
|
||||||
const rolesToRemove = colorRoles.filter(r => member.roles.cache.has(r));
|
}
|
||||||
if (rolesToRemove.length > 0) await member.roles.remove(rolesToRemove);
|
|
||||||
await member.roles.add(effect.roleId);
|
|
||||||
}
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.error("Failed to assign role in /use command:", e);
|
||||||
|
result.results.push("⚠️ Failed to assign role (Check bot permissions)");
|
||||||
}
|
}
|
||||||
} catch (e) {
|
|
||||||
console.error("Failed to assign role in /use command:", e);
|
|
||||||
result.results.push("⚠️ Failed to assign role (Check bot permissions)");
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const { embed, files } = getItemUseResultEmbed(result.results, result.item);
|
||||||
|
|
||||||
|
await interaction.editReply({ embeds: [embed], files });
|
||||||
}
|
}
|
||||||
|
);
|
||||||
const { embed, files } = getItemUseResultEmbed(result.results, result.item);
|
|
||||||
|
|
||||||
await interaction.editReply({ embeds: [embed], files });
|
|
||||||
|
|
||||||
} catch (error: any) {
|
|
||||||
if (error instanceof UserError) {
|
|
||||||
await interaction.editReply({ embeds: [createErrorEmbed(error.message)] });
|
|
||||||
} else {
|
|
||||||
console.error("Error using item:", error);
|
|
||||||
await interaction.editReply({ embeds: [createErrorEmbed("An unexpected error occurred while using the item.")] });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
autocomplete: async (interaction) => {
|
autocomplete: async (interaction) => {
|
||||||
const focusedValue = interaction.options.getFocused();
|
const focusedValue = interaction.options.getFocused();
|
||||||
|
|||||||
147
bot/lib/commandUtils.test.ts
Normal file
147
bot/lib/commandUtils.test.ts
Normal file
@@ -0,0 +1,147 @@
|
|||||||
|
import { describe, it, expect, mock, beforeEach, spyOn } from "bun:test";
|
||||||
|
import { UserError } from "@shared/lib/errors";
|
||||||
|
|
||||||
|
// --- Mocks ---
|
||||||
|
|
||||||
|
const mockDeferReply = mock(() => Promise.resolve());
|
||||||
|
const mockEditReply = mock(() => Promise.resolve());
|
||||||
|
|
||||||
|
const mockInteraction = {
|
||||||
|
deferReply: mockDeferReply,
|
||||||
|
editReply: mockEditReply,
|
||||||
|
} as any;
|
||||||
|
|
||||||
|
const mockCreateErrorEmbed = mock((msg: string) => ({ description: msg, type: "error" }));
|
||||||
|
|
||||||
|
mock.module("./embeds", () => ({
|
||||||
|
createErrorEmbed: mockCreateErrorEmbed,
|
||||||
|
}));
|
||||||
|
|
||||||
|
// Import AFTER mocking
|
||||||
|
const { withCommandErrorHandling } = await import("./commandUtils");
|
||||||
|
|
||||||
|
// --- Tests ---
|
||||||
|
|
||||||
|
describe("withCommandErrorHandling", () => {
|
||||||
|
let consoleErrorSpy: ReturnType<typeof spyOn>;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
mockDeferReply.mockClear();
|
||||||
|
mockEditReply.mockClear();
|
||||||
|
mockCreateErrorEmbed.mockClear();
|
||||||
|
consoleErrorSpy = spyOn(console, "error").mockImplementation(() => { });
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should always call deferReply", async () => {
|
||||||
|
await withCommandErrorHandling(
|
||||||
|
mockInteraction,
|
||||||
|
async () => "result"
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(mockDeferReply).toHaveBeenCalledTimes(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should pass ephemeral option to deferReply", async () => {
|
||||||
|
await withCommandErrorHandling(
|
||||||
|
mockInteraction,
|
||||||
|
async () => "result",
|
||||||
|
{ ephemeral: true }
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(mockDeferReply).toHaveBeenCalledWith({ ephemeral: true });
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should return the operation result on success", async () => {
|
||||||
|
const result = await withCommandErrorHandling(
|
||||||
|
mockInteraction,
|
||||||
|
async () => ({ data: "test" })
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(result).toEqual({ data: "test" });
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should call onSuccess with the result", async () => {
|
||||||
|
const onSuccess = mock(async (_result: string) => { });
|
||||||
|
|
||||||
|
await withCommandErrorHandling(
|
||||||
|
mockInteraction,
|
||||||
|
async () => "hello",
|
||||||
|
{ onSuccess }
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(onSuccess).toHaveBeenCalledWith("hello");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should send successMessage when no onSuccess is provided", async () => {
|
||||||
|
await withCommandErrorHandling(
|
||||||
|
mockInteraction,
|
||||||
|
async () => "result",
|
||||||
|
{ successMessage: "It worked!" }
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(mockEditReply).toHaveBeenCalledWith({
|
||||||
|
content: "It worked!",
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should prefer onSuccess over successMessage", async () => {
|
||||||
|
const onSuccess = mock(async (_result: string) => { });
|
||||||
|
|
||||||
|
await withCommandErrorHandling(
|
||||||
|
mockInteraction,
|
||||||
|
async () => "result",
|
||||||
|
{ successMessage: "This should not be sent", onSuccess }
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(onSuccess).toHaveBeenCalledTimes(1);
|
||||||
|
// editReply should NOT have been called with the successMessage
|
||||||
|
expect(mockEditReply).not.toHaveBeenCalledWith({
|
||||||
|
content: "This should not be sent",
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should show error embed for UserError", async () => {
|
||||||
|
const result = await withCommandErrorHandling(
|
||||||
|
mockInteraction,
|
||||||
|
async () => {
|
||||||
|
throw new UserError("You can't do that!");
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(result).toBeUndefined();
|
||||||
|
expect(mockCreateErrorEmbed).toHaveBeenCalledWith("You can't do that!");
|
||||||
|
expect(mockEditReply).toHaveBeenCalledTimes(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should show generic error and log for unexpected errors", async () => {
|
||||||
|
const unexpectedError = new Error("Database exploded");
|
||||||
|
|
||||||
|
const result = await withCommandErrorHandling(
|
||||||
|
mockInteraction,
|
||||||
|
async () => {
|
||||||
|
throw unexpectedError;
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(result).toBeUndefined();
|
||||||
|
expect(consoleErrorSpy).toHaveBeenCalledWith(
|
||||||
|
"Unexpected error in command:",
|
||||||
|
unexpectedError
|
||||||
|
);
|
||||||
|
expect(mockCreateErrorEmbed).toHaveBeenCalledWith(
|
||||||
|
"An unexpected error occurred."
|
||||||
|
);
|
||||||
|
expect(mockEditReply).toHaveBeenCalledTimes(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should return undefined on error", async () => {
|
||||||
|
const result = await withCommandErrorHandling(
|
||||||
|
mockInteraction,
|
||||||
|
async () => {
|
||||||
|
throw new Error("fail");
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(result).toBeUndefined();
|
||||||
|
});
|
||||||
|
});
|
||||||
79
bot/lib/commandUtils.ts
Normal file
79
bot/lib/commandUtils.ts
Normal file
@@ -0,0 +1,79 @@
|
|||||||
|
import type { ChatInputCommandInteraction } from "discord.js";
|
||||||
|
import { UserError } from "@shared/lib/errors";
|
||||||
|
import { createErrorEmbed } from "./embeds";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Wraps a command's core logic with standardized error handling.
|
||||||
|
*
|
||||||
|
* - Calls `interaction.deferReply()` automatically
|
||||||
|
* - On success, invokes `onSuccess` callback or sends `successMessage`
|
||||||
|
* - On `UserError`, shows the error message in an error embed
|
||||||
|
* - On unexpected errors, logs to console and shows a generic error embed
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* ```typescript
|
||||||
|
* export const myCommand = createCommand({
|
||||||
|
* execute: async (interaction) => {
|
||||||
|
* await withCommandErrorHandling(
|
||||||
|
* interaction,
|
||||||
|
* async () => {
|
||||||
|
* const result = await doSomething();
|
||||||
|
* await interaction.editReply({ embeds: [createSuccessEmbed(result)] });
|
||||||
|
* }
|
||||||
|
* );
|
||||||
|
* }
|
||||||
|
* });
|
||||||
|
* ```
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* ```typescript
|
||||||
|
* // With deferReply options (e.g. ephemeral)
|
||||||
|
* await withCommandErrorHandling(
|
||||||
|
* interaction,
|
||||||
|
* async () => doSomething(),
|
||||||
|
* {
|
||||||
|
* ephemeral: true,
|
||||||
|
* successMessage: "Done!",
|
||||||
|
* }
|
||||||
|
* );
|
||||||
|
* ```
|
||||||
|
*/
|
||||||
|
export async function withCommandErrorHandling<T>(
|
||||||
|
interaction: ChatInputCommandInteraction,
|
||||||
|
operation: () => Promise<T>,
|
||||||
|
options?: {
|
||||||
|
/** Message to send on success (if no onSuccess callback is provided) */
|
||||||
|
successMessage?: string;
|
||||||
|
/** Callback invoked with the operation result on success */
|
||||||
|
onSuccess?: (result: T) => Promise<void>;
|
||||||
|
/** Whether the deferred reply should be ephemeral */
|
||||||
|
ephemeral?: boolean;
|
||||||
|
}
|
||||||
|
): Promise<T | undefined> {
|
||||||
|
try {
|
||||||
|
await interaction.deferReply({ ephemeral: options?.ephemeral });
|
||||||
|
const result = await operation();
|
||||||
|
|
||||||
|
if (options?.onSuccess) {
|
||||||
|
await options.onSuccess(result);
|
||||||
|
} else if (options?.successMessage) {
|
||||||
|
await interaction.editReply({
|
||||||
|
content: options.successMessage,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
} catch (error) {
|
||||||
|
if (error instanceof UserError) {
|
||||||
|
await interaction.editReply({
|
||||||
|
embeds: [createErrorEmbed(error.message)],
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
console.error("Unexpected error in command:", error);
|
||||||
|
await interaction.editReply({
|
||||||
|
embeds: [createErrorEmbed("An unexpected error occurred.")],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user