]> de.git.xonotic.org Git - xonotic/xonstat.git/blobdiff - xonstat/views/submission.py
Check for distinct weapons fired during the match.
[xonotic/xonstat.git] / xonstat / views / submission.py
index a7ca8d29ea7f20e280647bec16e48c47be70d4de..81e639e531fcf0f0ba0894ad79fce080614a7100 100644 (file)
@@ -1,10 +1,10 @@
 import calendar
+import collections
 import datetime
 import logging
 import re
 
 import pyramid.httpexceptions
-import sqlalchemy.sql.expression as expr
 from sqlalchemy import Sequence
 from sqlalchemy.orm.exc import MultipleResultsFound, NoResultFound
 from xonstat.elo import EloProcessor
@@ -16,6 +16,109 @@ from xonstat.util import strip_colors, qfont_decode, verify_request, weapon_map
 log = logging.getLogger(__name__)
 
 
+class Submission(object):
+    """Parses an incoming POST request for stats submissions."""
+
+    def __init__(self, body, headers):
+        # a copy of the HTTP headers
+        self.headers = headers
+
+        # a copy of the HTTP POST body
+        self.body = body
+
+        # game metadata
+        self.meta = {}
+
+        # raw player events
+        self.players = []
+
+        # raw team events
+        self.teams = []
+
+        # distinct weapons that we have seen fired
+        self.weapons = set()
+
+        # the parsing deque (we use this to allow peeking)
+        self.q = collections.deque(self.body.split("\n"))
+
+    def next_item(self):
+        """Returns the next key:value pair off the queue."""
+        try:
+            items = self.q.popleft().strip().split(' ', 1)
+            if len(items) == 1:
+                return None, None
+            else:
+                return items
+        except:
+            return None, None
+
+    def check_for_new_weapon_fired(self, sub_key):
+        """Checks if a given player key (subkey, actually) is a new weapon fired in the match."""
+        if sub_key.endswith("cnt-fired"):
+            weapon = sub_key.split("-")[1]
+            if weapon not in self.weapons:
+                self.weapons.add(weapon)
+
+    def parse_player(self, key, pid):
+        """Construct a player events listing from the submission."""
+
+        # all of the keys related to player records
+        player_keys = ['i', 'n', 't', 'e']
+
+        player = {key: pid}
+
+        # Consume all following 'i' 'n' 't'  'e' records
+        while len(self.q) > 0:
+            (key, value) = self.next_item()
+            if key is None and value is None:
+                continue
+            elif key == 'e':
+                (sub_key, sub_value) = value.split(' ', 1)
+                player[sub_key] = sub_value
+
+                # keep track of the distinct weapons fired during the match
+                self.check_for_new_weapon_fired(sub_key)
+            elif key == 'n':
+                player[key] = unicode(value, 'utf-8')
+            elif key in player_keys:
+                player[key] = value
+            else:
+                # something we didn't expect - put it back on the deque
+                self.q.appendleft("{} {}".format(key, value))
+                break
+
+        self.players.append(player)
+
+    def parse_team(self, key, tid):
+        """Construct a team events listing from the submission."""
+        team = {key: tid}
+
+        # Consume all following 'e' records
+        while len(self.q) > 0 and self.q[0].startswith('e'):
+            (_, value) = self.next_item()
+            (sub_key, sub_value) = value.split(' ', 1)
+            team[sub_key] = sub_value
+
+        self.teams.append(team)
+
+    def parse(self):
+        """Parses the request body into instance variables."""
+        while len(self.q) > 0:
+            (key, value) = self.next_item()
+            if key is None and value is None:
+                continue
+            elif key == 'S':
+                self.meta[key] = unicode(value, 'utf-8')
+            elif key == 'P':
+                self.parse_player(key, value)
+            elif key == 'Q':
+                self.parse_team(key, value)
+            else:
+                self.meta[key] = value
+
+        return self
+
+
 def parse_stats_submission(body):
     """
     Parses the POST request body for a stats submission
@@ -150,6 +253,7 @@ def is_supported_gametype(gametype, version):
             'cts',
             'dm',
             'dom',
+            'duel',
             'ft', 'freezetag',
             'ka', 'keepaway',
             'kh',
@@ -176,26 +280,46 @@ def do_precondition_checks(request, game_meta, raw_players):
     """Precondition checks for ALL gametypes.
        These do not require a database connection."""
     if not has_required_metadata(game_meta):
-        log.debug("ERROR: Required game meta missing")
-        raise pyramid.httpexceptions.HTTPUnprocessableEntity("Missing game meta")
+        msg = "Missing required game metadata"
+        log.debug(msg)
+        raise pyramid.httpexceptions.HTTPUnprocessableEntity(
+            body=msg,
+            content_type="text/plain"
+        )
 
     try:
         version = int(game_meta['V'])
     except:
-        log.debug("ERROR: Required game meta invalid")
-        raise pyramid.httpexceptions.HTTPUnprocessableEntity("Invalid game meta")
+        msg = "Invalid or incorrect game metadata provided"
+        log.debug(msg)
+        raise pyramid.httpexceptions.HTTPUnprocessableEntity(
+            body=msg,
+            content_type="text/plain"
+        )
 
     if not is_supported_gametype(game_meta['G'], version):
-        log.debug("ERROR: Unsupported gametype")
-        raise pyramid.httpexceptions.HTTPOk("OK")
+        msg = "Unsupported game type ({})".format(game_meta['G'])
+        log.debug(msg)
+        raise pyramid.httpexceptions.HTTPOk(
+            body=msg,
+            content_type="text/plain"
+        )
 
     if not has_minimum_real_players(request.registry.settings, raw_players):
-        log.debug("ERROR: Not enough real players")
-        raise pyramid.httpexceptions.HTTPOk("OK")
+        msg = "Not enough real players"
+        log.debug(msg)
+        raise pyramid.httpexceptions.HTTPOk(
+            body=msg,
+            content_type="text/plain"
+        )
 
     if is_blank_game(game_meta['G'], raw_players):
-        log.debug("ERROR: Blank game")
-        raise pyramid.httpexceptions.HTTPOk("OK")
+        msg = "Blank game"
+        log.debug(msg)
+        raise pyramid.httpexceptions.HTTPOk(
+            body=msg,
+            content_type="text/plain"
+        )
 
 
 def is_real_player(events):