Skip to content

Commit 9397cca

Browse files
committed
CHANGES:
- RestAPI: Some mixed methods changed back to GET - RestAPI: /topkills, /topkdr, /trueskill will return "nick" and "date" now for players, no longer "fullNickname"!
1 parent 4e9eb6f commit 9397cca

File tree

3 files changed

+73
-30
lines changed

3 files changed

+73
-30
lines changed

plugins/restapi/README.md

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -16,8 +16,12 @@ DEFAULT:
1616
listen: 0.0.0.0 # the interface to bind the internal webserver to
1717
port: 9876 # the port the internal webserver is listening on
1818
prefix: /stats # use this prefix (optional)
19+
debug: false # Enable /openapi.json, /docs and /redoc endpoints to test the API (default: false)
1920
```
2021
22+
> [!WARNING]
23+
> Do NOT enable debug for normal operations, especially, if you expose the REST service to the outside world.
24+
2125
## RestAPI
2226
The following commands are available through the API
2327
@@ -26,10 +30,10 @@ The following commands are available through the API
2630
| /getuser | POST | nick: string | {"name": string, "date": date} | Return a list of players ordered by last seen that match this nick. |
2731
| /servers | GET | | [{"name": string, "status": string, "address": string, "password": string, "mission": {"name": string, "uptime": string, "date_time": string, "theatre", string, "blue_slots: int, "red_slots": int, "blue_slots_used": int, "red_slots_used": int, "restart_time": int}}] | Status for each server. |
2832
| /stats | POST | nick: string, date: date | {<br>"deaths": int,<br>"aakills": int,<br>"aakdr": float,<br>"lastSessionKills": int,<br>"lastSessionDeaths": int,<br>"killsbymodule": [<br>{"module": string, "kills": int}<br>],<br>"kdrByModule": [<br>{"module": string, "kdr": float}<br>]<br>} | Statistics of this player |
29-
| /highscore | GET,POST | [server_name: string], [period: string], [limit: int] | | Highscore output |
30-
| /topkills | GET,POST | [limit: int] | {"fullNickname": string, "AAkills": int, "deaths": int, "AAKDR": float} | Top x of players ordered by kills descending. |
31-
| /topkdr | GET,POST | [limit: int] | {"fullNickname": string, "AAkills": int, "deaths": int, "AAKDR": float} | Same as /topkills but ordered by AAKDR descending. |
32-
| /trueskill | GET,POST | [limit: int] | | Top x trueskill ratings. |
33+
| /highscore | GET | [server_name: string], [period: string], [limit: int] | | Highscore output |
34+
| /topkills | GET | [limit: int] | {"fullNickname": string, "AAkills": int, "deaths": int, "AAKDR": float} | Top x of players ordered by kills descending. |
35+
| /topkdr | GET | [limit: int] | {"fullNickname": string, "AAkills": int, "deaths": int, "AAKDR": float} | Same as /topkills but ordered by AAKDR descending. |
36+
| /trueskill | GET | [limit: int] | | Top x trueskill ratings. |
3337
| /missilepk | POST | nick: string, date: date | {"weapon": {"weapon-name": string, "pk": float}} | Probability of kill for each weapon per given user. |
3438
| /credits | POST | nick: string, date: date | [{"id": int, "name": string, "credits": float}] | Credits of a specific player. |
3539
| /traps | POST | nick: string, date: string, [limit: int] | | Lists the traps of that user. |
@@ -38,4 +42,4 @@ The following commands are available through the API
3842
| /linkme | POST | discord_id: string, force: bool | {"token": 1234, "timestamp": "2025-02-03 xx:xx:xx...", "rc": 0} | Same as /linkme in discord. Returns a new token that can be used in the in-game chat. |
3943
4044
> [!IMPORTANT]
41-
> It is advisable to use a reverse proxy like nginx and maybe an SSL encryption between your webserver and this endpoint.
45+
> It is advisable to use a reverse proxy like nginx and maybe SSL encryption between your webserver and this endpoint.

plugins/restapi/commands.py

Lines changed: 63 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -8,14 +8,18 @@
88

99
from core import Plugin, DEFAULT_TAG, Side, DataObjectFactory, utils, Status
1010
from datetime import datetime, timedelta, timezone
11-
from fastapi import FastAPI, APIRouter, Form
11+
from fastapi import FastAPI, APIRouter, Form, Query
12+
from fastapi.openapi.docs import get_swagger_ui_html, get_redoc_html
13+
from fastapi.openapi.utils import get_openapi
1214
from plugins.creditsystem.squadron import Squadron
1315
from plugins.userstats.filter import StatisticsFilter, PeriodFilter
1416
from psycopg.rows import dict_row
1517
from services.bot import DCSServerBot
1618
from typing import Optional, Any
1719
from uvicorn import Config
1820

21+
from . import __version__
22+
1923
app: Optional[FastAPI] = None
2024

2125

@@ -36,18 +40,52 @@ def __init__(self, bot: DCSServerBot):
3640
self.router = APIRouter()
3741
self.router.add_api_route(prefix + "/servers", self.servers, methods=["GET"])
3842
self.router.add_api_route(prefix + "/squadrons", self.squadrons, methods=["GET"])
39-
self.router.add_api_route(prefix + "/topkills", self.topkills, methods=["GET", "POST"])
40-
self.router.add_api_route(prefix + "/topkdr", self.topkdr, methods=["GET", "POST"])
41-
self.router.add_api_route(prefix + "/trueskill", self.trueskill, methods=["GET", "POST"])
43+
self.router.add_api_route(prefix + "/topkills", self.topkills, methods=["GET"])
44+
self.router.add_api_route(prefix + "/topkdr", self.topkdr, methods=["GET",])
45+
self.router.add_api_route(prefix + "/trueskill", self.trueskill, methods=["GET"])
4246
self.router.add_api_route(prefix + "/getuser", self.getuser, methods=["POST"])
4347
self.router.add_api_route(prefix + "/missilepk", self.missilepk, methods=["POST"])
4448
self.router.add_api_route(prefix + "/stats", self.stats, methods=["POST"])
45-
self.router.add_api_route(prefix + "/highscore", self.highscore, methods=["GET", "POST"])
49+
self.router.add_api_route(prefix + "/highscore", self.highscore, methods=["GET"])
4650
self.router.add_api_route(prefix + "/credits", self.credits, methods=["POST"])
4751
self.router.add_api_route(prefix + "/traps", self.traps, methods=["POST"])
4852
self.router.add_api_route(prefix + "/squadron_members", self.squadron_members, methods=["POST"])
4953
self.router.add_api_route(prefix + "/squadron_credits", self.squadron_credits, methods=["POST"])
5054
self.router.add_api_route(prefix + "/linkme", self.linkme, methods=["POST"])
55+
56+
# add debug endpoints
57+
if cfg.get('debug', False):
58+
self.log.warning("RestAPI: Debug is enabled, you might expose your API functions!")
59+
60+
# Enable OpenAPI schema
61+
app.add_api_route("/openapi.json",
62+
lambda: get_openapi(
63+
title="DCSServerBot REST API",
64+
version=__version__,
65+
description="REST functions to be used for DCSServerBot.",
66+
routes=app.routes,
67+
),
68+
include_in_schema=False
69+
)
70+
71+
# Enable Swagger UI
72+
app.add_api_route("/docs",
73+
lambda: get_swagger_ui_html(
74+
openapi_url="/openapi.json",
75+
title="DCSServerBot REST API - Swagger UI",
76+
),
77+
include_in_schema=False
78+
)
79+
80+
# Enable ReDoc
81+
app.add_api_route("/redoc",
82+
lambda: get_redoc_html(
83+
openapi_url="/openapi.json",
84+
title="DCSServerBot REST API - ReDoc",
85+
),
86+
include_in_schema=False
87+
)
88+
5189
self.app = app
5290
self.config = Config(app=self.app, host=cfg['listen'], port=cfg['port'], log_level=logging.ERROR,
5391
use_colors=False)
@@ -130,55 +168,56 @@ async def squadrons(self):
130168
})
131169
return squadrons
132170

133-
async def topkills(self, limit: int = Form(default=10)):
171+
async def topkills(self, limit: int = Query(default=10)):
134172
async with self.apool.connection() as conn:
135173
async with conn.cursor(row_factory=dict_row) as cursor:
136174
await cursor.execute("""
137-
SELECT p.name AS "fullNickname", SUM(pvp) AS "AAkills", SUM(deaths) AS "deaths",
138-
CASE WHEN SUM(deaths) = 0 THEN SUM(pvp) ELSE SUM(pvp)/SUM(deaths::DECIMAL) END AS "AAKDR"
175+
SELECT p.name AS "nick", DATE_TRUNC('second', p.last_seen) AS "date",
176+
SUM(pvp) AS "AAkills", SUM(deaths) AS "deaths",
177+
CASE WHEN SUM(deaths) = 0 THEN SUM(pvp) ELSE SUM(pvp)/SUM(deaths::DECIMAL) END AS "AAKDR"
139178
FROM statistics s, players p
140179
WHERE s.player_ucid = p.ucid
141180
AND hop_on > (now() AT TIME ZONE 'utc') - interval '1 month'
142-
GROUP BY 1 ORDER BY 2 DESC LIMIT {limit}
181+
GROUP BY 1, 2 ORDER BY 3 DESC LIMIT {limit}
143182
""".format(limit=limit if limit else 10))
144183
return await cursor.fetchall()
145184

146-
async def topkdr(self, limit: int = Form(default=10)):
185+
async def topkdr(self, limit: int = Query(default=10)):
147186
async with self.apool.connection() as conn:
148187
async with conn.cursor(row_factory=dict_row) as cursor:
149188
await cursor.execute("""
150-
SELECT p.name AS "fullNickname", SUM(pvp) AS "AAkills", SUM(deaths) AS "deaths",
151-
CASE WHEN SUM(deaths) = 0 THEN SUM(pvp) ELSE SUM(pvp)/SUM(deaths::DECIMAL) END AS "AAKDR"
189+
SELECT p.name AS "nick", DATE_TRUNC('second', p.last_seen) AS "date",
190+
SUM(pvp) AS "AAkills", SUM(deaths) AS "deaths",
191+
CASE WHEN SUM(deaths) = 0 THEN SUM(pvp) ELSE SUM(pvp)/SUM(deaths::DECIMAL) END AS "AAKDR"
152192
FROM statistics s, players p
153193
WHERE s.player_ucid = p.ucid
154194
AND hop_on > (now() AT TIME ZONE 'utc') - interval '1 month'
155-
GROUP BY 1 ORDER BY 4 DESC LIMIT {limit}
195+
GROUP BY 1, 2 ORDER BY 5 DESC LIMIT {limit}
156196
""".format(limit=limit if limit else 10))
157197
return await cursor.fetchall()
158198

159-
async def trueskill(self, limit: int = Form(default=10)):
199+
async def trueskill(self, limit: int = Query(default=10)):
160200
async with self.apool.connection() as conn:
161201
async with conn.cursor(row_factory=dict_row) as cursor:
162202
await cursor.execute("""
163-
SELECT
164-
p.name AS "fullNickname", SUM(pvp) AS "AAkills", SUM(deaths) AS "deaths",
165-
t.skill_mu AS "TrueSkill"
203+
SELECT p.name AS "nick", DATE_TRUNC('second', p.last_seen) AS "date",
204+
SUM(pvp) AS "AAkills", SUM(deaths) AS "deaths", t.skill_mu AS "TrueSkill"
166205
FROM statistics s, players p, trueskill t
167206
WHERE s.player_ucid = p.ucid
207+
AND t.player_ucid = p.ucid
168208
AND hop_on > (now() AT TIME ZONE 'utc') - interval '1 month'
169-
GROUP BY 1,4 ORDER BY 4 DESC LIMIT {limit}
209+
GROUP BY 1, 2, 5 ORDER BY 5 DESC LIMIT {limit}
170210
""".format(limit=limit if limit else 10))
171211
return await cursor.fetchall()
172212

173-
async def highscore(self, server_name: str = Form(default=None), period: str = Form(default='all'),
174-
limit: int = Form(default=10)):
213+
async def highscore(self, server_name: str = Query(default=None), period: str = Query(default='all'),
214+
limit: int = Query(default=10)):
175215
highscore = {}
176216
flt = StatisticsFilter.detect(self.bot, period) or PeriodFilter(period)
177217
async with self.apool.connection() as conn:
178218
async with conn.cursor(row_factory=dict_row) as cursor:
179219
sql = """
180-
SELECT p.name AS nick,
181-
DATE_TRUNC('second', p.last_seen) AS "date",
220+
SELECT p.name AS nick, DATE_TRUNC('second', p.last_seen) AS "date",
182221
ROUND(SUM(EXTRACT(EPOCH FROM(COALESCE(s.hop_off, NOW() AT TIME ZONE 'UTC') - s.hop_on)))) AS playtime
183222
FROM statistics s,
184223
players p,
@@ -209,8 +248,7 @@ async def highscore(self, server_name: str = Form(default=None), period: str = F
209248

210249
for kill_type in sql_parts.keys():
211250
sql = f"""
212-
SELECT p.name AS nick,
213-
DATE_TRUNC('second', p.last_seen) AS "date",
251+
SELECT p.name AS nick, DATE_TRUNC('second', p.last_seen) AS "date",
214252
{sql_parts[kill_type]} AS value
215253
FROM players p, statistics s, missions m
216254
WHERE s.player_ucid = p.ucid AND s.mission_id = m.id
@@ -472,7 +510,7 @@ async def create_token() -> str:
472510
async def setup(bot: DCSServerBot):
473511
global app
474512

475-
app = FastAPI(docs_url=None, redoc_url=None)
513+
app = FastAPI(docs_url=None, redoc_url=None, openapi_url=None)
476514
restapi = RestAPI(bot)
477515
await bot.add_cog(restapi)
478516
app.include_router(restapi.router)

plugins/restapi/schemas/restapi_schema.yaml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ mapping:
55
type: map
66
nullable: false
77
mapping:
8+
debug: {type: bool, nullable: false}
89
listen: {type: str, pattern: '\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}', nullable: false}
910
port: {type: int, range: {min: 80, max: 65535}, nullable: false}
1011
prefix: {type: str, nullable: false}

0 commit comments

Comments
 (0)