# ============================================================================== # 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 . # ============================================================================== """ Channel - The channel object represents an authenticated connection over a socket. - It has a token that it sends with every request to authenticate itself. """ import logging from tornado import gen from diplomacy.communication import requests from diplomacy.utils import strings, common LOGGER = logging.getLogger(__name__) def _req_fn(request_class, local_req_fn=None, **request_args): """ Create channel request method that sends request with channel token. :param request_class: class of request to send with channel request method. :param local_req_fn: (optional) Channel method to use locally to try retrieving a data instead of sending a request. If provided, local_req_fn is called with request args: - if it returns anything else than None, then returned data is returned by channel request method. - else, request class is still sent and channel request method follows standard path (request sent, response received, response handler called and final handler result returned). :param request_args: arguments to pass to request class to create the request object. :return: a Channel method. """ str_params = (', '.join('%s=%s' % (key, common.to_string(value)) for (key, value) in sorted(request_args.items()))) if request_args else '' @gen.coroutine def func(self, game=None, **kwargs): """ Send an instance of request_class with given kwargs and game object. :param self: Channel object who sends the request. :param game: (optional) a NetworkGame object (required for game requests). :param kwargs: request arguments. :return: Data returned after response is received and handled by associated response manager. See module diplomacy.client.response_managers about responses management. :type game: diplomacy.client.network_game.NetworkGame """ kwargs.update(request_args) if request_class.level == strings.GAME: assert game is not None kwargs[strings.TOKEN] = self.token kwargs[strings.GAME_ID] = game.game_id kwargs[strings.GAME_ROLE] = game.role kwargs[strings.PHASE] = game.current_short_phase else: assert game is None if request_class.level == strings.CHANNEL: kwargs[strings.TOKEN] = self.token if local_req_fn is not None: local_ret = local_req_fn(self, **kwargs) if local_ret is not None: return local_ret request = request_class(**kwargs) return (yield self.connection.send(request, game)) func.__request_name__ = request_class.__name__ func.__request_params__ = str_params func.__doc__ = """ Send request :class:`.%(request_name)s`%(with_params)s``kwargs``. Return response data returned by server for this request. See :class:`.%(request_name)s` about request parameters and response. """ % {'request_name': request_class.__name__, 'with_params': ' with forced parameters ``(%s)`` and additional request parameters ' % str_params if request_args else ' with request parameters '} return func class Channel: """ Channel - Represents an authenticated connection over a physical socket """ # pylint: disable=too-few-public-methods __slots__ = ['connection', 'token', 'game_id_to_instances', '__weakref__'] def __init__(self, connection, token): """ Initialize a channel. Properties: - **connection**: :class:`.Connection` object from which this channel originated. - **token**: Channel token, used to identify channel on server. - **game_id_to_instances**: Dictionary mapping a game ID to :class:`.NetworkGame` objects loaded for this game. Each :class:`.NetworkGame` has a specific role, which is either an observer role, an omniscient role, or a power (player) role. Network games for a specific game ID are managed within a :class:`.GameInstancesSet`, which makes sure that there will be at most 1 :class:`.NetworkGame` instance per possible role. :param connection: a Connection object. :param token: Channel token. :type connection: diplomacy.client.connection.Connection :type token: str """ self.connection = connection self.token = token self.game_id_to_instances = {} # {game id => GameInstances} def _local_join_game(self, **kwargs): """ Look for a local game with given kwargs intended to be used to build a JoinGame request. Return None if no local game found, else local game found. Game is identified with game ID **(required)** and power name *(optional)*. If power name is None, we look for a "special" game (observer or omniscient game) loaded locally. Note that there is at most 1 special game per (channel + game ID) couple: either observer or omniscient, not both. """ game_id = kwargs[strings.GAME_ID] power_name = kwargs.get(strings.POWER_NAME, None) if game_id in self.game_id_to_instances: if power_name is not None: return self.game_id_to_instances[game_id].get(power_name) return self.game_id_to_instances[game_id].get_special() return None # =================== # Public channel API. # =================== create_game = _req_fn(requests.CreateGame) get_available_maps = _req_fn(requests.GetAvailableMaps) get_playable_powers = _req_fn(requests.GetPlayablePowers) join_game = _req_fn(requests.JoinGame, local_req_fn=_local_join_game) join_powers = _req_fn(requests.JoinPowers) list_games = _req_fn(requests.ListGames) get_games_info = _req_fn(requests.GetGamesInfo) get_dummy_waiting_powers = _req_fn(requests.GetDummyWaitingPowers) # User Account API. delete_account = _req_fn(requests.DeleteAccount) logout = _req_fn(requests.Logout) # Admin / Moderator API. make_omniscient = _req_fn(requests.SetGrade, grade=strings.OMNISCIENT, grade_update=strings.PROMOTE) remove_omniscient = _req_fn(requests.SetGrade, grade=strings.OMNISCIENT, grade_update=strings.DEMOTE) promote_administrator = _req_fn(requests.SetGrade, grade=strings.ADMIN, grade_update=strings.PROMOTE) demote_administrator = _req_fn(requests.SetGrade, grade=strings.ADMIN, grade_update=strings.DEMOTE) promote_moderator = _req_fn(requests.SetGrade, grade=strings.MODERATOR, grade_update=strings.PROMOTE) demote_moderator = _req_fn(requests.SetGrade, grade=strings.MODERATOR, grade_update=strings.DEMOTE) # ==================================================================== # Game API. Intended to be called by NetworkGame object, not directly. # ==================================================================== _get_phase_history = _req_fn(requests.GetPhaseHistory) _leave_game = _req_fn(requests.LeaveGame) _send_game_message = _req_fn(requests.SendGameMessage) _set_orders = _req_fn(requests.SetOrders) _clear_centers = _req_fn(requests.ClearCenters) _clear_orders = _req_fn(requests.ClearOrders) _clear_units = _req_fn(requests.ClearUnits) _wait = _req_fn(requests.SetWaitFlag, wait=True) _no_wait = _req_fn(requests.SetWaitFlag, wait=False) _vote = _req_fn(requests.Vote) _save = _req_fn(requests.SaveGame) _synchronize = _req_fn(requests.Synchronize) # Admin / Moderator API. _delete_game = _req_fn(requests.DeleteGame) _kick_powers = _req_fn(requests.SetDummyPowers) _set_state = _req_fn(requests.SetGameState) _process = _req_fn(requests.ProcessGame) _query_schedule = _req_fn(requests.QuerySchedule) _start = _req_fn(requests.SetGameStatus, status=strings.ACTIVE) _pause = _req_fn(requests.SetGameStatus, status=strings.PAUSED) _resume = _req_fn(requests.SetGameStatus, status=strings.ACTIVE) _cancel = _req_fn(requests.SetGameStatus, status=strings.CANCELED) _draw = _req_fn(requests.SetGameStatus, status=strings.COMPLETED)