Document CSGOWTFD backend API and update domain modeling plans
Reflect backend audit findings in TODO.md: - 12 REST endpoints documented for player, match, matches, and sitemap - Data models aligned with backend schemas (Match, Player, MatchPlayer, etc.) - CS2 compatibility confirmed with Premier rating support (0-30000) Add comprehensive API documentation covering: - Endpoint specifications and response structures - Integration guide with TypeScript examples - Error handling and caching strategies - CS2 migration notes for rank system and MR12 changes
This commit is contained in:
998
docs/API.md
Normal file
998
docs/API.md
Normal file
@@ -0,0 +1,998 @@
|
||||
# CSGOWTFD Backend API Documentation
|
||||
|
||||
**Version**: 1.0
|
||||
**Base URL**: `http://localhost:8000` (default)
|
||||
**Backend Repository**: https://somegit.dev/CSGOWTF/csgowtfd
|
||||
**Framework**: Go + Gin + Ent ORM
|
||||
**Database**: PostgreSQL
|
||||
|
||||
---
|
||||
|
||||
## Table of Contents
|
||||
|
||||
1. [Overview](#overview)
|
||||
2. [API Endpoints](#api-endpoints)
|
||||
- [Player Endpoints](#player-endpoints)
|
||||
- [Match Endpoints](#match-endpoints)
|
||||
- [Matches Listing](#matches-listing)
|
||||
- [Sitemap](#sitemap)
|
||||
3. [Data Models](#data-models)
|
||||
4. [Integration Guide](#integration-guide)
|
||||
5. [Error Handling](#error-handling)
|
||||
|
||||
---
|
||||
|
||||
## Overview
|
||||
|
||||
The CSGOWTFD backend is a REST API service that provides Counter-Strike match statistics, player profiles, and demo parsing capabilities. It uses:
|
||||
|
||||
- **Web Framework**: Gin (Go)
|
||||
- **ORM**: Ent (Entity Framework for Go)
|
||||
- **Database**: PostgreSQL with pgx driver
|
||||
- **Cache**: Redis
|
||||
- **Steam Integration**: Steam API + demo parsing
|
||||
|
||||
### Configuration
|
||||
|
||||
Default backend configuration:
|
||||
```yaml
|
||||
httpd:
|
||||
listen: ":8000"
|
||||
cors_allow_domains: ["*"]
|
||||
|
||||
database:
|
||||
driver: "pgx"
|
||||
connection: "postgres://username:password@localhost:5432/database_name"
|
||||
|
||||
redis:
|
||||
addr: "localhost:6379"
|
||||
```
|
||||
|
||||
### CORS
|
||||
|
||||
The backend supports CORS with configurable allowed domains. By default, all origins are allowed in development.
|
||||
|
||||
---
|
||||
|
||||
## API Endpoints
|
||||
|
||||
### Player Endpoints
|
||||
|
||||
#### 1. Get Player Profile
|
||||
|
||||
Retrieves comprehensive player statistics and match history.
|
||||
|
||||
**Endpoint**: `GET /player/:id`
|
||||
**Alternative**: `GET /player/:id/next/:time`
|
||||
|
||||
**Parameters**:
|
||||
- `id` (path, required): Steam ID (uint64)
|
||||
- `time` (path, optional): Unix timestamp for pagination (get matches before this time)
|
||||
|
||||
**Response** (200 OK):
|
||||
```json
|
||||
{
|
||||
"id": 76561198012345678,
|
||||
"name": "PlayerName",
|
||||
"avatar": "https://steamcdn-a.akamaihd.net/steamcommunity/public/images/avatars/...",
|
||||
"vanity_url": "custom-url",
|
||||
"vanity_url_real": "custom-url",
|
||||
"steam_updated": "2024-11-04T10:30:00Z",
|
||||
"profile_created": "2015-03-12T00:00:00Z",
|
||||
"wins": 1250,
|
||||
"looses": 980,
|
||||
"ties": 45,
|
||||
"vac_count": 0,
|
||||
"vac_date": null,
|
||||
"game_ban_count": 0,
|
||||
"game_ban_date": null,
|
||||
"oldest_sharecode_seen": "CSGO-XXXXX-XXXXX-XXXXX-XXXXX-XXXXX",
|
||||
"matches": [
|
||||
{
|
||||
"match_id": 3589487716842078322,
|
||||
"map": "de_inferno",
|
||||
"date": "2024-11-01T18:45:00Z",
|
||||
"score_team_a": 13,
|
||||
"score_team_b": 10,
|
||||
"duration": 2456,
|
||||
"match_result": 1,
|
||||
"max_rounds": 24,
|
||||
"demo_parsed": true,
|
||||
"vac_present": false,
|
||||
"gameban_present": false,
|
||||
"tick_rate": 64.0,
|
||||
"stats": {
|
||||
"team_id": 2,
|
||||
"kills": 24,
|
||||
"deaths": 18,
|
||||
"assists": 6,
|
||||
"headshot": 12,
|
||||
"mvp": 3,
|
||||
"score": 56,
|
||||
"kast": 78,
|
||||
"rank_old": 18500,
|
||||
"rank_new": 18650,
|
||||
"dmg_enemy": 2450,
|
||||
"dmg_team": 120,
|
||||
"flash_assists": 4,
|
||||
"flash_duration_enemy": 15.6,
|
||||
"flash_total_enemy": 8,
|
||||
"ud_he": 450,
|
||||
"ud_flames": 230,
|
||||
"ud_flash": 5,
|
||||
"ud_smoke": 3,
|
||||
"avg_ping": 25.5,
|
||||
"color": "yellow"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
**Use Case**: Display player profile page with career stats and recent matches.
|
||||
|
||||
---
|
||||
|
||||
#### 2. Get Player Metadata
|
||||
|
||||
Retrieves lightweight player metadata (recent matches summary).
|
||||
|
||||
**Endpoint**: `GET /player/:id/meta`
|
||||
**Alternative**: `GET /player/:id/meta/:limit`
|
||||
|
||||
**Parameters**:
|
||||
- `id` (path, required): Steam ID
|
||||
- `limit` (path, optional): Number of recent matches to include (default: 10)
|
||||
|
||||
**Response** (200 OK):
|
||||
```json
|
||||
{
|
||||
"id": 76561198012345678,
|
||||
"name": "PlayerName",
|
||||
"avatar": "...",
|
||||
"recent_matches": 25,
|
||||
"last_match_date": "2024-11-01T18:45:00Z",
|
||||
"avg_kills": 21.3,
|
||||
"avg_deaths": 17.8,
|
||||
"avg_kast": 75.2,
|
||||
"win_rate": 56.5
|
||||
}
|
||||
```
|
||||
|
||||
**Use Case**: Quick player card/preview without full match data.
|
||||
|
||||
---
|
||||
|
||||
#### 3. Track Player
|
||||
|
||||
Adds a player to the tracking system for automatic match updates.
|
||||
|
||||
**Endpoint**: `POST /player/:id/track`
|
||||
|
||||
**Parameters**:
|
||||
- `id` (path, required): Steam ID
|
||||
|
||||
**Request Body**:
|
||||
```json
|
||||
{
|
||||
"auth_code": "XXXX-XXXXX-XXXX"
|
||||
}
|
||||
```
|
||||
|
||||
**Response** (200 OK):
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"message": "Player added to tracking queue"
|
||||
}
|
||||
```
|
||||
|
||||
**Use Case**: Allow users to track their own profile for automatic updates.
|
||||
|
||||
---
|
||||
|
||||
#### 4. Untrack Player
|
||||
|
||||
Removes a player from the tracking system.
|
||||
|
||||
**Endpoint**: `DELETE /player/:id/track`
|
||||
|
||||
**Parameters**:
|
||||
- `id` (path, required): Steam ID
|
||||
|
||||
**Response** (200 OK):
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"message": "Player removed from tracking"
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Match Endpoints
|
||||
|
||||
#### 5. Parse Match from Share Code
|
||||
|
||||
Triggers parsing of a CS:GO/CS2 match from a share code.
|
||||
|
||||
**Endpoint**: `GET /match/parse/:sharecode`
|
||||
|
||||
**Parameters**:
|
||||
- `sharecode` (path, required): CS:GO match share code (e.g., `CSGO-XXXXX-XXXXX-XXXXX-XXXXX-XXXXX`)
|
||||
|
||||
**Response** (200 OK):
|
||||
```json
|
||||
{
|
||||
"match_id": 3589487716842078322,
|
||||
"status": "parsing",
|
||||
"message": "Demo download and parsing initiated"
|
||||
}
|
||||
```
|
||||
|
||||
**Response** (202 Accepted):
|
||||
```json
|
||||
{
|
||||
"match_id": 3589487716842078322,
|
||||
"status": "queued",
|
||||
"estimated_time": 120
|
||||
}
|
||||
```
|
||||
|
||||
**Use Case**: User submits match share code for analysis.
|
||||
|
||||
---
|
||||
|
||||
#### 6. Get Match Details
|
||||
|
||||
Retrieves full match details including all player statistics.
|
||||
|
||||
**Endpoint**: `GET /match/:id`
|
||||
|
||||
**Parameters**:
|
||||
- `id` (path, required): Match ID (uint64)
|
||||
|
||||
**Response** (200 OK):
|
||||
```json
|
||||
{
|
||||
"match_id": 3589487716842078322,
|
||||
"share_code": "CSGO-XXXXX-XXXXX-XXXXX-XXXXX-XXXXX",
|
||||
"map": "de_inferno",
|
||||
"date": "2024-11-01T18:45:00Z",
|
||||
"score_team_a": 13,
|
||||
"score_team_b": 10,
|
||||
"duration": 2456,
|
||||
"match_result": 1,
|
||||
"max_rounds": 24,
|
||||
"demo_parsed": true,
|
||||
"vac_present": false,
|
||||
"gameban_present": false,
|
||||
"tick_rate": 64.0,
|
||||
"players": [
|
||||
{
|
||||
"id": 76561198012345678,
|
||||
"name": "Player1",
|
||||
"avatar": "...",
|
||||
"team_id": 2,
|
||||
"kills": 24,
|
||||
"deaths": 18,
|
||||
"assists": 6,
|
||||
"headshot": 12,
|
||||
"mvp": 3,
|
||||
"score": 56,
|
||||
"kast": 78,
|
||||
"rank_old": 18500,
|
||||
"rank_new": 18650,
|
||||
"dmg_enemy": 2450,
|
||||
"dmg_team": 120,
|
||||
"flash_assists": 4,
|
||||
"flash_duration_enemy": 15.6,
|
||||
"flash_duration_team": 2.3,
|
||||
"flash_duration_self": 1.2,
|
||||
"flash_total_enemy": 8,
|
||||
"flash_total_team": 1,
|
||||
"flash_total_self": 1,
|
||||
"ud_he": 450,
|
||||
"ud_flames": 230,
|
||||
"ud_flash": 5,
|
||||
"ud_smoke": 3,
|
||||
"ud_decoy": 0,
|
||||
"mk_2": 4,
|
||||
"mk_3": 2,
|
||||
"mk_4": 1,
|
||||
"mk_5": 0,
|
||||
"avg_ping": 25.5,
|
||||
"color": "yellow",
|
||||
"crosshair": "CSGO_XXXXXXXXXXXX"
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
**Use Case**: Display match overview page with scoreboard.
|
||||
|
||||
---
|
||||
|
||||
#### 7. Get Match Weapons
|
||||
|
||||
Retrieves weapon statistics for all players in a match.
|
||||
|
||||
**Endpoint**: `GET /match/:id/weapons`
|
||||
|
||||
**Parameters**:
|
||||
- `id` (path, required): Match ID
|
||||
|
||||
**Response** (200 OK):
|
||||
```json
|
||||
{
|
||||
"match_id": 3589487716842078322,
|
||||
"weapons": [
|
||||
{
|
||||
"player_id": 76561198012345678,
|
||||
"weapon_stats": [
|
||||
{
|
||||
"eq_type": 7,
|
||||
"weapon_name": "AK-47",
|
||||
"kills": 12,
|
||||
"damage": 1450,
|
||||
"hits": 48,
|
||||
"hit_groups": {
|
||||
"head": 8,
|
||||
"chest": 25,
|
||||
"stomach": 8,
|
||||
"left_arm": 3,
|
||||
"right_arm": 2,
|
||||
"left_leg": 1,
|
||||
"right_leg": 1
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
**Use Case**: Display detailed weapon performance page for match.
|
||||
|
||||
---
|
||||
|
||||
#### 8. Get Match Rounds
|
||||
|
||||
Retrieves round-by-round statistics for a match.
|
||||
|
||||
**Endpoint**: `GET /match/:id/rounds`
|
||||
|
||||
**Parameters**:
|
||||
- `id` (path, required): Match ID
|
||||
|
||||
**Response** (200 OK):
|
||||
```json
|
||||
{
|
||||
"match_id": 3589487716842078322,
|
||||
"rounds": [
|
||||
{
|
||||
"round": 1,
|
||||
"winner": 2,
|
||||
"win_reason": "elimination",
|
||||
"players": [
|
||||
{
|
||||
"player_id": 76561198012345678,
|
||||
"bank": 800,
|
||||
"equipment": 650,
|
||||
"spent": 650,
|
||||
"kills_in_round": 2,
|
||||
"damage_in_round": 234
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
**Use Case**: Display economy tab with round-by-round breakdown.
|
||||
|
||||
---
|
||||
|
||||
#### 9. Get Match Chat
|
||||
|
||||
Retrieves in-game chat messages from a match.
|
||||
|
||||
**Endpoint**: `GET /match/:id/chat`
|
||||
|
||||
**Parameters**:
|
||||
- `id` (path, required): Match ID
|
||||
|
||||
**Response** (200 OK):
|
||||
```json
|
||||
{
|
||||
"match_id": 3589487716842078322,
|
||||
"messages": [
|
||||
{
|
||||
"player_id": 76561198012345678,
|
||||
"player_name": "Player1",
|
||||
"message": "nice shot!",
|
||||
"tick": 15840,
|
||||
"round": 8,
|
||||
"all_chat": true,
|
||||
"timestamp": "2024-11-01T19:12:34Z"
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
**Use Case**: Display chat log page for match.
|
||||
|
||||
---
|
||||
|
||||
### Matches Listing
|
||||
|
||||
#### 10. Get Matches List
|
||||
|
||||
Retrieves a paginated list of matches.
|
||||
|
||||
**Endpoint**: `GET /matches`
|
||||
**Alternative**: `GET /matches/next/:time`
|
||||
|
||||
**Parameters**:
|
||||
- `time` (path, optional): Unix timestamp for pagination
|
||||
- Query parameters:
|
||||
- `limit` (optional): Number of matches to return (default: 50, max: 100)
|
||||
- `map` (optional): Filter by map name (e.g., `de_inferno`)
|
||||
- `player_id` (optional): Filter by player Steam ID
|
||||
|
||||
**Response** (200 OK):
|
||||
```json
|
||||
{
|
||||
"matches": [
|
||||
{
|
||||
"match_id": 3589487716842078322,
|
||||
"map": "de_inferno",
|
||||
"date": "2024-11-01T18:45:00Z",
|
||||
"score_team_a": 13,
|
||||
"score_team_b": 10,
|
||||
"duration": 2456,
|
||||
"demo_parsed": true,
|
||||
"player_count": 10
|
||||
}
|
||||
],
|
||||
"next_page_time": 1698871200,
|
||||
"has_more": true
|
||||
}
|
||||
```
|
||||
|
||||
**Use Case**: Display matches listing page with filters.
|
||||
|
||||
---
|
||||
|
||||
### Sitemap
|
||||
|
||||
#### 11. Get Sitemap Index
|
||||
|
||||
Returns XML sitemap index for SEO.
|
||||
|
||||
**Endpoint**: `GET /sitemap.xml`
|
||||
|
||||
**Response** (200 OK):
|
||||
```xml
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<sitemapindex xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
|
||||
<sitemap>
|
||||
<loc>https://csgow.tf/sitemap/1</loc>
|
||||
<lastmod>2024-11-04</lastmod>
|
||||
</sitemap>
|
||||
</sitemapindex>
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
#### 12. Get Sitemap Pages
|
||||
|
||||
Returns XML sitemap for specific page range.
|
||||
|
||||
**Endpoint**: `GET /sitemap/:id`
|
||||
|
||||
**Parameters**:
|
||||
- `id` (path, required): Sitemap page number
|
||||
|
||||
**Response** (200 OK):
|
||||
```xml
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
|
||||
<url>
|
||||
<loc>https://csgow.tf/match/3589487716842078322</loc>
|
||||
<lastmod>2024-11-01</lastmod>
|
||||
<changefreq>never</changefreq>
|
||||
<priority>0.8</priority>
|
||||
</url>
|
||||
</urlset>
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Data Models
|
||||
|
||||
### Match
|
||||
|
||||
```typescript
|
||||
interface Match {
|
||||
match_id: number; // uint64, unique identifier
|
||||
share_code: string; // CS:GO share code
|
||||
map: string; // Map name (e.g., "de_inferno")
|
||||
date: string; // ISO 8601 timestamp
|
||||
score_team_a: number; // Final score for team A (T/CT)
|
||||
score_team_b: number; // Final score for team B (CT/T)
|
||||
duration: number; // Match duration in seconds
|
||||
match_result: number; // 0 = tie, 1 = team_a win, 2 = team_b win
|
||||
max_rounds: number; // Max rounds (24 for MR12, 30 for MR15)
|
||||
demo_parsed: boolean; // Demo parsing status
|
||||
vac_present: boolean; // Any VAC bans in match
|
||||
gameban_present: boolean; // Any game bans in match
|
||||
tick_rate: number; // Server tick rate (64 or 128)
|
||||
players?: MatchPlayer[]; // Array of player stats
|
||||
}
|
||||
```
|
||||
|
||||
### Player
|
||||
|
||||
```typescript
|
||||
interface Player {
|
||||
id: number; // uint64, Steam ID
|
||||
name: string; // Steam display name
|
||||
avatar: string; // Avatar URL
|
||||
vanity_url?: string; // Custom Steam URL
|
||||
vanity_url_real?: string; // Actual vanity URL
|
||||
steam_updated: string; // Last Steam profile update
|
||||
profile_created?: string; // Steam account creation date
|
||||
wins?: number; // Total competitive wins
|
||||
looses?: number; // Total competitive losses (note: typo in backend)
|
||||
ties?: number; // Total ties
|
||||
vac_count?: number; // Number of VAC bans
|
||||
vac_date?: string; // Date of last VAC ban
|
||||
game_ban_count?: number; // Number of game bans
|
||||
game_ban_date?: string; // Date of last game ban
|
||||
oldest_sharecode_seen?: string; // Oldest match on record
|
||||
matches?: Match[]; // Recent matches
|
||||
}
|
||||
```
|
||||
|
||||
### MatchPlayer
|
||||
|
||||
```typescript
|
||||
interface MatchPlayer {
|
||||
match_stats: number; // Match ID reference
|
||||
player_stats: number; // Player ID reference
|
||||
team_id: number; // 2 = T, 3 = CT
|
||||
|
||||
// Performance
|
||||
kills: number;
|
||||
deaths: number;
|
||||
assists: number;
|
||||
headshot: number; // Headshot kills
|
||||
mvp: number; // MVP stars
|
||||
score: number; // In-game score
|
||||
kast: number; // KAST percentage (0-100)
|
||||
|
||||
// Rank
|
||||
rank_old?: number; // Rating before match (CS2: 0-30000)
|
||||
rank_new?: number; // Rating after match
|
||||
|
||||
// Damage
|
||||
dmg_enemy?: number; // Damage to enemies
|
||||
dmg_team?: number; // Team damage
|
||||
|
||||
// Multi-kills
|
||||
mk_2?: number; // Double kills
|
||||
mk_3?: number; // Triple kills
|
||||
mk_4?: number; // Quad kills
|
||||
mk_5?: number; // Ace (5k)
|
||||
|
||||
// Utility damage
|
||||
ud_he?: number; // HE grenade damage
|
||||
ud_flames?: number; // Molotov/Incendiary damage
|
||||
ud_flash?: number; // Flash grenades used
|
||||
ud_smoke?: number; // Smoke grenades used
|
||||
ud_decoy?: number; // Decoy grenades used
|
||||
|
||||
// Flash statistics
|
||||
flash_assists?: number; // Assists from flashes
|
||||
flash_duration_enemy?: number; // Total enemy blind time
|
||||
flash_duration_team?: number; // Total team blind time
|
||||
flash_duration_self?: number; // Self-flash time
|
||||
flash_total_enemy?: number; // Enemies flashed count
|
||||
flash_total_team?: number; // Teammates flashed count
|
||||
flash_total_self?: number; // Self-flash count
|
||||
|
||||
// Other
|
||||
crosshair?: string; // Crosshair settings
|
||||
color?: 'green' | 'yellow' | 'purple' | 'blue' | 'orange' | 'grey';
|
||||
avg_ping?: number; // Average ping
|
||||
}
|
||||
```
|
||||
|
||||
### RoundStats
|
||||
|
||||
```typescript
|
||||
interface RoundStats {
|
||||
round: number; // Round number (1-24 for MR12)
|
||||
bank: number; // Money at round start
|
||||
equipment: number; // Equipment value purchased
|
||||
spent: number; // Total money spent
|
||||
match_player_id: number; // Reference to MatchPlayer
|
||||
}
|
||||
```
|
||||
|
||||
### Weapon
|
||||
|
||||
```typescript
|
||||
interface Weapon {
|
||||
victim: number; // Player ID who was hit/killed
|
||||
dmg: number; // Damage dealt
|
||||
eq_type: number; // Weapon type ID
|
||||
hit_group: number; // Hit location (1=head, 2=chest, etc.)
|
||||
match_player_id: number; // Reference to MatchPlayer
|
||||
}
|
||||
```
|
||||
|
||||
### Message
|
||||
|
||||
```typescript
|
||||
interface Message {
|
||||
message: string; // Chat message content
|
||||
all_chat: boolean; // true = all chat, false = team chat
|
||||
tick: number; // Game tick when sent
|
||||
match_player_id: number; // Reference to MatchPlayer
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Integration Guide
|
||||
|
||||
### Frontend Setup
|
||||
|
||||
1. **Install HTTP Client**
|
||||
|
||||
```bash
|
||||
npm install axios
|
||||
# or use native fetch
|
||||
```
|
||||
|
||||
2. **Create API Client** (`src/lib/api/client.ts`)
|
||||
|
||||
```typescript
|
||||
import axios, { type AxiosInstance, type AxiosRequestConfig } from 'axios';
|
||||
|
||||
const API_BASE_URL = import.meta.env.VITE_API_BASE_URL || 'http://localhost:8000';
|
||||
const API_TIMEOUT = import.meta.env.VITE_API_TIMEOUT || 10000;
|
||||
|
||||
class APIClient {
|
||||
private client: AxiosInstance;
|
||||
|
||||
constructor() {
|
||||
this.client = axios.create({
|
||||
baseURL: API_BASE_URL,
|
||||
timeout: API_TIMEOUT,
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
});
|
||||
|
||||
// Response interceptor for error handling
|
||||
this.client.interceptors.response.use(
|
||||
(response) => response,
|
||||
(error) => {
|
||||
if (error.response) {
|
||||
// Server responded with error
|
||||
console.error('API Error:', error.response.status, error.response.data);
|
||||
} else if (error.request) {
|
||||
// Request made but no response
|
||||
console.error('Network Error:', error.message);
|
||||
}
|
||||
return Promise.reject(error);
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
async get<T>(url: string, config?: AxiosRequestConfig): Promise<T> {
|
||||
const response = await this.client.get<T>(url, config);
|
||||
return response.data;
|
||||
}
|
||||
|
||||
async post<T>(url: string, data?: any, config?: AxiosRequestConfig): Promise<T> {
|
||||
const response = await this.client.post<T>(url, data, config);
|
||||
return response.data;
|
||||
}
|
||||
|
||||
async delete<T>(url: string, config?: AxiosRequestConfig): Promise<T> {
|
||||
const response = await this.client.delete<T>(url, config);
|
||||
return response.data;
|
||||
}
|
||||
}
|
||||
|
||||
export const apiClient = new APIClient();
|
||||
```
|
||||
|
||||
3. **Create API Endpoints** (`src/lib/api/players.ts`)
|
||||
|
||||
```typescript
|
||||
import { apiClient } from './client';
|
||||
import type { Player, Match } from '$lib/types';
|
||||
|
||||
export const playersAPI = {
|
||||
async getPlayer(steamId: string | number, beforeTime?: number): Promise<Player> {
|
||||
const url = beforeTime
|
||||
? `/player/${steamId}/next/${beforeTime}`
|
||||
: `/player/${steamId}`;
|
||||
return apiClient.get<Player>(url);
|
||||
},
|
||||
|
||||
async getPlayerMeta(steamId: string | number, limit: number = 10) {
|
||||
return apiClient.get(`/player/${steamId}/meta/${limit}`);
|
||||
},
|
||||
|
||||
async trackPlayer(steamId: string | number, authCode: string) {
|
||||
return apiClient.post(`/player/${steamId}/track`, { auth_code: authCode });
|
||||
},
|
||||
|
||||
async untrackPlayer(steamId: string | number) {
|
||||
return apiClient.delete(`/player/${steamId}/track`);
|
||||
},
|
||||
};
|
||||
```
|
||||
|
||||
4. **Create API Endpoints** (`src/lib/api/matches.ts`)
|
||||
|
||||
```typescript
|
||||
import { apiClient } from './client';
|
||||
import type { Match } from '$lib/types';
|
||||
|
||||
export const matchesAPI = {
|
||||
async getMatch(matchId: string | number): Promise<Match> {
|
||||
return apiClient.get<Match>(`/match/${matchId}`);
|
||||
},
|
||||
|
||||
async getMatchWeapons(matchId: string | number) {
|
||||
return apiClient.get(`/match/${matchId}/weapons`);
|
||||
},
|
||||
|
||||
async getMatchRounds(matchId: string | number) {
|
||||
return apiClient.get(`/match/${matchId}/rounds`);
|
||||
},
|
||||
|
||||
async getMatchChat(matchId: string | number) {
|
||||
return apiClient.get(`/match/${matchId}/chat`);
|
||||
},
|
||||
|
||||
async parseMatch(shareCode: string) {
|
||||
return apiClient.get(`/match/parse/${shareCode}`);
|
||||
},
|
||||
|
||||
async getMatches(params?: {
|
||||
limit?: number;
|
||||
time?: number;
|
||||
map?: string;
|
||||
player_id?: number;
|
||||
}) {
|
||||
const queryString = params ? `?${new URLSearchParams(params as any).toString()}` : '';
|
||||
const url = params?.time
|
||||
? `/matches/next/${params.time}${queryString}`
|
||||
: `/matches${queryString}`;
|
||||
return apiClient.get(url);
|
||||
},
|
||||
};
|
||||
```
|
||||
|
||||
5. **Use in SvelteKit Routes** (`src/routes/player/[id]/+page.ts`)
|
||||
|
||||
```typescript
|
||||
import { playersAPI } from '$lib/api/players';
|
||||
import { error } from '@sveltejs/kit';
|
||||
import type { PageLoad } from './$types';
|
||||
|
||||
export const load: PageLoad = async ({ params }) => {
|
||||
try {
|
||||
const player = await playersAPI.getPlayer(params.id);
|
||||
return { player };
|
||||
} catch (err) {
|
||||
throw error(404, 'Player not found');
|
||||
}
|
||||
};
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Error Handling
|
||||
|
||||
### HTTP Status Codes
|
||||
|
||||
| Status | Meaning | Common Causes |
|
||||
|--------|---------|---------------|
|
||||
| 200 | Success | Request completed successfully |
|
||||
| 202 | Accepted | Request queued (async operations) |
|
||||
| 400 | Bad Request | Invalid parameters or malformed request |
|
||||
| 404 | Not Found | Player/match doesn't exist |
|
||||
| 429 | Too Many Requests | Rate limit exceeded |
|
||||
| 500 | Internal Server Error | Backend issue |
|
||||
| 503 | Service Unavailable | Backend down or maintenance |
|
||||
|
||||
### Error Response Format
|
||||
|
||||
```json
|
||||
{
|
||||
"error": "Match not found",
|
||||
"code": "MATCH_NOT_FOUND",
|
||||
"details": {
|
||||
"match_id": 3589487716842078322
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Retry Strategy
|
||||
|
||||
For transient errors (500, 503), implement exponential backoff:
|
||||
|
||||
```typescript
|
||||
async function retryRequest<T>(
|
||||
fn: () => Promise<T>,
|
||||
maxRetries: number = 3,
|
||||
baseDelay: number = 1000
|
||||
): Promise<T> {
|
||||
for (let i = 0; i < maxRetries; i++) {
|
||||
try {
|
||||
return await fn();
|
||||
} catch (error) {
|
||||
if (i === maxRetries - 1) throw error;
|
||||
|
||||
const delay = baseDelay * Math.pow(2, i);
|
||||
await new Promise(resolve => setTimeout(resolve, delay));
|
||||
}
|
||||
}
|
||||
throw new Error('Max retries exceeded');
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## CS2 Migration Notes
|
||||
|
||||
### Rank System Changes
|
||||
|
||||
**CS:GO** used 18 ranks (Silver I to Global Elite):
|
||||
- Values: 0-18
|
||||
|
||||
**CS2** uses Premier Rating:
|
||||
- Values: 0-30,000
|
||||
- No traditional ranks in Premier mode
|
||||
|
||||
**Backend Compatibility**:
|
||||
- `rank_old` and `rank_new` fields now store Premier rating (0-30000)
|
||||
- Frontend must detect game version and display accordingly
|
||||
|
||||
### MR12 vs MR15
|
||||
|
||||
**CS:GO**: MR15 (max 30 rounds)
|
||||
**CS2**: MR12 (max 24 rounds)
|
||||
|
||||
Check `max_rounds` field in Match data:
|
||||
- `24` = MR12 (CS2)
|
||||
- `30` = MR15 (CS:GO)
|
||||
|
||||
### Game Mode Detection
|
||||
|
||||
To determine if a match is CS:GO or CS2:
|
||||
1. Check `date` field (CS2 released Sept 2023)
|
||||
2. Check `max_rounds` (24 = likely CS2)
|
||||
3. Backend may add `game_version` field in future updates
|
||||
|
||||
---
|
||||
|
||||
## Rate Limiting
|
||||
|
||||
**Current Limits** (may vary by deployment):
|
||||
- **Steam API**: 1 request per second (backend handles this)
|
||||
- **Demo Parsing**: Max 6 concurrent parses
|
||||
- **Frontend API**: No explicit limit, but use reasonable request rates
|
||||
|
||||
**Best Practices**:
|
||||
- Implement client-side caching (5-15 minutes for match data)
|
||||
- Use debouncing for search inputs (300ms)
|
||||
- Batch requests when possible
|
||||
|
||||
---
|
||||
|
||||
## Caching Strategy
|
||||
|
||||
### Backend Caching (Redis)
|
||||
|
||||
The backend uses Redis for:
|
||||
- Steam API responses (7 days)
|
||||
- Match data (permanent until invalidated)
|
||||
- Player profiles (7 days)
|
||||
|
||||
### Frontend Caching Recommendations
|
||||
|
||||
```typescript
|
||||
// In-memory cache with TTL
|
||||
class DataCache<T> {
|
||||
private cache = new Map<string, { data: T; expires: number }>();
|
||||
|
||||
set(key: string, data: T, ttlMs: number) {
|
||||
this.cache.set(key, {
|
||||
data,
|
||||
expires: Date.now() + ttlMs,
|
||||
});
|
||||
}
|
||||
|
||||
get(key: string): T | null {
|
||||
const entry = this.cache.get(key);
|
||||
if (!entry) return null;
|
||||
if (Date.now() > entry.expires) {
|
||||
this.cache.delete(key);
|
||||
return null;
|
||||
}
|
||||
return entry.data;
|
||||
}
|
||||
}
|
||||
|
||||
// Usage
|
||||
const matchCache = new DataCache<Match>();
|
||||
const cachedMatch = matchCache.get(`match:${matchId}`);
|
||||
if (!cachedMatch) {
|
||||
const match = await matchesAPI.getMatch(matchId);
|
||||
matchCache.set(`match:${matchId}`, match, 15 * 60 * 1000); // 15 min
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Testing
|
||||
|
||||
### Mock Data
|
||||
|
||||
Use MSW (Mock Service Worker) for testing:
|
||||
|
||||
```typescript
|
||||
// src/mocks/handlers/matches.ts
|
||||
import { http, HttpResponse } from 'msw';
|
||||
|
||||
export const matchHandlers = [
|
||||
http.get('/match/:id', ({ params }) => {
|
||||
return HttpResponse.json({
|
||||
match_id: parseInt(params.id as string),
|
||||
map: 'de_inferno',
|
||||
date: '2024-11-01T18:45:00Z',
|
||||
score_team_a: 13,
|
||||
score_team_b: 10,
|
||||
// ... rest of mock data
|
||||
});
|
||||
}),
|
||||
];
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Swagger Documentation
|
||||
|
||||
The backend provides interactive API documentation at:
|
||||
|
||||
**URL**: `{API_BASE_URL}/api/swagger`
|
||||
|
||||
This Swagger UI allows you to:
|
||||
- Explore all endpoints
|
||||
- Test API calls directly
|
||||
- View request/response schemas
|
||||
- See authentication requirements
|
||||
|
||||
---
|
||||
|
||||
## Support & Updates
|
||||
|
||||
- **Backend Issues**: https://somegit.dev/CSGOWTF/csgowtfd/issues
|
||||
- **Frontend Issues**: https://somegit.dev/CSGOWTF/csgowtf/issues
|
||||
- **API Changes**: Watch the backend repository for updates
|
||||
|
||||
---
|
||||
|
||||
**Document Version**: 1.0
|
||||
**Last Updated**: 2025-11-04
|
||||
**Maintained By**: CSGOW.TF Development Team
|
||||
Reference in New Issue
Block a user