# ============================================================================== # Copyright (C) 2019 - Philip Paquette, Steven Bocco # # This program is free software: you can redistribute it and/or modify it under # the terms of the GNU Affero General Public License as published by the Free # Software Foundation, either version 3 of the License, or (at your option) any # later version. # # This program is distributed in the hope that it will be useful, but WITHOUT # ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS # FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more # details. # # You should have received a copy of the GNU Affero General Public License along # with this program. If not, see . # ============================================================================== """ Client -> Server requests. Notes: If an error occurred on server-side while handling a request, client will receive a ResponseException containing message about handling error. Request exceptions are currently not more typed on client-side. """ import inspect import logging from diplomacy.engine.message import Message from diplomacy.utils import common, exceptions, parsing, strings from diplomacy.utils.network_data import NetworkData from diplomacy.utils.parsing import OptionalValueType from diplomacy.utils.sorted_dict import SortedDict LOGGER = logging.getLogger(__name__) class _AbstractRequest(NetworkData): """ Abstract request class. Field request_id is auto-filled if not defined. Field name is auto-filled with snake case version of request class name. Field re_sent is False by default. It should be set to True if request is re-sent by client (currently done by Connection object when reconnecting). Timestamp field is auto-set with current local timestamp if not defined. For game request Synchronize, timestamp should be game latest timestamp instead (see method NetworkGame.synchronize()). """ __slots__ = ['request_id', 're_sent'] header = { strings.REQUEST_ID: str, strings.NAME: str, strings.RE_SENT: parsing.DefaultValueType(bool, False), } params = {} id_field = strings.REQUEST_ID level = None def __init__(self, **kwargs): self.request_id = None # type: str self.re_sent = None # type: bool super(_AbstractRequest, self).__init__(**kwargs) @classmethod def validate_params(cls): """ Hack: we just use it to validate level. """ assert cls.level is None or cls.level in strings.ALL_COMM_LEVELS class _AbstractChannelRequest(_AbstractRequest): """ Abstract class representing a channel request. Token field is automatically filled by a Channel object before sending request. """ __slots__ = ['token'] header = parsing.update_model(_AbstractRequest.header, { strings.TOKEN: str }) level = strings.CHANNEL def __init__(self, **kwargs): self.token = None # type: str super(_AbstractChannelRequest, self).__init__(**kwargs) class _AbstractGameRequest(_AbstractChannelRequest): """ Abstract class representing a game request. Game ID, game role and phase fields are automatically filled by a NetworkGame object before sending request. """ __slots__ = ['game_id', 'game_role', 'phase'] header = parsing.extend_model(_AbstractChannelRequest.header, { strings.GAME_ID: str, strings.GAME_ROLE: str, strings.PHASE: str, # Game short phase. }) level = strings.GAME # Game request class flag to indicate if this type of game request depends on game phase. # If True, phase indicated in request must match current game phase. phase_dependent = True def __init__(self, **kwargs): self.game_id = None # type: str self.game_role = None # type: str self.phase = None # type: str super(_AbstractGameRequest, self).__init__(**kwargs) # Return "address" of request sender inside related game (ie. channel token + game role). # Used by certain request managers to skip sender when notify related game. # See request managers in diplomacy.server.request_managers. address_in_game = property(lambda self: (self.game_role, self.token)) # ==================== # Connection requests. # ==================== class SignIn(_AbstractRequest): """ SignIn request. Expected response: responses.DataToken Expected response handler result: diplomacy.client.channel.Channel """ __slots__ = ['username', 'password', 'create_user'] params = { strings.USERNAME: str, strings.PASSWORD: str, strings.CREATE_USER: bool } def __init__(self, **kwargs): self.username = None self.password = None self.create_user = None super(SignIn, self).__init__(**kwargs) # ================= # Channel requests. # ================= class CreateGame(_AbstractChannelRequest): """ CreateGame request. Expected response: responses.DataGame Expected response handler result: diplomacy.client.network_game.NetworkGame """ __slots__ = ['game_id', 'power_name', 'state', 'map_name', 'rules', 'n_controls', 'deadline', 'registration_password'] params = { strings.GAME_ID: parsing.OptionalValueType(str), strings.N_CONTROLS: parsing.OptionalValueType(int), strings.DEADLINE: parsing.DefaultValueType(int, 300), # 300 seconds. Must be >= 0. strings.REGISTRATION_PASSWORD: parsing.OptionalValueType(str), strings.POWER_NAME: parsing.OptionalValueType(str), strings.STATE: parsing.OptionalValueType(dict), strings.MAP_NAME: parsing.DefaultValueType(str, 'standard'), strings.RULES: parsing.OptionalValueType(parsing.SequenceType(str, sequence_builder=set)), } def __init__(self, **kwargs): self.game_id = '' self.n_controls = 0 self.deadline = 0 self.registration_password = '' self.power_name = '' self.state = {} self.map_name = '' self.rules = set() super(CreateGame, self).__init__(**kwargs) class DeleteAccount(_AbstractChannelRequest): """ DeleteAccount request. Expected response: responses.Ok Expected response handler result: None """ __slots__ = ['username'] params = { strings.USERNAME: OptionalValueType(str) } def __init__(self, **kwargs): self.username = None super(DeleteAccount, self).__init__(**kwargs) class GetDummyWaitingPowers(_AbstractChannelRequest): """ GetDummyWaitingPowers request. Expected response: response.DataGamesToPowerNames Expected response handler result: {dict mapping game IDs to lists of dummy powers names} """ __slots__ = ['buffer_size'] params = { strings.BUFFER_SIZE: int, } def __init__(self, **kwargs): self.buffer_size = 0 super(GetDummyWaitingPowers, self).__init__(**kwargs) class GetAvailableMaps(_AbstractChannelRequest): """ GetAvailableMaps request. Expected response: responses.DataMaps Expected response handler result: {map name => [map power names]} """ __slots__ = [] class GetPlayablePowers(_AbstractChannelRequest): """ GetPlayablePowers request. Expected response: responses.DataPowerNames Expected response handler result: [power names] """ __slots__ = ['game_id'] params = { strings.GAME_ID: str } def __init__(self, **kwargs): self.game_id = None super(GetPlayablePowers, self).__init__(**kwargs) class JoinGame(_AbstractChannelRequest): """ JoinGame request. Expected response: responses.DataGame Expected response handler result: diplomacy.client.network_game.NetworkGame """ __slots__ = ['game_id', 'power_name', 'registration_password'] params = { strings.GAME_ID: str, strings.POWER_NAME: parsing.OptionalValueType(str), strings.REGISTRATION_PASSWORD: parsing.OptionalValueType(str) } def __init__(self, **kwargs): self.game_id = None self.power_name = None self.registration_password = None super(JoinGame, self).__init__(**kwargs) class JoinPowers(_AbstractChannelRequest): """ JoinPowers request to join many powers of a game with one query. Useful to control many powers while still working only with 1 client game instance. Expected response: responses.Ok Expected response handler result: None """ __slots__ = ['game_id', 'power_names', 'registration_password'] params = { strings.GAME_ID: str, strings.POWER_NAMES: parsing.SequenceType(str, sequence_builder=set), strings.REGISTRATION_PASSWORD: parsing.OptionalValueType(str) } def __init__(self, **kwargs): self.game_id = None self.power_names = None self.registration_password = None super(JoinPowers, self).__init__(**kwargs) class ListGames(_AbstractChannelRequest): """ ListGames request. Expected response: responses.DataGames Expected response handler result: responses.DataGames """ __slots__ = ['game_id', 'status', 'map_name', 'include_protected', 'for_omniscience'] params = { strings.STATUS: OptionalValueType(parsing.EnumerationType(strings.ALL_GAME_STATUSES)), strings.MAP_NAME: OptionalValueType(str), strings.INCLUDE_PROTECTED: parsing.DefaultValueType(bool, True), strings.FOR_OMNISCIENCE: parsing.DefaultValueType(bool, False), strings.GAME_ID: OptionalValueType(str), } def __init__(self, **kwargs): self.game_id = None self.status = None self.map_name = None self.include_protected = None self.for_omniscience = None super(ListGames, self).__init__(**kwargs) class GetGamesInfo(_AbstractChannelRequest): """ Request used to get info for a given list of game IDs. Expected response: responses.DataGames Expected response handler result: responses.DataGames """ __slots__ = ['games'] params = { strings.GAMES: parsing.SequenceType(str) } def __init__(self, **kwargs): self.games = [] super(GetGamesInfo, self).__init__(**kwargs) class Logout(_AbstractChannelRequest): """ Logout request. Expected response: responses.Ok Expected response handler result: None """ __slots__ = [] class SetGrade(_AbstractChannelRequest): """ SetGrade request. Expected response: responses.Ok Expected response handler result: None """ __slots__ = ['grade', 'grade_update', 'username', 'game_id'] params = { strings.GRADE: parsing.EnumerationType(strings.ALL_GRADES), strings.GRADE_UPDATE: parsing.EnumerationType(strings.ALL_GRADE_UPDATES), strings.USERNAME: str, strings.GAME_ID: parsing.OptionalValueType(str), } def __init__(self, **kwargs): self.grade = None self.grade_update = None self.username = None self.game_id = None super(SetGrade, self).__init__(**kwargs) # ============== # Game requests. # ============== class ClearCenters(_AbstractGameRequest): """ ClearCenters request. Expected response: responses.Ok Expected response handler result: None """ __slots__ = ['power_name'] params = { strings.POWER_NAME: parsing.OptionalValueType(str), } def __init__(self, **kwargs): self.power_name = None # type: str super(ClearCenters, self).__init__(**kwargs) class ClearOrders(_AbstractGameRequest): """ ClearOrders request. Expected response: responses.Ok Expected response handler result: None """ __slots__ = ['power_name'] params = { strings.POWER_NAME: parsing.OptionalValueType(str), } def __init__(self, **kwargs): self.power_name = None # type: str super(ClearOrders, self).__init__(**kwargs) class ClearUnits(_AbstractGameRequest): """ ClearUnits request. Expected response: responses.Ok Expected response handler result: None """ __slots__ = ['power_name'] params = { strings.POWER_NAME: parsing.OptionalValueType(str), } def __init__(self, **kwargs): self.power_name = None # type: str super(ClearUnits, self).__init__(**kwargs) class DeleteGame(_AbstractGameRequest): """ DeleteGame request. Expected response: responses.Ok Expected response handler result: None """ __slots__ = [] phase_dependent = False class GetAllPossibleOrders(_AbstractGameRequest): """ GetAllPossibleOrders request. Expected response: response.DataPossibleOrders Expected response handler result: response.DataPossibleOrders """ __slots__ = [] class GetPhaseHistory(_AbstractGameRequest): """ Get a list of game phase data from game history for given phases interval. A phase can be either None, a phase name (string) or a phase index (integer). Expected response: responses.DataGamePhases Expected response handler result: [GamePhaseData objects] """ __slots__ = ['from_phase', 'to_phase'] params = { strings.FROM_PHASE: parsing.OptionalValueType(parsing.SequenceOfPrimitivesType([str, int])), strings.TO_PHASE: parsing.OptionalValueType(parsing.SequenceOfPrimitivesType([str, int])), } phase_dependent = False def __init__(self, **kwargs): self.from_phase = '' self.to_phase = '' super(GetPhaseHistory, self).__init__(**kwargs) class LeaveGame(_AbstractGameRequest): """ LeaveGame request. Expected response: responses.Ok Expected response handler result: None """ __slots__ = [] class ProcessGame(_AbstractGameRequest): """ ProcessGame request. Expected response: responses.Ok Expected response handler result: None """ __slots__ = [] class QuerySchedule(_AbstractGameRequest): """ Query server for info about current scheduling for a game. Expected response: response.DataGameSchedule Expected response handler result: response.DataGameSchedule """ __slots__ = [] class SaveGame(_AbstractGameRequest): """ Get game saved format in JSON. Expected response: response.DataSavedGame Expected response handler result: response.DataSavedGame """ __slots__ = [] class SendGameMessage(_AbstractGameRequest): """ SendGameMessage request. Expected response: responses.DataTimeStamp Expected response handler result: None """ __slots__ = ['message'] params = { strings.MESSAGE: parsing.JsonableClassType(Message) } def __init__(self, **kwargs): self.message = None # type: Message super(SendGameMessage, self).__init__(**kwargs) class SetDummyPowers(_AbstractGameRequest): """ SetDummyPowers request. Expected response: responses.Ok Expected response handler result: None """ __slots__ = ['username', 'power_names'] params = { strings.USERNAME: parsing.OptionalValueType(str), strings.POWER_NAMES: parsing.OptionalValueType(parsing.SequenceType(str)), } def __init__(self, **kwargs): self.username = None self.power_names = None super(SetDummyPowers, self).__init__(**kwargs) class SetGameState(_AbstractGameRequest): """ Request to set a game state. Expected response: responses.Ok Expected response handler result: None """ __slots__ = ['state', 'orders', 'results', 'messages'] params = { strings.STATE: dict, strings.ORDERS: parsing.DictType(str, parsing.SequenceType(str)), strings.RESULTS: parsing.DictType(str, parsing.SequenceType(str)), strings.MESSAGES: parsing.DictType(int, parsing.JsonableClassType(Message), SortedDict.builder(int, Message)), } def __init__(self, **kwargs): self.state = {} self.orders = {} self.results = {} self.messages = {} # type: SortedDict super(SetGameState, self).__init__(**kwargs) class SetGameStatus(_AbstractGameRequest): """ SetGameStatus request. Expected response: responses.Ok Expected response handler result: None """ __slots__ = ['status'] params = { strings.STATUS: parsing.EnumerationType(strings.ALL_GAME_STATUSES), } def __init__(self, **kwargs): self.status = None super(SetGameStatus, self).__init__(**kwargs) class SetOrders(_AbstractGameRequest): """ SetOrders request. Expected response: responses.Ok Expected response handler result: None """ __slots__ = ['power_name', 'orders', 'wait'] params = { strings.POWER_NAME: parsing.OptionalValueType(str), # required only for game master. strings.ORDERS: parsing.SequenceType(str), strings.WAIT: parsing.OptionalValueType(bool) } def __init__(self, **kwargs): self.power_name = None self.orders = None self.wait = None super(SetOrders, self).__init__(**kwargs) class SetWaitFlag(_AbstractGameRequest): """ SetWaitFlag request. Expected response: responses.Ok Expected response handler result: None """ __slots__ = ['power_name', 'wait'] params = { strings.POWER_NAME: parsing.OptionalValueType(str), # required only for game master. strings.WAIT: bool } def __init__(self, **kwargs): self.power_name = None self.wait = None super(SetWaitFlag, self).__init__(**kwargs) class Synchronize(_AbstractGameRequest): """ Synchronize request. Expected response: responses.DataGameInfo Expected response handler result: DataGameInfo """ __slots__ = ['timestamp'] params = { strings.TIMESTAMP: int } phase_dependent = False def __init__(self, **kwargs): self.timestamp = None # type: int super(Synchronize, self).__init__(**kwargs) class Vote(_AbstractGameRequest): """ Vote request. For powers only. Allow a power to vote about game draw for current phase. Expected response: responses.Ok Expected response handler result: None """ __slots__ = ['power_name', 'vote'] params = { strings.POWER_NAME: parsing.OptionalValueType(str), strings.VOTE: strings.ALL_VOTE_DECISIONS } def __init__(self, **kwargs): self.power_name = '' self.vote = '' super(Vote, self).__init__(**kwargs) def parse_dict(json_request): """ Parse a JSON dictionary expected to represent a request. Raise an exception if parsing failed. :param json_request: JSON dictionary. :return: a request class instance. :type json_request: dict :rtype: _AbstractRequest | _AbstractChannelRequest | _AbstractGameRequest """ name = json_request.get(strings.NAME, None) if name is None: raise exceptions.RequestException() expected_class_name = common.snake_case_to_upper_camel_case(name) request_class = globals().get(expected_class_name, None) if request_class is None or not inspect.isclass(request_class) or not issubclass(request_class, _AbstractRequest): raise exceptions.RequestException('Unknown request name %s' % expected_class_name) try: return request_class.from_dict(json_request) except exceptions.DiplomacyException as exc: LOGGER.error('%s/%s', type(exc).__name__, exc.message) raise exceptions.RequestException('Wrong request format')