def validate(db: StandardDatabase, token, email=None): """ validate player token :param db: connection :param token: jwt token :param email: mail :return Player or None """ if not db.has_collection('players'): logger.error('cannot validate token if players do not exist') raise PlayerStateException('players collection does not exist') col = db.collection('players') if email: db_player = next(col.find({'mail': email}), None) if not db_player: logger.error(f'could not resolve player {email}') raise PlayerStateException(f'player {email} does not exist') if token in db_player['tokens']: player = Player(db_player['mail'], '') player.id = UUID(db_player['_key']) player.password = db_player['password'] return player else: for db_player in col.all(): if 'tokens' in db_player and token in db_player['tokens']: player = Player(db_player['mail'], '') player.id = UUID(db_player['_key']) player.password = db_player['password'] return player return None
def get_all_player_emails(db: StandardDatabase) -> [str]: """ return all player email addresses :param db: connection :return: list of known mail addresses """ if not db.has_collection('players'): logger.error('cannot resolve player if players do not exist') raise PlayerStateException('players collection does not exist') col = db.collection('players') return [p['mail'] for p in col.all()]
def delete(db: StandardDatabase, player: Player): """ remove existing player :param db: connection :param player: target """ if not db.has_collection('players'): logger.error('cannot remove player if players do not exist') raise PlayerStateException(f'players collection does not exist') col = db.collection('players') db_player = next(col.find({'mail': player.email}), None) if not db_player: logger.error(f'cannot find player {player.email}') raise PlayerStateException(f'could not find player {player.email}') col.delete(db_player)
def read(db: StandardDatabase, player_id): """ find existing player :param db: connection :param player_id: id :return: Player or None """ if not db.has_collection('players'): logger.error('cannot resolve player if players do not exist') raise PlayerStateException('players collection does not exist') col = db.collection('players') db_player = next(col.find({'_key': player_id}), None) if not db_player: return None return Player(db_player['email'], db_player['password'])
def save(db: StandardDatabase, instance: GameInstance): """ save a game instance :param db: connection :param instance: game """ logger.info(f'save called for {instance.id} ({instance.game.title})') if not db.has_collection('instances'): logger.info('creating collection instances') db.create_collection('instances') col = db.collection('instances') txn_db = db.begin_transaction(read=col.name, write=col.name) txn_col = txn_db.collection('instances') txn_col.insert(json.dumps(instance, cls=MarugotoEncoder)) txn_db.commit_transaction()
def remove_token(db: StandardDatabase, email, token): """ remove a token from player :param db: connection :param email: mail :param token: jwt token """ if not db.has_collection('players'): logger.error('cannot remove token if players do not exist') raise PlayerStateException('players collection does not exist') col = db.collection('players') db_player = next(col.find({'mail': email}), None) if not db_player: logger.error(f'could not resolve player {email}') raise PlayerStateException(f'player {email} does not exist') db_player['tokens'].remove(token) col.update(db_player)
def create(db: StandardDatabase, email, password, salt) -> Player: """ create a new player :param db: connection :param email: mail :param password: plain text password :param salt: encryption salt :return: Player """ if not db.has_collection('players'): logger.info('creating collection players') db.create_collection('players') player = Player(email, hash_password(password, salt)) col = db.collection('players') col.insert({ '_key': player.id.hex, 'mail': player.email, 'password': player.password, 'tokens': [] }) return player
def hosts(db: StandardDatabase, player: Player) -> [(datetime, str, str)]: """ list all games hosted by a player as game master :param db: connection :param player: game master :return: list of (creation date, game instance name, game title) """ logger.info(f'hosts requested for player {player.email}') if not db.has_collection('instances'): logger.warning('no collections for instances') raise InstanceStateException('no collections for instances') col = db.collection('instances') result = [] for db_game_instance in col.all(): game_instance = json.loads(json.dumps(db_game_instance), cls=MarugotoDecoder) game_instance.game = game_read(db, game_instance.game.title) if game_instance.game_master and game_instance.game_master.id == player.id: result.append((game_instance.created_at, game_instance.name, game_instance.game.title)) return result
def update(db: StandardDatabase, player: Player, salt, email: str = None, password: str = None, tokens: [str] = None) -> Player: """ update attributes of existing player :param db: connection :param player: target :param salt: encryption salt :param email: new mail :param password: new password in plain text :param tokens: non-expired tokens for player :return: Player """ if not db.has_collection('players'): logger.error('cannot resolve player if players do not exist') raise PlayerStateException('players collection does not exist') col = db.collection('players') db_player = next(col.find({'mail': player.email}), None) if not db_player: logger.error(f'cannot find player {player.email}') raise PlayerStateException(f'could not find player {player.email}') if email and password: player = Player(email, hash_password(password, salt)) elif email: player = Player(email, db_player['password']) elif password: player = Player(db_player['mail'], hash_password(password, salt)) else: player = Player(db_player['mail'], db_player['password']) player.id = UUID(db_player['_key']) col.update({ '_key': player.id.hex, 'mail': player.email, 'password': player.password }) return player
def load(db: StandardDatabase, game_id: str) -> GameInstance: """ load a game instance by id :param db: connection :param game_id: game instance id :return Game Instance """ logger.info(f'load called for {game_id}') if not db.has_collection('instances'): logger.warning('no collections for instances') raise InstanceStateException('no collections for instances') col = db.collection('instances') db_game_instance = next(col.find({'_key': game_id}), None) if not db_game_instance: logger.warning(f'could not find game instance {game_id}') raise InstanceStateException(f'could not find game instance {game_id}') game_instance = json.loads(json.dumps(db_game_instance), cls=MarugotoDecoder) game_instance.game = game_read(db, game_instance.game.title) for player_state in game_instance.player_states: if not isinstance(player_state.player, Player): player_state.player = player_read(db, player_state.player) player_state.game_instance = game_instance for npc_state in game_instance.npc_states: npc_state.game_instance = game_instance if isinstance(npc_state.dialog.start, UUID): npc_state.dialog = next( iter([ d for d in [n.dialog for n in game_instance.game.npcs] if d.id == npc_state.dialog.id ]), None) if not npc_state.dialog: logger.warning( f'could not find dialog for NPC {npc_state.first_name} {npc_state.last_name} ' f'in {game_instance.game.title} ({game_instance.id})') raise InstanceStateException( f'could not find dialog for NPC {npc_state.first_name} {npc_state.last_name} ' f'in {game_instance.game.title} ({game_instance.id})') return game_instance
def authenticate(db: StandardDatabase, email, password, salt): """ validate player password :param db: connection :param email: mail :param password: plain text password :param salt: encryption salt :return: Player or None """ if not db.has_collection('players'): logger.warning('cannot authenticate: no players defined') return None col = db.collection('players') db_player = next(col.find({'mail': email}), None) if not db_player: logger.warning(f'could not find player {email}') return None if verify_password(db_player['password'], hash_password(password, salt), salt): player = Player(db_player['mail'], '') player.id = UUID(db_player['_key']) player.password = db_player['password'] return player
def saves(db: StandardDatabase, player: Player) -> [(datetime, str, str, str)]: """ get all saves for a player :param db: connection :param player: target :return list of (creation date, game instance name, game title, pseudonym) """ logger.info(f'saves requested for player {player.email}') if not db.has_collection('instances'): logger.warning('no collections for instances') raise InstanceStateException('no collections for instances') col = db.collection('instances') result = [] for db_game_instance in col.all(): game_instance = json.loads(json.dumps(db_game_instance), cls=MarugotoDecoder) game_instance.game = game_read(db, game_instance.game.title) for player_state in game_instance.player_states: if player_state.player.id == player.id: result.append( (game_instance.created_at, game_instance.name, game_instance.game.title, f'{player_state.first_name} {player_state.last_name}')) return result
def create_collection(db: StandardDatabase, name: str, unique_fields: Tuple, _schema: Dict): if not db.has_collection(name): collection = db.create_collection(name=name, schema=_schema) collection.add_hash_index(fields=unique_fields, unique=True)
def create(db: StandardDatabase, game: Game, creator=None): """ save a game to the database :param db: connection :param game: target :param creator: creator of the game """ logger.info(f'create called for {game.title}') if not db.has_collection('games'): logger.info(f'games collection does not exist, creating it.') db.create_collection('games') db_games = db.collection('games') if db_games.find({'key': game.title}): logger.warning(f'{game.title} already in metadata') raise GameStateException(f'{game.title} already in metadata') if db.has_graph(game.title): logger.warning(f'{game.title} already defined') raise GameStateException(f'{game.title} already defined') if not nx.is_directed_acyclic_graph(game.graph): logger.warning(f'{game.title} is not acyclic') raise GameStateException(f'{game.title} is not acyclic') if not game.start_is_set(): logger.warning(f'{game.title} has no starting point') raise GameStateException(f'{game.title} has no starting point') db_game_graph = db.create_graph(f'game_{game.title}') if not db_game_graph.has_vertex_collection('waypoints'): logger.info(f'waypoints vertex collection does not exist, creating it.') db_game_graph.create_vertex_collection('waypoints') path = db_game_graph.create_edge_definition( edge_collection='path', from_vertex_collections=['waypoints'], to_vertex_collections=['waypoints'] ) # add all game nodes for waypoint_vertex in set(game.start.all_path_nodes()): logger.debug(f'inserting waypoint vertex {repr(waypoint_vertex)}') db_game_graph.insert_vertex('waypoints', json.dumps(waypoint_vertex, cls=MarugotoEncoder)) # tasks as documents if not db.has_collection('tasks'): logger.info(f'tasks collection does not exist, creating it.') db.create_collection('tasks') tasks = db.collection('tasks') # dialogs metadata if not db.has_collection('dialogs'): logger.info(f'dialogs does not exist, creating it.') db.create_collection('dialogs') db_dialogs = db.collection('dialogs') # npc dialog metadata if not db.has_collection('npcs'): logger.info(f'npcs collection does not exist, creating it.') db.create_collection('npcs') db_npcs = db.collection('npcs') for npc in game.npcs: if db_npcs.find({'_key': f'{game.title}-{npc.first_name}-{npc.last_name}'}): logger.warning(f'dialog {game.title}-{npc.first_name}-{npc.last_name} already in metadata') raise GameStateException(f'dialog {game.title}-{npc.first_name}-{npc.last_name} already in metadata') logger.debug(f'inserting npc {game.title}-{npc.first_name}-{npc.last_name} with dialog {npc.dialog.id.hex}') db_npcs.insert({ '_key': f'{game.title}-{npc.first_name}-{npc.last_name}', 'game': f'game_{game.title}', 'first_name': npc.first_name, 'last_name': npc.last_name, 'salutation': npc.salutation, 'mail': npc.mail, 'image': base64.encodebytes(npc.image) if npc.image else None, 'dialog': npc.dialog.id.hex }) if db_dialogs.find({'_key': npc.dialog.id.hex}): logger.warning(f'dialog {npc.dialog.id} already in metadata') raise GameStateException(f'dialog {npc.dialog.id} already in metadata') if db.has_graph(f'dialog_{npc.dialog.id.hex}'): logger.warning(f'dialog {npc.dialog.id} already defined') raise GameStateException(f'dialog {npc.dialog.id} already defined') if not nx.is_directed_acyclic_graph(npc.dialog.graph): logger.warning(f'dialog {npc.dialog.id} is not acyclic') raise GameStateException(f'dialog {npc.dialog.id} is not acyclic') if not npc.dialog.start_is_set(): logger.warning(f'dialog {npc.dialog.id} has no starting point') raise GameStateException(f'dialog {npc.dialog.id} has no starting point') db_dialog_graph = db.create_graph(f'dialog_{npc.dialog.id.hex}') if not db_dialog_graph.has_vertex_collection('interactions'): logger.debug(f'interaction vertex collection does not exist, creating it.') db_dialog_graph.create_vertex_collection('interactions') conversation = db_dialog_graph.create_edge_definition( edge_collection='conversation', from_vertex_collections=['interactions'], to_vertex_collections=['interactions'] ) # add all dialog nodes for interaction_vertex in set(npc.dialog.start.all_path_nodes()): logger.debug(f'inserting interaction vertex {repr(interaction_vertex)}') db_dialog_graph.insert_vertex('interactions', json.dumps(interaction_vertex, cls=MarugotoEncoder)) dia_visited = {} def dialog_traversal(s, i): """ Walk through the game path from a starting source :param s: starting waypoint for traversal :param i: dict containing identified steps """ for successor in npc.dialog.graph.successors(s): logger.debug(f'successor {repr(successor)} for {repr(s)}') if s not in i.keys(): logger.debug(f'{repr(s)} is new, adding it') i[s] = [] if successor not in i[s]: if successor.task: logger.debug(f'{repr(successor)} has {repr(successor.task)}, adding to tasks') task_dump = json.loads(json.dumps(successor.task, cls=MarugotoEncoder)) task_dump['_key'] = successor.task.id.hex task_dump['for'] = successor.id.hex if not tasks.find(task_dump): tasks.insert(json.dumps(task_dump)) logger.debug(f'inserting interaction edge {repr(s)} to {repr(successor)}') conversation.insert({ '_key': f'{s.id.hex}-{successor.id.hex}', '_from': f'interactions/{s.id.hex}', '_to': f'interactions/{successor.id.hex}' }) dia_visited[s].append(successor) dialog_traversal(successor, i) logger.debug(f'traversing dialog graph for {npc.first_name} {npc.last_name}') dialog_traversal(npc.dialog.start, dia_visited) logger.debug(f'inserting dialog {npc.dialog.id.hex}') db_dialogs.insert({ '_key': npc.dialog.id.hex, 'start': npc.dialog.start.id.hex }) wp_visited = {} def game_traversal(s, i): """ Walk through the game path from a starting source :param s: starting waypoint for traversal :param i: dict containing identified steps """ for successor in game.graph.successors(s): logger.debug(f'successor {repr(successor)} for {repr(s)}') if s not in i.keys(): logger.debug(f'{repr(s)} is new, adding it.') i[s] = [] if successor not in i[s]: for task in successor.tasks: logger.debug(f'{repr(successor)} has {repr(task)}, adding to tasks') task_dump = json.loads(json.dumps(task, cls=MarugotoEncoder)) task_dump['_key'] = task.id.hex task_dump['for'] = successor.id.hex if not tasks.find(task_dump): tasks.insert(json.dumps(task_dump)) logger.debug(f'waypoint interaction edge {repr(s)} to {repr(successor)}') weight = game.graph.edges[s, successor]['weight'] if 'weight' in game.graph.edges[s, successor] and game.graph.edges[s, successor]['weight'] else None path.insert({ '_key': f'{s.id.hex}-{successor.id.hex}', '_from': f'waypoints/{s.id.hex}', '_to': f'waypoints/{successor.id.hex}', 'weight': weight }) wp_visited[s].append(successor) game_traversal(successor, i) logger.debug(f'traversing game graph for {game.title}') game_traversal(game.start, wp_visited) logger.debug(f'inserting game {game.title}') db_games.insert({ '_key': game.title, 'start': game.start.id.hex, 'image': base64.encodebytes(game.image) if game.image else None, 'creator': creator })