233 lines
6.3 KiB
TypeScript
233 lines
6.3 KiB
TypeScript
const API_BASE_URL = process.env.NEXT_PUBLIC_MINECRAFT_API_URL || 'http://localhost:6767';
|
|
const API_KEY = process.env.NEXT_PUBLIC_MINECRAFT_API_KEY;
|
|
|
|
interface ApiResponse<T = any> {
|
|
success: boolean;
|
|
message?: string;
|
|
error?: string;
|
|
details?: string;
|
|
data?: T;
|
|
}
|
|
|
|
interface PlayerIdentifierInput {
|
|
type: 'java' | 'bedrock';
|
|
username: string;
|
|
}
|
|
|
|
interface PlayerIdentifierOutput {
|
|
platform: 'java' | 'bedrock';
|
|
username: string;
|
|
uuid?: string;
|
|
xuid?: string;
|
|
}
|
|
|
|
interface MojangResponse {
|
|
id: string;
|
|
name: string;
|
|
}
|
|
|
|
interface GeyserResponse {
|
|
xuid: string;
|
|
gamertag: string;
|
|
}
|
|
|
|
class MinecraftApiClient {
|
|
private cache = new Map<string, PlayerIdentifierOutput>();
|
|
private readonly CACHE_TTL = 5 * 60 * 1000; // 5 minutes
|
|
|
|
private isCacheValid(timestamp: number): boolean {
|
|
return Date.now() - timestamp < this.CACHE_TTL;
|
|
}
|
|
|
|
private getCacheKey(type: string, username: string): string {
|
|
return `${type}:${username.toLowerCase()}`;
|
|
}
|
|
|
|
private formatUUID(uuid: string): string {
|
|
// Convert UUID from Mojang format (no dashes) to dashed format
|
|
return `${uuid.slice(0,8)}-${uuid.slice(8,12)}-${uuid.slice(12,16)}-${uuid.slice(16,20)}-${uuid.slice(20)}`;
|
|
}
|
|
|
|
private async resolveJavaUsername(username: string): Promise<PlayerIdentifierOutput> {
|
|
const cacheKey = this.getCacheKey('java', username);
|
|
const cached = this.cache.get(cacheKey);
|
|
if (cached && this.isCacheValid((cached as any).timestamp)) {
|
|
return cached;
|
|
}
|
|
|
|
try {
|
|
const response = await fetch(`https://api.mojang.com/users/profiles/minecraft/${username}`, {
|
|
headers: {
|
|
'User-Agent': 'MinecraftResolver/1.0'
|
|
}
|
|
});
|
|
|
|
if (response.status === 404) {
|
|
throw new Error('Player not found');
|
|
}
|
|
|
|
if (response.status === 429) {
|
|
throw new Error('Rate limit exceeded');
|
|
}
|
|
|
|
if (!response.ok) {
|
|
throw new Error(`Mojang API error: ${response.status}`);
|
|
}
|
|
|
|
const data: MojangResponse = await response.json();
|
|
const result: PlayerIdentifierOutput = {
|
|
platform: 'java',
|
|
username: data.name,
|
|
uuid: this.formatUUID(data.id)
|
|
};
|
|
|
|
// Cache with timestamp
|
|
(result as any).timestamp = Date.now();
|
|
this.cache.set(cacheKey, result);
|
|
|
|
return result;
|
|
} catch (error) {
|
|
throw new Error(`Failed to resolve Java username: ${error instanceof Error ? error.message : 'Unknown error'}`);
|
|
}
|
|
}
|
|
|
|
private async resolveBedrockGamertag(gamertag: string): Promise<PlayerIdentifierOutput> {
|
|
const cacheKey = this.getCacheKey('bedrock', gamertag);
|
|
const cached = this.cache.get(cacheKey);
|
|
if (cached && this.isCacheValid((cached as any).timestamp)) {
|
|
return cached;
|
|
}
|
|
|
|
try {
|
|
// Try GeyserMC API first (most reliable for public use)
|
|
const response = await fetch(`https://api.geysermc.org/v2/xbox/xuid/${encodeURIComponent(gamertag)}`, {
|
|
headers: {
|
|
'User-Agent': 'MinecraftResolver/1.0'
|
|
}
|
|
});
|
|
|
|
if (response.status === 404) {
|
|
throw new Error('Gamertag not found');
|
|
}
|
|
|
|
if (response.status === 429) {
|
|
throw new Error('Rate limit exceeded');
|
|
}
|
|
|
|
if (!response.ok) {
|
|
throw new Error(`GeyserMC API error: ${response.status}`);
|
|
}
|
|
|
|
const data: GeyserResponse = await response.json();
|
|
const result: PlayerIdentifierOutput = {
|
|
platform: 'bedrock',
|
|
username: data.gamertag,
|
|
xuid: data.xuid
|
|
};
|
|
|
|
// Cache with timestamp
|
|
(result as any).timestamp = Date.now();
|
|
this.cache.set(cacheKey, result);
|
|
|
|
return result;
|
|
} catch (error) {
|
|
throw new Error(`Failed to resolve Bedrock gamertag: ${error instanceof Error ? error.message : 'Unknown error'}`);
|
|
}
|
|
}
|
|
|
|
async resolvePlayerIdentifier(input: PlayerIdentifierInput): Promise<PlayerIdentifierOutput> {
|
|
if (!input.username || input.username.trim().length === 0) {
|
|
throw new Error('Username is required');
|
|
}
|
|
|
|
const trimmedUsername = input.username.trim();
|
|
|
|
if (input.type === 'java') {
|
|
return this.resolveJavaUsername(trimmedUsername);
|
|
} else if (input.type === 'bedrock') {
|
|
return this.resolveBedrockGamertag(trimmedUsername);
|
|
} else {
|
|
throw new Error('Invalid platform type. Must be "java" or "bedrock"');
|
|
}
|
|
}
|
|
private async request<T = any>(
|
|
endpoint: string,
|
|
options: RequestInit = {}
|
|
): Promise<ApiResponse<T>> {
|
|
const url = `${API_BASE_URL}${endpoint}`;
|
|
const headers: Record<string, string> = {
|
|
'Content-Type': 'application/json',
|
|
...(options.headers as Record<string, string> || {}),
|
|
};
|
|
|
|
if (API_KEY) {
|
|
headers.Authorization = `Bearer ${API_KEY}`;
|
|
}
|
|
|
|
try {
|
|
const response = await fetch(url, {
|
|
...options,
|
|
headers,
|
|
});
|
|
|
|
const data = await response.json();
|
|
|
|
if (!response.ok) {
|
|
return {
|
|
success: false,
|
|
error: data.error || 'Request failed',
|
|
details: data.details,
|
|
};
|
|
}
|
|
|
|
return {
|
|
success: true,
|
|
data,
|
|
message: data.message,
|
|
};
|
|
} catch (error) {
|
|
return {
|
|
success: false,
|
|
error: 'Network error',
|
|
details: error instanceof Error ? error.message : 'Unknown error',
|
|
};
|
|
}
|
|
}
|
|
|
|
async rewardPlayer(playerName: string): Promise<ApiResponse> {
|
|
return this.request('/reward', {
|
|
method: 'POST',
|
|
body: JSON.stringify({ player: playerName }),
|
|
});
|
|
}
|
|
|
|
async addToWhitelist(playerName: string): Promise<ApiResponse> {
|
|
return this.request('/api/whitelist/add', {
|
|
method: 'POST',
|
|
body: JSON.stringify({ player: playerName }),
|
|
});
|
|
}
|
|
|
|
async removeFromWhitelist(playerName: string): Promise<ApiResponse> {
|
|
return this.request('/api/whitelist/remove', {
|
|
method: 'POST',
|
|
body: JSON.stringify({ player: playerName }),
|
|
});
|
|
}
|
|
|
|
async getWhitelist(): Promise<ApiResponse<{ whitelist: string }>> {
|
|
return this.request('/api/whitelist/list', {
|
|
method: 'GET',
|
|
});
|
|
}
|
|
|
|
async executeCommand(command: string): Promise<ApiResponse<{ response: string }>> {
|
|
return this.request('/api/command', {
|
|
method: 'POST',
|
|
body: JSON.stringify({ command }),
|
|
});
|
|
}
|
|
}
|
|
|
|
export const minecraftApi = new MinecraftApiClient();
|
|
export type { ApiResponse, PlayerIdentifierInput, PlayerIdentifierOutput };
|