X-Git-Url: http://de.git.xonotic.org/?a=blobdiff_plain;f=xonstat%2Fviews%2Fsubmission.py;h=81e639e531fcf0f0ba0894ad79fce080614a7100;hb=2e6546657de9449550c137426e06cb1e1a16706a;hp=68b68a9dd07515d0f783f6a476394c034f4461f9;hpb=a39afe9b54b0da86b1a48cf9d9328273d3ec2349;p=xonotic%2Fxonstat.git diff --git a/xonstat/views/submission.py b/xonstat/views/submission.py index 68b68a9..81e639e 100644 --- a/xonstat/views/submission.py +++ b/xonstat/views/submission.py @@ -1,4 +1,5 @@ import calendar +import collections import datetime import logging import re @@ -15,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 @@ -149,6 +253,7 @@ def is_supported_gametype(gametype, version): 'cts', 'dm', 'dom', + 'duel', 'ft', 'freezetag', 'ka', 'keepaway', 'kh', @@ -175,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):