5 Commits

10 changed files with 75 additions and 69 deletions

View File

@@ -10,7 +10,8 @@ import {
MessageFlags MessageFlags
} from "discord.js"; } from "discord.js";
import { inventoryService } from "@/modules/inventory/inventory.service"; import { inventoryService } from "@/modules/inventory/inventory.service";
import { createErrorEmbed, createWarningEmbed } from "@lib/embeds"; import { createSuccessEmbed, createErrorEmbed } from "@lib/embeds";
import { UserError } from "@/lib/errors";
import { items } from "@/db/schema"; import { items } from "@/db/schema";
import { ilike, isNotNull, and } from "drizzle-orm"; import { ilike, isNotNull, and } from "drizzle-orm";
import { DrizzleClient } from "@/lib/DrizzleClient"; import { DrizzleClient } from "@/lib/DrizzleClient";
@@ -49,7 +50,7 @@ export const listing = createCommand({
} }
if (!item.price) { if (!item.price) {
await interaction.editReply({ content: "", embeds: [createWarningEmbed(`Item "${item.name}" is not for sale (no price set).`)] }); await interaction.editReply({ content: "", embeds: [createErrorEmbed(`Item "${item.name}" is not for sale (no price set).`)] });
return; return;
} }
@@ -73,9 +74,13 @@ export const listing = createCommand({
try { try {
await targetChannel.send({ embeds: [embed], components: [actionRow] }); await targetChannel.send({ embeds: [embed], components: [actionRow] });
await interaction.editReply({ content: `✅ Listing for **${item.name}** posted in ${targetChannel}.` }); await interaction.editReply({ content: `✅ Listing for **${item.name}** posted in ${targetChannel}.` });
} catch (error) { } catch (error: any) {
console.error("Failed to send listing message:", error); if (error instanceof UserError) {
await interaction.editReply({ content: "", embeds: [createErrorEmbed("Failed to post the listing.")] }); await interaction.reply({ embeds: [createErrorEmbed(error.message)], ephemeral: true });
} else {
console.error("Error creating listing:", error);
await interaction.reply({ embeds: [createErrorEmbed("An unexpected error occurred.")], ephemeral: true });
}
} }
}, },
autocomplete: async (interaction) => { autocomplete: async (interaction) => {

View File

@@ -1,7 +1,9 @@
```typescript
import { createCommand } from "@/lib/utils"; import { createCommand } from "@/lib/utils";
import { SlashCommandBuilder, EmbedBuilder, MessageFlags } from "discord.js"; import { SlashCommandBuilder, EmbedBuilder, MessageFlags } from "discord.js";
import { economyService } from "@/modules/economy/economy.service"; import { economyService } from "@/modules/economy/economy.service";
import { createErrorEmbed, createWarningEmbed } from "@lib/embeds"; import { createSuccessEmbed, createErrorEmbed } from "@lib/embeds";
import { UserError } from "@/lib/errors";
export const daily = createCommand({ export const daily = createCommand({
data: new SlashCommandBuilder() data: new SlashCommandBuilder()
@@ -13,10 +15,10 @@ export const daily = createCommand({
const embed = new EmbedBuilder() const embed = new EmbedBuilder()
.setTitle("💰 Daily Reward Claimed!") .setTitle("💰 Daily Reward Claimed!")
.setDescription(`You claimed **${result.amount}** Astral Units!`) .setDescription(`You claimed ** ${ result.amount }** Astral Units!`)
.addFields( .addFields(
{ name: "Streak", value: `🔥 ${result.streak} days`, inline: true }, { name: "Streak", value: `🔥 ${ result.streak } days`, 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")
.setTimestamp(); .setTimestamp();
@@ -24,13 +26,13 @@ export const daily = createCommand({
await interaction.reply({ embeds: [embed] }); await interaction.reply({ embeds: [embed] });
} catch (error: any) { } catch (error: any) {
if (error.message.includes("Daily already claimed")) { if (error instanceof UserError) {
await interaction.reply({ embeds: [createWarningEmbed(error.message, "Cooldown")], flags: MessageFlags.Ephemeral }); await interaction.reply({ embeds: [createErrorEmbed(error.message)], ephemeral: true });
return; } else {
console.error("Error claiming daily:", error);
await interaction.reply({ embeds: [createErrorEmbed("An unexpected error occurred.")], ephemeral: true });
} }
console.error(error);
await interaction.reply({ embeds: [createErrorEmbed("An error occurred while claiming your daily reward.")], flags: MessageFlags.Ephemeral });
} }
} }
}); });
```

View File

@@ -2,6 +2,7 @@ import { createCommand } from "@/lib/utils";
import { SlashCommandBuilder } from "discord.js"; import { SlashCommandBuilder } from "discord.js";
import { userService } from "@/modules/user/user.service"; import { userService } from "@/modules/user/user.service";
import { createErrorEmbed, createSuccessEmbed } from "@lib/embeds"; import { createErrorEmbed, createSuccessEmbed } from "@lib/embeds";
import { UserError } from "@/lib/errors";
import { userTimers, users } from "@/db/schema"; import { userTimers, users } from "@/db/schema";
import { eq, and, sql } from "drizzle-orm"; import { eq, and, sql } from "drizzle-orm";
import { DrizzleClient } from "@/lib/DrizzleClient"; import { DrizzleClient } from "@/lib/DrizzleClient";
@@ -179,8 +180,12 @@ export const exam = createCommand({
}); });
} catch (error: any) { } catch (error: any) {
console.error("Exam command error:", error); if (error instanceof UserError) {
await interaction.editReply({ embeds: [createErrorEmbed("An error occurred while processing your exam.")] }); await interaction.reply({ embeds: [createErrorEmbed(error.message)], ephemeral: true });
} else {
console.error("Error in exam command:", error);
await interaction.reply({ embeds: [createErrorEmbed("An unexpected error occurred.")], ephemeral: true });
}
} }
} }
}); });

View File

@@ -1,9 +1,10 @@
```typescript
import { createCommand } from "@/lib/utils"; import { createCommand } from "@/lib/utils";
import { SlashCommandBuilder, EmbedBuilder, MessageFlags } from "discord.js"; import { SlashCommandBuilder, EmbedBuilder, MessageFlags } from "discord.js";
import { economyService } from "@/modules/economy/economy.service"; import { economyService } from "@/modules/economy/economy.service";
import { userService } from "@/modules/user/user.service"; import { userService } from "@/modules/user/user.service";
import { config } from "@/lib/config"; import { config } from "@/lib/config";
import { createErrorEmbed, createWarningEmbed } from "@/lib/embeds"; import { createSuccessEmbed, createErrorEmbed } from "@lib/embeds";
import { UserError } from "@/lib/errors"; import { UserError } from "@/lib/errors";
export const pay = createCommand({ export const pay = createCommand({
@@ -26,7 +27,7 @@ export const pay = createCommand({
const discordUser = interaction.options.getUser("user", true); const discordUser = interaction.options.getUser("user", true);
if (discordUser.bot) { if (discordUser.bot) {
await interaction.reply({ embeds: [createWarningEmbed("You cannot send money to bots.")], flags: MessageFlags.Ephemeral }); await interaction.reply({ embeds: [createErrorEmbed("You cannot send money to bots.")], flags: MessageFlags.Ephemeral });
return; return;
} }
@@ -35,33 +36,35 @@ export const pay = createCommand({
const receiverId = targetUser.id; const receiverId = targetUser.id;
if (amount < config.economy.transfers.minAmount) { if (amount < config.economy.transfers.minAmount) {
await interaction.reply({ embeds: [createWarningEmbed(`Amount must be at least ${config.economy.transfers.minAmount}.`)], flags: MessageFlags.Ephemeral }); await interaction.reply({ embeds: [createErrorEmbed(`Amount must be at least ${ config.economy.transfers.minAmount }.`)], flags: MessageFlags.Ephemeral });
return; return;
} }
if (senderId === receiverId) { if (senderId === receiverId) {
await interaction.reply({ embeds: [createWarningEmbed("You cannot pay yourself.")], flags: MessageFlags.Ephemeral }); await interaction.reply({ embeds: [createErrorEmbed("You cannot pay yourself.")], flags: MessageFlags.Ephemeral });
return; return;
} }
try { try {
await interaction.deferReply({ ephemeral: true });
await economyService.transfer(senderId, receiverId, amount); await economyService.transfer(senderId, receiverId, amount);
const embed = new EmbedBuilder() const embed = new EmbedBuilder()
.setTitle("💸 Transfer Successful") .setTitle("💸 Transfer Successful")
.setDescription(`Successfully sent **${amount}** Astral Units to <@${targetUser.id}>.`) .setDescription(`Successfully sent ** ${ amount }** Astral Units to < @${ targetUser.id }>.`)
.setColor("Green") .setColor("Green")
.setTimestamp(); .setTimestamp();
await interaction.reply({ embeds: [embed] }); await interaction.editReply({ embeds: [embed] });
} catch (error: any) { } catch (error: any) {
if (error instanceof UserError) { if (error instanceof UserError) {
await interaction.reply({ embeds: [createWarningEmbed(error.message)], flags: MessageFlags.Ephemeral }); await interaction.editReply({ embeds: [createErrorEmbed(error.message)] });
return; } else {
console.error("Error sending payment:", error);
await interaction.editReply({ embeds: [createErrorEmbed("An unexpected error occurred.")] });
} }
console.error(error);
await interaction.reply({ embeds: [createErrorEmbed("Transfer failed due to an unexpected error.")], flags: MessageFlags.Ephemeral });
} }
} }
}); });
```

View File

@@ -7,6 +7,7 @@ import { inventory, items } from "@/db/schema";
import { eq, and, like } from "drizzle-orm"; import { eq, and, like } from "drizzle-orm";
import { DrizzleClient } from "@/lib/DrizzleClient"; import { DrizzleClient } from "@/lib/DrizzleClient";
import type { ItemUsageData } from "@/lib/types"; import type { ItemUsageData } from "@/lib/types";
import { UserError } from "@/lib/errors";
export const use = createCommand({ export const use = createCommand({
data: new SlashCommandBuilder() data: new SlashCommandBuilder()
@@ -53,7 +54,12 @@ export const use = createCommand({
await interaction.editReply({ embeds: [embed] }); await interaction.editReply({ embeds: [embed] });
} catch (error: any) { } catch (error: any) {
if (error instanceof UserError) {
await interaction.editReply({ embeds: [createErrorEmbed(error.message)] }); 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) => {

View File

@@ -122,29 +122,9 @@ export function reloadConfig() {
const rawConfig = JSON.parse(raw); const rawConfig = JSON.parse(raw);
// Update config object in place // Update config object in place
config.leveling = rawConfig.leveling; // We use Object.assign to keep the reference to the exported 'config' object same
config.economy = { const validatedConfig = configSchema.parse(rawConfig);
daily: { Object.assign(config, validatedConfig);
...rawConfig.economy.daily,
amount: BigInt(rawConfig.economy.daily.amount),
streakBonus: BigInt(rawConfig.economy.daily.streakBonus),
},
transfers: {
...rawConfig.economy.transfers,
minAmount: BigInt(rawConfig.economy.transfers.minAmount),
},
exam: rawConfig.economy.exam,
};
config.inventory = {
...rawConfig.inventory,
maxStackSize: BigInt(rawConfig.inventory.maxStackSize),
};
config.commands = rawConfig.commands || {};
config.lootdrop = rawConfig.lootdrop;
config.studentRole = rawConfig.studentRole;
config.visitorRole = rawConfig.visitorRole;
config.welcomeChannelId = rawConfig.welcomeChannelId;
config.welcomeMessage = rawConfig.welcomeMessage;
console.log("🔄 Config reloaded from disk."); console.log("🔄 Config reloaded from disk.");
} }

View File

@@ -1,7 +1,8 @@
```typescript
import { classes, users } from "@/db/schema"; import { classes, users } from "@/db/schema";
import { eq, sql } from "drizzle-orm"; import { eq, sql } from "drizzle-orm";
import { DrizzleClient } from "@/lib/DrizzleClient"; import { DrizzleClient } from "@/lib/DrizzleClient";
import { UserError } from "@/lib/errors";
export const classService = { export const classService = {
getAllClasses: async () => { getAllClasses: async () => {
@@ -14,7 +15,7 @@ export const classService = {
where: eq(classes.id, classId), where: eq(classes.id, classId),
}); });
if (!cls) throw new Error("Class not found"); if (!cls) throw new UserError("Class not found");
const [user] = await txFn.update(users) const [user] = await txFn.update(users)
.set({ classId: classId }) .set({ classId: classId })
@@ -37,15 +38,15 @@ export const classService = {
where: eq(classes.id, classId), where: eq(classes.id, classId),
}); });
if (!cls) throw new Error("Class not found"); if (!cls) throw new UserError("Class not found");
if (amount < 0n && (cls.balance ?? 0n) < -amount) { if ((cls.balance ?? 0n) < amount) {
throw new Error("Insufficient class funds"); throw new UserError("Insufficient class funds");
} }
const [updatedClass] = await txFn.update(classes) const [updatedClass] = await txFn.update(classes)
.set({ .set({
balance: sql`${classes.balance} + ${amount}`, balance: sql`${ classes.balance } + ${ amount } `,
}) })
.where(eq(classes.id, classId)) .where(eq(classes.id, classId))
.returning(); .returning();

View File

@@ -4,6 +4,7 @@ import { DrizzleClient } from "@/lib/DrizzleClient";
import { economyService } from "@/modules/economy/economy.service"; import { economyService } from "@/modules/economy/economy.service";
import { levelingService } from "@/modules/leveling/leveling.service"; import { levelingService } from "@/modules/leveling/leveling.service";
import { config } from "@/lib/config"; import { config } from "@/lib/config";
import { UserError } from "@/lib/errors";
import { withTransaction } from "@/lib/db"; import { withTransaction } from "@/lib/db";
import type { Transaction, ItemUsageData } from "@/lib/types"; import type { Transaction, ItemUsageData } from "@/lib/types";
@@ -28,7 +29,7 @@ export const inventoryService = {
if (existing) { if (existing) {
const newQuantity = (existing.quantity ?? 0n) + quantity; const newQuantity = (existing.quantity ?? 0n) + quantity;
if (newQuantity > config.inventory.maxStackSize) { if (newQuantity > config.inventory.maxStackSize) {
throw new Error(`Cannot exceed max stack size of ${config.inventory.maxStackSize}`); throw new UserError(`Cannot exceed max stack size of ${config.inventory.maxStackSize}`);
} }
const [entry] = await txFn.update(inventory) const [entry] = await txFn.update(inventory)
@@ -49,11 +50,11 @@ export const inventoryService = {
.where(eq(inventory.userId, BigInt(userId))); .where(eq(inventory.userId, BigInt(userId)));
if (inventoryCount && inventoryCount.count >= config.inventory.maxSlots) { if (inventoryCount && inventoryCount.count >= config.inventory.maxSlots) {
throw new Error(`Inventory full (Max ${config.inventory.maxSlots} slots)`); throw new UserError(`Inventory full (Max ${config.inventory.maxSlots} slots)`);
} }
if (quantity > config.inventory.maxStackSize) { if (quantity > config.inventory.maxStackSize) {
throw new Error(`Cannot exceed max stack size of ${config.inventory.maxStackSize}`); throw new UserError(`Cannot exceed max stack size of ${config.inventory.maxStackSize}`);
} }
const [entry] = await txFn.insert(inventory) const [entry] = await txFn.insert(inventory)
@@ -78,7 +79,7 @@ export const inventoryService = {
}); });
if (!existing || (existing.quantity ?? 0n) < quantity) { if (!existing || (existing.quantity ?? 0n) < quantity) {
throw new Error("Insufficient item quantity"); throw new UserError("Insufficient item quantity");
} }
if ((existing.quantity ?? 0n) === quantity) { if ((existing.quantity ?? 0n) === quantity) {
@@ -119,8 +120,8 @@ export const inventoryService = {
where: eq(items.id, itemId), where: eq(items.id, itemId),
}); });
if (!item) throw new Error("Item not found"); if (!item) throw new UserError("Item not found");
if (!item.price) throw new Error("Item is not for sale"); if (!item.price) throw new UserError("Item is not for sale");
const totalPrice = item.price * quantity; const totalPrice = item.price * quantity;
@@ -151,14 +152,14 @@ export const inventoryService = {
}); });
if (!entry || (entry.quantity ?? 0n) < 1n) { if (!entry || (entry.quantity ?? 0n) < 1n) {
throw new Error("You do not own this item."); throw new UserError("You do not own this item.");
} }
const item = entry.item; const item = entry.item;
const usageData = item.usageData as ItemUsageData | null; const usageData = item.usageData as ItemUsageData | null;
if (!usageData || !usageData.effects || usageData.effects.length === 0) { if (!usageData || !usageData.effects || usageData.effects.length === 0) {
throw new Error("This item cannot be used."); throw new UserError("This item cannot be used.");
} }
const results: string[] = []; const results: string[] = [];

View File

@@ -1,6 +1,7 @@
```typescript
import { quests, userQuests, users } from "@/db/schema"; import { quests, userQuests, users } from "@/db/schema";
import { eq, and, sql } from "drizzle-orm"; import { eq, and, sql } from "drizzle-orm";
import { UserError } from "@/lib/errors";
import { DrizzleClient } from "@/lib/DrizzleClient"; import { DrizzleClient } from "@/lib/DrizzleClient";
import { economyService } from "@/modules/economy/economy.service"; import { economyService } from "@/modules/economy/economy.service";
import { levelingService } from "@/modules/leveling/leveling.service"; import { levelingService } from "@/modules/leveling/leveling.service";
@@ -45,8 +46,8 @@ export const questService = {
} }
}); });
if (!userQuest) throw new Error("Quest not assigned"); if (!userQuest) throw new UserError("Quest not assigned");
if (userQuest.completedAt) throw new Error("Quest already completed"); if (userQuest.completedAt) throw new UserError("Quest already completed");
// Mark completed // Mark completed
await txFn.update(userQuests) await txFn.update(userQuests)
@@ -62,7 +63,7 @@ export const questService = {
if (rewards?.balance) { if (rewards?.balance) {
const bal = BigInt(rewards.balance); const bal = BigInt(rewards.balance);
await economyService.modifyUserBalance(userId, bal, 'QUEST_REWARD', `Reward for quest ${questId}`, null, txFn); await economyService.modifyUserBalance(userId, bal, 'QUEST_REWARD', `Reward for quest ${ questId }`, null, txFn);
results.balance = bal; results.balance = bal;
} }

View File

@@ -1,5 +1,7 @@
import type { TradeSession, TradeParticipant } from "./trade.types"; import type { TradeSession, TradeParticipant } from "./trade.types";
import { DrizzleClient } from "@/lib/DrizzleClient"; import { DrizzleClient } from "@/lib/DrizzleClient";
import { withTransaction } from "@/lib/db";
import { UserError } from "@/lib/errors";
import { economyService } from "@/modules/economy/economy.service"; import { economyService } from "@/modules/economy/economy.service";
import { inventoryService } from "@/modules/inventory/inventory.service"; import { inventoryService } from "@/modules/inventory/inventory.service";
import { itemTransactions } from "@/db/schema"; import { itemTransactions } from "@/db/schema";