feat: Introduced an admin listing command and shop interaction module, replacing the sell command, and added a type-checking script.

This commit is contained in:
syntaxbullet
2025-12-15 22:52:26 +01:00
parent 727b63b4dc
commit 1d4263e178
5 changed files with 122 additions and 125 deletions

1
check.sh Executable file
View File

@@ -0,0 +1 @@
tsc --noEmit

View File

@@ -0,0 +1,77 @@
import { createCommand } from "@/lib/utils";
import {
SlashCommandBuilder,
EmbedBuilder,
ActionRowBuilder,
ButtonBuilder,
ButtonStyle,
type BaseGuildTextChannel,
PermissionFlagsBits,
MessageFlags
} from "discord.js";
import { inventoryService } from "@/modules/inventory/inventory.service";
import { createErrorEmbed, createWarningEmbed } from "@lib/embeds";
export const listing = createCommand({
data: new SlashCommandBuilder()
.setName("listing")
.setDescription("Post an item listing in the channel for users to buy")
.addNumberOption(option =>
option.setName("itemid")
.setDescription("The ID of the item to list")
.setRequired(true)
)
.addChannelOption(option =>
option.setName("channel")
.setDescription("The channel to post the listing in (defaults to current)")
.setRequired(false)
)
.setDefaultMemberPermissions(PermissionFlagsBits.Administrator),
execute: async (interaction) => {
await interaction.deferReply({ flags: MessageFlags.Ephemeral });
const itemId = interaction.options.getNumber("itemid", true);
const targetChannel = (interaction.options.getChannel("channel") as BaseGuildTextChannel) || interaction.channel as BaseGuildTextChannel;
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: [createWarningEmbed(`Item "${item.name}" is not for sale (no price set).`)] });
return;
}
const embed = new EmbedBuilder()
.setTitle(`Shop: ${item.name}`)
.setDescription(item.description || "No description available.")
.addFields({ name: "Price", value: `${item.price} 🪙`, inline: true })
.setColor("Green")
.setThumbnail(item.iconUrl || null)
.setImage(item.imageUrl || null)
.setFooter({ text: "Click the button below to purchase instantly." });
const buyButton = new ButtonBuilder()
.setCustomId(`shop_buy_${item.id}`)
.setLabel(`Buy for ${item.price} 🪙`)
.setStyle(ButtonStyle.Success)
.setEmoji("🛒");
const actionRow = new ActionRowBuilder<ButtonBuilder>().addComponents(buyButton);
try {
await targetChannel.send({ embeds: [embed], components: [actionRow] });
await interaction.editReply({ content: `✅ Listing for **${item.name}** posted in ${targetChannel}.` });
} catch (error) {
console.error("Failed to send listing message:", error);
await interaction.editReply({ content: "", embeds: [createErrorEmbed("Failed to post the listing.")] });
}
}
});

View File

@@ -1,125 +0,0 @@
import { createCommand } from "@/lib/utils";
import {
SlashCommandBuilder,
EmbedBuilder,
ActionRowBuilder,
ButtonBuilder,
ButtonStyle,
ComponentType,
type BaseGuildTextChannel,
type ButtonInteraction,
PermissionFlagsBits,
MessageFlags
} from "discord.js";
import { userService } from "@/modules/user/user.service";
import { inventoryService } from "@/modules/inventory/inventory.service";
import type { items } from "@db/schema";
import { createErrorEmbed, createWarningEmbed } from "@lib/embeds";
export const sell = createCommand({
data: new SlashCommandBuilder()
.setName("sell")
.setDescription("Post an item for sale in the current channel so regular users can buy it")
.addNumberOption(option =>
option.setName("itemid")
.setDescription("The ID of the item to sell")
.setRequired(true)
)
.addChannelOption(option =>
option.setName("channel")
.setDescription("The channel to post the item in")
.setRequired(false)
)
.setDefaultMemberPermissions(PermissionFlagsBits.Administrator),
execute: async (interaction) => {
await interaction.deferReply({ flags: MessageFlags.Ephemeral });
const itemId = interaction.options.getNumber("itemid", true);
const targetChannel = (interaction.options.getChannel("channel") as BaseGuildTextChannel) || interaction.channel as BaseGuildTextChannel;
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: [createWarningEmbed(`Item "${item.name}" is not for sale (no price set).`)] });
return;
}
const embed = new EmbedBuilder()
.setTitle(`Item for sale: ${item.name}`)
.setDescription(item.description || "No description available.")
.addFields({ name: "Price", value: `${item.price} 🪙`, inline: true })
.setColor("Yellow")
.setThumbnail(item.iconUrl || null)
.setImage(item.imageUrl || null);
const buyButton = new ButtonBuilder()
.setCustomId("buy")
.setLabel("Buy")
.setStyle(ButtonStyle.Success);
const actionRow = new ActionRowBuilder<ButtonBuilder>().addComponents(buyButton);
try {
const message = await targetChannel.send({ embeds: [embed], components: [actionRow] });
await interaction.editReply({ content: `Item posted in ${targetChannel}.` });
// Create a collector on the specific message
const collector = message.createMessageComponentCollector({
componentType: ComponentType.Button,
filter: (i) => i.customId === "buy",
});
collector.on("collect", async (i) => {
await handleBuyInteraction(i, item);
});
} catch (error) {
console.error("Failed to send sell message:", error);
await interaction.editReply({ content: "", embeds: [createErrorEmbed("Failed to post the item for sale.")] });
}
}
});
async function handleBuyInteraction(interaction: ButtonInteraction, item: typeof items.$inferSelect) {
try {
await interaction.deferReply({ flags: MessageFlags.Ephemeral });
const userId = interaction.user.id;
const user = await userService.getUserById(userId);
if (!user) {
await interaction.editReply({ content: "", embeds: [createErrorEmbed("User profile not found.")] });
return;
}
if ((user.balance ?? 0n) < (item.price ?? 0n)) {
await interaction.editReply({ content: "", embeds: [createWarningEmbed(`You don't have enough money! You need ${item.price} 🪙.`)] });
return;
}
const result = await inventoryService.buyItem(userId, item.id, 1n);
if (!result.success) {
await interaction.editReply({ content: "", embeds: [createErrorEmbed("Transaction failed. Please try again.")] });
return;
}
await interaction.editReply({ content: `Successfully bought **${item.name}** for ${item.price} 🪙!` });
} catch (error) {
console.error("Error processing purchase:", error);
if (interaction.deferred || interaction.replied) {
await interaction.editReply({ content: "", embeds: [createErrorEmbed("An error occurred while processing your purchase.")] });
} else {
await interaction.reply({ embeds: [createErrorEmbed("An error occurred while processing your purchase.")], flags: MessageFlags.Ephemeral });
}
}
}

View File

@@ -13,6 +13,10 @@ const event: Event<Events.InteractionCreate> = {
await import("@/modules/trade/trade.interaction").then(m => m.handleTradeInteraction(interaction));
return;
}
if (interaction.customId.startsWith("shop_buy_") && interaction.isButton()) {
await import("@/modules/economy/shop.interaction").then(m => m.handleShopInteraction(interaction));
return;
}
}
if (!interaction.isChatInputCommand()) return;

View File

@@ -0,0 +1,40 @@
import { ButtonInteraction, MessageFlags } from "discord.js";
import { inventoryService } from "@/modules/inventory/inventory.service";
import { userService } from "@/modules/user/user.service";
import { createErrorEmbed, createWarningEmbed } from "@/lib/embeds";
export async function handleShopInteraction(interaction: ButtonInteraction) {
if (!interaction.customId.startsWith("shop_buy_")) return;
try {
await interaction.deferReply({ flags: MessageFlags.Ephemeral });
const itemId = parseInt(interaction.customId.replace("shop_buy_", ""));
if (isNaN(itemId)) {
await interaction.editReply({ embeds: [createErrorEmbed("Invalid Item ID.")] });
return;
}
const item = await inventoryService.getItem(itemId);
if (!item || !item.price) {
await interaction.editReply({ embeds: [createErrorEmbed("Item not found or not for sale.")] });
return;
}
const user = await userService.getOrCreateUser(interaction.user.id, interaction.user.username);
// Double check balance here too, although service handles it, we want a nice message
if ((user.balance ?? 0n) < item.price) {
await interaction.editReply({ embeds: [createWarningEmbed(`You need ${item.price} 🪙 to buy this item. You have ${user.balance} 🪙.`)] });
return;
}
const result = await inventoryService.buyItem(user.id, item.id, 1n);
await interaction.editReply({ content: `✅ **Success!** You bought **${item.name}** for ${item.price} 🪙.` });
} catch (error: any) {
console.error("Shop Purchase Error:", error);
await interaction.editReply({ embeds: [createErrorEmbed(error.message || "An error occurred while processing your purchase.")] });
}
}