import { createCommand } from "@shared/lib/utils"; import { SlashCommandBuilder, type BaseGuildTextChannel, PermissionFlagsBits, MessageFlags } from "discord.js"; import { inventoryService } from "@shared/modules/inventory/inventory.service"; import { createErrorEmbed } from "@lib/embeds"; import { items } from "@db/schema"; import { ilike, isNotNull, and, inArray } from "drizzle-orm"; import { DrizzleClient } from "@shared/db/DrizzleClient"; import { getShopListingMessage } from "@/modules/economy/shop.view"; import { EffectType, LootType } from "@shared/lib/constants"; import { withCommandErrorHandling } from "@lib/commandUtils"; 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("item") .setDescription("The item to list") .setRequired(true) .setAutocomplete(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 withCommandErrorHandling( interaction, async () => { const itemId = interaction.options.getNumber("item", 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: [createErrorEmbed(`Item "${item.name}" is not for sale (no price set).`)] }); return; } // Prepare context for lootboxes const context: { referencedItems: Map } = { 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({ ...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) => { const focusedValue = interaction.options.getFocused(); const results = await DrizzleClient.select({ id: items.id, name: items.name, price: items.price }) .from(items) .where( and( ilike(items.name, `%${focusedValue}%`), isNotNull(items.price) ) ) .limit(20); await interaction.respond( results.map(item => ({ name: `${item.name} (Price: ${item.price})`, value: item.id })) ); } });