1919from uvicorn import Config
2020
2121from . import __version__
22+ from .models import (TopKill , ServerInfo , SquadronInfo , TopKDR , Trueskill , Highscore , UserEntry , MissilePK , PlayerStats ,
23+ CampaignCredits , TrapEntry , SquadronMember , SquadronCampaignCredit , LinkMeResponse )
2224
2325app : Optional [FastAPI ] = None
2426
@@ -38,20 +40,118 @@ def __init__(self, bot: DCSServerBot):
3840 cfg = self .locals [DEFAULT_TAG ]
3941 prefix = cfg .get ('prefix' , '' )
4042 self .router = APIRouter ()
41- self .router .add_api_route (prefix + "/servers" , self .servers , methods = ["GET" ])
42- self .router .add_api_route (prefix + "/squadrons" , self .squadrons , methods = ["GET" ])
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" ])
46- self .router .add_api_route (prefix + "/getuser" , self .getuser , methods = ["POST" ])
47- self .router .add_api_route (prefix + "/missilepk" , self .missilepk , methods = ["POST" ])
48- self .router .add_api_route (prefix + "/stats" , self .stats , methods = ["POST" ])
49- self .router .add_api_route (prefix + "/highscore" , self .highscore , methods = ["GET" ])
50- self .router .add_api_route (prefix + "/credits" , self .credits , methods = ["POST" ])
51- self .router .add_api_route (prefix + "/traps" , self .traps , methods = ["POST" ])
52- self .router .add_api_route (prefix + "/squadron_members" , self .squadron_members , methods = ["POST" ])
53- self .router .add_api_route (prefix + "/squadron_credits" , self .squadron_credits , methods = ["POST" ])
54- self .router .add_api_route (prefix + "/linkme" , self .linkme , methods = ["POST" ])
43+ self .router .add_api_route (
44+ prefix + "/servers" , self .servers ,
45+ methods = ["GET" ],
46+ response_model = list [ServerInfo ],
47+ description = "List all servers, the active mission (if any) and the active extensions" ,
48+ summary = "Server list" ,
49+ tags = ["Info" ]
50+ )
51+ self .router .add_api_route (
52+ prefix + "/squadrons" , self .squadrons ,
53+ methods = ["GET" ],
54+ response_model = list [SquadronInfo ],
55+ description = "List all squadrons and their roles" ,
56+ summary = "Squadron list" ,
57+ tags = ["Info" ]
58+ )
59+ self .router .add_api_route (
60+ prefix + "/squadron_members" , self .squadron_members ,
61+ methods = ["POST" ],
62+ response_model = list [SquadronMember ],
63+ description = "List squadron members" ,
64+ summary = "Squadron Members" ,
65+ tags = ["Info" ]
66+ )
67+ self .router .add_api_route (
68+ prefix + "/getuser" , self .getuser ,
69+ methods = ["POST" ],
70+ response_model = list [UserEntry ],
71+ description = "Get users by name" ,
72+ summary = "User list" ,
73+ tags = ["Info" ]
74+ )
75+ self .router .add_api_route (
76+ prefix + "/linkme" , self .linkme ,
77+ methods = ["POST" ],
78+ response_model = LinkMeResponse ,
79+ description = "Link your Discord account to your DCS account" ,
80+ summary = "Link Discord to DCS" ,
81+ tags = ["Info" ]
82+ )
83+ self .router .add_api_route (
84+ prefix + "/topkills" , self .topkills ,
85+ methods = ["GET" ],
86+ response_model = list [TopKill ],
87+ description = "Get AA top kills statistics for players" ,
88+ summary = "AA-Top Kills" ,
89+ tags = ["Statistics" ]
90+ )
91+ self .router .add_api_route (
92+ prefix + "/topkdr" , self .topkdr ,
93+ methods = ["GET" ],
94+ response_model = list [TopKDR ],
95+ description = "Get AA top KDR statistics for players" ,
96+ summary = "AA-Top KDR" ,
97+ tags = ["Statistics" ]
98+ )
99+ self .router .add_api_route (
100+ prefix + "/trueskill" , self .trueskill ,
101+ methods = ["GET" ],
102+ response_model = list [Trueskill ],
103+ description = "Get TrueSkill:tm: statistics for players" ,
104+ summary = "TrueSkill:tm:" ,
105+ tags = ["Statistics" ]
106+ )
107+ self .router .add_api_route (
108+ prefix + "/missilepk" , self .missilepk ,
109+ methods = ["POST" ],
110+ response_model = list [MissilePK ],
111+ description = "Get missile PK statistics for players" ,
112+ summary = "Missile PK" ,
113+ tags = ["Statistics" ]
114+ )
115+ self .router .add_api_route (
116+ prefix + "/stats" , self .stats ,
117+ methods = ["POST" ],
118+ response_model = PlayerStats ,
119+ description = "Get player statistics" ,
120+ summary = "Player Statistics" ,
121+ tags = ["Statistics" ]
122+ )
123+ self .router .add_api_route (
124+ prefix + "/highscore" , self .highscore ,
125+ methods = ["GET" ],
126+ response_model = Highscore ,
127+ description = "Get highscore statistics for players" ,
128+ summary = "Highscore" ,
129+ tags = ["Statistics" ]
130+ )
131+ self .router .add_api_route (
132+ prefix + "/traps" , self .traps ,
133+ methods = ["POST" ],
134+ response_model = list [TrapEntry ],
135+ description = "Get traps for players" ,
136+ summary = "Carrier Traps" ,
137+ tags = ["Statistics" ]
138+ )
139+ self .router .add_api_route (
140+ prefix + "/credits" , self .credits ,
141+ methods = ["POST" ],
142+ response_model = CampaignCredits ,
143+ description = "Get campaign credits for players" ,
144+ summary = "Campaign Credits" ,
145+ tags = ["Credits" ]
146+ )
147+ self .router .add_api_route (
148+ prefix + "/squadron_credits" , self .squadron_credits ,
149+ methods = ["POST" ],
150+ response_model = list [SquadronCampaignCredit ],
151+ description = "List squadron campaign credits" ,
152+ summary = "Squadron Credits" ,
153+ tags = ["Credits" ]
154+ )
55155
56156 # add debug endpoints
57157 if cfg .get ('debug' , False ):
@@ -149,7 +249,9 @@ async def servers(self):
149249 else :
150250 data ['extensions' ] = []
151251
152- servers .append (data )
252+ # validate the data against the schema and return it
253+ servers .append (ServerInfo .model_validate (data ))
254+
153255 return servers
154256
155257 async def squadrons (self ):
@@ -180,7 +282,7 @@ async def topkills(self, limit: int = Query(default=10)):
180282 AND hop_on > (now() AT TIME ZONE 'utc') - interval '1 month'
181283 GROUP BY 1, 2 ORDER BY 3 DESC LIMIT {limit}
182284 """ .format (limit = limit if limit else 10 ))
183- return await cursor .fetchall ()
285+ return [ TopKill . model_validate ( result ) for result in await cursor .fetchall ()]
184286
185287 async def topkdr (self , limit : int = Query (default = 10 )):
186288 async with self .apool .connection () as conn :
@@ -194,7 +296,7 @@ async def topkdr(self, limit: int = Query(default=10)):
194296 AND hop_on > (now() AT TIME ZONE 'utc') - interval '1 month'
195297 GROUP BY 1, 2 ORDER BY 5 DESC LIMIT {limit}
196298 """ .format (limit = limit if limit else 10 ))
197- return await cursor .fetchall ()
299+ return [ TopKDR . model_validate ( result ) for result in await cursor .fetchall ()]
198300
199301 async def trueskill (self , limit : int = Query (default = 10 )):
200302 async with self .apool .connection () as conn :
@@ -208,7 +310,7 @@ async def trueskill(self, limit: int = Query(default=10)):
208310 AND hop_on > (now() AT TIME ZONE 'utc') - interval '1 month'
209311 GROUP BY 1, 2, 5 ORDER BY 5 DESC LIMIT {limit}
210312 """ .format (limit = limit if limit else 10 ))
211- return await cursor .fetchall ()
313+ return [ Trueskill . model_validate ( result ) for result in await cursor .fetchall ()]
212314
213315 async def highscore (self , server_name : str = Query (default = None ), period : str = Query (default = 'all' ),
214316 limit : int = Query (default = 10 )):
@@ -218,7 +320,7 @@ async def highscore(self, server_name: str = Query(default=None), period: str =
218320 async with conn .cursor (row_factory = dict_row ) as cursor :
219321 sql = """
220322 SELECT p.name AS nick, DATE_TRUNC('second', p.last_seen) AS "date",
221- ROUND(SUM(EXTRACT(EPOCH FROM(COALESCE(s.hop_off, NOW() AT TIME ZONE 'UTC') - s.hop_on)))) AS playtime
323+ ROUND(SUM(EXTRACT(EPOCH FROM(COALESCE(s.hop_off, NOW() AT TIME ZONE 'UTC') - s.hop_on))))::INTEGER AS playtime
222324 FROM statistics s,
223325 players p,
224326 missions m
@@ -264,7 +366,7 @@ async def highscore(self, server_name: str = Query(default=None), period: str =
264366 await cursor .execute (sql , {"server_name" : server_name })
265367 highscore [kill_type ] = await cursor .fetchall ()
266368
267- return highscore
369+ return Highscore . model_validate ( highscore , by_alias = True )
268370
269371 async def getuser (self , nick : str = Form (default = None )):
270372 async with self .apool .connection () as conn :
@@ -277,7 +379,7 @@ async def getuser(self, nick: str = Form(default=None)):
277379 WHERE name ILIKE %s
278380 ORDER BY 2 DESC
279381 """ , ('%' + nick + '%' ,))
280- return await cursor .fetchall ()
382+ return [ UserEntry . model_validate ( result ) for result in await cursor .fetchall ()]
281383
282384 async def missilepk (self , nick : str = Form (default = None ), date : str = Form (default = None )):
283385 async with self .apool .connection () as conn :
@@ -355,7 +457,7 @@ async def stats(self, nick: str = Form(default=None), date: str = Form(default=N
355457 ORDER BY 2 DESC
356458 """ , (ucid ,))
357459 data ['kdrByModule' ] = await cursor .fetchall ()
358- return data
460+ return PlayerStats . model_validate ( data )
359461
360462 async def credits (self , nick : str = Form (default = None ), date : str = Form (default = None )):
361463 self .log .debug (f'Calling /credits with nick="{ nick } ", date="{ date } "' )
@@ -380,7 +482,7 @@ async def credits(self, nick: str = Form(default=None), date: str = Form(default
380482 WHERE (now() AT TIME ZONE 'utc') BETWEEN c.start AND COALESCE(c.stop, now() AT TIME ZONE 'utc')
381483 GROUP BY 1, 2
382484 """ , (ucid , ))
383- return await cursor .fetchone ()
485+ return CampaignCredits . model_validate ( await cursor .fetchone () )
384486
385487 async def traps (self , nick : str = Form (default = None ), date : str = Form (default = None ),
386488 limit : int = Form (default = 10 )):
@@ -406,7 +508,7 @@ async def traps(self, nick: str = Form(default=None), date: str = Form(default=N
406508 WHERE player_ucid = %s
407509 ORDER BY time DESC LIMIT { limit }
408510 """ , (ucid , ))
409- return await cursor .fetchall ()
511+ return [ TrapEntry . model_validate ( result ) for result in await cursor .fetchall ()]
410512
411513 async def squadron_members (self , name : str = Form (default = None )):
412514 self .log .debug (f'Calling /squadron_members with name="{ name } "' )
@@ -495,16 +597,16 @@ async def create_token() -> str:
495597 else :
496598 token = await create_token ()
497599 expiry_timestamp = (datetime .now () + timedelta (hours = 48 )).isoformat ()
498- # Set bit_field for new user
600+ # Set bit_field for a new user
499601 rc = 0 # Default bit_field for new user
500602 if force :
501603 rc |= BIT_FORCE_OPERATION # Set force operation flag
502604
503- return {
605+ return LinkMeResponse . model_validate ( {
504606 "token" : token ,
505607 "timestamp" : expiry_timestamp ,
506608 "rc" : rc
507- }
609+ })
508610
509611
510612async def setup (bot : DCSServerBot ):
0 commit comments