first commit
This commit is contained in:
495
routes/inventory.js
Normal file
495
routes/inventory.js
Normal file
@@ -0,0 +1,495 @@
|
||||
import axios from "axios";
|
||||
import { authenticate } from "../middleware/auth.js";
|
||||
import Item from "../models/Item.js";
|
||||
import { config } from "../config/index.js";
|
||||
import pricingService from "../services/pricing.js";
|
||||
import marketPriceService from "../services/marketPrice.js";
|
||||
|
||||
/**
|
||||
* Inventory routes for fetching and listing Steam items
|
||||
* @param {FastifyInstance} fastify
|
||||
* @param {Object} options
|
||||
*/
|
||||
export default async function inventoryRoutes(fastify, options) {
|
||||
// GET /inventory/steam - Fetch user's Steam inventory
|
||||
fastify.get(
|
||||
"/steam",
|
||||
{
|
||||
preHandler: authenticate,
|
||||
schema: {
|
||||
querystring: {
|
||||
type: "object",
|
||||
properties: {
|
||||
game: { type: "string", enum: ["cs2", "rust"], default: "cs2" },
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
async (request, reply) => {
|
||||
try {
|
||||
const { game = "cs2" } = request.query;
|
||||
const steamId = request.user.steamId;
|
||||
|
||||
if (!steamId) {
|
||||
return reply.status(400).send({
|
||||
success: false,
|
||||
message: "Steam ID not found",
|
||||
});
|
||||
}
|
||||
|
||||
// Map game to Steam app ID
|
||||
const appIds = {
|
||||
cs2: 730, // Counter-Strike 2
|
||||
rust: 252490, // Rust
|
||||
};
|
||||
|
||||
const appId = appIds[game];
|
||||
const contextId = 2; // Standard Steam inventory context
|
||||
|
||||
console.log(
|
||||
`🎮 Fetching ${game.toUpperCase()} inventory for Steam ID: ${steamId}`
|
||||
);
|
||||
|
||||
// Get Steam API key from environment (check both possible env var names)
|
||||
const steamApiKey =
|
||||
process.env.STEAM_APIS_KEY ||
|
||||
process.env.STEAM_API_KEY ||
|
||||
config.steam?.apiKey;
|
||||
|
||||
if (!steamApiKey) {
|
||||
console.error("❌ STEAM_API_KEY or STEAM_APIS_KEY not configured");
|
||||
return reply.status(500).send({
|
||||
success: false,
|
||||
message: "Steam API is not configured. Please contact support.",
|
||||
});
|
||||
}
|
||||
|
||||
// Fetch from SteamAPIs.com
|
||||
const steamApiUrl = `https://api.steamapis.com/steam/inventory/${steamId}/${appId}/${contextId}`;
|
||||
|
||||
console.log(`📡 Calling: ${steamApiUrl}`);
|
||||
|
||||
const response = await axios.get(steamApiUrl, {
|
||||
params: {
|
||||
api_key: steamApiKey,
|
||||
},
|
||||
timeout: 15000,
|
||||
headers: {
|
||||
"User-Agent": "TurboTrades/1.0",
|
||||
},
|
||||
});
|
||||
|
||||
if (!response.data || !response.data.assets) {
|
||||
console.log("⚠️ Empty inventory or private profile");
|
||||
return reply.send({
|
||||
success: true,
|
||||
items: [],
|
||||
message: "Inventory is empty or private",
|
||||
});
|
||||
}
|
||||
|
||||
const { assets, descriptions } = response.data;
|
||||
|
||||
// Create a map of descriptions for quick lookup
|
||||
const descMap = new Map();
|
||||
descriptions.forEach((desc) => {
|
||||
const key = `${desc.classid}_${desc.instanceid}`;
|
||||
descMap.set(key, desc);
|
||||
});
|
||||
|
||||
// Process items
|
||||
const items = assets
|
||||
.map((asset) => {
|
||||
const key = `${asset.classid}_${asset.instanceid}`;
|
||||
const desc = descMap.get(key);
|
||||
|
||||
if (!desc) return null;
|
||||
|
||||
// Parse item details
|
||||
const item = {
|
||||
assetid: asset.assetid,
|
||||
classid: asset.classid,
|
||||
instanceid: asset.instanceid,
|
||||
name: desc.market_hash_name || desc.name || "Unknown Item",
|
||||
type: desc.type || "",
|
||||
image: desc.icon_url
|
||||
? `https://community.cloudflare.steamstatic.com/economy/image/${desc.icon_url}`
|
||||
: null,
|
||||
nameColor: desc.name_color || null,
|
||||
backgroundColor: desc.background_color || null,
|
||||
marketable: desc.marketable === 1,
|
||||
tradable: desc.tradable === 1,
|
||||
commodity: desc.commodity === 1,
|
||||
tags: desc.tags || [],
|
||||
descriptions: desc.descriptions || [],
|
||||
};
|
||||
|
||||
// Extract rarity from tags
|
||||
const rarityTag = item.tags.find(
|
||||
(tag) => tag.category === "Rarity"
|
||||
);
|
||||
if (rarityTag) {
|
||||
item.rarity =
|
||||
rarityTag.internal_name || rarityTag.localized_tag_name;
|
||||
}
|
||||
|
||||
// Extract exterior (wear) from tags for CS2
|
||||
if (game === "cs2") {
|
||||
const exteriorTag = item.tags.find(
|
||||
(tag) => tag.category === "Exterior"
|
||||
);
|
||||
if (exteriorTag) {
|
||||
const wearMap = {
|
||||
"Factory New": "fn",
|
||||
"Minimal Wear": "mw",
|
||||
"Field-Tested": "ft",
|
||||
"Well-Worn": "ww",
|
||||
"Battle-Scarred": "bs",
|
||||
};
|
||||
item.wear = wearMap[exteriorTag.localized_tag_name] || null;
|
||||
item.wearName = exteriorTag.localized_tag_name;
|
||||
}
|
||||
}
|
||||
|
||||
// Extract category
|
||||
const categoryTag = item.tags.find(
|
||||
(tag) => tag.category === "Type" || tag.category === "Weapon"
|
||||
);
|
||||
if (categoryTag) {
|
||||
item.category =
|
||||
categoryTag.internal_name || categoryTag.localized_tag_name;
|
||||
}
|
||||
|
||||
// Check if StatTrak or Souvenir
|
||||
item.statTrak = item.name.includes("StatTrak™");
|
||||
item.souvenir = item.name.includes("Souvenir");
|
||||
|
||||
// Detect phase for Doppler items
|
||||
const descriptionText = item.descriptions
|
||||
.map((d) => d.value || "")
|
||||
.join(" ");
|
||||
item.phase = pricingService.detectPhase(item.name, descriptionText);
|
||||
|
||||
return item;
|
||||
})
|
||||
.filter((item) => item !== null && item.marketable && item.tradable);
|
||||
|
||||
console.log(`✅ Found ${items.length} marketable items in inventory`);
|
||||
|
||||
// Enrich items with market prices (fast database lookup)
|
||||
console.log(`💰 Adding market prices...`);
|
||||
|
||||
// Get all item names for batch lookup
|
||||
const itemNames = items.map((item) => item.name);
|
||||
console.log(`📋 Looking up prices for ${itemNames.length} items`);
|
||||
console.log(`🎮 Game: ${game}`);
|
||||
console.log(`📝 First 3 item names:`, itemNames.slice(0, 3));
|
||||
|
||||
const priceMap = await marketPriceService.getPrices(itemNames, game);
|
||||
const foundPrices = Object.keys(priceMap).length;
|
||||
console.log(
|
||||
`💰 Found prices for ${foundPrices}/${itemNames.length} items`
|
||||
);
|
||||
|
||||
// Add prices to items
|
||||
const enrichedItems = items.map((item) => ({
|
||||
...item,
|
||||
marketPrice: priceMap[item.name] || null,
|
||||
hasPriceData: !!priceMap[item.name],
|
||||
}));
|
||||
|
||||
// Log items without prices
|
||||
const itemsWithoutPrices = enrichedItems.filter(
|
||||
(item) => !item.marketPrice
|
||||
);
|
||||
if (itemsWithoutPrices.length > 0) {
|
||||
console.log(`⚠️ ${itemsWithoutPrices.length} items without prices:`);
|
||||
itemsWithoutPrices.slice(0, 5).forEach((item) => {
|
||||
console.log(` - ${item.name}`);
|
||||
});
|
||||
}
|
||||
|
||||
console.log(`✅ Prices added to ${enrichedItems.length} items`);
|
||||
|
||||
return reply.send({
|
||||
success: true,
|
||||
items: enrichedItems,
|
||||
total: enrichedItems.length,
|
||||
});
|
||||
} catch (error) {
|
||||
console.error("❌ Error fetching Steam inventory:", error.message);
|
||||
console.error("Error details:", error.response?.data || error.message);
|
||||
|
||||
if (error.response?.status === 401) {
|
||||
return reply.status(500).send({
|
||||
success: false,
|
||||
message: "Steam API authentication failed. Please contact support.",
|
||||
});
|
||||
}
|
||||
|
||||
if (error.response?.status === 403) {
|
||||
return reply.status(403).send({
|
||||
success: false,
|
||||
message:
|
||||
"Steam inventory is private. Please make your inventory public in Steam settings.",
|
||||
});
|
||||
}
|
||||
|
||||
if (error.response?.status === 404) {
|
||||
return reply.status(404).send({
|
||||
success: false,
|
||||
message: "Steam profile not found or inventory is empty.",
|
||||
});
|
||||
}
|
||||
|
||||
if (error.response?.status === 429) {
|
||||
return reply.status(429).send({
|
||||
success: false,
|
||||
message:
|
||||
"Steam API rate limit exceeded. Please try again in a few moments.",
|
||||
});
|
||||
}
|
||||
|
||||
if (error.code === "ECONNABORTED" || error.code === "ETIMEDOUT") {
|
||||
return reply.status(504).send({
|
||||
success: false,
|
||||
message: "Steam API request timed out. Please try again.",
|
||||
});
|
||||
}
|
||||
|
||||
return reply.status(500).send({
|
||||
success: false,
|
||||
message: "Failed to fetch Steam inventory. Please try again later.",
|
||||
});
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
// POST /inventory/price - Get pricing for items
|
||||
fastify.post(
|
||||
"/price",
|
||||
{
|
||||
preHandler: authenticate,
|
||||
schema: {
|
||||
body: {
|
||||
type: "object",
|
||||
required: ["items"],
|
||||
properties: {
|
||||
items: {
|
||||
type: "array",
|
||||
items: {
|
||||
type: "object",
|
||||
required: ["name"],
|
||||
properties: {
|
||||
name: { type: "string" },
|
||||
assetid: { type: "string" },
|
||||
wear: { type: "string" },
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
async (request, reply) => {
|
||||
try {
|
||||
const { items } = request.body;
|
||||
|
||||
// Use fast market price database for instant lookups
|
||||
const pricedItems = await Promise.all(
|
||||
items.map(async (item) => {
|
||||
try {
|
||||
// Use market price service for instant lookup (<1ms)
|
||||
const marketPrice = await marketPriceService.getPrice(
|
||||
item.name,
|
||||
"cs2" // TODO: Get from request or item
|
||||
);
|
||||
|
||||
return {
|
||||
...item,
|
||||
estimatedPrice: marketPrice,
|
||||
currency: "USD",
|
||||
hasPriceData: marketPrice !== null,
|
||||
};
|
||||
} catch (err) {
|
||||
console.error(`Error pricing item ${item.name}:`, err.message);
|
||||
return {
|
||||
...item,
|
||||
estimatedPrice: null,
|
||||
currency: "USD",
|
||||
hasPriceData: false,
|
||||
error: "Price data not available",
|
||||
};
|
||||
}
|
||||
})
|
||||
);
|
||||
|
||||
// Filter out items without price data
|
||||
const itemsWithPrices = pricedItems.filter(
|
||||
(item) => item.estimatedPrice !== null
|
||||
);
|
||||
|
||||
return reply.send({
|
||||
success: true,
|
||||
items: itemsWithPrices,
|
||||
total: items.length,
|
||||
priced: itemsWithPrices.length,
|
||||
noPriceData: items.length - itemsWithPrices.length,
|
||||
});
|
||||
} catch (error) {
|
||||
console.error("Error pricing items:", error);
|
||||
return reply.status(500).send({
|
||||
success: false,
|
||||
message: "Failed to calculate item prices",
|
||||
});
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
// POST /inventory/sell - Sell items to the site
|
||||
fastify.post(
|
||||
"/sell",
|
||||
{
|
||||
preHandler: authenticate,
|
||||
schema: {
|
||||
body: {
|
||||
type: "object",
|
||||
required: ["items"],
|
||||
properties: {
|
||||
items: {
|
||||
type: "array",
|
||||
items: {
|
||||
type: "object",
|
||||
required: ["assetid", "name", "price", "image"],
|
||||
properties: {
|
||||
assetid: { type: "string" },
|
||||
name: { type: "string" },
|
||||
price: { type: "number" },
|
||||
image: { type: "string" },
|
||||
wear: { type: "string" },
|
||||
rarity: { type: "string" },
|
||||
category: { type: "string" },
|
||||
statTrak: { type: "boolean" },
|
||||
souvenir: { type: "boolean" },
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
async (request, reply) => {
|
||||
try {
|
||||
const { items } = request.body;
|
||||
const userId = request.user._id;
|
||||
const steamId = request.user.steamId;
|
||||
|
||||
if (!items || items.length === 0) {
|
||||
return reply.status(400).send({
|
||||
success: false,
|
||||
message: "No items selected",
|
||||
});
|
||||
}
|
||||
|
||||
// Calculate total value
|
||||
const totalValue = items.reduce((sum, item) => sum + item.price, 0);
|
||||
|
||||
// Add items to marketplace
|
||||
const createdItems = [];
|
||||
for (const item of items) {
|
||||
// Determine game based on item characteristics
|
||||
const game = item.category?.includes("Rust") ? "rust" : "cs2";
|
||||
|
||||
// Map category
|
||||
let categoryMapped = "other";
|
||||
if (item.category) {
|
||||
const cat = item.category.toLowerCase();
|
||||
if (cat.includes("rifle")) categoryMapped = "rifles";
|
||||
else if (cat.includes("pistol")) categoryMapped = "pistols";
|
||||
else if (cat.includes("knife")) categoryMapped = "knives";
|
||||
else if (cat.includes("glove")) categoryMapped = "gloves";
|
||||
else if (cat.includes("smg")) categoryMapped = "smgs";
|
||||
else if (cat.includes("sticker")) categoryMapped = "stickers";
|
||||
}
|
||||
|
||||
// Map rarity
|
||||
let rarityMapped = "common";
|
||||
if (item.rarity) {
|
||||
const rar = item.rarity.toLowerCase();
|
||||
if (rar.includes("contraband") || rar.includes("ancient"))
|
||||
rarityMapped = "exceedingly";
|
||||
else if (rar.includes("covert") || rar.includes("legendary"))
|
||||
rarityMapped = "legendary";
|
||||
else if (rar.includes("classified") || rar.includes("mythical"))
|
||||
rarityMapped = "mythical";
|
||||
else if (rar.includes("restricted") || rar.includes("rare"))
|
||||
rarityMapped = "rare";
|
||||
else if (rar.includes("mil-spec") || rar.includes("uncommon"))
|
||||
rarityMapped = "uncommon";
|
||||
}
|
||||
|
||||
const newItem = new Item({
|
||||
name: item.name,
|
||||
description: `Listed from Steam inventory`,
|
||||
image: item.image,
|
||||
game: game,
|
||||
category: categoryMapped,
|
||||
rarity: rarityMapped,
|
||||
wear: item.wear || null,
|
||||
statTrak: item.statTrak || false,
|
||||
souvenir: item.souvenir || false,
|
||||
price: item.price,
|
||||
seller: userId,
|
||||
status: "active",
|
||||
featured: false,
|
||||
});
|
||||
|
||||
await newItem.save();
|
||||
createdItems.push(newItem);
|
||||
}
|
||||
|
||||
// Update user balance
|
||||
request.user.balance += totalValue;
|
||||
await request.user.save();
|
||||
|
||||
console.log(
|
||||
`✅ User ${request.user.username} sold ${
|
||||
items.length
|
||||
} items for $${totalValue.toFixed(2)}`
|
||||
);
|
||||
|
||||
// Broadcast to WebSocket if available
|
||||
if (fastify.websocketManager) {
|
||||
// Update user's balance
|
||||
fastify.websocketManager.sendToUser(steamId, {
|
||||
type: "balance_update",
|
||||
data: {
|
||||
balance: request.user.balance,
|
||||
},
|
||||
});
|
||||
|
||||
// Broadcast new items to marketplace
|
||||
fastify.websocketManager.broadcastPublic("new_items", {
|
||||
count: createdItems.length,
|
||||
});
|
||||
}
|
||||
|
||||
return reply.send({
|
||||
success: true,
|
||||
message: `Successfully sold ${items.length} item${
|
||||
items.length > 1 ? "s" : ""
|
||||
} for $${totalValue.toFixed(2)}`,
|
||||
itemsListed: createdItems.length,
|
||||
totalEarned: totalValue,
|
||||
newBalance: request.user.balance,
|
||||
});
|
||||
} catch (error) {
|
||||
console.error("Error selling items:", error);
|
||||
return reply.status(500).send({
|
||||
success: false,
|
||||
message: "Failed to process sale. Please try again.",
|
||||
});
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user