UNPKG

maplestorysea-mcp-server

Version:

NEXON MapleStory SEA API MCP Server for Claude Desktop - Complete character info, union details, guild data, rankings optimized for SEA servers

1,201 lines (1,200 loc) 81.5 kB
"use strict"; /** * NEXON MapleStory Open API Client * Provides methods to interact with NEXON's official MapleStory API */ var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.NexonApiClient = void 0; const axios_1 = __importDefault(require("axios")); const winston_1 = require("winston"); const errors_1 = require("../utils/errors"); const logger_1 = require("../utils/logger"); const constants_1 = require("./constants"); const cache_1 = require("../utils/cache"); const validation_1 = require("../utils/validation"); const equipment_analyzer_1 = require("../utils/equipment-analyzer"); const guild_utils_1 = require("../utils/guild-utils"); const server_utils_1 = require("../utils/server-utils"); const ranking_utils_1 = require("../utils/ranking-utils"); class NexonApiClient { client; logger; mcpLogger; apiKey; requestQueue = []; isProcessingQueue = false; cache; errorAggregator; constructor(config) { this.apiKey = config.apiKey; this.cache = config.cache || cache_1.defaultCache; this.errorAggregator = new errors_1.ErrorAggregator(); // Check if in MCP mode (no port specified) const isMcpMode = !process.env.MCP_PORT && !process.argv.includes('--port'); this.logger = (0, winston_1.createLogger)({ level: 'info', format: require('winston').format.combine(require('winston').format.timestamp(), require('winston').format.json()), silent: isMcpMode, transports: isMcpMode ? [] : [new (require('winston').transports.Console)()], }); // Create enhanced MCP logger this.mcpLogger = new logger_1.McpLogger('nexon-api-client'); // Create axios instance with default configuration this.client = axios_1.default.create({ baseURL: config.baseURL || constants_1.API_CONFIG.BASE_URL, timeout: config.timeout || constants_1.API_CONFIG.TIMEOUT, headers: { [constants_1.HEADERS.AUTHORIZATION]: this.apiKey, 'Content-Type': constants_1.HEADERS.CONTENT_TYPE, 'User-Agent': constants_1.HEADERS.USER_AGENT, }, }); this.setupInterceptors(); // Log client initialization this.mcpLogger.info('NEXON API Client initialized', { operation: 'client_initialization', baseURL: config.baseURL || constants_1.API_CONFIG.BASE_URL, timeout: config.timeout || constants_1.API_CONFIG.TIMEOUT, cacheEnabled: !!this.cache, }); } setupInterceptors() { // Request interceptor this.client.interceptors.request.use((config) => { const endpoint = config.url || 'unknown'; // Enhanced API request logging this.mcpLogger.logApiRequest(endpoint, config.params); // Performance monitoring const timer = logger_1.performanceMonitor.startTimer(`api_request_${endpoint}`); config._startTime = Date.now(); config._timer = timer; return config; }, (error) => { const sanitizedError = (0, errors_1.sanitizeErrorForLogging)(error); this.mcpLogger.error('API Request failed during setup', { operation: 'api_request_setup', error: sanitizedError, }); return Promise.reject((0, errors_1.createNexonApiError)(500, error.message)); }); // Response interceptor this.client.interceptors.response.use((response) => { const duration = Date.now() - (response.config._startTime || Date.now()); const endpoint = response.config.url || 'unknown'; // Complete performance timer if (response.config._timer) { response.config._timer(); } // Enhanced API response logging this.mcpLogger.logApiResponse(endpoint, duration, true); return response; }, async (error) => { const duration = Date.now() - (error.config?._startTime || Date.now()); const endpoint = error.config?.url || 'unknown'; // Complete performance timer if (error.config?._timer) { error.config._timer(); } // Enhanced error logging this.mcpLogger.logApiError(endpoint, error, duration); // Log security events for authentication failures if (error.response?.status === 401) { this.mcpLogger.logSecurityEvent('api_authentication_failed', { endpoint, statusCode: error.response.status, }); } // Create standardized error const mcpError = (0, errors_1.createNexonApiError)(error.response?.status || 500, error.response?.data?.message || error.message, endpoint, error.config?.params); // Try error recovery try { const recoveryResult = await errors_1.defaultErrorRecovery.attemptRecovery(mcpError, { operation: () => this.client.request(error.config), maxAttempts: 3, }); this.mcpLogger.logRecoveryAttempt('retry', mcpError, 1, true, { endpoint, recoveryStrategy: 'retry', }); return recoveryResult; } catch (recoveryError) { this.mcpLogger.logRecoveryAttempt('retry', mcpError, 1, false, { endpoint, recoveryStrategy: 'retry', finalError: recoveryError.message, }); // Transform to legacy format for backward compatibility const apiError = { error: { name: this.getErrorName(error.response?.status), message: this.getErrorMessage(error.response?.status, error.response?.data, endpoint), }, }; return Promise.reject(apiError); } }); } getErrorName(status) { switch (status) { case constants_1.HTTP_STATUS.UNAUTHORIZED: return 'UNAUTHORIZED'; case constants_1.HTTP_STATUS.FORBIDDEN: return 'FORBIDDEN'; case constants_1.HTTP_STATUS.NOT_FOUND: return 'NOT_FOUND'; case constants_1.HTTP_STATUS.TOO_MANY_REQUESTS: return 'RATE_LIMITED'; case constants_1.HTTP_STATUS.INTERNAL_SERVER_ERROR: return 'INTERNAL_ERROR'; case constants_1.HTTP_STATUS.SERVICE_UNAVAILABLE: return 'SERVICE_UNAVAILABLE'; default: return 'UNKNOWN_ERROR'; } } getErrorMessage(status, data, endpoint) { if (data?.message) { return data.message; } switch (status) { case constants_1.HTTP_STATUS.UNAUTHORIZED: return constants_1.ERROR_MESSAGES.INVALID_API_KEY; case constants_1.HTTP_STATUS.TOO_MANY_REQUESTS: return constants_1.ERROR_MESSAGES.RATE_LIMIT_EXCEEDED; case constants_1.HTTP_STATUS.NOT_FOUND: // More specific error messages based on SEA API endpoint if (endpoint?.includes('/guild/')) { return constants_1.ERROR_MESSAGES.GUILD_NOT_FOUND; } if (endpoint?.includes('/character/') || endpoint?.includes('/id')) { return constants_1.ERROR_MESSAGES.CHARACTER_NOT_FOUND; } return 'Resource not found in MapleStory SEA API'; case constants_1.HTTP_STATUS.BAD_REQUEST: return 'Invalid request parameters for MapleStory SEA API'; case constants_1.HTTP_STATUS.FORBIDDEN: return 'Access forbidden. Check your API key permissions for MapleStory SEA'; case constants_1.HTTP_STATUS.INTERNAL_SERVER_ERROR: return 'MapleStory SEA API server error. Please try again later'; case constants_1.HTTP_STATUS.SERVICE_UNAVAILABLE: return 'MapleStory SEA API is temporarily unavailable. Please try again later'; default: return constants_1.ERROR_MESSAGES.UNKNOWN_ERROR; } } async waitForRateLimit() { return new Promise((resolve) => { const now = Date.now(); this.requestQueue.push({ resolve, timestamp: now }); // Log rate limiting activity this.mcpLogger.logRateLimit('applied', { queueLength: this.requestQueue.length, timestamp: now, }); if (!this.isProcessingQueue) { this.processQueue(); } }); } async processQueue() { this.isProcessingQueue = true; const startTime = Date.now(); let processedCount = 0; this.mcpLogger.debug('Rate limit queue processing started', { operation: 'queue_processing', queueLength: this.requestQueue.length, }); while (this.requestQueue.length > 0) { const now = Date.now(); const recentRequests = this.requestQueue.filter((req) => now - req.timestamp < 1000); if (recentRequests.length >= constants_1.RATE_LIMIT.REQUESTS_PER_SECOND) { this.mcpLogger.logRateLimit('exceeded', { recentRequests: recentRequests.length, limit: constants_1.RATE_LIMIT.REQUESTS_PER_SECOND, delayMs: 100, }); await new Promise((resolve) => setTimeout(resolve, 100)); continue; } const request = this.requestQueue.shift(); if (request) { request.resolve(); processedCount++; } } const duration = Date.now() - startTime; this.mcpLogger.debug('Rate limit queue processing completed', { operation: 'queue_processing', duration, processedCount, }); // Record performance metric logger_1.performanceMonitor.recordMetric('rate_limit_queue_processing', duration); this.isProcessingQueue = false; } async retryRequest(operation, maxRetries = constants_1.API_CONFIG.RETRY_ATTEMPTS) { let lastError; const operationName = 'nexon_api_request'; for (let attempt = 0; attempt <= maxRetries; attempt++) { try { await this.waitForRateLimit(); // Log rate limiting if (this.requestQueue.length > 0) { this.mcpLogger.logRateLimit('applied', { queueLength: this.requestQueue.length, attempt: attempt + 1, }); } return await operation(); } catch (error) { lastError = error; const mcpError = error instanceof errors_1.McpMapleError ? error : new errors_1.McpMapleError(error?.message || 'Unknown error', 'API_REQUEST_ERROR'); const apiError = error; // Enhanced retry logic with error recovery if (apiError?.error?.name === 'RATE_LIMITED' || apiError?.error?.name === 'SERVICE_UNAVAILABLE' || (0, errors_1.isRetryableError)(mcpError)) { if (attempt < maxRetries) { const delay = (0, errors_1.getRetryDelay)(attempt + 1, constants_1.RATE_LIMIT.RETRY_DELAY_BASE); this.mcpLogger.logRecoveryAttempt('retry', mcpError, attempt + 1, false, { operation: operationName, delay, maxRetries, errorType: apiError?.error?.name || mcpError.code, }); // Handle rate limiting specifically if (apiError?.error?.name === 'RATE_LIMITED') { this.mcpLogger.logRateLimit('exceeded', { attempt: attempt + 1, retryDelay: delay, }); } await new Promise((resolve) => setTimeout(resolve, delay)); continue; } } // Log final failure this.mcpLogger.logRecoveryAttempt('retry', mcpError, attempt + 1, false, { operation: operationName, finalAttempt: true, maxRetries, }); // Add to error aggregator for batch analysis this.errorAggregator.addError(operationName, mcpError, { attempts: attempt + 1, maxRetries, }); throw error; } } throw lastError; } isRetryableError(error) { const apiError = error; const retryableErrors = ['RATE_LIMITED', 'SERVICE_UNAVAILABLE', 'TIMEOUT_ERROR']; // Also check using enhanced error utilities if (error instanceof Error) { return (0, errors_1.isRetryableError)(error) || retryableErrors.includes(apiError?.error?.name || ''); } return retryableErrors.includes(apiError?.error?.name || ''); } async request(endpoint, params) { return this.retryRequest(async () => { // Use extended timeout for ranking endpoints which are slower const timeout = endpoint.includes('/ranking/') ? constants_1.API_CONFIG.RANKING_TIMEOUT : constants_1.API_CONFIG.TIMEOUT; const isRankingEndpoint = endpoint.includes('/ranking/'); const requestStart = Date.now(); // Enhanced logging for API requests, especially ranking endpoints this.mcpLogger.logApiRequest(endpoint, params); try { const response = await this.client.get(endpoint, { params, timeout, }); const requestDuration = Date.now() - requestStart; // Log successful response with timing this.mcpLogger.logApiResponse(endpoint, requestDuration, true); return response.data; } catch (error) { const requestDuration = Date.now() - requestStart; // Enhanced error logging this.mcpLogger.logApiResponse(endpoint, requestDuration, false); this.mcpLogger.logApiError(endpoint, error, requestDuration); // Handle axios timeout errors specifically if (error.code === 'ECONNABORTED' || error.message?.includes('timeout')) { this.mcpLogger.logError(error, { endpoint, timeout, duration: requestDuration, isRankingEndpoint, params: params ? Object.keys(params) : [], }); if (endpoint.includes('/ranking/')) { throw new errors_1.RankingTimeoutError(timeout, endpoint, params); } else { throw new errors_1.TimeoutError(timeout, endpoint); } } // Handle other axios errors if (error.response?.status) { this.mcpLogger.logError(error, { endpoint, statusCode: error.response.status, responseData: error.response.data, duration: requestDuration, isRankingEndpoint, }); const nexonError = (0, errors_1.createNexonApiError)(error.response.status, error.response.data?.message || error.message, endpoint, params); throw nexonError; } // Log unknown errors this.mcpLogger.logError(error, { endpoint, duration: requestDuration, isRankingEndpoint, }); // Re-throw original error if not handled throw error; } }); } // Character API methods async getCharacterOcid(characterName) { const operationTimer = logger_1.performanceMonitor.startTimer('get_character_ocid'); try { // Validate and sanitize input const sanitizedName = (0, validation_1.sanitizeCharacterName)(characterName); (0, validation_1.validateCharacterName)(sanitizedName); this.mcpLogger.logCharacterOperation('ocid_lookup_started', sanitizedName, { operation: 'get_character_ocid', }); // Check cache first const cacheKey = cache_1.MemoryCache.generateOcidCacheKey(sanitizedName); const cachedResult = this.cache.get(cacheKey); if (cachedResult) { this.mcpLogger.logCacheOperation('hit', cacheKey, { characterName: sanitizedName, operation: 'get_character_ocid', }); operationTimer(); return cachedResult; } this.mcpLogger.logCacheOperation('miss', cacheKey, { characterName: sanitizedName, operation: 'get_character_ocid', }); const result = await this.request(constants_1.ENDPOINTS.CHARACTER.OCID, { character_name: sanitizedName, }); // Validate OCID before caching (0, validation_1.validateOcid)(result.ocid); // Cache for 1 hour (OCID rarely changes) this.cache.set(cacheKey, result, constants_1.CACHE_TTL.CHARACTER_OCID); this.mcpLogger.logCacheOperation('set', cacheKey, { characterName: sanitizedName, ocid: result.ocid, ttl: 3600000, }); this.mcpLogger.logCharacterOperation('ocid_lookup_completed', sanitizedName, { operation: 'get_character_ocid', ocid: result.ocid, cached: false, }); operationTimer(); return result; } catch (error) { const sanitizedError = (0, errors_1.sanitizeErrorForLogging)(error); this.mcpLogger.logCharacterOperation('ocid_lookup_failed', characterName, { operation: 'get_character_ocid', error: sanitizedError, }); operationTimer(); throw error; } } async getCharacterBasic(ocid, date) { // Validate inputs (0, validation_1.validateOcid)(ocid); if (date) { (0, validation_1.validateDate)(date); } // Check cache first const cacheKey = cache_1.MemoryCache.generateCharacterBasicCacheKey(ocid, date); const cachedResult = this.cache.get(cacheKey); if (cachedResult) { this.logger.info('Character basic info cache hit', { ocid, date }); return cachedResult; } try { const params = { ocid }; if (date) { params.date = date; } const result = await this.request(constants_1.ENDPOINTS.CHARACTER.BASIC, params); // Validate world name in response if (result.world_name) { (0, validation_1.validateWorldName)(result.world_name); } // Cache for 30 minutes (character info changes less frequently) this.cache.set(cacheKey, result, constants_1.CACHE_TTL.CHARACTER_BASIC); this.logger.info('Character basic info lookup successful', { ocid, date, characterName: result.character_name, world: result.world_name, }); return result; } catch (error) { this.logger.error('Character basic info lookup failed', { ocid, date, error, }); throw error; } } async getCharacterStat(ocid, date) { // Validate inputs (0, validation_1.validateOcid)(ocid); if (date) { (0, validation_1.validateDate)(date); } // Check cache first const cacheKey = cache_1.MemoryCache.generateApiCacheKey(constants_1.ENDPOINTS.CHARACTER.STAT, { ocid, date: date || 'latest', }); const cachedResult = this.cache.get(cacheKey); if (cachedResult) { this.logger.info('Character stat cache hit', { ocid, date }); return cachedResult; } try { const params = { ocid }; if (date) { params.date = date; } const result = await this.request(constants_1.ENDPOINTS.CHARACTER.STAT, params); // Cache for 15 minutes (stats can change more frequently) this.cache.set(cacheKey, result, constants_1.CACHE_TTL.CHARACTER_STATS); this.logger.info('Character stat lookup successful', { ocid, date, characterClass: result.character_class, }); return result; } catch (error) { this.logger.error('Character stat lookup failed', { ocid, date, error, }); throw error; } } async getCharacterHyperStat(ocid, date) { return this.request(constants_1.ENDPOINTS.CHARACTER.HYPER_STAT, { ocid, date }); } async getCharacterPropensity(ocid, date) { return this.request(constants_1.ENDPOINTS.CHARACTER.PROPENSITY, { ocid, date }); } async getCharacterAbility(ocid, date) { return this.request(constants_1.ENDPOINTS.CHARACTER.ABILITY, { ocid, date }); } async getCharacterItemEquipment(ocid, date) { // Validate inputs (0, validation_1.validateOcid)(ocid); if (date) { (0, validation_1.validateDate)(date); } // Check cache first const cacheKey = cache_1.MemoryCache.generateApiCacheKey(constants_1.ENDPOINTS.CHARACTER.ITEM_EQUIPMENT, { ocid, date: date || 'latest', }); const cachedResult = this.cache.get(cacheKey); if (cachedResult) { this.logger.info('Character equipment cache hit', { ocid, date }); return cachedResult; } try { const params = { ocid }; if (date) { params.date = date; } const result = await this.request(constants_1.ENDPOINTS.CHARACTER.ITEM_EQUIPMENT, params); // Cache for 20 minutes (equipment changes less frequently) this.cache.set(cacheKey, result, constants_1.CACHE_TTL.CHARACTER_EQUIPMENT); this.logger.info('Character equipment lookup successful', { ocid, date, equipmentCount: result.item_equipment?.length || 0, }); return result; } catch (error) { this.logger.error('Character equipment lookup failed', { ocid, date, error, }); throw error; } } async getCharacterCashItemEquipment(ocid, date) { // Validate inputs (0, validation_1.validateOcid)(ocid); if (date) { (0, validation_1.validateDate)(date); } // Check cache first const cacheKey = cache_1.MemoryCache.generateApiCacheKey(constants_1.ENDPOINTS.CHARACTER.CASHITEM_EQUIPMENT, { ocid, date: date || 'latest', }); const cachedResult = this.cache.get(cacheKey); if (cachedResult) { this.logger.info('Character cash item cache hit', { ocid, date }); return cachedResult; } try { const params = { ocid }; if (date) { params.date = date; } const result = await this.request(constants_1.ENDPOINTS.CHARACTER.CASHITEM_EQUIPMENT, params); // Cache for 30 minutes (cash items change less frequently) this.cache.set(cacheKey, result, constants_1.CACHE_TTL.CHARACTER_EQUIPMENT); this.logger.info('Character cash item lookup successful', { ocid, date, }); return result; } catch (error) { this.logger.error('Character cash item lookup failed', { ocid, date, error, }); throw error; } } async getCharacterBeautyEquipment(ocid, date) { // Validate inputs (0, validation_1.validateOcid)(ocid); if (date) { (0, validation_1.validateDate)(date); } // Check cache first const cacheKey = cache_1.MemoryCache.generateApiCacheKey(constants_1.ENDPOINTS.CHARACTER.BEAUTY_EQUIPMENT, { ocid, date: date || 'latest', }); const cachedResult = this.cache.get(cacheKey); if (cachedResult) { this.logger.info('Character beauty equipment cache hit', { ocid, date }); return cachedResult; } try { const params = { ocid }; if (date) { params.date = date; } const result = await this.request(constants_1.ENDPOINTS.CHARACTER.BEAUTY_EQUIPMENT, params); // Cache for 1 hour (beauty equipment rarely changes) this.cache.set(cacheKey, result, constants_1.CACHE_TTL.CHARACTER_EQUIPMENT); this.logger.info('Character beauty equipment lookup successful', { ocid, date, }); return result; } catch (error) { this.logger.error('Character beauty equipment lookup failed', { ocid, date, error, }); throw error; } } // Union API methods async getUnionInfo(ocid, date) { return this.request(constants_1.ENDPOINTS.UNION.BASIC, { ocid, date }); } async getUnionRaider(ocid, date) { return this.request(constants_1.ENDPOINTS.UNION.RAIDER, { ocid, date }); } // Guild API methods async getGuildId(guildName, worldName) { // Validate and sanitize inputs const sanitizedGuildName = (0, guild_utils_1.sanitizeGuildName)(guildName); const sanitizedWorldName = (0, validation_1.sanitizeWorldName)(worldName); (0, guild_utils_1.validateGuildName)(sanitizedGuildName); (0, validation_1.validateWorldName)(sanitizedWorldName); // Check cache first const cacheKey = guild_utils_1.GuildCacheKeys.guildId(sanitizedGuildName, sanitizedWorldName); const cachedResult = this.cache.get(cacheKey); if (cachedResult) { this.logger.info('Guild ID lookup cache hit', { guildName: sanitizedGuildName, worldName: sanitizedWorldName, }); return cachedResult; } try { const result = await this.request(constants_1.ENDPOINTS.GUILD.ID, { guild_name: sanitizedGuildName, world_name: sanitizedWorldName, }); // Validate guild ID before caching (0, guild_utils_1.validateGuildId)(result.oguild_id); // Cache for 2 hours (guild IDs rarely change) this.cache.set(cacheKey, result, constants_1.CACHE_TTL.UNION_RAIDER); this.logger.info('Guild ID lookup successful', { guildName: sanitizedGuildName, worldName: sanitizedWorldName, guildId: result.oguild_id, }); return result; } catch (error) { this.logger.error('Guild ID lookup failed', { guildName: sanitizedGuildName, worldName: sanitizedWorldName, error, }); throw error; } } async getGuildBasic(oguildId, date) { // Validate inputs (0, guild_utils_1.validateGuildId)(oguildId); if (date) { (0, validation_1.validateDate)(date); } // Check cache first const cacheKey = guild_utils_1.GuildCacheKeys.guildBasic(oguildId, date); const cachedResult = this.cache.get(cacheKey); if (cachedResult) { this.logger.info('Guild basic info cache hit', { oguildId, date }); return cachedResult; } try { const params = { oguild_id: oguildId }; if (date) { params.date = date; } const result = await this.request(constants_1.ENDPOINTS.GUILD.BASIC, params); // Cache for 1 hour (guild info changes moderately) this.cache.set(cacheKey, result, constants_1.CACHE_TTL.GUILD_BASIC); this.logger.info('Guild basic info lookup successful', { oguildId, date, guildName: result.guild_name, guildLevel: result.guild_level, memberCount: result.guild_member_count, }); return result; } catch (error) { this.logger.error('Guild basic info lookup failed', { oguildId, date, error, }); throw error; } } /** * Search for guilds with fuzzy matching */ async searchGuilds(searchTerm, worldName, limit = 10) { const sanitizedSearchTerm = (0, guild_utils_1.sanitizeGuildName)(searchTerm); const sanitizedWorldName = (0, validation_1.sanitizeWorldName)(worldName); (0, guild_utils_1.validateGuildName)(sanitizedSearchTerm); (0, validation_1.validateWorldName)(sanitizedWorldName); // Check cache first const cacheKey = guild_utils_1.GuildCacheKeys.guildSearch(sanitizedSearchTerm, sanitizedWorldName); const cachedResult = this.cache.get(cacheKey); if (cachedResult) { this.logger.info('Guild search cache hit', { searchTerm: sanitizedSearchTerm, worldName: sanitizedWorldName, }); return cachedResult.slice(0, limit); } try { // Generate name variations for better search results const nameVariations = (0, guild_utils_1.generateGuildNameVariations)(sanitizedSearchTerm); const searchResults = []; // Try each variation for (const variation of nameVariations) { try { const result = await this.getGuildId(variation, sanitizedWorldName); const guildInfo = await this.getGuildBasic(result.oguild_id); const matchScore = (0, guild_utils_1.calculateFuzzyScore)(sanitizedSearchTerm, guildInfo.guild_name || variation); searchResults.push({ guildName: guildInfo.guild_name || variation, guildId: result.oguild_id, matchScore, guildInfo, }); } catch (error) { // Guild not found with this variation, continue this.logger.debug('Guild not found with variation', { variation, error }); } } // Sort by match score and remove duplicates const uniqueResults = searchResults .filter((result, index, array) => array.findIndex((r) => r.guildId === result.guildId) === index) .sort((a, b) => b.matchScore - a.matchScore) .slice(0, limit); // Cache guild search for 15 minutes this.cache.set(cacheKey, uniqueResults, constants_1.CACHE_TTL.RANKING_SEARCH); this.logger.info('Guild search completed', { searchTerm: sanitizedSearchTerm, worldName: sanitizedWorldName, resultsCount: uniqueResults.length, }); return uniqueResults; } catch (error) { this.logger.error('Guild search failed', { searchTerm: sanitizedSearchTerm, worldName: sanitizedWorldName, error, }); return []; } } /** * Get comprehensive guild analysis */ async getGuildAnalysis(guildName, worldName, date) { try { const { oguild_id } = await this.getGuildId(guildName, worldName); const guildBasic = await this.getGuildBasic(oguild_id, date); // Calculate guild metrics const guildScore = (0, guild_utils_1.calculateGuildScore)(guildBasic); // Get guild ranking position (if available) let rankingPosition = null; try { const ranking = await this.getGuildRanking(worldName, 0, guildName, 1, date); if (ranking.ranking && ranking.ranking.length > 0) { rankingPosition = ranking.ranking[0]?.ranking || null; } } catch (error) { this.logger.debug('Guild ranking lookup failed', { guildName, worldName, error }); } const analysis = { basic: guildBasic, metrics: { guildScore, level: guildBasic.guild_level, memberCount: guildBasic.guild_member_count, rankingPosition, }, recommendations: this.generateGuildRecommendations(guildBasic, rankingPosition), }; this.logger.info('Guild analysis completed', { guildName, worldName, guildScore, rankingPosition, }); return { guildId: oguild_id, ...analysis, }; } catch (error) { this.logger.error('Guild analysis failed', { guildName, worldName, error, }); throw error; } } /** * Generate guild improvement recommendations */ generateGuildRecommendations(guildBasic, rankingPosition) { const recommendations = []; const level = guildBasic.guild_level || 0; const memberCount = guildBasic.guild_member_count || 0; // Level recommendations if (level < 10) { recommendations.push('Level up the guild to receive more benefits and bonuses.'); } // Member count recommendations if (memberCount < 50) { recommendations.push('Recruit more members to increase guild activity and engagement.'); } // Ranking recommendations if (rankingPosition && rankingPosition > 100) { recommendations.push('Increase member activity to improve guild ranking position.'); } // General recommendations if (recommendations.length === 0) { recommendations.push('Excellent guild! Keep up the current status.'); } return recommendations; } // Ranking API methods async getOverallRanking(worldName, worldType, className, ocid, page, date) { // Validate inputs if (worldName) { (0, validation_1.validateWorldName)(worldName); } if (page) { (0, ranking_utils_1.validatePage)(page); } if (date) { (0, validation_1.validateDate)(date); } // Check cache first const cacheKey = ranking_utils_1.RankingCacheKeys.overall(worldName, worldType, className, page, date); const cachedResult = this.cache.get(cacheKey); if (cachedResult) { this.logger.info('Overall ranking cache hit', { worldName, page, className }); return cachedResult; } try { const params = {}; if (worldName) params.world_name = worldName; if (worldType) params.world_type = worldType; if (className) params.class = className; if (ocid) params.ocid = ocid; if (page) params.page = page; if (date) params.date = date; const result = await this.request(constants_1.ENDPOINTS.RANKING.OVERALL, params); // Cache for 30 minutes (rankings update periodically) this.cache.set(cacheKey, result, constants_1.CACHE_TTL.RANKINGS); this.logger.info('Overall ranking retrieved successfully', { worldName, page, className, rankingCount: result.ranking?.length || 0, }); return result; } catch (error) { this.logger.error('Overall ranking retrieval failed', { worldName, page, className, error, }); throw error; } } async getUnionRanking(worldName, ocid, page, date) { return this.request(constants_1.ENDPOINTS.RANKING.UNION, { world_name: worldName, ocid, page, date, }); } async getGuildRanking(worldName, rankingType, guildName, page, date) { // Validate inputs (0, validation_1.validateWorldName)(worldName); (0, ranking_utils_1.validateGuildRankingType)(rankingType); if (page) { (0, ranking_utils_1.validatePage)(page); } if (date) { (0, validation_1.validateDate)(date); } // Check cache first const cacheKey = ranking_utils_1.RankingCacheKeys.guild(worldName, rankingType, page, date); const cachedResult = this.cache.get(cacheKey); if (cachedResult) { this.logger.info('Guild ranking cache hit', { worldName, rankingType, page }); return cachedResult; } try { const params = { world_name: worldName, ranking_type: rankingType, }; if (guildName) params.guild_name = guildName; if (page) params.page = page; if (date) params.date = date; const result = await this.request(constants_1.ENDPOINTS.RANKING.GUILD, params); // Cache for 30 minutes this.cache.set(cacheKey, result, constants_1.CACHE_TTL.RANKINGS); this.logger.info('Guild ranking retrieved successfully', { worldName, rankingType, page, rankingCount: result.ranking?.length || 0, }); return result; } catch (error) { this.logger.error('Guild ranking retrieval failed', { worldName, rankingType, page, error, }); throw error; } } // Convenience methods async getCharacterFullInfo(characterName, date) { try { // First get OCID const { ocid } = await this.getCharacterOcid(characterName); // Then fetch all character information in parallel const [basic, stat, hyperStat, propensity, ability, equipment, cashItems, beautyEquipment] = await Promise.all([ this.getCharacterBasic(ocid, date), this.getCharacterStat(ocid, date), this.getCharacterHyperStat(ocid, date), this.getCharacterPropensity(ocid, date), this.getCharacterAbility(ocid, date), this.getCharacterItemEquipment(ocid, date), this.getCharacterCashItemEquipment(ocid, date).catch(() => null), // Optional this.getCharacterBeautyEquipment(ocid, date).catch(() => null), // Optional ]); return { ocid, basic, stat, hyperStat, propensity, ability, equipment, cashItems, beautyEquipment, }; } catch (error) { this.logger.error('Error fetching character full info', { characterName, error, }); throw error; } } /** * Get comprehensive character analysis including equipment stats and set effects */ async getCharacterAnalysis(characterName, date) { try { const fullInfo = await this.getCharacterFullInfo(characterName, date); // Analyze equipment const equipmentAnalysis = { setEffects: (0, equipment_analyzer_1.analyzeSetEffects)(fullInfo.equipment?.item_equipment || []), enhancementScores: (fullInfo.equipment?.item_equipment || []).map((item) => ({ itemName: item.item_name, slot: item.item_equipment_part, enhancement: (0, equipment_analyzer_1.analyzeEquipmentPiece)(item), score: (0, equipment_analyzer_1.calculateEnhancementScore)((0, equipment_analyzer_1.analyzeEquipmentPiece)(item)), })), totalCombatPower: this.calculateTotalCombatPower(fullInfo.stat), }; // Calculate overall character score const characterScore = this.calculateCharacterScore(fullInfo, equipmentAnalysis); return { ...fullInfo, analysis: { equipment: equipmentAnalysis, characterScore, recommendations: this.generateRecommendations(fullInfo, equipmentAnalysis), }, }; } catch (error) { this.logger.error('Error performing character analysis', { characterName, error, }); throw error; } } /** * Calculate total combat power from character stats */ calculateTotalCombatPower(statData) { if (!statData?.final_stat) return 0; const stats = {}; statData.final_stat.forEach((stat) => { if (stat.stat_name && stat.stat_value) { const value = parseInt(stat.stat_value.replace(/,/g, ''), 10); stats[stat.stat_name.toLowerCase().replace(/\s+/g, '_')] = isNaN(value) ? 0 : value; } }); return (0, equipment_analyzer_1.calculateCombatPower)(stats); } /** * Calculate overall character score */ calculateCharacterScore(fullInfo, equipmentAnalysis) { let score = 0; // Level contribution (0-300 points) const level = fullInfo.basic?.character_level || 1; score += Math.min(level, 300); // Equipment enhancement contribution (0-500 points) const enhancementScore = equipmentAnalysis.enhancementScores.reduce((total, item) => total + item.score, 0); score += Math.min(enhancementScore / 10, 500); // Set effects contribution (0-200 points) const setEffectBonus = equipmentAnalysis.setEffects.length * 50; score += Math.min(setEffectBonus, 200); // Combat power contribution (scaled) const combatPowerBonus = Math.min(equipmentAnalysis.totalCombatPower / 1000, 1000); score += combatPowerBonus; return Math.round(score); } /** * Generate improvement recommendations */ generateRecommendations(fullInfo, equipmentAnalysis) { const recommendations = []; const level = fullInfo.basic?.character_level || 1; // Level recommendations if (level < 200) { recommendations.push('Level up to equip stronger gear and improve overall stats.'); } // Equipment enhancement recommendations const lowEnhancementItems = equipmentAnalysis.enhancementScores.filter((item) => item.score < 50); if (lowEnhancementItems.length > 0) { recommendations.push(`${lowEnhancementItems.length} equipment pieces need enhancement. Consider improving starforce and potential options.`); } // Set effect recommendations if (equipmentAnalysis.setEffects.length === 0) { recommendations.push('Equip set items to gain additional stat bonuses and effects.'); } // Combat power recommendations if (equipmentAnalysis.totalCombatPower < 100000) { recommendations.push('Consider upgrading equipment to improve overall combat power.'); } return recommendations; } async getGuildFullInfo(guildName, worldName, date) { try { // First get guild ID const { oguild_id } = await this.getGuildId(guildName, worldName); // Then fetch guild information const basic = await this.getGuildBasic(oguild_id, date); return { oguild_id, basic, }; } catch (error) { this.logger.error('Error fetching guild full info', { guildName, worldName, error, }); throw error; } } // Health check method async healthCheck() { try { // Try to get ranking data as a health check await this.getOverallRanking(undefined, undefined, undefined, undefined, 1); return { status: 'healthy', timestamp: new Date().toISOString(), }; } catch (error) { this.logger.error('Health check failed', { error }); return { status: 'unhealthy', timestamp: new Date().toISOString(), }; } } // Server status and game information methods /** * Get comprehensive server status for all worlds */ async getServerStatus(worldName) { const cacheKey = server_utils_1.ServerCacheKeys.serverStatus(worldName); const cachedResult = this.cache.get(cacheKey); if (cachedResult) { this.logger.info('Server status cache hit', { worldName }); return cachedResult; } try { const worlds = worldName ? [worldName] : constants_1.WORLDS.slice(); const worldStatuses = []; let overallStatus = server_utils_1.ServerStatus.ONLINE; let errorCount = 0; for (const world of worlds) { try { // Test API availability by getting ranking data const ranking = await this.getOverallRanking(world, undefined, undefined, undefined, 1); const population = (0, server_utils_1.estimateWorldPopulation)(ranking); const worldStatus = (0, server_utils_1.determineServerStatus)(true, [], 0); worldStatuses.push({ worldName: world, status: worldStatus, population, lastUpdate: (0, server_utils_1.formatSEADate)(new Date()), }); if (worldStatus !== server_utils_1.ServerStatus.ONLINE) { errorCount++; } } catch (error) { errorCount++; const worldStatus = (0, server_utils_1.determineServerStatus)(false, [], 1); worldStatuses.push({ worldName: world, status: worldStatus, population: 'unknown', lastUpdate: (0, server_utils_1.formatSEADate)(new Date()), }); } } // Determine overall status const errorRate = errorCount / worlds.length; overallStatus = (0, server_utils_1.determineServerStatus)(errorRate < 1, [], errorRate); const result = { status: overallStatus, worlds: worldStatuses, timestamp: (0, server_utils_1.formatSEADate)(new Date()), }; // Cache for 5 minutes - API health status this.cache.set(cacheKey, result, constants_1.CACHE_TTL.API_HEALTH); this.logger.info('Server status check completed', { worldName, overallStatus, worldCount: worldStatuses.length, errorRate, });