#!/usr/bin/env node import https from 'https'; /** * Blizzard API client for fetching item data * To use this, you'll need to: * 1. Create an app at https://develop.battle.net/ * 2. Get your client ID and client secret * 3. Set environment variables: BLIZZARD_CLIENT_ID and BLIZZARD_CLIENT_SECRET */ let accessToken = null; let tokenExpiry = null; /** * Gets an access token from Blizzard API * @returns {Promise} Access token or null if failed */ async function getAccessToken() { const clientId = process.env.BLIZZARD_CLIENT_ID; const clientSecret = process.env.BLIZZARD_CLIENT_SECRET; if (!clientId || !clientSecret) { console.error('Blizzard API credentials not found. Please set BLIZZARD_CLIENT_ID and BLIZZARD_CLIENT_SECRET environment variables.'); return null; } // Check if we have a valid token if (accessToken && tokenExpiry && Date.now() < tokenExpiry) { return accessToken; } return new Promise((resolve) => { const auth = Buffer.from(`${clientId}:${clientSecret}`).toString('base64'); const postData = 'grant_type=client_credentials'; const options = { hostname: 'oauth.battle.net', port: 443, path: '/token', method: 'POST', headers: { 'Authorization': `Basic ${auth}`, 'Content-Type': 'application/x-www-form-urlencoded', 'Content-Length': Buffer.byteLength(postData) } }; const req = https.request(options, (res) => { let data = ''; res.on('data', (chunk) => { data += chunk; }); res.on('end', () => { try { const response = JSON.parse(data); if (response.access_token) { accessToken = response.access_token; tokenExpiry = Date.now() + (response.expires_in * 1000); resolve(accessToken); } else { console.error('Failed to get access token:', response); resolve(null); } } catch (error) { console.error('Error parsing token response:', error.message); resolve(null); } }); }); req.on('error', (error) => { console.error('Error getting access token:', error.message); resolve(null); }); req.write(postData); req.end(); }); } /** * Fetches item data from Blizzard API * @param {number} itemId - The item ID to fetch * @param {string} region - The region (default: 'us') * @returns {Promise} Item data or null if failed */ async function fetchFromBlizzard(itemId, region = 'us') { const token = await getAccessToken(); if (!token) { return null; } return new Promise((resolve) => { const options = { hostname: `${region}.api.blizzard.com`, port: 443, path: `/data/wow/item/${itemId}?namespace=static-${region}&locale=en_US&access_token=${token}`, method: 'GET' }; https.get(options, (res) => { let data = ''; res.on('data', (chunk) => { data += chunk; }); res.on('end', () => { try { const response = JSON.parse(data); if (response.name) { const itemData = { id: itemId, name: response.name, slot: mapSlotFromBlizzard(response.inventory_type?.type), quality: response.quality?.name?.toLowerCase(), icon: response.media ? `blizzard-${response.media.id}` : null, source: 'blizzard', blizzardData: { itemClass: response.item_class?.name, itemSubclass: response.item_subclass?.name, inventoryType: response.inventory_type?.name, bindType: response.bind_type?.name, level: response.level, requiredLevel: response.required_level } }; resolve(itemData); } else { console.log(`No data found for item ${itemId} on Blizzard API`); resolve(null); } } catch (error) { console.error(`Error parsing Blizzard data for item ${itemId}:`, error.message); resolve(null); } }); }).on('error', (error) => { console.error(`Error fetching from Blizzard for item ${itemId}:`, error.message); resolve(null); }); }); } /** * Maps Blizzard inventory types to our slot names * @param {string} inventoryType - Blizzard inventory type * @returns {string|null} Our slot name or null */ function mapSlotFromBlizzard(inventoryType) { const typeMap = { 'HEAD': 'head', 'NECK': 'neck', 'SHOULDER': 'shoulders', 'BODY': 'chest', 'CHEST': 'chest', 'WAIST': 'waist', 'LEGS': 'legs', 'FEET': 'feet', 'WRIST': 'wrist', 'HAND': 'hands', 'FINGER': 'finger', 'TRINKET': 'trinket', 'WEAPON': '1h-weapon', 'SHIELD': 'off-hand', 'RANGED': '2h-weapon', 'CLOAK': 'back', 'WEAPON_2H': '2h-weapon', 'WEAPON_MAIN_HAND': '1h-weapon', 'WEAPON_OFF_HAND': 'off-hand', 'HOLDABLE': 'off-hand', 'AMMO': null, 'THROWN': '1h-weapon', 'RANGED_RIGHT': '2h-weapon', 'QUIVER': null, 'RELIC': 'trinket' }; return typeMap[inventoryType] || null; } /** * Fetches item media (icon) from Blizzard API * @param {number} itemId - The item ID * @param {string} region - The region (default: 'us') * @returns {Promise} Icon URL or null if failed */ async function fetchItemMedia(itemId, region = 'us') { const token = await getAccessToken(); if (!token) { return null; } return new Promise((resolve) => { const options = { hostname: `${region}.api.blizzard.com`, port: 443, path: `/data/wow/media/item/${itemId}?namespace=static-${region}&locale=en_US&access_token=${token}`, method: 'GET' }; https.get(options, (res) => { let data = ''; res.on('data', (chunk) => { data += chunk; }); res.on('end', () => { try { const response = JSON.parse(data); if (response.assets && response.assets.length > 0) { const iconAsset = response.assets.find(asset => asset.key === 'icon'); resolve(iconAsset ? iconAsset.value : null); } else { resolve(null); } } catch (error) { console.error(`Error parsing media data for item ${itemId}:`, error.message); resolve(null); } }); }).on('error', (error) => { console.error(`Error fetching media for item ${itemId}:`, error.message); resolve(null); }); }); } export { fetchFromBlizzard, fetchItemMedia, getAccessToken, mapSlotFromBlizzard };