]> de.git.xonotic.org Git - xonotic/xonstat.git/blob - xonstat/views/submission.py
Fix issue where servers on the same host resolve to the only one server (the one...
[xonotic/xonstat.git] / xonstat / views / submission.py
1 import datetime\r
2 import logging\r
3 import re\r
4 import time\r
5 from pyramid.config import get_current_registry\r
6 from pyramid.response import Response\r
7 from sqlalchemy.orm.exc import MultipleResultsFound, NoResultFound\r
8 from xonstat.d0_blind_id import d0_blind_id_verify\r
9 from xonstat.models import *\r
10 from xonstat.util import strip_colors\r
11 \r
12 log = logging.getLogger(__name__)\r
13 \r
14 def is_supported_gametype(gametype):\r
15     """Whether a gametype is supported or not"""\r
16     flg_supported = True\r
17 \r
18     if gametype == 'cts' or gametype == 'ca' or gametype == 'lms':\r
19         flg_supported = False\r
20 \r
21     return flg_supported\r
22 \r
23 \r
24 def verify_request(request):\r
25     (idfp, status) = d0_blind_id_verify(\r
26             sig=request.headers['X-D0-Blind-Id-Detached-Signature'],\r
27             querystring='',\r
28             postdata=request.body)\r
29 \r
30     log.debug('\nidfp: {0}\nstatus: {1}'.format(idfp, status))\r
31 \r
32     return (idfp, status)\r
33 \r
34 \r
35 def has_minimum_real_players(player_events):\r
36     """\r
37     Determines if the collection of player events has enough "real" players\r
38     to store in the database. The minimum setting comes from the config file\r
39     under the setting xonstat.minimum_real_players.\r
40     """\r
41     flg_has_min_real_players = True\r
42 \r
43     settings = get_current_registry().settings\r
44     try: \r
45         minimum_required_players = int(\r
46                 settings['xonstat.minimum_required_players'])\r
47     except:\r
48         minimum_required_players = 2\r
49 \r
50     real_players = 0\r
51     for events in player_events:\r
52         if is_real_player(events):\r
53             real_players += 1\r
54 \r
55     #TODO: put this into a config setting in the ini file?\r
56     if real_players < minimum_required_players:\r
57         flg_has_min_real_players = False\r
58 \r
59     return flg_has_min_real_players\r
60 \r
61 \r
62 def has_required_metadata(metadata):\r
63     """\r
64     Determines if a give set of metadata has enough data to create a game,\r
65     server, and map with.\r
66     """\r
67     flg_has_req_metadata = True\r
68 \r
69     if 'T' not in metadata or\\r
70         'G' not in metadata or\\r
71         'M' not in metadata or\\r
72         'S' not in metadata:\r
73             flg_has_req_metadata = False\r
74 \r
75     return flg_has_req_metadata\r
76 \r
77     \r
78 def is_real_player(events):\r
79     """\r
80     Determines if a given set of player events correspond with a player who\r
81     \r
82     1) is not a bot (P event does not look like a bot)\r
83     2) played in the game (matches 1)\r
84     3) was present at the end of the game (scoreboardvalid 1)\r
85 \r
86     Returns True if the player meets the above conditions, and false otherwise.\r
87     """\r
88     flg_is_real = False\r
89 \r
90     if not events['P'].startswith('bot'):\r
91         # removing 'joins' here due to bug, but it should be here\r
92         if 'matches' in events and 'scoreboardvalid' in events:\r
93             flg_is_real = True\r
94 \r
95     return flg_is_real\r
96 \r
97 \r
98 def register_new_nick(session, player, new_nick):\r
99     """\r
100     Change the player record's nick to the newly found nick. Store the old\r
101     nick in the player_nicks table for that player.\r
102 \r
103     session - SQLAlchemy database session factory\r
104     player - player record whose nick is changing\r
105     new_nick - the new nickname\r
106     """\r
107     # see if that nick already exists\r
108     stripped_nick = strip_colors(player.nick)\r
109     try:\r
110         player_nick = session.query(PlayerNick).filter_by(\r
111                 player_id=player.player_id, stripped_nick=stripped_nick).one()\r
112     except NoResultFound, e:\r
113             # player_id/stripped_nick not found, create one\r
114         # but we don't store "Anonymous Player #N"\r
115         if not re.search('^Anonymous Player #\d+$', player.nick):\r
116             player_nick = PlayerNick()\r
117             player_nick.player_id = player.player_id\r
118             player_nick.stripped_nick = stripped_nick\r
119             player_nick.nick = player.nick\r
120             session.add(player_nick)\r
121 \r
122     # We change to the new nick regardless\r
123     player.nick = new_nick\r
124     session.add(player)\r
125 \r
126 \r
127 def get_or_create_server(session=None, name=None, hashkey=None):\r
128     """\r
129     Find a server by name or create one if not found. Parameters:\r
130 \r
131     session - SQLAlchemy database session factory\r
132     name - server name of the server to be found or created\r
133     hashkey - server hashkey\r
134     """\r
135     try:\r
136         # find one by that name, if it exists\r
137         server = session.query(Server).filter_by(name=name).one()\r
138         log.debug("Found existing server {0}".format(server.server_id))\r
139 \r
140     except MultipleResultsFound, e:\r
141         # multiple found, so also filter by hashkey\r
142         server = session.query(Server).filter_by(name=name).\\r
143                 filter_by(hashkey=hashkey).one()\r
144         log.debug("Found existing server {0}".format(server.server_id))\r
145 \r
146     except NoResultFound, e:\r
147         # not found, create one\r
148         server = Server(name=name, hashkey=hashkey)\r
149         session.add(server)\r
150         session.flush()\r
151         log.debug("Created server {0} with hashkey {1}".format(\r
152             server.server_id, server.hashkey))\r
153 \r
154     return server\r
155 \r
156 \r
157 def get_or_create_map(session=None, name=None):\r
158     """\r
159     Find a map by name or create one if not found. Parameters:\r
160 \r
161     session - SQLAlchemy database session factory\r
162     name - map name of the map to be found or created\r
163     """\r
164     try:\r
165         # find one by the name, if it exists\r
166         gmap = session.query(Map).filter_by(name=name).one()\r
167         log.debug("Found map id {0}: {1}".format(gmap.map_id, \r
168             gmap.name))\r
169     except NoResultFound, e:\r
170         gmap = Map(name=name)\r
171         session.add(gmap)\r
172         session.flush()\r
173         log.debug("Created map id {0}: {1}".format(gmap.map_id,\r
174             gmap.name))\r
175     except MultipleResultsFound, e:\r
176         # multiple found, so use the first one but warn\r
177         log.debug(e)\r
178         gmaps = session.query(Map).filter_by(name=name).order_by(\r
179                 Map.map_id).all()\r
180         gmap = gmaps[0]\r
181         log.debug("Found map id {0}: {1} but found \\r
182                 multiple".format(gmap.map_id, gmap.name))\r
183 \r
184     return gmap\r
185 \r
186 \r
187 def create_game(session=None, start_dt=None, game_type_cd=None, \r
188         server_id=None, map_id=None, winner=None):\r
189     """\r
190     Creates a game. Parameters:\r
191 \r
192     session - SQLAlchemy database session factory\r
193     start_dt - when the game started (datetime object)\r
194     game_type_cd - the game type of the game being played\r
195     server_id - server identifier of the server hosting the game\r
196     map_id - map on which the game was played\r
197     winner - the team id of the team that won\r
198     """\r
199 \r
200     game = Game(start_dt=start_dt, game_type_cd=game_type_cd,\r
201                 server_id=server_id, map_id=map_id, winner=winner)\r
202     session.add(game)\r
203     session.flush()\r
204     log.debug("Created game id {0} on server {1}, map {2} at \\r
205             {3}".format(game.game_id, \r
206                 server_id, map_id, start_dt))\r
207 \r
208     return game\r
209 \r
210 \r
211 def get_or_create_player(session=None, hashkey=None, nick=None):\r
212     """\r
213     Finds a player by hashkey or creates a new one (along with a\r
214     corresponding hashkey entry. Parameters:\r
215 \r
216     session - SQLAlchemy database session factory\r
217     hashkey - hashkey of the player to be found or created\r
218     nick - nick of the player (in case of a first time create)\r
219     """\r
220     # if we have a bot\r
221     if re.search('^bot#\d+$', hashkey):\r
222         player = session.query(Player).filter_by(player_id=1).one()\r
223     # if we have an untracked player\r
224     elif re.search('^player#\d+$', hashkey):\r
225         player = session.query(Player).filter_by(player_id=2).one()\r
226     # else it is a tracked player\r
227     else:\r
228         # see if the player is already in the database\r
229         # if not, create one and the hashkey along with it\r
230         try:\r
231             hashkey = session.query(Hashkey).filter_by(\r
232                     hashkey=hashkey).one()\r
233             player = session.query(Player).filter_by(\r
234                     player_id=hashkey.player_id).one()\r
235             log.debug("Found existing player {0} with hashkey {1}".format(\r
236                 player.player_id, hashkey.hashkey))\r
237         except:\r
238             player = Player()\r
239             session.add(player)\r
240             session.flush()\r
241 \r
242             # if nick is given to us, use it. If not, use "Anonymous Player"\r
243             # with a suffix added for uniqueness.\r
244             if nick:\r
245                 player.nick = nick[:128]\r
246             else:\r
247                 player.nick = "Anonymous Player #{0}".format(player.player_id)\r
248 \r
249             hashkey = Hashkey(player_id=player.player_id, hashkey=hashkey)\r
250             session.add(hashkey)\r
251             log.debug("Created player {0} ({2}) with hashkey {1}".format(\r
252                 player.player_id, hashkey.hashkey, player.nick.encode('utf-8')))\r
253 \r
254     return player\r
255 \r
256 def create_player_game_stat(session=None, player=None, \r
257         game=None, player_events=None):\r
258     """\r
259     Creates game statistics for a given player in a given game. Parameters:\r
260 \r
261     session - SQLAlchemy session factory\r
262     player - Player record of the player who owns the stats\r
263     game - Game record for the game to which the stats pertain\r
264     player_events - dictionary for the actual stats that need to be transformed\r
265     """\r
266 \r
267     # in here setup default values (e.g. if game type is CTF then\r
268     # set kills=0, score=0, captures=0, pickups=0, fckills=0, etc\r
269     # TODO: use game's create date here instead of now()\r
270     pgstat = PlayerGameStat(create_dt=datetime.datetime.now())\r
271 \r
272     # set player id from player record\r
273     pgstat.player_id = player.player_id\r
274 \r
275     #set game id from game record\r
276     pgstat.game_id = game.game_id\r
277 \r
278     # all games have a score\r
279     pgstat.score = 0\r
280 \r
281     if game.game_type_cd == 'dm':\r
282         pgstat.kills = 0\r
283         pgstat.deaths = 0\r
284         pgstat.suicides = 0\r
285     elif game.game_type_cd == 'ctf':\r
286         pgstat.kills = 0\r
287         pgstat.captures = 0\r
288         pgstat.pickups = 0\r
289         pgstat.drops = 0\r
290         pgstat.returns = 0\r
291         pgstat.carrier_frags = 0\r
292 \r
293     for (key,value) in player_events.items():\r
294         if key == 'n': pgstat.nick = value[:128]\r
295         if key == 't': pgstat.team = value\r
296         if key == 'rank': pgstat.rank = value\r
297         if key == 'alivetime': \r
298             pgstat.alivetime = datetime.timedelta(seconds=int(round(float(value))))\r
299         if key == 'scoreboard-drops': pgstat.drops = value\r
300         if key == 'scoreboard-returns': pgstat.returns = value\r
301         if key == 'scoreboard-fckills': pgstat.carrier_frags = value\r
302         if key == 'scoreboard-pickups': pgstat.pickups = value\r
303         if key == 'scoreboard-caps': pgstat.captures = value\r
304         if key == 'scoreboard-score': pgstat.score = value\r
305         if key == 'scoreboard-deaths': pgstat.deaths = value\r
306         if key == 'scoreboard-kills': pgstat.kills = value\r
307         if key == 'scoreboard-suicides': pgstat.suicides = value\r
308 \r
309     # check to see if we had a name, and if \r
310     # not use the name from the player id\r
311     if pgstat.nick == None:\r
312         pgstat.nick = player.nick\r
313 \r
314     # if the nick we end up with is different from the one in the\r
315     # player record, change the nick to reflect the new value\r
316     if pgstat.nick != player.nick and player.player_id > 1:\r
317         register_new_nick(session, player, pgstat.nick)\r
318 \r
319     # if the player is ranked #1 and it is a team game, set the game's winner\r
320     # to be the team of that player\r
321     # FIXME: this is a hack, should be using the 'W' field (not present)\r
322     if pgstat.rank == '1' and pgstat.team:\r
323         game.winner = pgstat.team\r
324         session.add(game)\r
325 \r
326     session.add(pgstat)\r
327     session.flush()\r
328 \r
329     return pgstat\r
330 \r
331 \r
332 def create_player_weapon_stats(session=None, player=None, \r
333         game=None, pgstat=None, player_events=None):\r
334     """\r
335     Creates accuracy records for each weapon used by a given player in a\r
336     given game. Parameters:\r
337 \r
338     session - SQLAlchemy session factory object\r
339     player - Player record who owns the weapon stats\r
340     game - Game record in which the stats were created\r
341     pgstat - Corresponding PlayerGameStat record for these weapon stats\r
342     player_events - dictionary containing the raw weapon values that need to be\r
343         transformed\r
344     """\r
345     pwstats = []\r
346 \r
347     for (key,value) in player_events.items():\r
348         matched = re.search("acc-(.*?)-cnt-fired", key)\r
349         if matched:\r
350             weapon_cd = matched.group(1)\r
351             pwstat = PlayerWeaponStat()\r
352             pwstat.player_id = player.player_id\r
353             pwstat.game_id = game.game_id\r
354             pwstat.player_game_stat_id = pgstat.player_game_stat_id\r
355             pwstat.weapon_cd = weapon_cd\r
356 \r
357             if 'n' in player_events:\r
358                 pwstat.nick = player_events['n']\r
359             else:\r
360                 pwstat.nick = player_events['P']\r
361 \r
362             if 'acc-' + weapon_cd + '-cnt-fired' in player_events:\r
363                 pwstat.fired = int(round(float(\r
364                         player_events['acc-' + weapon_cd + '-cnt-fired'])))\r
365             if 'acc-' + weapon_cd + '-fired' in player_events:\r
366                 pwstat.max = int(round(float(\r
367                         player_events['acc-' + weapon_cd + '-fired'])))\r
368             if 'acc-' + weapon_cd + '-cnt-hit' in player_events:\r
369                 pwstat.hit = int(round(float(\r
370                         player_events['acc-' + weapon_cd + '-cnt-hit'])))\r
371             if 'acc-' + weapon_cd + '-hit' in player_events:\r
372                 pwstat.actual = int(round(float(\r
373                         player_events['acc-' + weapon_cd + '-hit'])))\r
374             if 'acc-' + weapon_cd + '-frags' in player_events:\r
375                 pwstat.frags = int(round(float(\r
376                         player_events['acc-' + weapon_cd + '-frags'])))\r
377 \r
378             session.add(pwstat)\r
379             pwstats.append(pwstat)\r
380 \r
381     return pwstats\r
382 \r
383 \r
384 def parse_body(request):\r
385     """\r
386     Parses the POST request body for a stats submission\r
387     """\r
388     # storage vars for the request body\r
389     game_meta = {}\r
390     player_events = {}\r
391     current_team = None\r
392     players = []\r
393     \r
394     log.debug("----- BEGIN REQUEST BODY -----")\r
395     log.debug(request.body)\r
396     log.debug("----- END REQUEST BODY -----")\r
397 \r
398     for line in request.body.split('\n'):\r
399         try:\r
400             (key, value) = line.strip().split(' ', 1)\r
401 \r
402             # Server (S) and Nick (n) fields can have international characters.\r
403             # We encode these as UTF-8.\r
404             if key in 'S' 'n':\r
405                 value = unicode(value, 'utf-8')\r
406     \r
407             if key in 'V' 'T' 'G' 'M' 'S' 'C' 'R' 'W':\r
408                 game_meta[key] = value\r
409 \r
410             if key == 'P':\r
411                 # if we were working on a player record already, append\r
412                 # it and work on a new one (only set team info)\r
413                 if len(player_events) != 0:\r
414                     players.append(player_events)\r
415                     player_events = {}\r
416     \r
417                 player_events[key] = value\r
418 \r
419             if key == 'e':\r
420                 (subkey, subvalue) = value.split(' ', 1)\r
421                 player_events[subkey] = subvalue\r
422             if key == 'n':\r
423                 player_events[key] = value\r
424             if key == 't':\r
425                 player_events[key] = value\r
426         except:\r
427             # no key/value pair - move on to the next line\r
428             pass\r
429     \r
430     # add the last player we were working on\r
431     if len(player_events) > 0:\r
432         players.append(player_events)\r
433 \r
434     return (game_meta, players)\r
435 \r
436 \r
437 def create_player_stats(session=None, player=None, game=None, \r
438         player_events=None):\r
439     """\r
440     Creates player game and weapon stats according to what type of player\r
441     """\r
442     pgstat = create_player_game_stat(session=session, \r
443         player=player, game=game, player_events=player_events)\r
444 \r
445     #TODO: put this into a config setting in the ini file?\r
446     if not re.search('^bot#\d+$', player_events['P']):\r
447         create_player_weapon_stats(session=session, \r
448             player=player, game=game, pgstat=pgstat,\r
449             player_events=player_events)\r
450     \r
451 \r
452 def stats_submit(request):\r
453     """\r
454     Entry handler for POST stats submissions.\r
455     """\r
456     try:\r
457         session = DBSession()\r
458 \r
459         (idfp, status) = verify_request(request)\r
460         if not idfp:\r
461             raise Exception("Request is not verified.")\r
462 \r
463         (game_meta, players) = parse_body(request)  \r
464     \r
465         if not has_required_metadata(game_meta):\r
466             log.debug("Required game meta fields (T, G, M, or S) missing. "\\r
467                     "Can't continue.")\r
468             raise Exception("Required game meta fields (T, G, M, or S) missing.")\r
469    \r
470         if not is_supported_gametype(game_meta['G']):\r
471             raise Exception("Gametype not supported.")\r
472      \r
473         if not has_minimum_real_players(players):\r
474             raise Exception("The number of real players is below the minimum. "\\r
475                     "Stats will be ignored.")\r
476 \r
477         server = get_or_create_server(session=session, hashkey=idfp, \r
478                 name=game_meta['S'])\r
479 \r
480         gmap = get_or_create_map(session=session, name=game_meta['M'])\r
481 \r
482         game = create_game(session=session, \r
483                 start_dt=datetime.datetime(\r
484                     *time.gmtime(float(game_meta['T']))[:6]), \r
485                 server_id=server.server_id, game_type_cd=game_meta['G'], \r
486                 map_id=gmap.map_id)\r
487     \r
488         # find or create a record for each player\r
489         # and add stats for each if they were present at the end\r
490         # of the game\r
491         for player_events in players:\r
492             if 'n' in player_events:\r
493                 nick = player_events['n']\r
494             else:\r
495                 nick = None\r
496 \r
497             if 'matches' in player_events and 'scoreboardvalid' \\r
498                     in player_events:\r
499                 player = get_or_create_player(session=session, \r
500                     hashkey=player_events['P'], nick=nick)\r
501                 log.debug('Creating stats for %s' % player_events['P'])\r
502                 create_player_stats(session=session, player=player, game=game, \r
503                         player_events=player_events)\r
504     \r
505         session.commit()\r
506         log.debug('Success! Stats recorded.')\r
507         return Response('200 OK')\r
508     except Exception as e:\r
509         session.rollback()\r
510         raise e\r