Files
aurorabot/bot/commands/inventory/inventory.ts
syntaxbullet 5188d86d61
Some checks failed
Deploy to Production / test (push) Failing after 31s
fix(inventory): address code review findings
- Replace setTimeout race in use-item flow with explicit Back button
- Fix collector end handler to re-render current view instead of blanking
- Add appendUseBackButton helper to attach navigation to use results
- Remove unused isInventoryInteraction import
- Fix rarity test type assertions

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-28 17:49:12 +01:00

323 lines
13 KiB
TypeScript

import { createCommand } from "@shared/lib/utils";
import { SlashCommandBuilder, MessageFlags, ComponentType } from "discord.js";
import { inventoryService } from "@shared/modules/inventory/inventory.service";
import { userService } from "@shared/modules/user/user.service";
import { createWarningEmbed, createErrorEmbed } from "@lib/embeds";
import {
getInventoryListMessage,
getEmptyInventoryMessage,
getItemDetailMessage,
getDiscardConfirmMessage,
appendUseBackButton,
sortInventoryItems,
ITEMS_PER_PAGE,
type InventoryEntry,
} from "@/modules/inventory/inventory.view";
import { getLootboxResultMessage } from "@/modules/inventory/inventory.view";
import {
parseInventoryCustomId,
executeItemUse,
} from "@/modules/inventory/inventory.interaction";
import { UserError } from "@shared/lib/errors";
export const inventory = createCommand({
data: new SlashCommandBuilder()
.setName("inventory")
.setDescription("View your or another user's inventory")
.addSubcommand(sub =>
sub.setName("list")
.setDescription("View your or another user's inventory")
.addUserOption(option =>
option.setName("user")
.setDescription("User to view")
.setRequired(false)
)
)
.addSubcommand(sub =>
sub.setName("view")
.setDescription("View details of a specific item")
.addNumberOption(option =>
option.setName("item")
.setDescription("The item to view")
.setRequired(true)
.setAutocomplete(true)
)
),
execute: async (interaction) => {
await interaction.deferReply();
const viewerId = interaction.user.id;
const subcommand = interaction.options.getSubcommand();
if (subcommand === "view") {
// Direct item detail view
const itemId = interaction.options.getNumber("item", true);
const user = await userService.getOrCreateUser(viewerId, interaction.user.username);
if (!user) {
await interaction.editReply({ embeds: [createWarningEmbed("Failed to load user data.", "Error")] });
return;
}
const entries = await inventoryService.getInventory(user.id.toString());
const entry = entries.find((e: any) => e.item.id === itemId);
if (!entry) {
await interaction.editReply({ embeds: [createWarningEmbed("Item not found in your inventory.", "Not Found")] });
return;
}
const ownerId = user.id.toString();
let currentPage = 0;
let selectedItemId: number | null = itemId;
const response = await interaction.editReply(
getItemDetailMessage(entry as InventoryEntry, viewerId, ownerId) as any
);
await setupCollector(interaction, response, viewerId, ownerId, user.username, currentPage, selectedItemId);
return;
}
// "list" subcommand
const targetUser = interaction.options.getUser("user") || interaction.user;
if (targetUser.bot) {
await interaction.editReply({ embeds: [createWarningEmbed("Bots do not have inventories.", "Inventory Check")] });
return;
}
const user = await userService.getOrCreateUser(targetUser.id, targetUser.username);
if (!user) {
await interaction.editReply({ embeds: [createWarningEmbed("Failed to load user data.", "Error")] });
return;
}
const ownerId = user.id.toString();
const entries = await inventoryService.getInventory(ownerId);
if (!entries || entries.length === 0) {
await interaction.editReply(getEmptyInventoryMessage(user.username) as any);
return;
}
let currentPage = 0;
let selectedItemId: number | null = null;
const response = await interaction.editReply(
getInventoryListMessage(entries as InventoryEntry[], user.username, currentPage, viewerId, ownerId) as any
);
await setupCollector(interaction, response, viewerId, ownerId, user.username, currentPage, selectedItemId);
},
autocomplete: async (interaction) => {
const focusedValue = interaction.options.getFocused();
const userId = interaction.user.id;
const results = await inventoryService.getAutocompleteItems(userId, focusedValue);
await interaction.respond(results);
},
});
async function setupCollector(
interaction: any,
response: any,
viewerId: string,
ownerId: string,
username: string,
initialPage: number,
initialItemId: number | null,
) {
let currentPage = initialPage;
let selectedItemId = initialItemId;
const collector = response.createMessageComponentCollector({
time: 120_000,
});
collector.on("collect", async (i: any) => {
if (i.user.id !== viewerId) return;
const parsed = parseInventoryCustomId(i.customId);
if (!parsed) return;
try {
await i.deferUpdate();
// Re-fetch inventory for fresh data
const entries = await inventoryService.getInventory(ownerId);
const sorted = sortInventoryItems(entries as InventoryEntry[]);
switch (parsed.action) {
case "select": {
const itemId = parseInt(i.values[0]);
const entry = sorted.find(e => e.item.id === itemId);
if (!entry) break;
selectedItemId = itemId;
await interaction.editReply(
getItemDetailMessage(entry, viewerId, ownerId)
);
break;
}
case "prev": {
currentPage = Math.max(0, currentPage - 1);
selectedItemId = null;
await interaction.editReply(
getInventoryListMessage(sorted, username, currentPage, viewerId, ownerId)
);
break;
}
case "next": {
currentPage = currentPage + 1;
selectedItemId = null;
await interaction.editReply(
getInventoryListMessage(sorted, username, currentPage, viewerId, ownerId)
);
break;
}
case "back": {
selectedItemId = null;
if (sorted.length === 0) {
await interaction.editReply(getEmptyInventoryMessage(username));
} else {
await interaction.editReply(
getInventoryListMessage(sorted, username, currentPage, viewerId, ownerId)
);
}
break;
}
case "use": {
if (viewerId !== ownerId || !selectedItemId) break;
try {
const result = await executeItemUse(i, viewerId, selectedItemId);
const message = getLootboxResultMessage(result.results, result.item);
await interaction.editReply(appendUseBackButton(message, viewerId) as any);
} catch (error) {
if (error instanceof UserError) {
await interaction.editReply({
embeds: [createErrorEmbed(error.message)],
components: [],
flags: undefined,
});
} else {
throw error;
}
}
break;
}
case "use_back": {
// Return from use result to detail or list
if (!selectedItemId) break;
const entry = sorted.find(e => e.item.id === selectedItemId);
if (entry) {
await interaction.editReply(
getItemDetailMessage(entry, viewerId, ownerId)
);
} else {
selectedItemId = null;
if (sorted.length === 0) {
await interaction.editReply(getEmptyInventoryMessage(username));
} else {
await interaction.editReply(
getInventoryListMessage(sorted, username, currentPage, viewerId, ownerId)
);
}
}
break;
}
case "discard": {
if (viewerId !== ownerId || !selectedItemId) break;
const entry = sorted.find(e => e.item.id === selectedItemId);
if (!entry) break;
await interaction.editReply(
getDiscardConfirmMessage(entry, viewerId)
);
break;
}
case "discard_confirm": {
if (viewerId !== ownerId || !selectedItemId) break;
try {
await inventoryService.removeItem(ownerId, selectedItemId, 1n);
const freshEntries = await inventoryService.getInventory(ownerId);
const freshSorted = sortInventoryItems(freshEntries as InventoryEntry[]);
const freshEntry = freshSorted.find(e => e.item.id === selectedItemId);
if (freshEntry) {
await interaction.editReply(
getItemDetailMessage(freshEntry, viewerId, ownerId)
);
} else {
selectedItemId = null;
if (freshSorted.length === 0) {
await interaction.editReply(getEmptyInventoryMessage(username));
} else {
await interaction.editReply(
getInventoryListMessage(freshSorted, username, currentPage, viewerId, ownerId)
);
}
}
} catch (error) {
if (error instanceof UserError) {
await interaction.editReply({
embeds: [createErrorEmbed(error.message)],
components: [],
flags: undefined,
});
} else {
throw error;
}
}
break;
}
case "discard_cancel": {
if (!selectedItemId) break;
const entry = sorted.find(e => e.item.id === selectedItemId);
if (!entry) break;
await interaction.editReply(
getItemDetailMessage(entry, viewerId, ownerId)
);
break;
}
}
} catch (error) {
console.error("Inventory interaction error:", error);
}
});
collector.on("end", async () => {
try {
// Re-render current view as static (no interactive components)
const entries = await inventoryService.getInventory(ownerId);
const sorted = sortInventoryItems(entries as InventoryEntry[]);
if (selectedItemId) {
const entry = sorted.find(e => e.item.id === selectedItemId);
if (entry) {
// Show detail view without action buttons
const msg = getItemDetailMessage(entry, viewerId, ownerId);
// Replace components with empty to remove buttons but keep container content
await interaction.editReply(msg);
return;
}
}
if (sorted.length === 0) {
await interaction.editReply(getEmptyInventoryMessage(username));
} else {
await interaction.editReply(
getInventoryListMessage(sorted, username, currentPage, viewerId, ownerId)
);
}
} catch {
// If re-rendering fails, at least try to clear gracefully
interaction.editReply({ components: [] }).catch(() => {});
}
});
}