Skip to content

Latest commit

 

History

History
824 lines (673 loc) · 15.2 KB

File metadata and controls

824 lines (673 loc) · 15.2 KB

Nimaora - API Documentation

Complete API Reference for the Nimaora Platform

RESTful API + WebSocket Events


Table of Contents


Overview

The Nimaora API is a RESTful API that supports:

  • JSON request/response format
  • Session-based authentication via headers
  • Real-time updates via WebSocket
  • Rate limiting per endpoint

Authentication

Authentication is session-based using the X-Session-Id header.

Headers

Content-Type: application/json
Accept: application/json
X-Session-Id: your-unique-session-id

Session Flow

  1. Client generates a unique session ID
  2. Client joins a battle with username + session ID
  3. Server validates username uniqueness per battle
  4. Session ID is used for all subsequent requests
  5. Heartbeat keeps session alive

Base URL

Environment URL
Development http://localhost:8000/api
Staging https://api.staging.nimaora.dwin.codes
Production https://api.nimaora.dwin.codes

Rate Limiting

Endpoint Category Limit Window
Join/Leave 10 requests 1 minute
Answer Submission 30 requests 1 minute
Attack 20 requests 1 minute
Leaderboard 60 requests 1 minute
Heartbeat 60 requests 1 minute

Response headers include rate limit info:

X-RateLimit-Limit: 60
X-RateLimit-Remaining: 59
X-RateLimit-Reset: 1699999999

Endpoints

Health Check

GET /api/health

Check API health status.

Request:

GET /api/health

Response:

{
  "status": "ok",
  "timestamp": "2024-12-05T10:30:00.000Z",
  "services": {
    "database": "ok",
    "redis": "ok",
    "queue": "ok"
  }
}

Battles

GET /api/battles

List all active battles.

Request:

GET /api/battles

Response:

{
  "success": true,
  "data": [
    {
      "id": 1,
      "name": "Fall Programming Competition",
      "description": "Nimaora Programming Competition - Fall 2024",
      "starts_at": "2024-12-01T10:00:00.000Z",
      "ends_at": "2024-12-08T10:00:00.000Z",
      "is_active": true,
      "questions_per_participant": 15,
      "participant_count": 1250
    }
  ]
}

GET /api/battles/{id}

Get battle details.

Request:

GET /api/battles/1

Response:

{
  "success": true,
  "data": {
    "id": 1,
    "name": "Fall Programming Competition",
    "description": "Nimaora Programming Competition - Fall 2024",
    "starts_at": "2024-12-01T10:00:00.000Z",
    "ends_at": "2024-12-08T10:00:00.000Z",
    "is_active": true,
    "questions_per_participant": 15,
    "participant_count": 1250,
    "status": "running"
  }
}

POST /api/battles/{id}/join

Join a battle with a username.

Request:

POST /api/battles/1/join
Content-Type: application/json
X-Session-Id: abc123-unique-session

{
  "username": "player_one"
}

Success Response (200):

{
  "success": true,
  "data": {
    "id": 12345,
    "username": "player_one",
    "points": 0,
    "shields": 0,
    "arrows": 0,
    "questions": [
      {
        "id": 1,
        "title": "Sum of Numbers",
        "description": "Write a program that calculates the sum of numbers from 1 to n.",
        "points": 10,
        "solved": null
      },
      {
        "id": 5,
        "title": "Fibonacci Sequence",
        "description": "Write a program that prints the first n terms of the Fibonacci sequence.",
        "points": 25,
        "solved": null
      }
    ]
  }
}

Error Response (409 - Username Taken):

{
  "success": false,
  "message": "Another person is already competing with this username",
  "error_code": "USERNAME_IN_USE"
}

POST /api/battles/{id}/leave

Leave the current battle.

Request:

POST /api/battles/1/leave
X-Session-Id: abc123-unique-session

Response:

{
  "success": true,
  "message": "Successfully left the battle"
}

POST /api/battles/{id}/heartbeat

Keep session alive and get current stats.

Request:

POST /api/battles/1/heartbeat
X-Session-Id: abc123-unique-session

Response:

{
  "success": true,
  "data": {
    "points": 45,
    "shields": 2,
    "arrows": 3,
    "rank": 15,
    "online_participants": 1250
  }
}

Questions

GET /api/battles/{id}/questions

Get all questions assigned to the participant.

Request:

GET /api/battles/1/questions
X-Session-Id: abc123-unique-session

Response:

{
  "success": true,
  "data": [
    {
      "id": 1,
      "title": "Sum of Numbers",
      "description": "Write a program that calculates the sum of numbers from 1 to n.",
      "points": 10,
      "difficulty": "easy",
      "solved": null
    },
    {
      "id": 5,
      "title": "Fibonacci Sequence",
      "description": "Write a program that prints the first n terms of the Fibonacci sequence.",
      "points": 25,
      "difficulty": "medium",
      "solved": true
    },
    {
      "id": 12,
      "title": "Dijkstra's Algorithm",
      "description": "Implement Dijkstra's algorithm for finding the shortest path.",
      "points": 50,
      "difficulty": "hard",
      "solved": false
    }
  ]
}

GET /api/battles/{id}/questions/{questionId}

Get a specific question.

Request:

GET /api/battles/1/questions/5
X-Session-Id: abc123-unique-session

Response:

{
  "success": true,
  "data": {
    "id": 5,
    "title": "Fibonacci Sequence",
    "description": "Write a program that prints the first n terms of the Fibonacci sequence. The sequence is: 0, 1, 1, 2, 3, 5, 8, 13, ...",
    "points": 25,
    "difficulty": "medium",
    "solved": null
  }
}

POST /api/battles/{id}/questions/{questionId}/submit

Submit an answer for a question.

Request:

POST /api/battles/1/questions/5/submit
Content-Type: application/json
X-Session-Id: abc123-unique-session

{
  "is_correct": true
}

Success Response (200):

{
  "success": true,
  "data": {
    "is_correct": true,
    "points_earned": 25,
    "total_points": 70,
    "needs_reward_selection": true
  }
}

Error Response (400 - Already Answered):

{
  "success": false,
  "message": "You have already answered this question",
  "error_code": "ALREADY_ANSWERED"
}

POST /api/battles/{id}/reward

Select reward after correct answer (arrow or shield).

Request:

POST /api/battles/1/reward
Content-Type: application/json
X-Session-Id: abc123-unique-session

{
  "reward_type": "arrow"
}

Response:

{
  "success": true,
  "data": {
    "arrows": 4,
    "shields": 2
  }
}

Combat

POST /api/battles/{id}/attack

Attack another participant.

Request:

POST /api/battles/1/attack
Content-Type: application/json
X-Session-Id: abc123-unique-session

{
  "target_username": "player_two"
}

Success Response (200 - Attack Hit):

{
  "success": true,
  "data": {
    "blocked": false,
    "points_deducted": 1,
    "attacker_arrows": 3,
    "target_new_points": 44
  }
}

Success Response (200 - Attack Blocked):

{
  "success": true,
  "data": {
    "blocked": true,
    "points_deducted": 0,
    "attacker_arrows": 3,
    "message": "Target blocked the attack with a shield"
  }
}

Error Response (400 - No Arrows):

{
  "success": false,
  "message": "You have no arrows to attack",
  "error_code": "NO_ARROWS"
}

Error Response (400 - Invalid Target):

{
  "success": false,
  "message": "This player has no points to lose",
  "error_code": "INVALID_TARGET"
}

Leaderboard

GET /api/battles/{id}/leaderboard

Get the battle leaderboard.

Request:

GET /api/battles/1/leaderboard?limit=100&offset=0
X-Session-Id: abc123-unique-session

Query Parameters:

Parameter Type Default Description
limit integer 100 Number of entries to return
offset integer 0 Offset for pagination

Response:

{
  "success": true,
  "data": {
    "leaderboard": [
      {
        "rank": 1,
        "username": "top_coder",
        "points": 450,
        "shields": 5,
        "arrows": 2,
        "is_online": true
      },
      {
        "rank": 2,
        "username": "algo_master",
        "points": 425,
        "shields": 3,
        "arrows": 4,
        "is_online": true
      },
      {
        "rank": 3,
        "username": "player_one",
        "points": 380,
        "shields": 2,
        "arrows": 3,
        "is_online": true
      }
    ],
    "total_participants": 1250,
    "my_rank": 3,
    "updated_at": "2024-12-05T10:30:00.000Z"
  }
}

GET /api/battles/{id}/leaderboard/me

Get current user's position and surrounding ranks.

Request:

GET /api/battles/1/leaderboard/me
X-Session-Id: abc123-unique-session

Response:

{
  "success": true,
  "data": {
    "my_rank": 15,
    "my_points": 120,
    "surrounding": [
      { "rank": 13, "username": "user_13", "points": 125 },
      { "rank": 14, "username": "user_14", "points": 122 },
      { "rank": 15, "username": "player_one", "points": 120, "is_me": true },
      { "rank": 16, "username": "user_16", "points": 118 },
      { "rank": 17, "username": "user_17", "points": 115 }
    ]
  }
}

WebSocket Events

Connection

Connect to WebSocket server using Laravel Echo:

import Echo from 'laravel-echo';
import Pusher from 'pusher-js';

window.Pusher = Pusher;

const echo = new Echo({
  broadcaster: 'reverb',
  key: process.env.NEXT_PUBLIC_REVERB_APP_KEY,
  wsHost: process.env.NEXT_PUBLIC_REVERB_HOST,
  wsPort: process.env.NEXT_PUBLIC_REVERB_PORT,
  wssPort: process.env.NEXT_PUBLIC_REVERB_PORT,
  forceTLS: true,
  enabledTransports: ['ws', 'wss'],
});

Channels

Channel Type Description
battle.{id} Public Leaderboard updates
presence-battle.{id} Presence Online participants
private-participant.{id} Private Attack notifications

Events

leaderboard.updated

Broadcast when leaderboard changes.

Channel: battle.{id}

Payload:

{
  "event": "leaderboard.updated",
  "data": {
    "leaderboard": [
      {
        "rank": 1,
        "username": "top_coder",
        "points": 450,
        "shields": 5,
        "arrows": 2,
        "is_online": true
      }
    ],
    "updated_at": "2024-12-05T10:30:00.000Z"
  }
}

attack.received

Sent to a specific participant when they are attacked.

Channel: private-participant.{id}

Payload:

{
  "event": "attack.received",
  "data": {
    "attacker": "enemy_player",
    "blocked": false,
    "points_lost": 1,
    "timestamp": "2024-12-05T10:30:00.000Z"
  }
}

participant.updated

Broadcast when participant stats change.

Channel: battle.{id}

Payload:

{
  "event": "participant.updated",
  "data": {
    "username": "player_one",
    "points": 125,
    "shields": 3,
    "arrows": 2,
    "rank": 14
  }
}

Subscribing to Events

// Subscribe to leaderboard updates
echo.channel(`battle.${battleId}`)
  .listen('.leaderboard.updated', (data) => {
    console.log('Leaderboard updated:', data.leaderboard);
  });

// Subscribe to attack notifications
echo.private(`participant.${participantId}`)
  .listen('.attack.received', (data) => {
    console.log('You were attacked by:', data.attacker);
    if (data.blocked) {
      console.log('Attack blocked by shield!');
    } else {
      console.log('You lost', data.points_lost, 'points');
    }
  });

// Subscribe to presence channel
echo.join(`presence-battle.${battleId}`)
  .here((users) => {
    console.log('Online users:', users);
  })
  .joining((user) => {
    console.log('User joined:', user);
  })
  .leaving((user) => {
    console.log('User left:', user);
  });

Error Handling

Error Response Format

{
  "success": false,
  "message": "Human-readable error message",
  "error_code": "MACHINE_READABLE_CODE",
  "errors": {
    "field_name": ["Validation error message"]
  }
}

HTTP Status Codes

Code Description
200 Success
201 Created
400 Bad Request
401 Unauthorized
403 Forbidden
404 Not Found
409 Conflict
422 Validation Error
429 Too Many Requests
500 Server Error

Error Codes

Code Description
USERNAME_IN_USE Another session is using this username
BATTLE_NOT_ACTIVE Battle is not currently active
NOT_PARTICIPANT User is not a participant in this battle
ALREADY_ANSWERED Question has already been answered
INVALID_REWARD Invalid reward type
NO_ARROWS No arrows available to attack
INVALID_TARGET Target cannot be attacked
SELF_ATTACK Cannot attack yourself
RATE_LIMITED Too many requests

Examples

Complete Battle Flow

const sessionId = generateUUID();
const headers = {
  'Content-Type': 'application/json',
  'X-Session-Id': sessionId,
};

// 1. Join battle
const joinResponse = await fetch('/api/battles/1/join', {
  method: 'POST',
  headers,
  body: JSON.stringify({ username: 'my_username' }),
});
const { data: participant } = await joinResponse.json();

// 2. Answer a question
const answerResponse = await fetch('/api/battles/1/questions/5/submit', {
  method: 'POST',
  headers,
  body: JSON.stringify({ is_correct: true }),
});
const { data: result } = await answerResponse.json();

// 3. Select reward
if (result.needs_reward_selection) {
  await fetch('/api/battles/1/reward', {
    method: 'POST',
    headers,
    body: JSON.stringify({ reward_type: 'arrow' }),
  });
}

// 4. Attack another player
const attackResponse = await fetch('/api/battles/1/attack', {
  method: 'POST',
  headers,
  body: JSON.stringify({ target_username: 'other_player' }),
});

// 5. Keep session alive (call every 30 seconds)
setInterval(async () => {
  await fetch('/api/battles/1/heartbeat', {
    method: 'POST',
    headers,
  });
}, 30000);

cURL Examples

# Join battle
curl -X POST https://api.nimaora.dwin.codes/api/battles/1/join \
  -H "Content-Type: application/json" \
  -H "X-Session-Id: my-session-123" \
  -d '{"username": "test_user"}'

# Submit answer
curl -X POST https://api.nimaora.dwin.codes/api/battles/1/questions/5/submit \
  -H "Content-Type: application/json" \
  -H "X-Session-Id: my-session-123" \
  -d '{"is_correct": true}'

# Get leaderboard
curl -X GET "https://api.nimaora.dwin.codes/api/battles/1/leaderboard?limit=50" \
  -H "X-Session-Id: my-session-123"

# Attack
curl -X POST https://api.nimaora.dwin.codes/api/battles/1/attack \
  -H "Content-Type: application/json" \
  -H "X-Session-Id: my-session-123" \
  -d '{"target_username": "enemy"}'

RESTful API | WebSocket Events | Real-time Updates