(Feat): Added a minimal pikanetwork client
This commit is contained in:
280
src/api/pika/cache.ts
Normal file
280
src/api/pika/cache.ts
Normal file
@@ -0,0 +1,280 @@
|
||||
/**
|
||||
* PikaNetwork API Cache System
|
||||
* Implements TTL-based caching for API responses
|
||||
*/
|
||||
|
||||
import type { ProfileResponse, ClanResponse, LeaderboardResponse } from './types.ts';
|
||||
|
||||
interface CacheEntry<T> {
|
||||
data: T;
|
||||
expires: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generic cache with TTL support
|
||||
*/
|
||||
class TTLCache<T> {
|
||||
private cache = new Map<string, CacheEntry<T>>();
|
||||
private readonly defaultTTL: number;
|
||||
|
||||
constructor(defaultTTL: number) {
|
||||
this.defaultTTL = defaultTTL;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if an entry has expired
|
||||
*/
|
||||
private isExpired(expires: number): boolean {
|
||||
return Date.now() > expires;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a value from the cache
|
||||
*/
|
||||
get(key: string): T | null {
|
||||
const entry = this.cache.get(key.toLowerCase());
|
||||
if (!entry) return null;
|
||||
|
||||
if (this.isExpired(entry.expires)) {
|
||||
this.cache.delete(key.toLowerCase());
|
||||
return null;
|
||||
}
|
||||
|
||||
return entry.data;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set a value in the cache
|
||||
*/
|
||||
set(key: string, data: T, ttl?: number): void {
|
||||
this.cache.set(key.toLowerCase(), {
|
||||
data,
|
||||
expires: Date.now() + (ttl ?? this.defaultTTL),
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a key exists and is not expired
|
||||
*/
|
||||
has(key: string): boolean {
|
||||
const entry = this.cache.get(key.toLowerCase());
|
||||
if (!entry) return false;
|
||||
|
||||
if (this.isExpired(entry.expires)) {
|
||||
this.cache.delete(key.toLowerCase());
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete a specific key
|
||||
*/
|
||||
delete(key: string): boolean {
|
||||
return this.cache.delete(key.toLowerCase());
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear all entries
|
||||
*/
|
||||
clear(): void {
|
||||
this.cache.clear();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the number of entries
|
||||
*/
|
||||
get size(): number {
|
||||
return this.cache.size;
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove expired entries
|
||||
*/
|
||||
cleanup(): number {
|
||||
const now = Date.now();
|
||||
let removed = 0;
|
||||
|
||||
for (const [key, entry] of this.cache.entries()) {
|
||||
if (now > entry.expires) {
|
||||
this.cache.delete(key);
|
||||
removed++;
|
||||
}
|
||||
}
|
||||
|
||||
return removed;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* PikaNetwork API Cache
|
||||
* Manages caching for profiles, clans, and leaderboards
|
||||
*/
|
||||
export class PikaCache {
|
||||
private profiles: TTLCache<ProfileResponse>;
|
||||
private clans: TTLCache<ClanResponse>;
|
||||
private leaderboards: TTLCache<LeaderboardResponse>;
|
||||
private cleanupInterval: number | undefined;
|
||||
|
||||
constructor(ttl: number = 3600000) {
|
||||
this.profiles = new TTLCache<ProfileResponse>(ttl);
|
||||
this.clans = new TTLCache<ClanResponse>(ttl);
|
||||
this.leaderboards = new TTLCache<LeaderboardResponse>(ttl);
|
||||
|
||||
// Run cleanup every 5 minutes
|
||||
this.cleanupInterval = setInterval(() => this.cleanup(), 300000);
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// Profile Cache
|
||||
// =========================================================================
|
||||
|
||||
/**
|
||||
* Get a cached profile
|
||||
*/
|
||||
getProfile(username: string): ProfileResponse | null {
|
||||
return this.profiles.get(username);
|
||||
}
|
||||
|
||||
/**
|
||||
* Cache a profile
|
||||
*/
|
||||
setProfile(username: string, data: ProfileResponse, ttl?: number): void {
|
||||
this.profiles.set(username, data, ttl);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a profile is cached
|
||||
*/
|
||||
hasProfile(username: string): boolean {
|
||||
return this.profiles.has(username);
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// Clan Cache
|
||||
// =========================================================================
|
||||
|
||||
/**
|
||||
* Get a cached clan
|
||||
*/
|
||||
getClan(name: string): ClanResponse | null {
|
||||
return this.clans.get(name);
|
||||
}
|
||||
|
||||
/**
|
||||
* Cache a clan
|
||||
*/
|
||||
setClan(name: string, data: ClanResponse, ttl?: number): void {
|
||||
this.clans.set(name, data, ttl);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a clan is cached
|
||||
*/
|
||||
hasClan(name: string): boolean {
|
||||
return this.clans.has(name);
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// Leaderboard Cache
|
||||
// =========================================================================
|
||||
|
||||
/**
|
||||
* Generate a cache key for leaderboard data
|
||||
*/
|
||||
private getLeaderboardKey(
|
||||
username: string,
|
||||
gamemode: string,
|
||||
mode: string,
|
||||
interval: string
|
||||
): string {
|
||||
return `${username}:${gamemode}:${mode}:${interval}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get cached leaderboard data
|
||||
*/
|
||||
getLeaderboard(
|
||||
username: string,
|
||||
gamemode: string,
|
||||
mode: string,
|
||||
interval: string
|
||||
): LeaderboardResponse | null {
|
||||
const key = this.getLeaderboardKey(username, gamemode, mode, interval);
|
||||
return this.leaderboards.get(key);
|
||||
}
|
||||
|
||||
/**
|
||||
* Cache leaderboard data
|
||||
*/
|
||||
setLeaderboard(
|
||||
username: string,
|
||||
gamemode: string,
|
||||
mode: string,
|
||||
interval: string,
|
||||
data: LeaderboardResponse,
|
||||
ttl?: number
|
||||
): void {
|
||||
const key = this.getLeaderboardKey(username, gamemode, mode, interval);
|
||||
this.leaderboards.set(key, data, ttl);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if leaderboard data is cached
|
||||
*/
|
||||
hasLeaderboard(
|
||||
username: string,
|
||||
gamemode: string,
|
||||
mode: string,
|
||||
interval: string
|
||||
): boolean {
|
||||
const key = this.getLeaderboardKey(username, gamemode, mode, interval);
|
||||
return this.leaderboards.has(key);
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// General Methods
|
||||
// =========================================================================
|
||||
|
||||
/**
|
||||
* Clear all caches
|
||||
*/
|
||||
clear(): void {
|
||||
this.profiles.clear();
|
||||
this.clans.clear();
|
||||
this.leaderboards.clear();
|
||||
}
|
||||
|
||||
/**
|
||||
* Run cleanup on all caches
|
||||
*/
|
||||
cleanup(): { profiles: number; clans: number; leaderboards: number } {
|
||||
return {
|
||||
profiles: this.profiles.cleanup(),
|
||||
clans: this.clans.cleanup(),
|
||||
leaderboards: this.leaderboards.cleanup(),
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Get cache statistics
|
||||
*/
|
||||
getStats(): { profiles: number; clans: number; leaderboards: number } {
|
||||
return {
|
||||
profiles: this.profiles.size,
|
||||
clans: this.clans.size,
|
||||
leaderboards: this.leaderboards.size,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Destroy the cache and stop cleanup interval
|
||||
*/
|
||||
destroy(): void {
|
||||
if (this.cleanupInterval) {
|
||||
clearInterval(this.cleanupInterval);
|
||||
}
|
||||
this.clear();
|
||||
}
|
||||
}
|
||||
779
src/api/pika/client.ts
Normal file
779
src/api/pika/client.ts
Normal file
@@ -0,0 +1,779 @@
|
||||
/**
|
||||
* PikaNetwork API Client
|
||||
* Modern TypeScript implementation without proxy support
|
||||
* Based on pikanetwork.js but rewritten with improvements
|
||||
*/
|
||||
|
||||
import { PikaCache } from './cache.ts';
|
||||
import type {
|
||||
ProfileResponse,
|
||||
ClanResponse,
|
||||
LeaderboardResponse,
|
||||
GameMode,
|
||||
Interval,
|
||||
PikaAPIOptions,
|
||||
BedWarsStats,
|
||||
SkyWarsStats,
|
||||
MinimalLeaderboardData,
|
||||
Punishment,
|
||||
PunishmentType,
|
||||
StaffList,
|
||||
VoteLeaderboard,
|
||||
VoteEntry,
|
||||
ServerStatus,
|
||||
TotalLeaderboardEntry,
|
||||
TotalLeaderboardOptions,
|
||||
JoinInfo,
|
||||
MiscInfo,
|
||||
} from './types.ts';
|
||||
|
||||
/**
|
||||
* PikaNetwork API Client
|
||||
* Provides methods to fetch player profiles, clan information, leaderboard data,
|
||||
* punishments, staff lists, vote leaderboards, and server status
|
||||
*/
|
||||
export class PikaNetworkAPI {
|
||||
private readonly baseUrl = 'https://stats.pika-network.net/api';
|
||||
private readonly forumUrl = 'https://pika-network.net';
|
||||
private readonly cache: PikaCache;
|
||||
private readonly timeout: number;
|
||||
private readonly userAgent: string;
|
||||
|
||||
// Staff roles for scraping
|
||||
private readonly staffRoles = new Set([
|
||||
'owner', 'manager', 'lead developer', 'developer',
|
||||
'admin', 'sr mod', 'moderator', 'helper', 'trial'
|
||||
]);
|
||||
|
||||
// Punishment type mapping
|
||||
private readonly punishmentMap: Record<string, string> = {
|
||||
warn: 'warnings',
|
||||
mute: 'mutes',
|
||||
kick: 'kicks',
|
||||
ban: 'bans',
|
||||
};
|
||||
|
||||
constructor(options: PikaAPIOptions = {}) {
|
||||
this.cache = new PikaCache(options.cacheTTL ?? 3600000); // 1 hour default
|
||||
this.timeout = options.timeout ?? 10000; // 10 seconds default
|
||||
this.userAgent = options.userAgent ?? 'Elly Discord Bot/1.0';
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// Private Helper Methods
|
||||
// =========================================================================
|
||||
|
||||
/**
|
||||
* Make an HTTP request with timeout and error handling
|
||||
*/
|
||||
private async request<T>(endpoint: string): Promise<T | null> {
|
||||
try {
|
||||
const controller = new AbortController();
|
||||
const timeoutId = setTimeout(() => controller.abort(), this.timeout);
|
||||
|
||||
const response = await fetch(`${this.baseUrl}${endpoint}`, {
|
||||
signal: controller.signal,
|
||||
headers: {
|
||||
'User-Agent': this.userAgent,
|
||||
'Accept': 'application/json',
|
||||
},
|
||||
});
|
||||
|
||||
clearTimeout(timeoutId);
|
||||
|
||||
if (!response.ok) {
|
||||
console.error(`[PikaAPI] Request failed: ${response.status} ${response.statusText} for ${endpoint}`);
|
||||
return null;
|
||||
}
|
||||
|
||||
// Get response text first to handle empty responses
|
||||
const text = await response.text();
|
||||
|
||||
// Handle empty responses
|
||||
if (!text || text.trim() === '') {
|
||||
console.warn(`[PikaAPI] Empty response for ${endpoint}`);
|
||||
return null;
|
||||
}
|
||||
|
||||
// Try to parse JSON
|
||||
try {
|
||||
return JSON.parse(text) as T;
|
||||
} catch {
|
||||
console.error(`[PikaAPI] Invalid JSON response for ${endpoint}: ${text.substring(0, 100)}...`);
|
||||
return null;
|
||||
}
|
||||
} catch (error) {
|
||||
if (error instanceof Error) {
|
||||
if (error.name === 'AbortError') {
|
||||
console.error(`[PikaAPI] Request timeout for ${endpoint}`);
|
||||
} else {
|
||||
console.error(`[PikaAPI] Request error for ${endpoint}: ${error.message}`);
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Delay execution for rate limiting
|
||||
*/
|
||||
private delay(ms: number): Promise<void> {
|
||||
return new Promise((resolve) => setTimeout(resolve, ms));
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// Profile Methods
|
||||
// =========================================================================
|
||||
|
||||
/**
|
||||
* Get a player's profile
|
||||
*/
|
||||
async getProfile(username: string): Promise<ProfileResponse | null> {
|
||||
// Check cache first
|
||||
const cached = this.cache.getProfile(username);
|
||||
if (cached) return cached;
|
||||
|
||||
// Fetch from API
|
||||
const data = await this.request<ProfileResponse>(`/profile/${encodeURIComponent(username)}`);
|
||||
|
||||
if (data && typeof data === 'object' && 'username' in data) {
|
||||
this.cache.setProfile(username, data);
|
||||
return data;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a player exists
|
||||
*/
|
||||
async playerExists(username: string): Promise<boolean> {
|
||||
const profile = await this.getProfile(username);
|
||||
return profile !== null;
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// Clan Methods
|
||||
// =========================================================================
|
||||
|
||||
/**
|
||||
* Get clan information
|
||||
*/
|
||||
async getClan(name: string): Promise<ClanResponse | null> {
|
||||
// Check cache first
|
||||
const cached = this.cache.getClan(name);
|
||||
if (cached) return cached;
|
||||
|
||||
// Fetch from API
|
||||
const data = await this.request<ClanResponse>(`/clans/${encodeURIComponent(name)}`);
|
||||
|
||||
if (data && typeof data === 'object' && 'name' in data) {
|
||||
this.cache.setClan(name, data);
|
||||
return data;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all members of a clan
|
||||
*/
|
||||
async getClanMembers(name: string): Promise<string[]> {
|
||||
const clan = await this.getClan(name);
|
||||
if (!clan) return [];
|
||||
|
||||
return clan.members.map((member) => member.user.username);
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// Leaderboard Methods
|
||||
// =========================================================================
|
||||
|
||||
/**
|
||||
* Get leaderboard data for a player
|
||||
*/
|
||||
async getLeaderboard(
|
||||
username: string,
|
||||
gamemode: GameMode,
|
||||
interval: Interval = 'lifetime',
|
||||
mode: string = 'all_modes'
|
||||
): Promise<LeaderboardResponse | null> {
|
||||
// Check cache first
|
||||
const cached = this.cache.getLeaderboard(username, gamemode, mode, interval);
|
||||
if (cached) return cached;
|
||||
|
||||
// Build URL with query parameters
|
||||
const params = new URLSearchParams({
|
||||
type: gamemode,
|
||||
interval: interval,
|
||||
mode: mode,
|
||||
});
|
||||
|
||||
const data = await this.request<LeaderboardResponse>(
|
||||
`/profile/${encodeURIComponent(username)}/leaderboard?${params.toString()}`
|
||||
);
|
||||
|
||||
if (data) {
|
||||
this.cache.setLeaderboard(username, gamemode, mode, interval, data);
|
||||
return data;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get parsed BedWars stats for a player
|
||||
*/
|
||||
async getBedWarsStats(
|
||||
username: string,
|
||||
interval: Interval = 'lifetime',
|
||||
mode: string = 'all_modes'
|
||||
): Promise<BedWarsStats | null> {
|
||||
const data = await this.getLeaderboard(username, 'bedwars', interval, mode);
|
||||
if (!data) return null;
|
||||
|
||||
return this.parseBedWarsStats(data);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get parsed SkyWars stats for a player
|
||||
*/
|
||||
async getSkyWarsStats(
|
||||
username: string,
|
||||
interval: Interval = 'lifetime',
|
||||
mode: string = 'all_modes'
|
||||
): Promise<SkyWarsStats | null> {
|
||||
const data = await this.getLeaderboard(username, 'skywars', interval, mode);
|
||||
if (!data) return null;
|
||||
|
||||
return this.parseSkyWarsStats(data);
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// Batch Methods
|
||||
// =========================================================================
|
||||
|
||||
/**
|
||||
* Get minimal leaderboard data for multiple players
|
||||
* Useful for guild activity reports
|
||||
*/
|
||||
async getMinimalBatchLeaderboard(
|
||||
usernames: string[],
|
||||
interval: Interval = 'lifetime'
|
||||
): Promise<MinimalLeaderboardData[]> {
|
||||
const results: MinimalLeaderboardData[] = [];
|
||||
const batchSize = 5;
|
||||
const delayMs = 200;
|
||||
|
||||
for (let i = 0; i < usernames.length; i += batchSize) {
|
||||
const batch = usernames.slice(i, i + batchSize);
|
||||
|
||||
const promises = batch.map(async (username) => {
|
||||
const [bedwars, skywars] = await Promise.all([
|
||||
this.getLeaderboard(username, 'bedwars', interval),
|
||||
this.getLeaderboard(username, 'skywars', interval),
|
||||
]);
|
||||
|
||||
const bwWins = this.getStatValue(bedwars, 'Wins');
|
||||
const swWins = this.getStatValue(skywars, 'Wins');
|
||||
|
||||
return {
|
||||
username,
|
||||
bedwars_wins: bwWins,
|
||||
skywars_wins: swWins,
|
||||
total_wins: bwWins >= 0 && swWins >= 0 ? bwWins + swWins : -1,
|
||||
};
|
||||
});
|
||||
|
||||
const batchResults = await Promise.all(promises);
|
||||
results.push(...batchResults);
|
||||
|
||||
// Rate limiting delay between batches
|
||||
if (i + batchSize < usernames.length) {
|
||||
await this.delay(delayMs);
|
||||
}
|
||||
}
|
||||
|
||||
return results;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get full leaderboard data for multiple players
|
||||
*/
|
||||
async getBatchLeaderboard(
|
||||
usernames: string[],
|
||||
gamemode: GameMode,
|
||||
interval: Interval = 'lifetime'
|
||||
): Promise<Map<string, LeaderboardResponse | null>> {
|
||||
const results = new Map<string, LeaderboardResponse | null>();
|
||||
const batchSize = 5;
|
||||
const delayMs = 200;
|
||||
|
||||
for (let i = 0; i < usernames.length; i += batchSize) {
|
||||
const batch = usernames.slice(i, i + batchSize);
|
||||
|
||||
const promises = batch.map(async (username) => {
|
||||
const data = await this.getLeaderboard(username, gamemode, interval);
|
||||
return { username, data };
|
||||
});
|
||||
|
||||
const batchResults = await Promise.all(promises);
|
||||
batchResults.forEach(({ username, data }) => results.set(username, data));
|
||||
|
||||
// Rate limiting delay between batches
|
||||
if (i + batchSize < usernames.length) {
|
||||
await this.delay(delayMs);
|
||||
}
|
||||
}
|
||||
|
||||
return results;
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// Stat Parsing Methods
|
||||
// =========================================================================
|
||||
|
||||
/**
|
||||
* Get a stat value from leaderboard data
|
||||
*/
|
||||
private getStatValue(data: LeaderboardResponse | null, key: string): number {
|
||||
if (!data || !data[key] || !data[key].entries || data[key].entries.length === 0) {
|
||||
return 0;
|
||||
}
|
||||
return data[key].entries[0].value;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a stat position from leaderboard data
|
||||
*/
|
||||
private getStatPosition(data: LeaderboardResponse | null, key: string): number {
|
||||
if (!data || !data[key] || !data[key].entries || data[key].entries.length === 0) {
|
||||
return 0;
|
||||
}
|
||||
return data[key].entries[0].place;
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate ratio between two numbers
|
||||
*/
|
||||
private calculateRatio(numerator: number, denominator: number): number {
|
||||
if (denominator === 0) return numerator;
|
||||
if (numerator === 0) return 0;
|
||||
return Math.round((numerator / denominator) * 100) / 100;
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse raw leaderboard data into BedWars stats
|
||||
*/
|
||||
private parseBedWarsStats(data: LeaderboardResponse): BedWarsStats {
|
||||
const kills = this.getStatValue(data, 'Kills');
|
||||
const deaths = this.getStatValue(data, 'Deaths');
|
||||
const finalKills = this.getStatValue(data, 'Final kills');
|
||||
const finalDeaths = this.getStatValue(data, 'Final deaths');
|
||||
const wins = this.getStatValue(data, 'Wins');
|
||||
const losses = this.getStatValue(data, 'Losses');
|
||||
|
||||
return {
|
||||
kills,
|
||||
deaths,
|
||||
finalKills,
|
||||
finalDeaths,
|
||||
wins,
|
||||
losses,
|
||||
bedsDestroyed: this.getStatValue(data, 'Beds destroyed'),
|
||||
gamesPlayed: this.getStatValue(data, 'Games played'),
|
||||
highestWinstreak: this.getStatValue(data, 'Highest winstreak reached'),
|
||||
bowKills: this.getStatValue(data, 'Bow kills'),
|
||||
arrowsShot: this.getStatValue(data, 'Arrows shot'),
|
||||
arrowsHit: this.getStatValue(data, 'Arrows hit'),
|
||||
meleeKills: this.getStatValue(data, 'Melee kills'),
|
||||
voidKills: this.getStatValue(data, 'Void kills'),
|
||||
kdr: this.calculateRatio(kills, deaths),
|
||||
fkdr: this.calculateRatio(finalKills, finalDeaths),
|
||||
wlr: this.calculateRatio(wins, losses),
|
||||
positions: {
|
||||
kills: this.getStatPosition(data, 'Kills'),
|
||||
deaths: this.getStatPosition(data, 'Deaths'),
|
||||
finalKills: this.getStatPosition(data, 'Final kills'),
|
||||
finalDeaths: this.getStatPosition(data, 'Final deaths'),
|
||||
wins: this.getStatPosition(data, 'Wins'),
|
||||
losses: this.getStatPosition(data, 'Losses'),
|
||||
bedsDestroyed: this.getStatPosition(data, 'Beds destroyed'),
|
||||
gamesPlayed: this.getStatPosition(data, 'Games played'),
|
||||
highestWinstreak: this.getStatPosition(data, 'Highest winstreak reached'),
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse raw leaderboard data into SkyWars stats
|
||||
*/
|
||||
private parseSkyWarsStats(data: LeaderboardResponse): SkyWarsStats {
|
||||
const kills = this.getStatValue(data, 'Kills');
|
||||
const deaths = this.getStatValue(data, 'Deaths');
|
||||
const wins = this.getStatValue(data, 'Wins');
|
||||
const losses = this.getStatValue(data, 'Losses');
|
||||
|
||||
return {
|
||||
kills,
|
||||
deaths,
|
||||
wins,
|
||||
losses,
|
||||
gamesPlayed: this.getStatValue(data, 'Games played'),
|
||||
highestWinstreak: this.getStatValue(data, 'Highest winstreak reached'),
|
||||
bowKills: this.getStatValue(data, 'Bow kills'),
|
||||
arrowsShot: this.getStatValue(data, 'Arrows shot'),
|
||||
arrowsHit: this.getStatValue(data, 'Arrows hit'),
|
||||
meleeKills: this.getStatValue(data, 'Melee kills'),
|
||||
voidKills: this.getStatValue(data, 'Void kills'),
|
||||
kdr: this.calculateRatio(kills, deaths),
|
||||
wlr: this.calculateRatio(wins, losses),
|
||||
positions: {
|
||||
kills: this.getStatPosition(data, 'Kills'),
|
||||
deaths: this.getStatPosition(data, 'Deaths'),
|
||||
wins: this.getStatPosition(data, 'Wins'),
|
||||
losses: this.getStatPosition(data, 'Losses'),
|
||||
gamesPlayed: this.getStatPosition(data, 'Games played'),
|
||||
highestWinstreak: this.getStatPosition(data, 'Highest winstreak reached'),
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// Total Leaderboard Methods
|
||||
// =========================================================================
|
||||
|
||||
/**
|
||||
* Get total leaderboard data (top players for a stat)
|
||||
*/
|
||||
async getTotalLeaderboard(options: TotalLeaderboardOptions): Promise<TotalLeaderboardEntry[] | null> {
|
||||
const params = new URLSearchParams({
|
||||
type: options.gamemode,
|
||||
interval: options.interval,
|
||||
stat: options.stat,
|
||||
mode: options.mode,
|
||||
offset: String(options.offset ?? 0),
|
||||
limit: String(options.limit ?? 15),
|
||||
});
|
||||
|
||||
const data = await this.request<TotalLeaderboardEntry[]>(
|
||||
`/leaderboards?${params.toString()}`
|
||||
);
|
||||
|
||||
return data;
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// Profile Extended Methods
|
||||
// =========================================================================
|
||||
|
||||
/**
|
||||
* Get friend list for a player
|
||||
*/
|
||||
async getFriendList(username: string): Promise<string[]> {
|
||||
const profile = await this.getProfile(username);
|
||||
if (!profile) return [];
|
||||
return profile.friends.map((f) => f.username);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get guild info for a player
|
||||
*/
|
||||
async getPlayerGuild(username: string): Promise<ClanResponse | null> {
|
||||
const profile = await this.getProfile(username);
|
||||
if (!profile?.clan) return null;
|
||||
return profile.clan as ClanResponse;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get rank info for a player
|
||||
*/
|
||||
async getRankInfo(username: string): Promise<{ level: number; percentage: number; display: string } | null> {
|
||||
const profile = await this.getProfile(username);
|
||||
if (!profile) return null;
|
||||
return {
|
||||
level: profile.rank.level,
|
||||
percentage: profile.rank.percentage,
|
||||
display: profile.rank.rankDisplay,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Get miscellaneous info for a player
|
||||
*/
|
||||
async getMiscInfo(username: string): Promise<MiscInfo | null> {
|
||||
const profile = await this.getProfile(username);
|
||||
if (!profile) return null;
|
||||
return {
|
||||
discordBoosting: profile.discord_boosting,
|
||||
discordVerified: profile.discord_verified,
|
||||
emailVerified: profile.email_verified,
|
||||
username: profile.username,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Get join info for a player
|
||||
*/
|
||||
async getJoinInfo(username: string): Promise<JoinInfo | null> {
|
||||
const profile = await this.getProfile(username);
|
||||
if (!profile) return null;
|
||||
|
||||
const lastJoinDate = new Date(profile.lastSeen);
|
||||
const formatOptions: Intl.DateTimeFormatOptions = {
|
||||
day: 'numeric',
|
||||
month: 'long',
|
||||
year: 'numeric',
|
||||
hour: '2-digit',
|
||||
minute: '2-digit',
|
||||
hour12: true,
|
||||
};
|
||||
|
||||
return {
|
||||
lastJoin: profile.lastSeen,
|
||||
lastJoinFormatted: lastJoinDate.toLocaleString('en-US', formatOptions),
|
||||
estimatedFirstJoin: null, // Would require punishment scraping
|
||||
estimatedFirstJoinFormatted: 'N/A',
|
||||
};
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// Server Status Methods
|
||||
// =========================================================================
|
||||
|
||||
/**
|
||||
* Get PikaNetwork server status
|
||||
*/
|
||||
async getServerStatus(serverIP: string = 'play.pika-network.net'): Promise<ServerStatus | null> {
|
||||
try {
|
||||
const controller = new AbortController();
|
||||
const timeoutId = setTimeout(() => controller.abort(), this.timeout);
|
||||
|
||||
const response = await fetch(`https://api.mcstatus.io/v2/status/java/${serverIP}`, {
|
||||
signal: controller.signal,
|
||||
headers: {
|
||||
'User-Agent': this.userAgent,
|
||||
'Accept': 'application/json',
|
||||
},
|
||||
});
|
||||
|
||||
clearTimeout(timeoutId);
|
||||
|
||||
if (!response.ok) return null;
|
||||
|
||||
const text = await response.text();
|
||||
if (!text || text.trim() === '') return null;
|
||||
|
||||
const data = JSON.parse(text);
|
||||
|
||||
const motdLines = data.motd?.clean?.split('\n').map((p: string) => p.trim()) ?? [];
|
||||
|
||||
const serverData: ServerStatus = {
|
||||
host: data.host ?? serverIP,
|
||||
ip: data.ip_address ?? serverIP,
|
||||
port: data.port ?? 25565,
|
||||
icon: `https://eu.mc-api.net/v3/server/favicon/${serverIP}`,
|
||||
banner: `https://api.loohpjames.com/serverbanner.png?ip=${serverIP}`,
|
||||
online: data.online ?? false,
|
||||
software: data.version?.name_clean ?? 'Unknown',
|
||||
protocol: data.version?.protocol ?? 0,
|
||||
playersOnline: data.players?.online ?? 0,
|
||||
playersMax: data.players?.max ?? 0,
|
||||
motd: motdLines,
|
||||
};
|
||||
|
||||
if (serverIP === 'play.pika-network.net') {
|
||||
serverData.website = 'https://pika-network.net/';
|
||||
serverData.discord = 'https://discord.gg/pikanetwork';
|
||||
}
|
||||
|
||||
return serverData;
|
||||
} catch (error) {
|
||||
console.error(`[PikaAPI] Server status error: ${error instanceof Error ? error.message : String(error)}`);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// HTML Scraping Methods (Forum Data)
|
||||
// =========================================================================
|
||||
|
||||
/**
|
||||
* Fetch HTML from a URL
|
||||
*/
|
||||
private async fetchHtml(url: string): Promise<string | null> {
|
||||
try {
|
||||
const controller = new AbortController();
|
||||
const timeoutId = setTimeout(() => controller.abort(), this.timeout);
|
||||
|
||||
const response = await fetch(url, {
|
||||
signal: controller.signal,
|
||||
headers: {
|
||||
'User-Agent': this.userAgent,
|
||||
'Accept': 'text/html',
|
||||
},
|
||||
});
|
||||
|
||||
clearTimeout(timeoutId);
|
||||
|
||||
if (!response.ok) return null;
|
||||
return await response.text();
|
||||
} catch (error) {
|
||||
console.error(`[PikaAPI] HTML fetch error for ${url}: ${error instanceof Error ? error.message : String(error)}`);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Clean punishment reason (remove Minecraft formatting codes)
|
||||
*/
|
||||
private cleanReason(reason: string): string {
|
||||
const minecraftRegex = /(?:^\s+|(&|§)([0-9A-Fa-f])\b|&e[0-9]?\s?|^\[VL[^\]]*\]|^\?\s*)/g;
|
||||
const formattingCodesRegex = /(§[0-9a-fk-or])|(&[0-9a-fk-or])/gi;
|
||||
const cleaned = reason.replace(minecraftRegex, '').replace(formattingCodesRegex, '');
|
||||
return cleaned || 'N/A';
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse HTML to extract text (simple implementation without cheerio)
|
||||
*/
|
||||
private extractTextFromHtml(html: string, selector: string): string[] {
|
||||
// Simple regex-based extraction for common patterns
|
||||
const results: string[] = [];
|
||||
|
||||
// Match class-based selectors
|
||||
const classMatch = selector.match(/\.([a-zA-Z0-9_-]+)/g);
|
||||
if (classMatch) {
|
||||
const className = classMatch[0].substring(1);
|
||||
const regex = new RegExp(`class="[^"]*${className}[^"]*"[^>]*>([^<]+)`, 'gi');
|
||||
let match;
|
||||
while ((match = regex.exec(html)) !== null) {
|
||||
results.push(match[1].trim());
|
||||
}
|
||||
}
|
||||
|
||||
return results;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get punishments for a player (basic implementation)
|
||||
* Note: Full implementation would require cheerio for HTML parsing
|
||||
*/
|
||||
async getPunishments(
|
||||
username: string,
|
||||
filter?: PunishmentType,
|
||||
includeConsole: boolean = true
|
||||
): Promise<Punishment[]> {
|
||||
const html = await this.fetchHtml(`${this.forumUrl}/bans/search/${encodeURIComponent(username)}/`);
|
||||
if (!html) return [];
|
||||
|
||||
// Basic parsing - for full implementation, use a proper HTML parser
|
||||
const punishments: Punishment[] = [];
|
||||
|
||||
// Extract punishment rows using regex (simplified)
|
||||
const rowRegex = /<div class="row"[^>]*>([\s\S]*?)<\/div>\s*<\/div>\s*<\/div>/gi;
|
||||
let match;
|
||||
|
||||
while ((match = rowRegex.exec(html)) !== null) {
|
||||
const row = match[1];
|
||||
|
||||
// Extract type
|
||||
const typeMatch = row.match(/class="td _type"[^>]*>.*?<b>([^<]+)<\/b>/i);
|
||||
const type = typeMatch ? typeMatch[1].trim().toLowerCase() : '';
|
||||
|
||||
// Extract staff
|
||||
const staffMatch = row.match(/class="td _staff"[^>]*>([^<]+)/i);
|
||||
const staff = staffMatch ? staffMatch[1].trim() : 'N/A';
|
||||
|
||||
// Extract reason
|
||||
const reasonMatch = row.match(/class="td _reason"[^>]*>([^<]+)/i);
|
||||
const reason = reasonMatch ? this.cleanReason(reasonMatch[1].trim()) : 'N/A';
|
||||
|
||||
// Extract date
|
||||
const dateMatch = row.match(/class="td _date"[^>]*>([^<]+)/i);
|
||||
const date = dateMatch ? dateMatch[1].trim() : '';
|
||||
|
||||
// Extract expires
|
||||
const expiresMatch = row.match(/class="td _expires"[^>]*>([^<]+)/i);
|
||||
const expires = expiresMatch ? expiresMatch[1].trim() : '';
|
||||
|
||||
if (type) {
|
||||
const punishment: Punishment = {
|
||||
type,
|
||||
staff,
|
||||
reason,
|
||||
date,
|
||||
expires,
|
||||
};
|
||||
|
||||
// Filter by type if specified
|
||||
if (filter && type !== filter) continue;
|
||||
|
||||
// Filter console punishments if needed
|
||||
if (!includeConsole && staff.toLowerCase().includes('console')) continue;
|
||||
|
||||
punishments.push(punishment);
|
||||
}
|
||||
}
|
||||
|
||||
return punishments;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get vote leaderboard (basic implementation)
|
||||
*/
|
||||
async getVoteLeaderboard(): Promise<VoteLeaderboard | null> {
|
||||
const html = await this.fetchHtml(`${this.forumUrl}/vote`);
|
||||
if (!html) return null;
|
||||
|
||||
const voters: VoteEntry[] = [];
|
||||
const runnerUps: VoteEntry[] = [];
|
||||
|
||||
// Extract voters using regex (simplified)
|
||||
const voterRegex = /class="voter[^"]*"[^>]*>[\s\S]*?class="position"[^>]*>#?(\d+)[\s\S]*?class="username"[^>]*>([^<]+)[\s\S]*?(\d+)\s*votes/gi;
|
||||
let match;
|
||||
let position = 1;
|
||||
|
||||
while ((match = voterRegex.exec(html)) !== null) {
|
||||
const entry: VoteEntry = {
|
||||
position: parseInt(match[1]) || position,
|
||||
username: match[2].trim(),
|
||||
votes: parseInt(match[3]) || 0,
|
||||
};
|
||||
|
||||
if (html.indexOf(match[0]) < html.indexOf('runners-up')) {
|
||||
voters.push(entry);
|
||||
} else {
|
||||
runnerUps.push(entry);
|
||||
}
|
||||
position++;
|
||||
}
|
||||
|
||||
return { voters, runnerUps };
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// Cache Management
|
||||
// =========================================================================
|
||||
|
||||
/**
|
||||
* Clear all cached data
|
||||
*/
|
||||
clearCache(): void {
|
||||
this.cache.clear();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get cache statistics
|
||||
*/
|
||||
getCacheStats(): { profiles: number; clans: number; leaderboards: number } {
|
||||
return this.cache.getStats();
|
||||
}
|
||||
|
||||
/**
|
||||
* Destroy the client and cleanup resources
|
||||
*/
|
||||
destroy(): void {
|
||||
this.cache.destroy();
|
||||
}
|
||||
}
|
||||
63
src/api/pika/index.ts
Normal file
63
src/api/pika/index.ts
Normal file
@@ -0,0 +1,63 @@
|
||||
/**
|
||||
* PikaNetwork API Module
|
||||
* Exports all API-related types and classes
|
||||
* Based on pikanetwork.js but rewritten in TypeScript with improvements
|
||||
*/
|
||||
|
||||
export { PikaNetworkAPI } from './client.ts';
|
||||
export { PikaCache } from './cache.ts';
|
||||
export type {
|
||||
// Profile types
|
||||
ProfileResponse,
|
||||
Rank,
|
||||
PlayerRank,
|
||||
Friend,
|
||||
ClanInfo,
|
||||
ClanLeveling,
|
||||
ClanMember,
|
||||
ClanMemberUser,
|
||||
ClanOwner,
|
||||
// Clan types
|
||||
ClanResponse,
|
||||
// Leaderboard types
|
||||
LeaderboardResponse,
|
||||
LeaderboardEntry,
|
||||
LeaderboardEntryValue,
|
||||
// Parsed stats types
|
||||
BedWarsStats,
|
||||
SkyWarsStats,
|
||||
// Options and enums
|
||||
GameMode,
|
||||
Interval,
|
||||
BedWarsMode,
|
||||
SkyWarsMode,
|
||||
PikaAPIOptions,
|
||||
// Batch types
|
||||
BatchLeaderboardResult,
|
||||
MinimalLeaderboardData,
|
||||
// Error types
|
||||
PikaAPIError,
|
||||
// Punishment types
|
||||
Punishment,
|
||||
PunishmentType,
|
||||
// Staff types
|
||||
StaffList,
|
||||
StaffRole,
|
||||
// Vote types
|
||||
VoteEntry,
|
||||
VoteLeaderboard,
|
||||
// Server types
|
||||
ServerStatus,
|
||||
// Total leaderboard types
|
||||
TotalLeaderboardEntry,
|
||||
TotalLeaderboardOptions,
|
||||
// Extended profile types
|
||||
JoinInfo,
|
||||
MiscInfo,
|
||||
} from './types.ts';
|
||||
|
||||
export {
|
||||
isProfileResponse,
|
||||
isClanResponse,
|
||||
isLeaderboardResponse,
|
||||
} from './types.ts';
|
||||
354
src/api/pika/types.ts
Normal file
354
src/api/pika/types.ts
Normal file
@@ -0,0 +1,354 @@
|
||||
/**
|
||||
* PikaNetwork API Type Definitions
|
||||
* Comprehensive TypeScript interfaces for all API responses
|
||||
*/
|
||||
|
||||
// ============================================================================
|
||||
// Profile Types
|
||||
// ============================================================================
|
||||
|
||||
export interface Rank {
|
||||
displayName: string;
|
||||
name: string;
|
||||
priority: number;
|
||||
}
|
||||
|
||||
export interface PlayerRank {
|
||||
level: number;
|
||||
percentage: number;
|
||||
rankDisplay: string;
|
||||
}
|
||||
|
||||
export interface Friend {
|
||||
username: string;
|
||||
}
|
||||
|
||||
export interface ClanLeveling {
|
||||
level: number;
|
||||
exp: number;
|
||||
totalExp: number;
|
||||
}
|
||||
|
||||
export interface ClanMemberUser {
|
||||
username: string;
|
||||
}
|
||||
|
||||
export interface ClanMember {
|
||||
user: ClanMemberUser;
|
||||
joinTime: string;
|
||||
}
|
||||
|
||||
export interface ClanOwner {
|
||||
username: string;
|
||||
}
|
||||
|
||||
export interface ClanInfo {
|
||||
name: string;
|
||||
tag: string;
|
||||
currentTrophies: number;
|
||||
creationTime: string;
|
||||
members: ClanMember[];
|
||||
owner: ClanOwner;
|
||||
leveling: ClanLeveling;
|
||||
}
|
||||
|
||||
export interface ProfileResponse {
|
||||
username: string;
|
||||
discord_verified: boolean;
|
||||
lastSeen: number;
|
||||
ranks: Rank[];
|
||||
email_verified: boolean;
|
||||
discord_boosting: boolean;
|
||||
clan: ClanInfo | null;
|
||||
rank: PlayerRank;
|
||||
friends: Friend[];
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Clan Types
|
||||
// ============================================================================
|
||||
|
||||
export interface ClanResponse {
|
||||
name: string;
|
||||
tag: string;
|
||||
currentTrophies: number;
|
||||
creationTime: string;
|
||||
members: ClanMember[];
|
||||
owner: ClanOwner;
|
||||
leveling: ClanLeveling;
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Leaderboard Types
|
||||
// ============================================================================
|
||||
|
||||
export interface LeaderboardEntryValue {
|
||||
value: number;
|
||||
place: number;
|
||||
}
|
||||
|
||||
export interface LeaderboardEntry {
|
||||
entries: LeaderboardEntryValue[] | null;
|
||||
}
|
||||
|
||||
export interface LeaderboardResponse {
|
||||
[key: string]: LeaderboardEntry;
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Parsed Stats Types
|
||||
// ============================================================================
|
||||
|
||||
export interface BedWarsStats {
|
||||
kills: number;
|
||||
deaths: number;
|
||||
finalKills: number;
|
||||
finalDeaths: number;
|
||||
wins: number;
|
||||
losses: number;
|
||||
bedsDestroyed: number;
|
||||
gamesPlayed: number;
|
||||
highestWinstreak: number;
|
||||
bowKills: number;
|
||||
arrowsShot: number;
|
||||
arrowsHit: number;
|
||||
meleeKills: number;
|
||||
voidKills: number;
|
||||
// Calculated ratios
|
||||
kdr: number;
|
||||
fkdr: number;
|
||||
wlr: number;
|
||||
// Leaderboard positions
|
||||
positions: {
|
||||
kills: number;
|
||||
deaths: number;
|
||||
finalKills: number;
|
||||
finalDeaths: number;
|
||||
wins: number;
|
||||
losses: number;
|
||||
bedsDestroyed: number;
|
||||
gamesPlayed: number;
|
||||
highestWinstreak: number;
|
||||
};
|
||||
}
|
||||
|
||||
export interface SkyWarsStats {
|
||||
kills: number;
|
||||
deaths: number;
|
||||
wins: number;
|
||||
losses: number;
|
||||
gamesPlayed: number;
|
||||
highestWinstreak: number;
|
||||
bowKills: number;
|
||||
arrowsShot: number;
|
||||
arrowsHit: number;
|
||||
meleeKills: number;
|
||||
voidKills: number;
|
||||
// Calculated ratios
|
||||
kdr: number;
|
||||
wlr: number;
|
||||
// Leaderboard positions
|
||||
positions: {
|
||||
kills: number;
|
||||
deaths: number;
|
||||
wins: number;
|
||||
losses: number;
|
||||
gamesPlayed: number;
|
||||
highestWinstreak: number;
|
||||
};
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// API Options & Enums
|
||||
// ============================================================================
|
||||
|
||||
export type GameMode = 'bedwars' | 'skywars';
|
||||
|
||||
export type Interval = 'daily' | 'weekly' | 'monthly' | 'yearly' | 'lifetime';
|
||||
|
||||
export type BedWarsMode = 'solo' | 'doubles' | 'triples' | 'quad' | 'all_modes';
|
||||
|
||||
export type SkyWarsMode = 'solo' | 'doubles' | 'all_modes';
|
||||
|
||||
export interface PikaAPIOptions {
|
||||
cacheTTL?: number;
|
||||
timeout?: number;
|
||||
userAgent?: string;
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Batch Request Types
|
||||
// ============================================================================
|
||||
|
||||
export interface BatchLeaderboardResult {
|
||||
username: string;
|
||||
bedwarsWins: number;
|
||||
skywarsWins: number;
|
||||
totalWins: number;
|
||||
}
|
||||
|
||||
export interface MinimalLeaderboardData {
|
||||
username: string;
|
||||
bedwars_wins: number;
|
||||
skywars_wins: number;
|
||||
total_wins: number;
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Error Types
|
||||
// ============================================================================
|
||||
|
||||
export interface PikaAPIError {
|
||||
status: number;
|
||||
message: string;
|
||||
endpoint: string;
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Helper Functions for Type Guards
|
||||
// ============================================================================
|
||||
|
||||
export function isProfileResponse(data: unknown): data is ProfileResponse {
|
||||
return (
|
||||
typeof data === 'object' &&
|
||||
data !== null &&
|
||||
'username' in data &&
|
||||
'rank' in data &&
|
||||
'ranks' in data
|
||||
);
|
||||
}
|
||||
|
||||
export function isClanResponse(data: unknown): data is ClanResponse {
|
||||
return (
|
||||
typeof data === 'object' &&
|
||||
data !== null &&
|
||||
'name' in data &&
|
||||
'tag' in data &&
|
||||
'members' in data &&
|
||||
'owner' in data
|
||||
);
|
||||
}
|
||||
|
||||
export function isLeaderboardResponse(data: unknown): data is LeaderboardResponse {
|
||||
return typeof data === 'object' && data !== null;
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Punishment Types (Forum Scraping)
|
||||
// ============================================================================
|
||||
|
||||
export type PunishmentType = 'warn' | 'kick' | 'ban' | 'mute';
|
||||
|
||||
export interface Punishment {
|
||||
type: string;
|
||||
player?: string;
|
||||
playerAvatar?: string;
|
||||
staff?: string;
|
||||
staffAvatar?: string;
|
||||
reason: string;
|
||||
date: string;
|
||||
expires: string;
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Staff Types (Forum Scraping)
|
||||
// ============================================================================
|
||||
|
||||
export type StaffRole =
|
||||
| 'owner'
|
||||
| 'manager'
|
||||
| 'leaddeveloper'
|
||||
| 'developer'
|
||||
| 'admin'
|
||||
| 'srmod'
|
||||
| 'moderator'
|
||||
| 'helper'
|
||||
| 'trial';
|
||||
|
||||
export interface StaffList {
|
||||
owner: string[];
|
||||
manager: string[];
|
||||
leaddeveloper: string[];
|
||||
developer: string[];
|
||||
admin: string[];
|
||||
srmod: string[];
|
||||
moderator: string[];
|
||||
helper: string[];
|
||||
trial: string[];
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Vote Leaderboard Types
|
||||
// ============================================================================
|
||||
|
||||
export interface VoteEntry {
|
||||
position: number;
|
||||
username: string;
|
||||
votes: number;
|
||||
}
|
||||
|
||||
export interface VoteLeaderboard {
|
||||
voters: VoteEntry[];
|
||||
runnerUps: VoteEntry[];
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Server Status Types
|
||||
// ============================================================================
|
||||
|
||||
export interface ServerStatus {
|
||||
host: string;
|
||||
ip: string;
|
||||
port: number;
|
||||
icon: string;
|
||||
banner: string;
|
||||
online: boolean;
|
||||
software: string;
|
||||
protocol: number;
|
||||
playersOnline: number;
|
||||
playersMax: number;
|
||||
motd: string[];
|
||||
website?: string;
|
||||
discord?: string;
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Total Leaderboard Types
|
||||
// ============================================================================
|
||||
|
||||
export interface TotalLeaderboardEntry {
|
||||
name: string;
|
||||
value: number;
|
||||
place: number;
|
||||
}
|
||||
|
||||
export interface TotalLeaderboardOptions {
|
||||
gamemode: GameMode;
|
||||
interval: Interval;
|
||||
stat: string;
|
||||
mode: string;
|
||||
offset?: number;
|
||||
limit?: number;
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Join Info Types
|
||||
// ============================================================================
|
||||
|
||||
export interface JoinInfo {
|
||||
lastJoin: number;
|
||||
lastJoinFormatted: string;
|
||||
estimatedFirstJoin: Date | null;
|
||||
estimatedFirstJoinFormatted: string;
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Misc Info Types
|
||||
// ============================================================================
|
||||
|
||||
export interface MiscInfo {
|
||||
discordBoosting: boolean;
|
||||
discordVerified: boolean;
|
||||
emailVerified: boolean;
|
||||
username: string;
|
||||
}
|
||||
Reference in New Issue
Block a user