From a4d9fa797cd77abdb8d9e800d9113b07a578e230 Mon Sep 17 00:00:00 2001 From: codey Date: Thu, 19 Sep 2024 20:20:16 -0400 Subject: [PATCH] revisions / additions / GOD DAMN IT --- cah/constructors.py | 25 ++++- cah/websocket_conn.py | 21 +++- endpoints/cah.py | 230 +++++++++++++++++++++++++++++++++++++----- 3 files changed, 242 insertions(+), 34 deletions(-) diff --git a/cah/constructors.py b/cah/constructors.py index 0d2ef86..4a08a0c 100644 --- a/cah/constructors.py +++ b/cah/constructors.py @@ -6,21 +6,22 @@ class CAHClient: platform: str, csid: str, connected_at: int, - current_game: str | None): + players: list): self.resource: str = resource self.platform: str = platform self.csid: str = csid self.connected_at: int = connected_at - self.current_game: str | None = current_game + self.players: list = players def __iter__(self): return [value for value in self.__dict__.values() if isinstance(value, int) or isinstance(value, float)].__iter__() - + class CAHGame: def __init__(self, id: str, rounds: int, + resources: list[dict], players: list[dict], created_at: int, state: int, @@ -29,6 +30,7 @@ class CAHGame: ): self.id: str = id self.rounds: int = rounds + self.resources: list[dict] = resources self.players: list[dict] = players self.created_at: int = created_at self.state: int = state @@ -37,4 +39,21 @@ class CAHGame: def __iter__(self): return [value for value in self.__dict__.values() if isinstance(value, int) or isinstance(value, float)].__iter__() + +class CAHPlayer: + def __init__(self, + id: str, + current_game: CAHGame, + platform: str, + related_resource: str, + joined_at: str, + handle: str): + self.id = id + self.current_game = current_game + self.platform = platform + self.related_resource = related_resource + self.joined_at = joined_at + self.handle = handle + + \ No newline at end of file diff --git a/cah/websocket_conn.py b/cah/websocket_conn.py index 88114de..2a86a6c 100644 --- a/cah/websocket_conn.py +++ b/cah/websocket_conn.py @@ -29,7 +29,6 @@ class ConnectionManager: 'resource': _client.resource, 'platform': _client.platform, 'connected_at': _client.connected_at, - 'current_game': _client.current_game if _client.current_game else None, }) await websocket.send_json({ @@ -62,6 +61,8 @@ class ConnectionManager: csid: str, handshakedClient: CAHClient): + if websocket in self.active_connections: + self.active_connections.pop(websocket) self.active_connections[websocket] = { 'websocket': websocket, 'csid': csid, @@ -80,12 +81,26 @@ class ConnectionManager: await self.send_client_and_game_lists(state, websocket) - def disconnect(self, websocket: WebSocket, csid: str = None): + async def disconnect(self, state, websocket: WebSocket, csid: str = None): + disconnected = self.get_connection_by_ws(websocket) + disconnected_client = disconnected.get('client') + disconnected_resource = disconnected_client.resource + await self.broadcast({ + "event": "client_disconnected", + "ts": int(time.time()), + "data": { + "disconnected_resource": disconnected_resource, + } + }) self.active_connections.pop(websocket) + async def send(self, message: str, websocket: WebSocket): await websocket.send_json(message) async def broadcast(self, message: str): for connection in self.active_connections: - await connection.send_json(message) \ No newline at end of file + try: + await connection.send_json(message) + except: + continue \ No newline at end of file diff --git a/endpoints/cah.py b/endpoints/cah.py index a17294d..40c690a 100644 --- a/endpoints/cah.py +++ b/endpoints/cah.py @@ -1,12 +1,14 @@ #!/usr/bin/env python3.12 from fastapi import FastAPI, WebSocket, WebSocketDisconnect +from fastapi_utils.tasks import repeat_every import time import uuid import json +import asyncio import traceback import random -from cah.constructors import CAHClient, CAHGame +from cah.constructors import CAHClient, CAHPlayer, CAHGame from cah.websocket_conn import ConnectionManager class CAH(FastAPI): @@ -36,6 +38,78 @@ class CAH(FastAPI): for endpoint, handler in self.endpoints.items(): app.add_api_route(f"/{endpoint}/", handler, methods=["POST"]) + + asyncio.get_event_loop().create_task(self.send_heartbeats()) + + + + async def send_heartbeats(self): + while True: + print("Heartbeat!") + await self.connection_manager.broadcast({ + "event": "heartbeat", + "ts": int(time.time()) + }) + await asyncio.sleep(5) + + + async def remove_player(self, game: str, player: str): + try: + _game = None + for __game in self.games: + if __game.id == game: + _game = __game + print(f"Got game!!!\n{game}") + for _player in _game.players: + if _player.get('id') == player: + _game.players.pop(_player) + await self.connection_manager.broadcast({ + 'event': 'player_left', + 'ts': int(time.time()), + 'data': { + 'player': _player + } + }) # Change to broadcast to current game members only + except: + print(traceback.format_exc()) + return { + 'err': True, + 'errorText': 'Server error' + } + + + + async def join_player(self, player: CAHPlayer, game: str): + joined_game = self.get_game_by_id(game) + if not joined_game: + return { + 'err': True, + 'errorText': 'Game not found', + 'data': { + 'requestedGame': game + } + } + if player.current_game == joined_game: + return { + 'err': True, + 'errorText': 'You are already here.', + } + + + joined_game.players.append(player.__dict__) + await self.connection_manager.broadcast({ + 'event': 'player_joined', + 'ts': int(time.time()), + 'data': { + 'player': player + } + }) # Change to broadcast to current game members only + return joined_game + + + + + async def cah_handler(self, websocket: WebSocket): """/cah WebSocket""" @@ -62,7 +136,54 @@ class CAH(FastAPI): data) case 'create_game': await self.create_game(websocket, - data) + data) + case 'join_game': + sender = self.connection_manager.get_connection_by_ws(websocket) + sender_client = sender.get('client') + handle = data.get('handle') + game = data.get('game') + player = CAHPlayer(id=str(uuid.uuid4()), + current_game=game, + platform=sender_client.platform, + related_resource=sender_client.resource, + joined_at=int(time.time()), + handle=handle) + joined = await self.join_player(player, game) + if type(joined) == dict and joined.get('err'): + await sender.get('websocket').send_json({ + 'event': 'join_game_response', + 'ts': int(time.time()), + 'err': True, + 'errorText': joined.get('errorText'), + }) + else: + await sender.get('websocket').send_json({ + 'event': 'join_game_response', + 'ts': int(time.time()), + 'success': True, + 'data': { + 'game': joined.__dict__, + } + }) + case 'leave_game': + sender = self.connection_manager.get_connection_by_ws(websocket) + player_handle = data.get('handle') + game = data.get('game') + player = self.get_player_by_handle(game, player_handle) + left = await self.remove_player(game, player) + if type(left) == dict and left.get('err'): + await sender.get('websocket').send_json({ + 'event': 'leave_game_response', + 'ts': int(time.time()), + 'err': True, + 'errorText': left.get('errorText'), + }) + else: + await sender.get('websocket').send_json({ + 'event': 'leave_game_response', + 'ts': int(time.time()), + 'success': True, + }) case _: sender = self.connection_manager.get_connection_by_ws(websocket) await self.connection_manager.broadcast({ @@ -72,17 +193,46 @@ class CAH(FastAPI): "data": data, }) except WebSocketDisconnect: - disconnected = self.connection_manager.get_connection_by_ws(websocket) - self.connection_manager.disconnect(websocket) - await self.connection_manager.broadcast({ - "event": "client_disconnected", - "ts": int(time.time()), - "data": { - "disconnected_resource": disconnected.get('client').resource, - } - }) + await self.connection_manager.disconnect(self, websocket) + + + def get_game_by_id(self, _id: str): + for game in self.games: + if game.id == _id: + return game + return + + def get_player_by_id(self, game: str, player: str): + game = self.get_game_by_id(game) + if not game: + return { + 'err': True, + 'errorText': f'Cannot lookup player for unknown game {game}' + } + for _player in game.players: + if _player.id == player: + return player + return { + 'err': True, + 'errorText': f'Player w/ uuid {player} not found in {game}' + } + + def get_player_by_handle(self, game: str, player: str): + game = self.get_game_by_id(game) + if not game: + return { + 'err': True, + 'errorText': f'Cannot lookup player for unknown game {game}' + } + for _player in game.players: + if _player.get('handle') == player: + return player + return { + 'err': True, + 'errorText': f'Player {player} not found in {game}' + } + - def get_games(self): try: _games: list = [] @@ -92,6 +242,7 @@ class CAH(FastAPI): _games.append({ 'id': game.id, 'rounds': game.rounds, + 'resources': game.resources, 'players': game.players, 'created_at': game.created_at, 'state': game.state, @@ -126,7 +277,7 @@ class CAH(FastAPI): platform=platform, csid=csid, connected_at=int(time.time()), - current_game=None, + players=[], ) await self.connection_manager.handshake_complete(self, websocket, csid, client) @@ -144,6 +295,12 @@ class CAH(FastAPI): async def create_game(self, websocket, data: str): data = data.get('data') + if not self.connection_manager.get_connection_by_ws(websocket).get('client'): # No client set, valid handshake not completed + return await websocket.send_json({ + "event": "create_game_response", + "err": True, + "errorText": "Unauthorized", + }) if not data.get('rounds') or not str(data.get('rounds')).isnumeric(): return await websocket.send_json({ "event": "create_game_response", @@ -154,24 +311,41 @@ class CAH(FastAPI): "recvdData": data, } }) - client = self.connection_manager.get_connection_by_ws(websocket).get('client') - rounds = int(data.get('rounds')) - game_uuid = str(uuid.uuid4()) - game = CAHGame(id=game_uuid, - rounds=rounds, - players=[vars(client),], - created_at=int(time.time()), - state=-1, - started_at=0, - state_changed_at=int(time.time())) - self.games.append(game) - client.current_game = game.id - await websocket.send_json({ + if len(self.games): + await websocket.send_json({ + 'event': 'create_game_response', + 'ts': int(time.time()), + 'data': { + 'err': True, + 'errorText': 'A game already exists' # One game limit + } + }) + else: + client = self.connection_manager.get_connection_by_ws(websocket).get('client') + rounds = int(data.get('rounds')) + game_uuid = str(uuid.uuid4()) + game = CAHGame(id=game_uuid, + rounds=rounds, + resources=[client.resource,], + players=[], + created_at=int(time.time()), + state=-1, + started_at=0, + state_changed_at=int(time.time())) + await websocket.send_json({ "event": "create_game_response", "ts": int(time.time()), "data": { "success": True, - "createdGame": client.current_game, + "createdGame": game.__dict__, } }) - await self.connection_manager.send_client_and_game_lists(self, websocket) + await self.connection_manager.broadcast({ + 'event': 'game_created', + 'ts': int(time.time()), + 'data': { + 'game': game.__dict__, + } + }) + self.games.append(game) + await self.connection_manager.send_client_and_game_lists(self, websocket)