import asyncio import websockets import json from session_manager import SessionManager from user_manager import UserManager from match_manager import MatchManager from match import Match from tools import debug, elo_p_win, elo_update import datetime import re def parse_message(msg: str): # TODO: make it more robust by validating each part of a message msg_obj = json.loads(msg) if "type" not in msg_obj or "data" not in msg_obj: debug("got strange message") return None return msg_obj def valid_name(name: str) -> bool: return re.match('^[a-z0-9_-]{3,16}$', name) is not None class Connection(object): def __init__(self, id, user_name: str, registered: bool, websocket: websockets.WebSocketServerProtocol): self.id = id self.user_name = user_name self.websocket = websocket self.is_registered_user = registered self.is_closed = False async def send(self, msg): try: await self.websocket.send(msg) except Exception as e: debug("error sending message to user " + self.user_name + ". Reason: " + str(e)) async def close(self): try: if self.is_closed: return self.websocket.close() self.is_closed = True except Exception as e: debug("error closing session to user " + self.user_name + ". Reason: " + str(e)) class ConnectionHandler(object): def __init__(self, session_manager: SessionManager, user_manager: UserManager, match_manager: MatchManager, revoke_check_timedelta): self.session_manager = session_manager self.user_manager = user_manager self.match_manager = match_manager self.revoke_check_timedelta = revoke_check_timedelta self.lastCheck = datetime.datetime.now() self.open_connections_by_user = {} self.open_connections_by_id = {} self.match_queue = set() def reconnect_session(self, socket: websockets.WebSocketServerProtocol, session_id: str): # check whether id exists tmp = self.session_manager.get_session_by_id(session_id) if len(tmp) == 0: # session not available! return None session_obj = tmp[0] is_registerd = session_obj['registered_user'] is not None user_name = session_obj['registered_user'] if is_registerd else session_obj['temp_user'] conn = Connection(id=session_id, user_name=user_name, registered=is_registerd, websocket=socket) self.session_manager.touch_session(session_id) return conn def close_connection(self, conn: Connection): if conn.id in self.open_connections_by_id: del(self.open_connections_by_id[conn.id]) del(self.open_connections_by_user[conn.user_name]) conn.close() def close_session(self, id): tmp = self.session_manager.get_session_by_id(id) if len(tmp) == 0: return if id in self.open_connections_by_id: self.close_connection([id]) self.session_manager.delete_session(id) def _add_connection(self, conn): self.open_connections_by_id[conn.id] = conn self.open_connections_by_user[conn.user_name] = conn def _del_connection(self, conn): del(self.open_connections_by_id[conn.id]) del(self.open_connections_by_user[conn.user_name]) async def new_connection(self, socket: websockets.WebSocketServerProtocol, login_msg: str): msg = parse_message(login_msg) debug("new incomming connection...") if msg is None: return None if msg['type'] == 'reconnect': conn = self.reconnect_session(socket, msg['data']['id']) if conn is not None: self._add_connection(conn) await socket.send(json.dumps({ "type": "reconnect_response", "data": { "success": True, "id": conn.id, "user": conn.user_name, "registered": conn.is_registered_user, "msg": "" } })) await self.send_elo(conn) if conn.is_registered_user: await self.send_friends(conn) await self._on_match_state_req(conn, None) return conn await socket.send(json.dumps({ "type": "reconnect_response", "data": { "success": False, "id": None, "user": None, "registered": False, "msg": "session not available" } })) return None elif msg['type'] == 'login': response_msg = "" success = False session_id = None name = None pw = None conn = None registered_user = True try: name = msg['data']['name'].lower() pw = msg['data']['pw'] if valid_name(name) and len(name) <= 16 and len(pw) <= 32 and len(name) > 0 and len(pw) > 0: users = self.user_manager.get_user(name) temp_sessions = self.session_manager.get_session_by_temp_user(name) if len(users) == 0 and len(temp_sessions) == 0: # user does not exists: self.user_manager.create_user(name, pw) session_id = self.session_manager.create_session_for_registered_user( name) response_msg = "successful registered and logged in user " + name success = True elif self.user_manager.verify_user(name, pw): session_id = self.session_manager.create_session_for_registered_user( name) response_msg = "successful logged in as user " + name success = True else: if len(temp_sessions) == 0: response_msg = "invalid password for user " + name else: response_msg = f"the username {name} is currently not available" elif valid_name(name) and len(name) <= 16 and len(pw) == 0: # no password -> temporary account registered_user = False # check whether already a temporary session exists for that user: if (len(self.session_manager.get_session_by_temp_user(name)) > 0) or (len(self.user_manager.get_user(name)) > 0): response_msg = f"user '{name}' already exists" success = False else: session_id = self.session_manager.create_session_for_temp_user( name) response_msg = f"created a temporary session for user {name}" success = True else: response_msg = "invalid username or pw. Only usernames containing alphanumerical symbols (including '_','-') between 3 and 16 characters are allowed!" except Exception as e: print("error: " + str(e)) response_msg = "invalid username or pw. Only usernames containing alphanumerical symbols (including '_','-') between 3 and 16 characters are allowed!" if success: conn = Connection(id=session_id, user_name=name, registered=registered_user, websocket=socket) self._add_connection(conn) await socket.send(json.dumps({ "type": "login_response", "data": { "success": success, "id": session_id, "registered": registered_user, "msg": response_msg } })) if success: await self.send_elo(conn) await self.send_friends(conn) await self._on_match_state_req(conn, None) return conn return None async def _start_match(self, user_a, user_b): m = self.match_manager.create_new_match(user_a, user_b) state = json.loads(m.to_json_state()) if user_a in self.open_connections_by_user: await self.open_connections_by_user[user_a].websocket.send( json.dumps( { "type": "match_update", "data": { "id": m.id, "revoke_time": m.get_sql_revoke_time(), "ranked": m.ranked, "match_state": state } } ) ) if user_b in self.open_connections_by_user: await self.open_connections_by_user[user_b].websocket.send( json.dumps( { "type": "match_update", "data": { "id": m.id, "revoke_time": m.get_sql_revoke_time(), "ranked": m.ranked, "match_state": state } } ) ) async def _on_match_req(self, conn, data): n_open_matches = len( self.match_manager.get_matches_for_user(conn.user_name)) if n_open_matches >= 5: await conn.websocket.send( json.dumps( { "type": "match_request_response", "data": { "success": False, "msg": "you have too many active matches to search for a new one" } } ) ) return if data['player'] is None: if conn.user_name in self.match_queue: await conn.websocket.send( json.dumps( { "type": "match_request_response", "data": { "success": False, "msg": "you are already searching for a random match" } } ) ) return if len(self.match_queue) > 0: # it's a match! user_a = self.match_queue.pop() await self._start_match(user_a, conn.user_name) else: if conn.user_name not in self.match_queue: self.match_queue.add(conn.user_name) await conn.websocket.send( json.dumps( { "type": "match_request_response", "data": { "success": True, "msg": "created match request" } } ) ) else: opponent = data['player'].lower() if opponent == conn.user_name: await conn.websocket.send( json.dumps( { "type": "match_request_response", "data": { "success": False, "msg": "you cannot play against yourself" } } ) ) return try: if valid_name(opponent): if len(self.user_manager.get_user(opponent)) > 0 or len(self.session_manager.get_session_by_temp_user(opponent)) > 0: if len(self.match_manager.get_matches_for_user(opponent)) >= 5: await conn.websocket.send( json.dumps( { "type": "match_request_response", "data": { "success": False, "msg": "player " + opponent + " has too many open matches" } } ) ) return if (self.match_manager.is_match(conn.user_name, opponent)): await conn.websocket.send( json.dumps( { "type": "match_request_response", "data": { "success": False, "msg": "you are already plaing against " + opponent } } ) ) return await self._start_match(conn.user_name, opponent) await conn.websocket.send( json.dumps( { "type": "match_request_response", "data": { "success": True, "msg": "startet match against " + opponent } } ) ) else: await conn.websocket.send( json.dumps( { "type": "match_request_response", "data": { "success": False, "msg": "user " + opponent + " not found :(" } } ) ) else: await conn.websocket.send( json.dumps( { "type": "match_request_response", "data": { "success": False, "msg": "user " + opponent + " not found :(" } } ) ) except Exception as e: debug("error processing match request: " + str(data) + str(e)) async def _on_match_state_req(self, conn, data): db_matches = self.match_manager.get_matches_for_user(conn.user_name) for db_match in db_matches: match = self.match_manager.get_match(db_match['id']) await conn.send(json.dumps({ "type": "match_update", "data": { "id": db_match['id'], "revoke_time": match.get_sql_revoke_time(), "ranked": match.ranked, "match_state": json.loads(match.to_json_state()) } })) if match.game_over: if match.player_won is None or match.player_won != conn.user_name: self.match_manager.delete_match(match.id) async def _on_match_move(self, conn, data): match = None try: sub_x = int(data['sub_x']) sub_y = int(data['sub_y']) x = int(data['x']) y = int(data['y']) if type(sub_x) is int and type(sub_y) is int: if type(x) is int and type(y) is int: if type(data['id']) is str: match = self.match_manager.apply_move(data) finally: match_state = None if match is not None: match_state = match.to_json_state() debug(match_state) await conn.send(json.dumps({ 'type': 'match_update', 'data': { 'id': match.id, 'revoke_time': match.get_sql_revoke_time(), 'ranked': match.ranked, 'match_state': json.loads(match_state) } })) if match.game_over: if match.is_draw or (match.player_won is not None): # send rank update await self.send_elo(conn) other_user = match.player_a_name if conn.user_name == match.player_b_name else match.player_b_name if other_user in self.open_connections_by_user: other_conn = self.open_connections_by_user[other_user] await other_conn.send(json.dumps({ 'type': 'match_update', 'data': { 'id': match.id, 'revoke_time': match.get_sql_revoke_time(), 'ranked': match.ranked, 'match_state': json.loads(match_state) } })) if match.game_over: self.match_manager.delete_match(match.id) if match.is_draw or (match.player_won is not None): # send rank update await self.send_elo(other_conn) async def _on_match_close(self, conn, data): match = None try: match_id = data['id'] if type(match_id) is str: match = self.match_manager.get_match(match_id) if (match is None): return if not match.game_over: if match.ranked: # check whether both player made a move. If so, the match is ranked as lost for the player who aborted the match if match.complete_field.__contains__(Match.FIELD_USER_A) and match.complete_field.__contains__(Match.FIELD_USER_B): # update rankings: player_lost = conn.user_name player_won = match.player_a_name if player_lost == match.player_b_name else match.player_b_name elo_won = self.user_manager.get_elo(player_won) elo_lost = self.user_manager.get_elo(player_lost) # calculate elo values: p_won = elo_p_win(elo_won, elo_lost) p_lost = 1 - p_won new_elo_won = elo_update(elo_won, 1, p_won) new_elo_lost = elo_update(elo_lost, 0, p_lost) self.user_manager.update_elo( player_won, new_elo_won) self.user_manager.update_elo( player_lost, new_elo_lost) await self.send_elo(conn) if player_won in self.open_connections_by_user: other_conn = self.open_connections_by_user[player_won] await self.send_elo(other_conn) debug( f"Match {match.id} is aborted by {player_lost} (against {player_won}). Update elo-rankings: {elo_won}->{new_elo_won} and {elo_lost}->{new_elo_lost}") match.game_over = True match_state = match.to_json_state() opponent = match.player_a_name if match.player_a_name != conn.user_name else match.player_b_name response = json.dumps({ 'type': 'match_update', 'data': { 'id': match_id, 'revoke_time': match.get_sql_revoke_time(), 'ranked': match.ranked, 'match_state': json.loads(match_state) } }) if opponent in self.open_connections_by_user: await self.open_connections_by_user[opponent].websocket.send(response) await conn.send(response) self.match_manager.delete_match(match_id) except Exception as e: match_state = None if match is not None: match_state = match.to_json_state() if match_state is not None: await conn.send(json.dumps({ 'type': 'match_update', 'data': { 'id': match.id, 'revoke_time': match.get_sql_revoke_time(), 'ranked': match.ranked, 'match_state': json.loads(match_state) } })) async def _on_friend_request(self, conn, data): msg = "error in handling friend request" success = False try: friend = data['user'].lower() # check for user: if valid_name(friend): if not conn.is_registered_user: success = False msg = "you cannot have friends without an account" if friend in self.user_manager.get_friends_for_user(conn.user_name): success = False msg = f"'{friend}' is already your friend" elif self.user_manager.add_friend_to_user(conn.user_name, friend): success = True msg = f"added '{friend}' as a friend" else: success = False msg = f"player '{friend}' not found" else: success = False msg = "misformated friend request" finally: await conn.send(json.dumps({ 'type': 'friend_request_response', 'data': { 'success': success, 'msg': msg } })) if success: await self.send_friends(conn) async def _on_unfriend_request(self, conn, data): success = False msg = "error in handling unfriend request" try: friend = data['user'].lower() if valid_name(friend): if friend not in self.user_manager.get_friends_for_user(conn.user_name): success = False msg = f"cannot end friendship with '{friend}': it's not one of your friends" else: self.user_manager.remove_friend_from_user( conn.user_name, friend) success = True msg = f"removed '{friend}' from your friend list" finally: await conn.send(json.dumps({ 'type': 'unfriend_request_response', 'data': { 'success': success, 'msg': msg } })) if success: await self.send_friends(conn) async def send_friends(self, conn): friends, elos = self.user_manager.get_friends_and_elos_for_user( conn.user_name) await conn.send(json.dumps({ 'type': 'friends_update', 'data': { 'friends': friends, 'elos': elos } })) async def send_elo(self, conn): top_names, top_elos = self.user_manager.get_highscores(100, 0) if not conn.is_registered_user: await conn.send(json.dumps({ 'type': 'elo_update', 'data': { 'elo': None, 'rank': None, 'top_names': top_names, 'top_elos': top_elos } })) return await conn.send(json.dumps({ 'type': 'elo_update', 'data': { 'elo': self.user_manager.get_elo(conn.user_name), 'rank': self.user_manager.get_rank_for_user(conn.user_name), 'top_names': top_names, 'top_elos': top_elos } })) async def disconnect(self, conn): self._del_connection(conn) async def revoke_check(self): now = datetime.datetime.now() if self.lastCheck + self.revoke_check_timedelta < now: self.lastCheck = datetime.datetime.now() self.match_manager.check_matches_lifespan() self.session_manager.revoke_inactive_sessions() self.session_manager.revoke_inactive_temporary_sessions() self.user_manager.revoke_inactive_accounts() async def handle_message(self, conn, msg_str): msg = parse_message(msg_str) debug("incoming message" + str(msg)) if msg is None: return None t = msg['type'] if conn.is_registered_user: self.user_manager.touch_user(conn.user_name) self.session_manager.touch_session(conn.id) if t == "match_request": await self._on_match_req(conn, msg['data']) elif t == "move": await self._on_match_move(conn, msg['data']) elif t == "end_match": await self._on_match_close(conn, msg['data']) elif t == "match_states_request": await self._on_match_state_req(conn, msg['data']) elif t == "friend_request": await self._on_friend_request(conn, msg['data']) elif t == "unfriend_request": await self._on_unfriend_request(conn, msg['data']) else: debug("could not interpret message: " + msg_str) await self.revoke_check()