]> de.git.xonotic.org Git - xonotic/xonstat.git/blobdiff - xonstat/views/submission.py
Fix accuracy of comment to clarify intent.
[xonotic/xonstat.git] / xonstat / views / submission.py
index 97836de085276a56218f45b27dc8d4e694e9e499..36e094155953258fbf41ec33f95857c7b35a3e78 100755 (executable)
 import datetime\r
 import logging\r
+import pyramid.httpexceptions\r
 import re\r
 import time\r
 from pyramid.response import Response\r
+from sqlalchemy import Sequence\r
 from sqlalchemy.orm.exc import MultipleResultsFound, NoResultFound\r
+from xonstat.d0_blind_id import d0_blind_id_verify\r
 from xonstat.models import *\r
+from xonstat.util import strip_colors, qfont_decode\r
 \r
 log = logging.getLogger(__name__)\r
 \r
+def is_supported_gametype(gametype):\r
+    """Whether a gametype is supported or not"""\r
+    flg_supported = True\r
 \r
-def get_or_create_server(session=None, name=None):\r
+    if gametype == 'cts' or gametype == 'ca' or gametype == 'lms':\r
+        flg_supported = False\r
+\r
+    return flg_supported\r
+\r
+\r
+def verify_request(request):\r
+    try:\r
+        (idfp, status) = d0_blind_id_verify(\r
+                sig=request.headers['X-D0-Blind-Id-Detached-Signature'],\r
+                querystring='',\r
+                postdata=request.body)\r
+\r
+        log.debug('\nidfp: {0}\nstatus: {1}'.format(idfp, status))\r
+    except: \r
+        idfp = None\r
+        status = None\r
+\r
+    return (idfp, status)\r
+\r
+\r
+def has_minimum_real_players(settings, player_events):\r
+    """\r
+    Determines if the collection of player events has enough "real" players\r
+    to store in the database. The minimum setting comes from the config file\r
+    under the setting xonstat.minimum_real_players.\r
+    """\r
+    flg_has_min_real_players = True\r
+\r
+    try: \r
+        minimum_required_players = int(\r
+                settings['xonstat.minimum_required_players'])\r
+    except:\r
+        minimum_required_players = 2\r
+\r
+    real_players = 0\r
+    for events in player_events:\r
+        if is_real_player(events):\r
+            real_players += 1\r
+\r
+    #TODO: put this into a config setting in the ini file?\r
+    if real_players < minimum_required_players:\r
+        flg_has_min_real_players = False\r
+\r
+    return flg_has_min_real_players\r
+\r
+\r
+def has_required_metadata(metadata):\r
+    """\r
+    Determines if a give set of metadata has enough data to create a game,\r
+    server, and map with.\r
+    """\r
+    flg_has_req_metadata = True\r
+\r
+    if 'T' not in metadata or\\r
+        'G' not in metadata or\\r
+        'M' not in metadata or\\r
+        'S' not in metadata:\r
+            flg_has_req_metadata = False\r
+\r
+    return flg_has_req_metadata\r
+\r
+    \r
+def is_real_player(events):\r
+    """\r
+    Determines if a given set of player events correspond with a player who\r
+    \r
+    1) is not a bot (P event does not look like a bot)\r
+    2) played in the game (matches 1)\r
+    3) was present at the end of the game (scoreboardvalid 1)\r
+\r
+    Returns True if the player meets the above conditions, and false otherwise.\r
+    """\r
+    flg_is_real = False\r
+\r
+    if not events['P'].startswith('bot'):\r
+        # removing 'joins' here due to bug, but it should be here\r
+        if 'matches' in events and 'scoreboardvalid' in events:\r
+            flg_is_real = True\r
+\r
+    return flg_is_real\r
+\r
+\r
+def register_new_nick(session, player, new_nick):\r
+    """\r
+    Change the player record's nick to the newly found nick. Store the old\r
+    nick in the player_nicks table for that player.\r
+\r
+    session - SQLAlchemy database session factory\r
+    player - player record whose nick is changing\r
+    new_nick - the new nickname\r
+    """\r
+    # see if that nick already exists\r
+    stripped_nick = strip_colors(player.nick)\r
+    try:\r
+       player_nick = session.query(PlayerNick).filter_by(\r
+               player_id=player.player_id, stripped_nick=stripped_nick).one()\r
+    except NoResultFound, e:\r
+           # player_id/stripped_nick not found, create one\r
+        # but we don't store "Anonymous Player #N"\r
+        if not re.search('^Anonymous Player #\d+$', player.nick):\r
+            player_nick = PlayerNick()\r
+            player_nick.player_id = player.player_id\r
+            player_nick.stripped_nick = player.stripped_nick\r
+            player_nick.nick = player.nick\r
+            session.add(player_nick)\r
+\r
+    # We change to the new nick regardless\r
+    player.nick = new_nick\r
+    player.stripped_nick = strip_colors(new_nick)\r
+    session.add(player)\r
+\r
+\r
+def get_or_create_server(session=None, name=None, hashkey=None):\r
     """\r
     Find a server by name or create one if not found. Parameters:\r
 \r
     session - SQLAlchemy database session factory\r
     name - server name of the server to be found or created\r
+    hashkey - server hashkey\r
     """\r
     try:\r
         # find one by that name, if it exists\r
         server = session.query(Server).filter_by(name=name).one()\r
-        log.debug("Found server id {0} with name {1}.".format(\r
-            server.server_id, server.name))\r
+\r
+        # store new hashkey\r
+        if server.hashkey != hashkey:\r
+            server.hashkey = hashkey\r
+            session.add(server)\r
+\r
+        log.debug("Found existing server {0}".format(server.server_id))\r
+\r
+    except MultipleResultsFound, e:\r
+        # multiple found, so also filter by hashkey\r
+        server = session.query(Server).filter_by(name=name).\\r
+                filter_by(hashkey=hashkey).one()\r
+        log.debug("Found existing server {0}".format(server.server_id))\r
+\r
     except NoResultFound, e:\r
-        server = Server(name=name)\r
+        # not found, create one\r
+        server = Server(name=name, hashkey=hashkey)\r
         session.add(server)\r
         session.flush()\r
-        log.debug("Created server id {0} with name {1}".format(\r
-            server.server_id, server.name))\r
-    except MultipleResultsFound, e:\r
-        # multiple found, so use the first one but warn\r
-        log.debug(e)\r
-        servers = session.query(Server).filter_by(name=name).order_by(\r
-                Server.server_id).all()\r
-        server = servers[0]\r
-        log.debug("Created server id {0} with name {1} but found \\r
-                multiple".format(\r
-            server.server_id, server.name))\r
+        log.debug("Created server {0} with hashkey {1}".format(\r
+            server.server_id, server.hashkey))\r
 \r
     return server\r
 \r
+\r
 def get_or_create_map(session=None, name=None):\r
     """\r
     Find a map by name or create one if not found. Parameters:\r
@@ -49,13 +175,13 @@ def get_or_create_map(session=None, name=None):
     try:\r
         # find one by the name, if it exists\r
         gmap = session.query(Map).filter_by(name=name).one()\r
-        log.debug("Found map id {0} with name {1}.".format(gmap.map_id, \r
+        log.debug("Found map id {0}: {1}".format(gmap.map_id, \r
             gmap.name))\r
     except NoResultFound, e:\r
         gmap = Map(name=name)\r
         session.add(gmap)\r
         session.flush()\r
-        log.debug("Created map id {0} with name {1}.".format(gmap.map_id,\r
+        log.debug("Created map id {0}: {1}".format(gmap.map_id,\r
             gmap.name))\r
     except MultipleResultsFound, e:\r
         # multiple found, so use the first one but warn\r
@@ -63,8 +189,8 @@ def get_or_create_map(session=None, name=None):
         gmaps = session.query(Map).filter_by(name=name).order_by(\r
                 Map.map_id).all()\r
         gmap = gmaps[0]\r
-        log.debug("Found map id {0} with name {1} but found \\r
-                multiple.".format(gmap.map_id, gmap.name))\r
+        log.debug("Found map id {0}: {1} but found \\r
+                multiple".format(gmap.map_id, gmap.name))\r
 \r
     return gmap\r
 \r
@@ -81,14 +207,14 @@ def create_game(session=None, start_dt=None, game_type_cd=None,
     map_id - map on which the game was played\r
     winner - the team id of the team that won\r
     """\r
-\r
-    game = Game(start_dt=start_dt, game_type_cd=game_type_cd,\r
+    seq = Sequence('games_game_id_seq')\r
+    game_id = session.execute(seq)\r
+    game = Game(game_id=game_id, start_dt=start_dt, game_type_cd=game_type_cd,\r
                 server_id=server_id, map_id=map_id, winner=winner)\r
     session.add(game)\r
-    session.flush()\r
-    log.debug("Created game id {0} on server {1}, map {2} at time \\r
-            {3} and on map {4}".format(game.game_id, \r
-                server_id, map_id, start_dt, map_id))\r
+    log.debug("Created game id {0} on server {1}, map {2} at \\r
+            {3}".format(game.game_id, \r
+                server_id, map_id, start_dt))\r
 \r
     return game\r
 \r
@@ -117,20 +243,26 @@ def get_or_create_player(session=None, hashkey=None, nick=None):
                     hashkey=hashkey).one()\r
             player = session.query(Player).filter_by(\r
                     player_id=hashkey.player_id).one()\r
-            log.debug("Found existing player {0} with hashkey {1}.".format(\r
+            log.debug("Found existing player {0} with hashkey {1}".format(\r
                 player.player_id, hashkey.hashkey))\r
         except:\r
             player = Player()\r
-\r
-            if nick:\r
-                player.nick = nick\r
-\r
             session.add(player)\r
             session.flush()\r
-            hashkey = Hashkey(player_id=player.player_id, hashkey=hashkey)\r
-            session.add(hashkey)\r
-            log.debug("Created player {0} with hashkey {1}.".format(\r
-                player.player_id, hashkey.hashkey))\r
+\r
+        # if nick is given to us, use it. If not, use "Anonymous Player"\r
+        # with a suffix added for uniqueness.\r
+        if nick:\r
+            player.nick = nick[:128]\r
+            player.stripped_nick = strip_colors(nick[:128])\r
+        else:\r
+            player.nick = "Anonymous Player #{0}".format(player.player_id)\r
+            player.stripped_nick = player.nick\r
+\r
+        hashkey = Hashkey(player_id=player.player_id, hashkey=hashkey)\r
+        session.add(hashkey)\r
+        log.debug("Created player {0} ({2}) with hashkey {1}".format(\r
+            player.player_id, hashkey.hashkey, player.nick.encode('utf-8')))\r
 \r
     return player\r
 \r
@@ -148,7 +280,10 @@ def create_player_game_stat(session=None, player=None,
     # in here setup default values (e.g. if game type is CTF then\r
     # set kills=0, score=0, captures=0, pickups=0, fckills=0, etc\r
     # TODO: use game's create date here instead of now()\r
-    pgstat = PlayerGameStat(create_dt=datetime.datetime.now())\r
+    seq = Sequence('player_game_stats_player_game_stat_id_seq')\r
+    pgstat_id = session.execute(seq)\r
+    pgstat = PlayerGameStat(player_game_stat_id=pgstat_id, \r
+            create_dt=datetime.datetime.utcnow())\r
 \r
     # set player id from player record\r
     pgstat.player_id = player.player_id\r
@@ -172,7 +307,7 @@ def create_player_game_stat(session=None, player=None,
         pgstat.carrier_frags = 0\r
 \r
     for (key,value) in player_events.items():\r
-        if key == 'n': pgstat.nick = value\r
+        if key == 'n': pgstat.nick = value[:128]\r
         if key == 't': pgstat.team = value\r
         if key == 'rank': pgstat.rank = value\r
         if key == 'alivetime': \r
@@ -192,8 +327,19 @@ def create_player_game_stat(session=None, player=None,
     if pgstat.nick == None:\r
         pgstat.nick = player.nick\r
 \r
+    # if the nick we end up with is different from the one in the\r
+    # player record, change the nick to reflect the new value\r
+    if pgstat.nick != player.nick and player.player_id > 2:\r
+        register_new_nick(session, player, pgstat.nick)\r
+\r
+    # if the player is ranked #1 and it is a team game, set the game's winner\r
+    # to be the team of that player\r
+    # FIXME: this is a hack, should be using the 'W' field (not present)\r
+    if pgstat.rank == '1' and pgstat.team:\r
+        game.winner = pgstat.team\r
+        session.add(game)\r
+\r
     session.add(pgstat)\r
-    session.flush()\r
 \r
     return pgstat\r
 \r
@@ -217,7 +363,10 @@ def create_player_weapon_stats(session=None, player=None,
         matched = re.search("acc-(.*?)-cnt-fired", key)\r
         if matched:\r
             weapon_cd = matched.group(1)\r
+            seq = Sequence('player_weapon_stats_player_weapon_stats_id_seq')\r
+            pwstat_id = session.execute(seq)\r
             pwstat = PlayerWeaponStat()\r
+            pwstat.player_weapon_stats_id = pwstat_id\r
             pwstat.player_id = player.player_id\r
             pwstat.game_id = game.game_id\r
             pwstat.player_game_stat_id = pgstat.player_game_stat_id\r
@@ -244,7 +393,9 @@ def create_player_weapon_stats(session=None, player=None,
                 pwstat.frags = int(round(float(\r
                         player_events['acc-' + weapon_cd + '-frags'])))\r
 \r
+            log.debug(pwstat)\r
             session.add(pwstat)\r
+            log.debug(pwstat)\r
             pwstats.append(pwstat)\r
 \r
     return pwstats\r
@@ -260,11 +411,20 @@ def parse_body(request):
     current_team = None\r
     players = []\r
     \r
+    log.debug("----- BEGIN REQUEST BODY -----")\r
     log.debug(request.body)\r
+    log.debug("----- END REQUEST BODY -----")\r
 \r
     for line in request.body.split('\n'):\r
         try:\r
             (key, value) = line.strip().split(' ', 1)\r
+\r
+            # Server (S) and Nick (n) fields can have international characters.\r
+            # We first convert to UTF-8, then to ASCII. Characters will be lost\r
+            # in this conversion for the sake of presenting what otherwise \r
+            # would have to use CSS sprites.\r
+            if key in 'S' 'n':\r
+                value = qfont_decode(unicode(value, 'utf-8'))\r
     \r
             if key in 'V' 'T' 'G' 'M' 'S' 'C' 'R' 'W':\r
                 game_meta[key] = value\r
@@ -301,14 +461,14 @@ def create_player_stats(session=None, player=None, game=None,
     """\r
     Creates player game and weapon stats according to what type of player\r
     """\r
-    if 'joins' in player_events and 'matches' in player_events\\r
-            and 'scoreboardvalid' in player_events:\r
-                pgstat = create_player_game_stat(session=session, \r
-                        player=player, game=game, player_events=player_events)\r
-                if not re.search('^bot#\d+$', player_events['P']):\r
-                        create_player_weapon_stats(session=session, \r
-                            player=player, game=game, pgstat=pgstat,\r
-                            player_events=player_events)\r
+    pgstat = create_player_game_stat(session=session, \r
+        player=player, game=game, player_events=player_events)\r
+\r
+    #TODO: put this into a config setting in the ini file?\r
+    if not re.search('^bot#\d+$', player_events['P']):\r
+        create_player_weapon_stats(session=session, \r
+            player=player, game=game, pgstat=pgstat,\r
+            player_events=player_events)\r
     \r
 \r
 def stats_submit(request):\r
@@ -318,41 +478,38 @@ def stats_submit(request):
     try:\r
         session = DBSession()\r
 \r
+        (idfp, status) = verify_request(request)\r
+        if not idfp:\r
+            raise pyramid.httpexceptions.HTTPUnauthorized\r
+     \r
         (game_meta, players) = parse_body(request)  \r
-    \r
-        # verify required metadata is present\r
-        if 'T' not in game_meta or\\r
-            'G' not in game_meta or\\r
-            'M' not in game_meta or\\r
-            'S' not in game_meta:\r
-            log.debug("Required game meta fields (T, G, M, or S) missing. "\\r
+     \r
+        if not has_required_metadata(game_meta):\r
+            log.debug("Required game meta fields missing. "\\r
                     "Can't continue.")\r
-            raise Exception("Required game meta fields (T, G, M, or S) missing.")\r
-    \r
-        has_real_players = False\r
-        for player_events in players:\r
-            if not player_events['P'].startswith('bot'):\r
-                if 'joins' in player_events and 'matches' in player_events\\r
-                    and 'scoreboardvalid' in player_events:\r
-                    has_real_players = True\r
-\r
-        if not has_real_players:\r
-            raise Exception("No real players found. Stats ignored.")\r
-\r
-        server = get_or_create_server(session=session, name=game_meta['S'])\r
+            raise pyramid.exceptions.HTTPUnprocessableEntity\r
+     \r
+        if not is_supported_gametype(game_meta['G']):\r
+            raise pyramid.httpexceptions.HTTPOk\r
+     \r
+        if not has_minimum_real_players(request.registry.settings, players):\r
+            log.debug("The number of real players is below the minimum. " + \r
+                "Stats will be ignored.")\r
+            raise pyramid.httpexceptions.HTTPOk\r
+     \r
+        server = get_or_create_server(session=session, hashkey=idfp, \r
+                name=game_meta['S'])\r
+     \r
         gmap = get_or_create_map(session=session, name=game_meta['M'])\r
-\r
-        if 'W' in game_meta:\r
-            winner = game_meta['W']\r
-        else:\r
-            winner = None\r
-\r
+        log.debug(gmap)\r
+     \r
         game = create_game(session=session, \r
                 start_dt=datetime.datetime(\r
                     *time.gmtime(float(game_meta['T']))[:6]), \r
                 server_id=server.server_id, game_type_cd=game_meta['G'], \r
-                map_id=gmap.map_id, winner=winner)\r
-    \r
+                   map_id=gmap.map_id)\r
+        log.debug(gmap)\r
+     \r
         # find or create a record for each player\r
         # and add stats for each if they were present at the end\r
         # of the game\r
@@ -361,13 +518,15 @@ def stats_submit(request):
                 nick = player_events['n']\r
             else:\r
                 nick = None\r
-\r
-            player = get_or_create_player(session=session, \r
\r
+            if 'matches' in player_events and 'scoreboardvalid' \\r
+                in player_events:\r
+                player = get_or_create_player(session=session, \r
                     hashkey=player_events['P'], nick=nick)\r
-            log.debug('Creating stats for %s' % player_events['P'])\r
-            create_player_stats(session=session, player=player, game=game, \r
-                    player_events=player_events)\r
-    \r
+                log.debug('Creating stats for %s' % player_events['P'])\r
+                create_player_stats(session=session, player=player, game=game, \r
+                        player_events=player_events)\r
+     \r
         session.commit()\r
         log.debug('Success! Stats recorded.')\r
         return Response('200 OK')\r