3 import pyramid.httpexceptions
\r
6 from pyramid.response import Response
\r
7 from sqlalchemy import Sequence
\r
8 from sqlalchemy.orm.exc import MultipleResultsFound, NoResultFound
\r
9 from xonstat.d0_blind_id import d0_blind_id_verify
\r
10 from xonstat.models import *
\r
11 from xonstat.util import strip_colors, qfont_decode
\r
13 log = logging.getLogger(__name__)
\r
15 def get_remote_addr(request):
\r
16 """Get the Xonotic server's IP address"""
\r
17 if 'X-Server-IP' in request.headers:
\r
18 return request.headers['X-Server-IP']
\r
20 return request.remote_addr
\r
23 def is_supported_gametype(gametype):
\r
24 """Whether a gametype is supported or not"""
\r
25 flg_supported = True
\r
27 if gametype == 'cts' or gametype == 'ca' or gametype == 'lms':
\r
28 flg_supported = False
\r
30 return flg_supported
\r
33 def verify_request(request):
\r
35 (idfp, status) = d0_blind_id_verify(
\r
36 sig=request.headers['X-D0-Blind-Id-Detached-Signature'],
\r
38 postdata=request.body)
\r
40 log.debug('\nidfp: {0}\nstatus: {1}'.format(idfp, status))
\r
45 return (idfp, status)
\r
48 def has_minimum_real_players(settings, player_events):
\r
50 Determines if the collection of player events has enough "real" players
\r
51 to store in the database. The minimum setting comes from the config file
\r
52 under the setting xonstat.minimum_real_players.
\r
54 flg_has_min_real_players = True
\r
57 minimum_required_players = int(
\r
58 settings['xonstat.minimum_required_players'])
\r
60 minimum_required_players = 2
\r
63 for events in player_events:
\r
64 if is_real_player(events):
\r
67 #TODO: put this into a config setting in the ini file?
\r
68 if real_players < minimum_required_players:
\r
69 flg_has_min_real_players = False
\r
71 return flg_has_min_real_players
\r
74 def has_required_metadata(metadata):
\r
76 Determines if a give set of metadata has enough data to create a game,
\r
77 server, and map with.
\r
79 flg_has_req_metadata = True
\r
81 if 'T' not in metadata or\
\r
82 'G' not in metadata or\
\r
83 'M' not in metadata or\
\r
84 'I' not in metadata or\
\r
85 'S' not in metadata:
\r
86 flg_has_req_metadata = False
\r
88 return flg_has_req_metadata
\r
91 def is_real_player(events):
\r
93 Determines if a given set of player events correspond with a player who
\r
95 1) is not a bot (P event does not look like a bot)
\r
96 2) played in the game (matches 1)
\r
97 3) was present at the end of the game (scoreboardvalid 1)
\r
99 Returns True if the player meets the above conditions, and false otherwise.
\r
101 flg_is_real = False
\r
103 if not events['P'].startswith('bot'):
\r
104 # removing 'joins' here due to bug, but it should be here
\r
105 if 'matches' in events and 'scoreboardvalid' in events:
\r
111 def register_new_nick(session, player, new_nick):
\r
113 Change the player record's nick to the newly found nick. Store the old
\r
114 nick in the player_nicks table for that player.
\r
116 session - SQLAlchemy database session factory
\r
117 player - player record whose nick is changing
\r
118 new_nick - the new nickname
\r
120 # see if that nick already exists
\r
121 stripped_nick = strip_colors(player.nick)
\r
123 player_nick = session.query(PlayerNick).filter_by(
\r
124 player_id=player.player_id, stripped_nick=stripped_nick).one()
\r
125 except NoResultFound, e:
\r
126 # player_id/stripped_nick not found, create one
\r
127 # but we don't store "Anonymous Player #N"
\r
128 if not re.search('^Anonymous Player #\d+$', player.nick):
\r
129 player_nick = PlayerNick()
\r
130 player_nick.player_id = player.player_id
\r
131 player_nick.stripped_nick = player.stripped_nick
\r
132 player_nick.nick = player.nick
\r
133 session.add(player_nick)
\r
135 # We change to the new nick regardless
\r
136 player.nick = new_nick
\r
137 player.stripped_nick = strip_colors(new_nick)
\r
138 session.add(player)
\r
141 def get_or_create_server(session=None, name=None, hashkey=None, ip_addr=None,
\r
144 Find a server by name or create one if not found. Parameters:
\r
146 session - SQLAlchemy database session factory
\r
147 name - server name of the server to be found or created
\r
148 hashkey - server hashkey
\r
151 # find one by that name, if it exists
\r
152 server = session.query(Server).filter_by(name=name).one()
\r
154 # store new hashkey
\r
155 if server.hashkey != hashkey:
\r
156 server.hashkey = hashkey
\r
157 session.add(server)
\r
159 # store new IP address
\r
160 if server.ip_addr != ip_addr:
\r
161 server.ip_addr = ip_addr
\r
162 session.add(server)
\r
164 # store new revision
\r
165 if server.revision != revision:
\r
166 server.revision = revision
\r
167 session.add(server)
\r
169 log.debug("Found existing server {0}".format(server.server_id))
\r
171 except MultipleResultsFound, e:
\r
172 # multiple found, so also filter by hashkey
\r
173 server = session.query(Server).filter_by(name=name).\
\r
174 filter_by(hashkey=hashkey).one()
\r
175 log.debug("Found existing server {0}".format(server.server_id))
\r
177 except NoResultFound, e:
\r
178 # not found, create one
\r
179 server = Server(name=name, hashkey=hashkey)
\r
180 session.add(server)
\r
182 log.debug("Created server {0} with hashkey {1}".format(
\r
183 server.server_id, server.hashkey))
\r
188 def get_or_create_map(session=None, name=None):
\r
190 Find a map by name or create one if not found. Parameters:
\r
192 session - SQLAlchemy database session factory
\r
193 name - map name of the map to be found or created
\r
196 # find one by the name, if it exists
\r
197 gmap = session.query(Map).filter_by(name=name).one()
\r
198 log.debug("Found map id {0}: {1}".format(gmap.map_id,
\r
200 except NoResultFound, e:
\r
201 gmap = Map(name=name)
\r
204 log.debug("Created map id {0}: {1}".format(gmap.map_id,
\r
206 except MultipleResultsFound, e:
\r
207 # multiple found, so use the first one but warn
\r
209 gmaps = session.query(Map).filter_by(name=name).order_by(
\r
212 log.debug("Found map id {0}: {1} but found \
\r
213 multiple".format(gmap.map_id, gmap.name))
\r
218 def create_game(session=None, start_dt=None, game_type_cd=None,
\r
219 server_id=None, map_id=None, winner=None, match_id=None):
\r
221 Creates a game. Parameters:
\r
223 session - SQLAlchemy database session factory
\r
224 start_dt - when the game started (datetime object)
\r
225 game_type_cd - the game type of the game being played
\r
226 server_id - server identifier of the server hosting the game
\r
227 map_id - map on which the game was played
\r
228 winner - the team id of the team that won
\r
230 seq = Sequence('games_game_id_seq')
\r
231 game_id = session.execute(seq)
\r
232 game = Game(game_id=game_id, start_dt=start_dt, game_type_cd=game_type_cd,
\r
233 server_id=server_id, map_id=map_id, winner=winner)
\r
234 game.match_id = match_id
\r
237 session.query(Game).filter(Game.server_id==server_id).\
\r
238 filter(Game.match_id==match_id).one()
\r
239 # if a game under the same server and match_id found,
\r
240 # this is a duplicate game and can be ignored
\r
241 raise pyramid.httpexceptions.HTTPOk
\r
242 except NoResultFound, e:
\r
243 # server_id/match_id combination not found. game is ok to insert
\r
245 log.debug("Created game id {0} on server {1}, map {2} at \
\r
246 {3}".format(game.game_id,
\r
247 server_id, map_id, start_dt))
\r
252 def get_or_create_player(session=None, hashkey=None, nick=None):
\r
254 Finds a player by hashkey or creates a new one (along with a
\r
255 corresponding hashkey entry. Parameters:
\r
257 session - SQLAlchemy database session factory
\r
258 hashkey - hashkey of the player to be found or created
\r
259 nick - nick of the player (in case of a first time create)
\r
262 if re.search('^bot#\d+$', hashkey):
\r
263 player = session.query(Player).filter_by(player_id=1).one()
\r
264 # if we have an untracked player
\r
265 elif re.search('^player#\d+$', hashkey):
\r
266 player = session.query(Player).filter_by(player_id=2).one()
\r
267 # else it is a tracked player
\r
269 # see if the player is already in the database
\r
270 # if not, create one and the hashkey along with it
\r
272 hk = session.query(Hashkey).filter_by(
\r
273 hashkey=hashkey).one()
\r
274 player = session.query(Player).filter_by(
\r
275 player_id=hk.player_id).one()
\r
276 log.debug("Found existing player {0} with hashkey {1}".format(
\r
277 player.player_id, hashkey))
\r
280 session.add(player)
\r
283 # if nick is given to us, use it. If not, use "Anonymous Player"
\r
284 # with a suffix added for uniqueness.
\r
286 player.nick = nick[:128]
\r
287 player.stripped_nick = strip_colors(nick[:128])
\r
289 player.nick = "Anonymous Player #{0}".format(player.player_id)
\r
290 player.stripped_nick = player.nick
\r
292 hk = Hashkey(player_id=player.player_id, hashkey=hashkey)
\r
294 log.debug("Created player {0} ({2}) with hashkey {1}".format(
\r
295 player.player_id, hashkey, player.nick.encode('utf-8')))
\r
299 def create_player_game_stat(session=None, player=None,
\r
300 game=None, player_events=None):
\r
302 Creates game statistics for a given player in a given game. Parameters:
\r
304 session - SQLAlchemy session factory
\r
305 player - Player record of the player who owns the stats
\r
306 game - Game record for the game to which the stats pertain
\r
307 player_events - dictionary for the actual stats that need to be transformed
\r
310 # in here setup default values (e.g. if game type is CTF then
\r
311 # set kills=0, score=0, captures=0, pickups=0, fckills=0, etc
\r
312 # TODO: use game's create date here instead of now()
\r
313 seq = Sequence('player_game_stats_player_game_stat_id_seq')
\r
314 pgstat_id = session.execute(seq)
\r
315 pgstat = PlayerGameStat(player_game_stat_id=pgstat_id,
\r
316 create_dt=datetime.datetime.utcnow())
\r
318 # set player id from player record
\r
319 pgstat.player_id = player.player_id
\r
321 #set game id from game record
\r
322 pgstat.game_id = game.game_id
\r
324 # all games have a score
\r
327 if game.game_type_cd == 'dm':
\r
330 pgstat.suicides = 0
\r
331 elif game.game_type_cd == 'ctf':
\r
333 pgstat.captures = 0
\r
337 pgstat.carrier_frags = 0
\r
339 for (key,value) in player_events.items():
\r
340 if key == 'n': pgstat.nick = value[:128]
\r
341 if key == 't': pgstat.team = value
\r
342 if key == 'rank': pgstat.rank = value
\r
343 if key == 'alivetime':
\r
344 pgstat.alivetime = datetime.timedelta(seconds=int(round(float(value))))
\r
345 if key == 'scoreboard-drops': pgstat.drops = value
\r
346 if key == 'scoreboard-returns': pgstat.returns = value
\r
347 if key == 'scoreboard-fckills': pgstat.carrier_frags = value
\r
348 if key == 'scoreboard-pickups': pgstat.pickups = value
\r
349 if key == 'scoreboard-caps': pgstat.captures = value
\r
350 if key == 'scoreboard-score': pgstat.score = value
\r
351 if key == 'scoreboard-deaths': pgstat.deaths = value
\r
352 if key == 'scoreboard-kills': pgstat.kills = value
\r
353 if key == 'scoreboard-suicides': pgstat.suicides = value
\r
355 # check to see if we had a name, and if
\r
356 # not use the name from the player id
\r
357 if pgstat.nick == None:
\r
358 pgstat.nick = player.nick
\r
360 # if the nick we end up with is different from the one in the
\r
361 # player record, change the nick to reflect the new value
\r
362 if pgstat.nick != player.nick and player.player_id > 2:
\r
363 register_new_nick(session, player, pgstat.nick)
\r
365 # if the player is ranked #1 and it is a team game, set the game's winner
\r
366 # to be the team of that player
\r
367 # FIXME: this is a hack, should be using the 'W' field (not present)
\r
368 if pgstat.rank == '1' and pgstat.team:
\r
369 game.winner = pgstat.team
\r
372 session.add(pgstat)
\r
377 def create_player_weapon_stats(session=None, player=None,
\r
378 game=None, pgstat=None, player_events=None):
\r
380 Creates accuracy records for each weapon used by a given player in a
\r
381 given game. Parameters:
\r
383 session - SQLAlchemy session factory object
\r
384 player - Player record who owns the weapon stats
\r
385 game - Game record in which the stats were created
\r
386 pgstat - Corresponding PlayerGameStat record for these weapon stats
\r
387 player_events - dictionary containing the raw weapon values that need to be
\r
392 for (key,value) in player_events.items():
\r
393 matched = re.search("acc-(.*?)-cnt-fired", key)
\r
395 weapon_cd = matched.group(1)
\r
396 seq = Sequence('player_weapon_stats_player_weapon_stats_id_seq')
\r
397 pwstat_id = session.execute(seq)
\r
398 pwstat = PlayerWeaponStat()
\r
399 pwstat.player_weapon_stats_id = pwstat_id
\r
400 pwstat.player_id = player.player_id
\r
401 pwstat.game_id = game.game_id
\r
402 pwstat.player_game_stat_id = pgstat.player_game_stat_id
\r
403 pwstat.weapon_cd = weapon_cd
\r
405 if 'n' in player_events:
\r
406 pwstat.nick = player_events['n']
\r
408 pwstat.nick = player_events['P']
\r
410 if 'acc-' + weapon_cd + '-cnt-fired' in player_events:
\r
411 pwstat.fired = int(round(float(
\r
412 player_events['acc-' + weapon_cd + '-cnt-fired'])))
\r
413 if 'acc-' + weapon_cd + '-fired' in player_events:
\r
414 pwstat.max = int(round(float(
\r
415 player_events['acc-' + weapon_cd + '-fired'])))
\r
416 if 'acc-' + weapon_cd + '-cnt-hit' in player_events:
\r
417 pwstat.hit = int(round(float(
\r
418 player_events['acc-' + weapon_cd + '-cnt-hit'])))
\r
419 if 'acc-' + weapon_cd + '-hit' in player_events:
\r
420 pwstat.actual = int(round(float(
\r
421 player_events['acc-' + weapon_cd + '-hit'])))
\r
422 if 'acc-' + weapon_cd + '-frags' in player_events:
\r
423 pwstat.frags = int(round(float(
\r
424 player_events['acc-' + weapon_cd + '-frags'])))
\r
426 session.add(pwstat)
\r
427 pwstats.append(pwstat)
\r
432 def parse_body(request):
\r
434 Parses the POST request body for a stats submission
\r
436 # storage vars for the request body
\r
439 current_team = None
\r
442 for line in request.body.split('\n'):
\r
444 (key, value) = line.strip().split(' ', 1)
\r
446 # Server (S) and Nick (n) fields can have international characters.
\r
447 # We convert to UTF-8.
\r
449 value = unicode(value, 'utf-8')
\r
451 if key in 'V' 'T' 'G' 'M' 'S' 'C' 'R' 'W' 'I':
\r
452 game_meta[key] = value
\r
455 # if we were working on a player record already, append
\r
456 # it and work on a new one (only set team info)
\r
457 if len(player_events) != 0:
\r
458 players.append(player_events)
\r
461 player_events[key] = value
\r
464 (subkey, subvalue) = value.split(' ', 1)
\r
465 player_events[subkey] = subvalue
\r
467 player_events[key] = value
\r
469 player_events[key] = value
\r
471 # no key/value pair - move on to the next line
\r
474 # add the last player we were working on
\r
475 if len(player_events) > 0:
\r
476 players.append(player_events)
\r
478 return (game_meta, players)
\r
481 def create_player_stats(session=None, player=None, game=None,
\r
482 player_events=None):
\r
484 Creates player game and weapon stats according to what type of player
\r
486 pgstat = create_player_game_stat(session=session,
\r
487 player=player, game=game, player_events=player_events)
\r
489 #TODO: put this into a config setting in the ini file?
\r
490 if not re.search('^bot#\d+$', player_events['P']):
\r
491 create_player_weapon_stats(session=session,
\r
492 player=player, game=game, pgstat=pgstat,
\r
493 player_events=player_events)
\r
496 def stats_submit(request):
\r
498 Entry handler for POST stats submissions.
\r
501 session = DBSession()
\r
503 log.debug("\n----- BEGIN REQUEST BODY -----\n" + request.body +
\r
504 "----- END REQUEST BODY -----\n\n")
\r
506 (idfp, status) = verify_request(request)
\r
508 log.debug("ERROR: Unverified request")
\r
509 raise pyramid.httpexceptions.HTTPUnauthorized("Unverified request")
\r
511 (game_meta, players) = parse_body(request)
\r
513 if not has_required_metadata(game_meta):
\r
514 log.debug("ERROR: Required game meta missing")
\r
515 raise pyramid.exceptions.HTTPUnprocessableEntity("Missing game meta")
\r
517 if not is_supported_gametype(game_meta['G']):
\r
518 log.debug("ERROR: Unsupported gametype")
\r
519 raise pyramid.httpexceptions.HTTPOk("OK")
\r
521 if not has_minimum_real_players(request.registry.settings, players):
\r
522 log.debug("ERROR: Not enough real players")
\r
523 raise pyramid.httpexceptions.HTTPOk("OK")
\r
525 server = get_or_create_server(session=session, hashkey=idfp,
\r
526 name=game_meta['S'], revision=game_meta['R'],
\r
527 ip_addr=get_remote_addr(request))
\r
529 gmap = get_or_create_map(session=session, name=game_meta['M'])
\r
531 game = create_game(session=session,
\r
532 start_dt=datetime.datetime(
\r
533 *time.gmtime(float(game_meta['T']))[:6]),
\r
534 server_id=server.server_id, game_type_cd=game_meta['G'],
\r
535 map_id=gmap.map_id, match_id=game_meta['I'])
\r
537 # find or create a record for each player
\r
538 # and add stats for each if they were present at the end
\r
540 for player_events in players:
\r
541 if 'n' in player_events:
\r
542 nick = player_events['n']
\r
546 if 'matches' in player_events and 'scoreboardvalid' \
\r
548 player = get_or_create_player(session=session,
\r
549 hashkey=player_events['P'], nick=nick)
\r
550 log.debug('Creating stats for %s' % player_events['P'])
\r
551 create_player_stats(session=session, player=player, game=game,
\r
552 player_events=player_events)
\r
555 log.debug('Success! Stats recorded.')
\r
556 return Response('200 OK')
\r
557 except Exception as e:
\r