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 'S' not in metadata:
\r
85 flg_has_req_metadata = False
\r
87 return flg_has_req_metadata
\r
90 def is_real_player(events):
\r
92 Determines if a given set of player events correspond with a player who
\r
94 1) is not a bot (P event does not look like a bot)
\r
95 2) played in the game (matches 1)
\r
96 3) was present at the end of the game (scoreboardvalid 1)
\r
98 Returns True if the player meets the above conditions, and false otherwise.
\r
100 flg_is_real = False
\r
102 if not events['P'].startswith('bot'):
\r
103 # removing 'joins' here due to bug, but it should be here
\r
104 if 'matches' in events and 'scoreboardvalid' in events:
\r
110 def register_new_nick(session, player, new_nick):
\r
112 Change the player record's nick to the newly found nick. Store the old
\r
113 nick in the player_nicks table for that player.
\r
115 session - SQLAlchemy database session factory
\r
116 player - player record whose nick is changing
\r
117 new_nick - the new nickname
\r
119 # see if that nick already exists
\r
120 stripped_nick = strip_colors(player.nick)
\r
122 player_nick = session.query(PlayerNick).filter_by(
\r
123 player_id=player.player_id, stripped_nick=stripped_nick).one()
\r
124 except NoResultFound, e:
\r
125 # player_id/stripped_nick not found, create one
\r
126 # but we don't store "Anonymous Player #N"
\r
127 if not re.search('^Anonymous Player #\d+$', player.nick):
\r
128 player_nick = PlayerNick()
\r
129 player_nick.player_id = player.player_id
\r
130 player_nick.stripped_nick = player.stripped_nick
\r
131 player_nick.nick = player.nick
\r
132 session.add(player_nick)
\r
134 # We change to the new nick regardless
\r
135 player.nick = new_nick
\r
136 player.stripped_nick = strip_colors(new_nick)
\r
137 session.add(player)
\r
140 def get_or_create_server(session=None, name=None, hashkey=None, ip_addr=None,
\r
143 Find a server by name or create one if not found. Parameters:
\r
145 session - SQLAlchemy database session factory
\r
146 name - server name of the server to be found or created
\r
147 hashkey - server hashkey
\r
150 # find one by that name, if it exists
\r
151 server = session.query(Server).filter_by(name=name).one()
\r
153 # store new hashkey
\r
154 if server.hashkey != hashkey:
\r
155 server.hashkey = hashkey
\r
156 session.add(server)
\r
158 # store new IP address
\r
159 if server.ip_addr != ip_addr:
\r
160 server.ip_addr = ip_addr
\r
161 session.add(server)
\r
163 # store new revision
\r
164 if server.revision != revision:
\r
165 server.revision = revision
\r
166 session.add(server)
\r
168 log.debug("Found existing server {0}".format(server.server_id))
\r
170 except MultipleResultsFound, e:
\r
171 # multiple found, so also filter by hashkey
\r
172 server = session.query(Server).filter_by(name=name).\
\r
173 filter_by(hashkey=hashkey).one()
\r
174 log.debug("Found existing server {0}".format(server.server_id))
\r
176 except NoResultFound, e:
\r
177 # not found, create one
\r
178 server = Server(name=name, hashkey=hashkey)
\r
179 session.add(server)
\r
181 log.debug("Created server {0} with hashkey {1}".format(
\r
182 server.server_id, server.hashkey))
\r
187 def get_or_create_map(session=None, name=None):
\r
189 Find a map by name or create one if not found. Parameters:
\r
191 session - SQLAlchemy database session factory
\r
192 name - map name of the map to be found or created
\r
195 # find one by the name, if it exists
\r
196 gmap = session.query(Map).filter_by(name=name).one()
\r
197 log.debug("Found map id {0}: {1}".format(gmap.map_id,
\r
199 except NoResultFound, e:
\r
200 gmap = Map(name=name)
\r
203 log.debug("Created map id {0}: {1}".format(gmap.map_id,
\r
205 except MultipleResultsFound, e:
\r
206 # multiple found, so use the first one but warn
\r
208 gmaps = session.query(Map).filter_by(name=name).order_by(
\r
211 log.debug("Found map id {0}: {1} but found \
\r
212 multiple".format(gmap.map_id, gmap.name))
\r
217 def create_game(session=None, start_dt=None, game_type_cd=None,
\r
218 server_id=None, map_id=None, winner=None):
\r
220 Creates a game. Parameters:
\r
222 session - SQLAlchemy database session factory
\r
223 start_dt - when the game started (datetime object)
\r
224 game_type_cd - the game type of the game being played
\r
225 server_id - server identifier of the server hosting the game
\r
226 map_id - map on which the game was played
\r
227 winner - the team id of the team that won
\r
229 seq = Sequence('games_game_id_seq')
\r
230 game_id = session.execute(seq)
\r
231 game = Game(game_id=game_id, start_dt=start_dt, game_type_cd=game_type_cd,
\r
232 server_id=server_id, map_id=map_id, winner=winner)
\r
234 log.debug("Created game id {0} on server {1}, map {2} at \
\r
235 {3}".format(game.game_id,
\r
236 server_id, map_id, start_dt))
\r
241 def get_or_create_player(session=None, hashkey=None, nick=None):
\r
243 Finds a player by hashkey or creates a new one (along with a
\r
244 corresponding hashkey entry. Parameters:
\r
246 session - SQLAlchemy database session factory
\r
247 hashkey - hashkey of the player to be found or created
\r
248 nick - nick of the player (in case of a first time create)
\r
251 if re.search('^bot#\d+$', hashkey):
\r
252 player = session.query(Player).filter_by(player_id=1).one()
\r
253 # if we have an untracked player
\r
254 elif re.search('^player#\d+$', hashkey):
\r
255 player = session.query(Player).filter_by(player_id=2).one()
\r
256 # else it is a tracked player
\r
258 # see if the player is already in the database
\r
259 # if not, create one and the hashkey along with it
\r
261 hk = session.query(Hashkey).filter_by(
\r
262 hashkey=hashkey).one()
\r
263 player = session.query(Player).filter_by(
\r
264 player_id=hk.player_id).one()
\r
265 log.debug("Found existing player {0} with hashkey {1}".format(
\r
266 player.player_id, hashkey))
\r
269 session.add(player)
\r
272 # if nick is given to us, use it. If not, use "Anonymous Player"
\r
273 # with a suffix added for uniqueness.
\r
275 player.nick = nick[:128]
\r
276 player.stripped_nick = strip_colors(nick[:128])
\r
278 player.nick = "Anonymous Player #{0}".format(player.player_id)
\r
279 player.stripped_nick = player.nick
\r
281 hk = Hashkey(player_id=player.player_id, hashkey=hashkey)
\r
283 log.debug("Created player {0} ({2}) with hashkey {1}".format(
\r
284 player.player_id, hashkey, player.nick.encode('utf-8')))
\r
288 def create_player_game_stat(session=None, player=None,
\r
289 game=None, player_events=None):
\r
291 Creates game statistics for a given player in a given game. Parameters:
\r
293 session - SQLAlchemy session factory
\r
294 player - Player record of the player who owns the stats
\r
295 game - Game record for the game to which the stats pertain
\r
296 player_events - dictionary for the actual stats that need to be transformed
\r
299 # in here setup default values (e.g. if game type is CTF then
\r
300 # set kills=0, score=0, captures=0, pickups=0, fckills=0, etc
\r
301 # TODO: use game's create date here instead of now()
\r
302 seq = Sequence('player_game_stats_player_game_stat_id_seq')
\r
303 pgstat_id = session.execute(seq)
\r
304 pgstat = PlayerGameStat(player_game_stat_id=pgstat_id,
\r
305 create_dt=datetime.datetime.utcnow())
\r
307 # set player id from player record
\r
308 pgstat.player_id = player.player_id
\r
310 #set game id from game record
\r
311 pgstat.game_id = game.game_id
\r
313 # all games have a score
\r
316 if game.game_type_cd == 'dm':
\r
319 pgstat.suicides = 0
\r
320 elif game.game_type_cd == 'ctf':
\r
322 pgstat.captures = 0
\r
326 pgstat.carrier_frags = 0
\r
328 for (key,value) in player_events.items():
\r
329 if key == 'n': pgstat.nick = value[:128]
\r
330 if key == 't': pgstat.team = value
\r
331 if key == 'rank': pgstat.rank = value
\r
332 if key == 'alivetime':
\r
333 pgstat.alivetime = datetime.timedelta(seconds=int(round(float(value))))
\r
334 if key == 'scoreboard-drops': pgstat.drops = value
\r
335 if key == 'scoreboard-returns': pgstat.returns = value
\r
336 if key == 'scoreboard-fckills': pgstat.carrier_frags = value
\r
337 if key == 'scoreboard-pickups': pgstat.pickups = value
\r
338 if key == 'scoreboard-caps': pgstat.captures = value
\r
339 if key == 'scoreboard-score': pgstat.score = value
\r
340 if key == 'scoreboard-deaths': pgstat.deaths = value
\r
341 if key == 'scoreboard-kills': pgstat.kills = value
\r
342 if key == 'scoreboard-suicides': pgstat.suicides = value
\r
344 # check to see if we had a name, and if
\r
345 # not use the name from the player id
\r
346 if pgstat.nick == None:
\r
347 pgstat.nick = player.nick
\r
349 # if the nick we end up with is different from the one in the
\r
350 # player record, change the nick to reflect the new value
\r
351 if pgstat.nick != player.nick and player.player_id > 2:
\r
352 register_new_nick(session, player, pgstat.nick)
\r
354 # if the player is ranked #1 and it is a team game, set the game's winner
\r
355 # to be the team of that player
\r
356 # FIXME: this is a hack, should be using the 'W' field (not present)
\r
357 if pgstat.rank == '1' and pgstat.team:
\r
358 game.winner = pgstat.team
\r
361 session.add(pgstat)
\r
366 def create_player_weapon_stats(session=None, player=None,
\r
367 game=None, pgstat=None, player_events=None):
\r
369 Creates accuracy records for each weapon used by a given player in a
\r
370 given game. Parameters:
\r
372 session - SQLAlchemy session factory object
\r
373 player - Player record who owns the weapon stats
\r
374 game - Game record in which the stats were created
\r
375 pgstat - Corresponding PlayerGameStat record for these weapon stats
\r
376 player_events - dictionary containing the raw weapon values that need to be
\r
381 for (key,value) in player_events.items():
\r
382 matched = re.search("acc-(.*?)-cnt-fired", key)
\r
384 weapon_cd = matched.group(1)
\r
385 seq = Sequence('player_weapon_stats_player_weapon_stats_id_seq')
\r
386 pwstat_id = session.execute(seq)
\r
387 pwstat = PlayerWeaponStat()
\r
388 pwstat.player_weapon_stats_id = pwstat_id
\r
389 pwstat.player_id = player.player_id
\r
390 pwstat.game_id = game.game_id
\r
391 pwstat.player_game_stat_id = pgstat.player_game_stat_id
\r
392 pwstat.weapon_cd = weapon_cd
\r
394 if 'n' in player_events:
\r
395 pwstat.nick = player_events['n']
\r
397 pwstat.nick = player_events['P']
\r
399 if 'acc-' + weapon_cd + '-cnt-fired' in player_events:
\r
400 pwstat.fired = int(round(float(
\r
401 player_events['acc-' + weapon_cd + '-cnt-fired'])))
\r
402 if 'acc-' + weapon_cd + '-fired' in player_events:
\r
403 pwstat.max = int(round(float(
\r
404 player_events['acc-' + weapon_cd + '-fired'])))
\r
405 if 'acc-' + weapon_cd + '-cnt-hit' in player_events:
\r
406 pwstat.hit = int(round(float(
\r
407 player_events['acc-' + weapon_cd + '-cnt-hit'])))
\r
408 if 'acc-' + weapon_cd + '-hit' in player_events:
\r
409 pwstat.actual = int(round(float(
\r
410 player_events['acc-' + weapon_cd + '-hit'])))
\r
411 if 'acc-' + weapon_cd + '-frags' in player_events:
\r
412 pwstat.frags = int(round(float(
\r
413 player_events['acc-' + weapon_cd + '-frags'])))
\r
415 session.add(pwstat)
\r
416 pwstats.append(pwstat)
\r
421 def parse_body(request):
\r
423 Parses the POST request body for a stats submission
\r
425 # storage vars for the request body
\r
428 current_team = None
\r
431 for line in request.body.split('\n'):
\r
433 (key, value) = line.strip().split(' ', 1)
\r
435 # Server (S) and Nick (n) fields can have international characters.
\r
436 # We convert to UTF-8.
\r
438 value = unicode(value, 'utf-8')
\r
440 if key in 'V' 'T' 'G' 'M' 'S' 'C' 'R' 'W':
\r
441 game_meta[key] = value
\r
444 # if we were working on a player record already, append
\r
445 # it and work on a new one (only set team info)
\r
446 if len(player_events) != 0:
\r
447 players.append(player_events)
\r
450 player_events[key] = value
\r
453 (subkey, subvalue) = value.split(' ', 1)
\r
454 player_events[subkey] = subvalue
\r
456 player_events[key] = value
\r
458 player_events[key] = value
\r
460 # no key/value pair - move on to the next line
\r
463 # add the last player we were working on
\r
464 if len(player_events) > 0:
\r
465 players.append(player_events)
\r
467 return (game_meta, players)
\r
470 def create_player_stats(session=None, player=None, game=None,
\r
471 player_events=None):
\r
473 Creates player game and weapon stats according to what type of player
\r
475 pgstat = create_player_game_stat(session=session,
\r
476 player=player, game=game, player_events=player_events)
\r
478 #TODO: put this into a config setting in the ini file?
\r
479 if not re.search('^bot#\d+$', player_events['P']):
\r
480 create_player_weapon_stats(session=session,
\r
481 player=player, game=game, pgstat=pgstat,
\r
482 player_events=player_events)
\r
485 def stats_submit(request):
\r
487 Entry handler for POST stats submissions.
\r
490 session = DBSession()
\r
492 log.debug("\n----- BEGIN REQUEST BODY -----\n" + request.body +
\r
493 "----- END REQUEST BODY -----\n\n")
\r
495 (idfp, status) = verify_request(request)
\r
497 raise pyramid.httpexceptions.HTTPUnauthorized
\r
499 log.debug('Remote address:')
\r
500 log.debug(get_remote_addr(request))
\r
502 (game_meta, players) = parse_body(request)
\r
504 if not has_required_metadata(game_meta):
\r
505 log.debug("Required game meta fields missing. "\
\r
507 raise pyramid.exceptions.HTTPUnprocessableEntity
\r
509 if not is_supported_gametype(game_meta['G']):
\r
510 raise pyramid.httpexceptions.HTTPOk
\r
512 if not has_minimum_real_players(request.registry.settings, players):
\r
513 log.debug("The number of real players is below the minimum. " +
\r
514 "Stats will be ignored.")
\r
515 raise pyramid.httpexceptions.HTTPOk
\r
517 server = get_or_create_server(session=session, hashkey=idfp,
\r
518 name=game_meta['S'], revision=game_meta['R'],
\r
519 ip_addr=get_remote_addr(request))
\r
521 gmap = get_or_create_map(session=session, name=game_meta['M'])
\r
524 game = create_game(session=session,
\r
525 start_dt=datetime.datetime(
\r
526 *time.gmtime(float(game_meta['T']))[:6]),
\r
527 server_id=server.server_id, game_type_cd=game_meta['G'],
\r
528 map_id=gmap.map_id)
\r
531 # find or create a record for each player
\r
532 # and add stats for each if they were present at the end
\r
534 for player_events in players:
\r
535 if 'n' in player_events:
\r
536 nick = player_events['n']
\r
540 if 'matches' in player_events and 'scoreboardvalid' \
\r
542 player = get_or_create_player(session=session,
\r
543 hashkey=player_events['P'], nick=nick)
\r
544 log.debug('Creating stats for %s' % player_events['P'])
\r
545 create_player_stats(session=session, player=player, game=game,
\r
546 player_events=player_events)
\r
549 log.debug('Success! Stats recorded.')
\r
550 return Response('200 OK')
\r
551 except Exception as e:
\r