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:
77
src/commands/admin/listing.ts
Normal file
77
src/commands/admin/listing.ts
Normal 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.")] });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
@@ -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 });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -13,6 +13,10 @@ const event: Event<Events.InteractionCreate> = {
|
|||||||
await import("@/modules/trade/trade.interaction").then(m => m.handleTradeInteraction(interaction));
|
await import("@/modules/trade/trade.interaction").then(m => m.handleTradeInteraction(interaction));
|
||||||
return;
|
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;
|
if (!interaction.isChatInputCommand()) return;
|
||||||
|
|||||||
40
src/modules/economy/shop.interaction.ts
Normal file
40
src/modules/economy/shop.interaction.ts
Normal 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.")] });
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user