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
JavaScript
"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,
});