]> de.git.xonotic.org Git - xonotic/xonstat.git/blobdiff - xonstat/views/submission.py
Do not use x-server-ip.
[xonotic/xonstat.git] / xonstat / views / submission.py
index ea3e5c1a37f5f74b07ea3d83161f0533529b99eb..66dcd1107dc8ca57a30d683ea7eaa616680f85aa 100755 (executable)
@@ -1,17 +1,26 @@
 import datetime\r
 import logging\r
+import os\r
 import pyramid.httpexceptions\r
 import re\r
 import time\r
-from pyramid.config import get_current_registry\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\r
+from xonstat.util import strip_colors, qfont_decode\r
 \r
 log = logging.getLogger(__name__)\r
 \r
+def get_remote_addr(request):\r
+    """Get the Xonotic server's IP address"""\r
+    if 'X-Forwarded-For' in request.headers:\r
+        return request.headers['X-Forwarded-For']\r
+    else:\r
+        return request.remote_addr\r
+\r
+\r
 def is_supported_gametype(gametype):\r
     """Whether a gametype is supported or not"""\r
     flg_supported = True\r
@@ -37,7 +46,21 @@ def verify_request(request):
     return (idfp, status)\r
 \r
 \r
-def has_minimum_real_players(player_events):\r
+def num_real_players(player_events):\r
+    """\r
+    Returns the number of real players (those who played \r
+    and are on the scoreboard).\r
+    """\r
+    real_players = 0\r
+\r
+    for events in player_events:\r
+        if is_real_player(events):\r
+            real_players += 1\r
+\r
+    return real_players\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
@@ -45,17 +68,13 @@ def has_minimum_real_players(player_events):
     """\r
     flg_has_min_real_players = True\r
 \r
-    settings = get_current_registry().settings\r
-    try: \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
+    real_players = num_real_players(player_events)\r
 \r
     #TODO: put this into a config setting in the ini file?\r
     if real_players < minimum_required_players:\r
@@ -74,16 +93,17 @@ def has_required_metadata(metadata):
     if 'T' not in metadata or\\r
         'G' not in metadata or\\r
         'M' not in metadata or\\r
+        'I' 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
+\r
 def is_real_player(events):\r
     """\r
     Determines if a given set of player events correspond with a player who\r
-    \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
@@ -112,24 +132,26 @@ def register_new_nick(session, player, new_nick):
     # 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
+        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
+        # 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 = PlayerNick()\r
             player_nick.player_id = player.player_id\r
-            player_nick.stripped_nick = stripped_nick\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
+def get_or_create_server(session=None, name=None, hashkey=None, ip_addr=None,\r
+        revision=None):\r
     """\r
     Find a server by name or create one if not found. Parameters:\r
 \r
@@ -146,6 +168,16 @@ def get_or_create_server(session=None, name=None, hashkey=None):
             server.hashkey = hashkey\r
             session.add(server)\r
 \r
+        # store new IP address\r
+        if server.ip_addr != ip_addr:\r
+            server.ip_addr = ip_addr\r
+            session.add(server)\r
+\r
+        # store new revision\r
+        if server.revision != revision:\r
+            server.revision = revision\r
+            session.add(server)\r
+\r
         log.debug("Found existing server {0}".format(server.server_id))\r
 \r
     except MultipleResultsFound, e:\r
@@ -196,7 +228,7 @@ def get_or_create_map(session=None, name=None):
 \r
 \r
 def create_game(session=None, start_dt=None, game_type_cd=None, \r
-        server_id=None, map_id=None, winner=None):\r
+        server_id=None, map_id=None, winner=None, match_id=None):\r
     """\r
     Creates a game. Parameters:\r
 \r
@@ -207,14 +239,24 @@ 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 \\r
-            {3}".format(game.game_id, \r
-                server_id, map_id, start_dt))\r
+    game.match_id = match_id\r
+\r
+    try:\r
+        session.query(Game).filter(Game.server_id==server_id).\\r
+                filter(Game.match_id==match_id).one()\r
+        # if a game under the same server and match_id found, \r
+        # this is a duplicate game and can be ignored\r
+        raise pyramid.httpexceptions.HTTPOk\r
+    except NoResultFound, e:\r
+        # server_id/match_id combination not found. game is ok to insert\r
+        session.add(game)\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
@@ -239,28 +281,30 @@ def get_or_create_player(session=None, hashkey=None, nick=None):
         # see if the player is already in the database\r
         # if not, create one and the hashkey along with it\r
         try:\r
-            hashkey = session.query(Hashkey).filter_by(\r
+            hk = session.query(Hashkey).filter_by(\r
                     hashkey=hashkey).one()\r
             player = session.query(Player).filter_by(\r
-                    player_id=hashkey.player_id).one()\r
+                    player_id=hk.player_id).one()\r
             log.debug("Found existing player {0} with hashkey {1}".format(\r
-                player.player_id, hashkey.hashkey))\r
+                player.player_id, hashkey))\r
         except:\r
             player = Player()\r
             session.add(player)\r
             session.flush()\r
 \r
-           # if nick is given to us, use it. If not, use "Anonymous Player"\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
-           else:\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
+            hk = Hashkey(player_id=player.player_id, hashkey=hashkey)\r
+            session.add(hk)\r
             log.debug("Created player {0} ({2}) with hashkey {1}".format(\r
-                player.player_id, hashkey.hashkey, player.nick.encode('utf-8')))\r
+                player.player_id, hashkey, player.nick.encode('utf-8')))\r
 \r
     return player\r
 \r
@@ -278,7 +322,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
@@ -289,7 +336,7 @@ def create_player_game_stat(session=None, player=None,
     # all games have a score\r
     pgstat.score = 0\r
 \r
-    if game.game_type_cd == 'dm':\r
+    if game.game_type_cd == 'dm' or game.game_type_cd == 'tdm' or game.game_type_cd == 'duel':\r
         pgstat.kills = 0\r
         pgstat.deaths = 0\r
         pgstat.suicides = 0\r
@@ -322,6 +369,9 @@ def create_player_game_stat(session=None, player=None,
     if pgstat.nick == None:\r
         pgstat.nick = player.nick\r
 \r
+    # whichever nick we ended up with, strip it and store as the stripped_nick\r
+    pgstat.stripped_nick = qfont_decode(strip_colors(pgstat.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
@@ -335,7 +385,6 @@ def create_player_game_stat(session=None, player=None,
         session.add(game)\r
 \r
     session.add(pgstat)\r
-    session.flush()\r
 \r
     return pgstat\r
 \r
@@ -359,7 +408,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
@@ -401,21 +453,17 @@ def parse_body(request):
     player_events = {}\r
     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 encode these as UTF-8.\r
+            # We convert to UTF-8.\r
             if key in 'S' 'n':\r
                 value = unicode(value, 'utf-8')\r
-    \r
-            if key in 'V' 'T' 'G' 'M' 'S' 'C' 'R' 'W':\r
+\r
+            if key in 'V' 'T' 'G' 'M' 'S' 'C' 'R' 'W' 'I':\r
                 game_meta[key] = value\r
 \r
             if key == 'P':\r
@@ -424,7 +472,7 @@ def parse_body(request):
                 if len(player_events) != 0:\r
                     players.append(player_events)\r
                     player_events = {}\r
-    \r
+\r
                 player_events[key] = value\r
 \r
             if key == 'e':\r
@@ -437,7 +485,7 @@ def parse_body(request):
         except:\r
             # no key/value pair - move on to the next line\r
             pass\r
-    \r
+\r
     # add the last player we were working on\r
     if len(player_events) > 0:\r
         players.append(player_events)\r
@@ -458,7 +506,7 @@ def create_player_stats(session=None, player=None, game=None,
         create_player_weapon_stats(session=session, \r
             player=player, game=game, pgstat=pgstat,\r
             player_events=player_events)\r
-    \r
+\r
 \r
 def stats_submit(request):\r
     """\r
@@ -467,36 +515,49 @@ def stats_submit(request):
     try:\r
         session = DBSession()\r
 \r
+        log.debug("\n----- BEGIN REQUEST BODY -----\n" + request.body +\r
+                "----- END REQUEST BODY -----\n\n")\r
+\r
         (idfp, status) = verify_request(request)\r
         if not idfp:\r
-            raise pyramid.httpexceptions.HTTPUnauthorized\r
+            log.debug("ERROR: Unverified request")\r
+            raise pyramid.httpexceptions.HTTPUnauthorized("Unverified request")\r
 \r
         (game_meta, players) = parse_body(request)  \r
-    \r
+\r
         if not has_required_metadata(game_meta):\r
-            log.debug("Required game meta fields missing. "\\r
-                    "Can't continue.")\r
-            raise pyramid.exceptions.HTTPUnprocessableEntity\r
-   \r
+            log.debug("ERROR: Required game meta missing")\r
+            raise pyramid.exceptions.HTTPUnprocessableEntity("Missing game meta")\r
+\r
         if not is_supported_gametype(game_meta['G']):\r
-            raise pyramid.httpexceptions.HTTPOk\r
-     \r
-        if not has_minimum_real_players(players):\r
-            log.debug("The number of real players is below the minimum. " + \r
-                "Stats will be ignored.")\r
-            raise pyramid.httpexceptions.HTTPOk\r
+            log.debug("ERROR: Unsupported gametype")\r
+            raise pyramid.httpexceptions.HTTPOk("OK")\r
+\r
+        if not has_minimum_real_players(request.registry.settings, players):\r
+            log.debug("ERROR: Not enough real players")\r
+            raise pyramid.httpexceptions.HTTPOk("OK")\r
+\r
+        # FIXME: if we have two players and game type is 'dm',\r
+        # change this into a 'duel' gametype. This should be\r
+        # removed when the stats actually send 'duel' instead of 'dm'\r
+        if num_real_players(players) == 2 and game_meta['G'] == 'dm':\r
+            game_meta['G'] = 'duel'\r
 \r
         server = get_or_create_server(session=session, hashkey=idfp, \r
-                name=game_meta['S'])\r
+                name=game_meta['S'], revision=game_meta['R'],\r
+                ip_addr=get_remote_addr(request))\r
 \r
         gmap = get_or_create_map(session=session, name=game_meta['M'])\r
 \r
+        # FIXME: use the gmtime instead of utcnow() when the timezone bug is\r
+        # fixed\r
         game = create_game(session=session, \r
-                start_dt=datetime.datetime(\r
-                    *time.gmtime(float(game_meta['T']))[:6]), \r
+                start_dt=datetime.datetime.utcnow(),\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)\r
-    \r
+                   map_id=gmap.map_id, match_id=game_meta['I'])\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
@@ -507,16 +568,16 @@ def stats_submit(request):
                 nick = None\r
 \r
             if 'matches' in player_events and 'scoreboardvalid' \\r
-                    in player_events:\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
+\r
         session.commit()\r
         log.debug('Success! Stats recorded.')\r
         return Response('200 OK')\r
     except Exception as e:\r
         session.rollback()\r
-        raise e\r
+        return e\r