Пример #1
0
 def __init__(self, reader, writer, encoder=json.JSONEncoder()):
     self._active = True
     self._reader = StreamLineReader(reader)
     self._writer = writer
     self._encoder = encoder
     self._methods = {}
     self._notifications = {}
     self._task_manager = TaskManager("jsonrpc server")
Пример #2
0
 def __init__(self, reader, writer, encoder=json.JSONEncoder()):
     self._active = True
     self._reader = StreamLineReader(reader)
     self._writer = writer
     self._encoder = encoder
     self._methods = {}
     self._notifications = {}
     self._task_manager = TaskManager("jsonrpc server")
     self._write_lock = asyncio.Lock()
     self._last_request_id = 0
     self._requests_futures = {}
Пример #3
0
class NotificationClient():
    def __init__(self, writer, encoder=json.JSONEncoder()):
        self._writer = writer
        self._encoder = encoder
        self._methods = {}
        self._task_manager = TaskManager("notification client")
        self._write_lock = asyncio.Lock()

    def notify(self, method, params, sensitive_params=False):
        """
        Send notification

        :param method:
        :param params:
        :param sensitive_params: list of parameters that are anonymized before logging; \
            if False - no params are considered sensitive, if True - all params are considered sensitive
        """
        notification = {"jsonrpc": "2.0", "method": method, "params": params}
        self._log(method, params, sensitive_params)
        self._send(notification)

    async def close(self):
        self._task_manager.cancel()
        await self._task_manager.wait()

    def _send(self, data):
        async def send_task(data_):
            async with self._write_lock:
                self._writer.write(data_)
                await self._writer.drain()

        try:
            line = self._encoder.encode(data)
            data = (line + "\n").encode("utf-8")
            logging.debug("Sending %d byte of data", len(data))
            self._task_manager.create_task(send_task(data), "send")
        except TypeError as error:
            logging.error("Failed to parse outgoing message: %s", str(error))

    @staticmethod
    def _log(method, params, sensitive_params):
        params = anonymise_sensitive_params(params, sensitive_params)
        logging.info("Sending notification: method=%s, params=%s", method,
                     params)
Пример #4
0
    def __init__(self, platform, version, reader, writer, handshake_token):
        logging.info("Creating plugin for platform %s, version %s",
                     platform.value, version)
        self._platform = platform
        self._version = version

        self._features: Set[Feature] = set()
        self._active = True

        self._reader, self._writer = reader, writer
        self._handshake_token = handshake_token

        encoder = JSONEncoder()
        self._server = Server(self._reader, self._writer, encoder)
        self._notification_client = NotificationClient(self._writer, encoder)

        self._achievements_import_in_progress = False
        self._game_times_import_in_progress = False

        self._persistent_cache = dict()

        self._internal_task_manager = TaskManager("plugin internal")
        self._external_task_manager = TaskManager("plugin external")

        # internal
        self._register_method("shutdown", self._shutdown, internal=True)
        self._register_method("get_capabilities",
                              self._get_capabilities,
                              internal=True,
                              immediate=True)
        self._register_method("initialize_cache",
                              self._initialize_cache,
                              internal=True,
                              immediate=True,
                              sensitive_params="data")
        self._register_method("ping",
                              self._ping,
                              internal=True,
                              immediate=True)

        # implemented by developer
        self._register_method("init_authentication",
                              self.authenticate,
                              sensitive_params=["stored_credentials"])
        self._register_method("pass_login_credentials",
                              self.pass_login_credentials,
                              sensitive_params=["cookies", "credentials"])
        self._register_method("import_owned_games",
                              self.get_owned_games,
                              result_name="owned_games")
        self._detect_feature(Feature.ImportOwnedGames, ["get_owned_games"])

        self._register_method("start_achievements_import",
                              self._start_achievements_import)
        self._detect_feature(Feature.ImportAchievements,
                             ["get_unlocked_achievements"])

        self._register_method("import_local_games",
                              self.get_local_games,
                              result_name="local_games")
        self._detect_feature(Feature.ImportInstalledGames, ["get_local_games"])

        self._register_notification("launch_game", self.launch_game)
        self._detect_feature(Feature.LaunchGame, ["launch_game"])

        self._register_notification("install_game", self.install_game)
        self._detect_feature(Feature.InstallGame, ["install_game"])

        self._register_notification("uninstall_game", self.uninstall_game)
        self._detect_feature(Feature.UninstallGame, ["uninstall_game"])

        self._register_notification("shutdown_platform_client",
                                    self.shutdown_platform_client)
        self._detect_feature(Feature.ShutdownPlatformClient,
                             ["shutdown_platform_client"])

        self._register_notification("launch_platform_client",
                                    self.launch_platform_client)
        self._detect_feature(Feature.LaunchPlatformClient,
                             ["launch_platform_client"])

        self._register_method("import_friends",
                              self.get_friends,
                              result_name="friend_info_list")
        self._detect_feature(Feature.ImportFriends, ["get_friends"])

        self._register_method("start_game_times_import",
                              self._start_game_times_import)
        self._detect_feature(Feature.ImportGameTime, ["get_game_time"])
Пример #5
0
class Plugin:
    """Use and override methods of this class to create a new platform integration."""
    def __init__(self, platform, version, reader, writer, handshake_token):
        logging.info("Creating plugin for platform %s, version %s",
                     platform.value, version)
        self._platform = platform
        self._version = version

        self._features: Set[Feature] = set()
        self._active = True

        self._reader, self._writer = reader, writer
        self._handshake_token = handshake_token

        encoder = JSONEncoder()
        self._server = Server(self._reader, self._writer, encoder)
        self._notification_client = NotificationClient(self._writer, encoder)

        self._achievements_import_in_progress = False
        self._game_times_import_in_progress = False

        self._persistent_cache = dict()

        self._internal_task_manager = TaskManager("plugin internal")
        self._external_task_manager = TaskManager("plugin external")

        # internal
        self._register_method("shutdown", self._shutdown, internal=True)
        self._register_method("get_capabilities",
                              self._get_capabilities,
                              internal=True,
                              immediate=True)
        self._register_method("initialize_cache",
                              self._initialize_cache,
                              internal=True,
                              immediate=True,
                              sensitive_params="data")
        self._register_method("ping",
                              self._ping,
                              internal=True,
                              immediate=True)

        # implemented by developer
        self._register_method("init_authentication",
                              self.authenticate,
                              sensitive_params=["stored_credentials"])
        self._register_method("pass_login_credentials",
                              self.pass_login_credentials,
                              sensitive_params=["cookies", "credentials"])
        self._register_method("import_owned_games",
                              self.get_owned_games,
                              result_name="owned_games")
        self._detect_feature(Feature.ImportOwnedGames, ["get_owned_games"])

        self._register_method("start_achievements_import",
                              self._start_achievements_import)
        self._detect_feature(Feature.ImportAchievements,
                             ["get_unlocked_achievements"])

        self._register_method("import_local_games",
                              self.get_local_games,
                              result_name="local_games")
        self._detect_feature(Feature.ImportInstalledGames, ["get_local_games"])

        self._register_notification("launch_game", self.launch_game)
        self._detect_feature(Feature.LaunchGame, ["launch_game"])

        self._register_notification("install_game", self.install_game)
        self._detect_feature(Feature.InstallGame, ["install_game"])

        self._register_notification("uninstall_game", self.uninstall_game)
        self._detect_feature(Feature.UninstallGame, ["uninstall_game"])

        self._register_notification("shutdown_platform_client",
                                    self.shutdown_platform_client)
        self._detect_feature(Feature.ShutdownPlatformClient,
                             ["shutdown_platform_client"])

        self._register_notification("launch_platform_client",
                                    self.launch_platform_client)
        self._detect_feature(Feature.LaunchPlatformClient,
                             ["launch_platform_client"])

        self._register_method("import_friends",
                              self.get_friends,
                              result_name="friend_info_list")
        self._detect_feature(Feature.ImportFriends, ["get_friends"])

        self._register_method("start_game_times_import",
                              self._start_game_times_import)
        self._detect_feature(Feature.ImportGameTime, ["get_game_time"])

    async def __aenter__(self):
        return self

    async def __aexit__(self, exc_type, exc, tb):
        self.close()
        await self.wait_closed()

    @property
    def features(self) -> List[Feature]:
        return list(self._features)

    @property
    def persistent_cache(self) -> Dict[str, str]:
        """The cache is only available after the :meth:`~.handshake_complete()` is called.
        """
        return self._persistent_cache

    def _implements(self, methods: List[str]) -> bool:
        for method in methods:
            if method not in self.__class__.__dict__:
                return False
        return True

    def _detect_feature(self, feature: Feature, methods: List[str]):
        if self._implements(methods):
            self._features.add(feature)

    def _register_method(self,
                         name,
                         handler,
                         result_name=None,
                         internal=False,
                         immediate=False,
                         sensitive_params=False):
        def wrap_result(result):
            if result_name:
                result = {result_name: result}
            return result

        if immediate:

            def method(*args, **kwargs):
                result = handler(*args, **kwargs)
                return wrap_result(result)

            self._server.register_method(name, method, True, sensitive_params)
        else:

            async def method(*args, **kwargs):
                if not internal:
                    handler_ = self._wrap_external_method(handler, name)
                else:
                    handler_ = handler
                result = await handler_(*args, **kwargs)
                return wrap_result(result)

            self._server.register_method(name, method, False, sensitive_params)

    def _register_notification(self,
                               name,
                               handler,
                               internal=False,
                               immediate=False,
                               sensitive_params=False):
        if not internal and not immediate:
            handler = self._wrap_external_method(handler, name)
        self._server.register_notification(name, handler, immediate,
                                           sensitive_params)

    def _wrap_external_method(self, handler, name: str):
        async def wrapper(*args, **kwargs):
            return await self._external_task_manager.create_task(
                handler(*args, **kwargs), name, False)

        return wrapper

    async def run(self):
        """Plugin's main coroutine."""
        await self._server.run()
        await self._external_task_manager.wait()

    def close(self) -> None:
        if not self._active:
            return

        logging.info("Closing plugin")
        self._server.close()
        self._external_task_manager.cancel()
        self._internal_task_manager.create_task(self.shutdown(), "shutdown")
        self._active = False

    async def wait_closed(self) -> None:
        await self._external_task_manager.wait()
        await self._internal_task_manager.wait()
        await self._server.wait_closed()
        await self._notification_client.close()

    def create_task(self, coro, description):
        """Wrapper around asyncio.create_task - takes care of canceling tasks on shutdown"""
        return self._external_task_manager.create_task(coro, description)

    async def _pass_control(self):
        while self._active:
            try:
                self.tick()
            except Exception:
                logging.exception("Unexpected exception raised in plugin tick")
            await asyncio.sleep(1)

    async def _shutdown(self):
        logging.info("Shutting down")
        self.close()
        await self._external_task_manager.wait()
        await self._internal_task_manager.wait()

    def _get_capabilities(self):
        return {
            "platform_name": self._platform,
            "features": self.features,
            "token": self._handshake_token
        }

    def _initialize_cache(self, data: Dict):
        self._persistent_cache = data
        try:
            self.handshake_complete()
        except Exception:
            logging.exception(
                "Unhandled exception during `handshake_complete` step")
        self._internal_task_manager.create_task(self._pass_control(), "tick")

    @staticmethod
    def _ping():
        pass

    # notifications
    def store_credentials(self, credentials: Dict[str, Any]) -> None:
        """Notify the client to store authentication credentials.
        Credentials are passed on the next authenticate call.

        :param credentials: credentials that client will store; they are stored locally on a user pc

        Example use case of store_credentials:

        .. code-block:: python
            :linenos:

            async def pass_login_credentials(self, step, credentials, cookies):
                if self.got_everything(credentials,cookies):
                    user_data = await self.parse_credentials(credentials,cookies)
                else:
                    next_params = self.get_next_params(credentials,cookies)
                    next_cookies = self.get_next_cookies(credentials,cookies)
                    return NextStep("web_session", next_params, cookies=next_cookies)
                self.store_credentials(user_data['credentials'])
                return Authentication(user_data['userId'], user_data['username'])

        """
        # temporary solution for persistent_cache vs credentials issue
        self.persistent_cache['credentials'] = credentials  # type: ignore

        self._notification_client.notify("store_credentials",
                                         credentials,
                                         sensitive_params=True)

    def add_game(self, game: Game) -> None:
        """Notify the client to add game to the list of owned games
        of the currently authenticated user.

        :param game: Game to add to the list of owned games

        Example use case of add_game:

        .. code-block:: python
            :linenos:

            async def check_for_new_games(self):
                games = await self.get_owned_games()
                for game in games:
                    if game not in self.owned_games_cache:
                        self.owned_games_cache.append(game)
                        self.add_game(game)

        """
        params = {"owned_game": game}
        self._notification_client.notify("owned_game_added", params)

    def remove_game(self, game_id: str) -> None:
        """Notify the client to remove game from the list of owned games
        of the currently authenticated user.

        :param game_id: the id of the game to remove from the list of owned games

        Example use case of remove_game:

        .. code-block:: python
            :linenos:

            async def check_for_removed_games(self):
                games = await self.get_owned_games()
                for game in self.owned_games_cache:
                    if game not in games:
                        self.owned_games_cache.remove(game)
                        self.remove_game(game.game_id)

        """
        params = {"game_id": game_id}
        self._notification_client.notify("owned_game_removed", params)

    def update_game(self, game: Game) -> None:
        """Notify the client to update the status of a game
        owned by the currently authenticated user.

        :param game: Game to update
        """
        params = {"owned_game": game}
        self._notification_client.notify("owned_game_updated", params)

    def unlock_achievement(self, game_id: str,
                           achievement: Achievement) -> None:
        """Notify the client to unlock an achievement for a specific game.

        :param game_id: the id of the game for which to unlock an achievement.
        :param achievement: achievement to unlock.
        """
        params = {"game_id": game_id, "achievement": achievement}
        self._notification_client.notify("achievement_unlocked", params)

    def _game_achievements_import_success(
            self, game_id: str, achievements: List[Achievement]) -> None:
        params = {"game_id": game_id, "unlocked_achievements": achievements}
        self._notification_client.notify("game_achievements_import_success",
                                         params)

    def _game_achievements_import_failure(self, game_id: str,
                                          error: ApplicationError) -> None:
        params = {
            "game_id": game_id,
            "error": {
                "code": error.code,
                "message": error.message
            }
        }
        self._notification_client.notify("game_achievements_import_failure",
                                         params)

    def _achievements_import_finished(self) -> None:
        self._notification_client.notify("achievements_import_finished", None)

    def update_local_game_status(self, local_game: LocalGame) -> None:
        """Notify the client to update the status of a local game.

        :param local_game: the LocalGame to update

        Example use case triggered by the :meth:`.tick` method:

        .. code-block:: python
            :linenos:
            :emphasize-lines: 5

            async def _check_statuses(self):
                for game in await self._get_local_games():
                    if game.status == self._cached_game_statuses.get(game.id):
                        continue
                    self.update_local_game_status(LocalGame(game.id, game.status))
                    self._cached_games_statuses[game.id] = game.status
                await asyncio.sleep(5)  # interval

            def tick(self):
                if self._check_statuses_task is None or self._check_statuses_task.done():
                    self._check_statuses_task = asyncio.create_task(self._check_statuses())
        """
        params = {"local_game": local_game}
        self._notification_client.notify("local_game_status_changed", params)

    def add_friend(self, user: FriendInfo) -> None:
        """Notify the client to add a user to friends list of the currently authenticated user.

        :param user: FriendInfo of a user that the client will add to friends list
        """
        params = {"friend_info": user}
        self._notification_client.notify("friend_added", params)

    def remove_friend(self, user_id: str) -> None:
        """Notify the client to remove a user from friends list of the currently authenticated user.

        :param user_id: id of the user to remove from friends list
        """
        params = {"user_id": user_id}
        self._notification_client.notify("friend_removed", params)

    def update_game_time(self, game_time: GameTime) -> None:
        """Notify the client to update game time for a game.

        :param game_time: game time to update
        """
        params = {"game_time": game_time}
        self._notification_client.notify("game_time_updated", params)

    def _game_time_import_success(self, game_time: GameTime) -> None:
        params = {"game_time": game_time}
        self._notification_client.notify("game_time_import_success", params)

    def _game_time_import_failure(self, game_id: str,
                                  error: ApplicationError) -> None:
        params = {
            "game_id": game_id,
            "error": {
                "code": error.code,
                "message": error.message
            }
        }
        self._notification_client.notify("game_time_import_failure", params)

    def _game_times_import_finished(self) -> None:
        self._notification_client.notify("game_times_import_finished", None)

    def lost_authentication(self) -> None:
        """Notify the client that integration has lost authentication for the
         current user and is unable to perform actions which would require it.
         """
        self._notification_client.notify("authentication_lost", None)

    def push_cache(self) -> None:
        """Push local copy of the persistent cache to the GOG Galaxy Client replacing existing one.
        """
        self._notification_client.notify(
            "push_cache",
            params={"data": self._persistent_cache},
            sensitive_params="data")

    # handlers
    def handshake_complete(self) -> None:
        """This method is called right after the handshake with the GOG Galaxy Client is complete and
        before any other operations are called by the GOG Galaxy Client.
        Persistent cache is available when this method is called.
        Override it if you need to do additional plugin initializations.
        This method is called internally."""

    def tick(self) -> None:
        """This method is called periodically.
        Override it to implement periodical non-blocking tasks.
        This method is called internally.

        Example of possible override of the method:

        .. code-block:: python
            :linenos:

            def tick(self):
                if not self.checking_for_new_games:
                    asyncio.create_task(self.check_for_new_games())
                if not self.checking_for_removed_games:
                    asyncio.create_task(self.check_for_removed_games())
                if not self.updating_game_statuses:
                    asyncio.create_task(self.update_game_statuses())

        """

    async def shutdown(self) -> None:
        """This method is called on integration shutdown.
        Override it to implement tear down.
        This method is called by the GOG Galaxy Client."""

    # methods
    async def authenticate(
        self,
        stored_credentials: Optional[Dict] = None
    ) -> Union[NextStep, Authentication]:
        """Override this method to handle user authentication.
        This method should either return :class:`~galaxy.api.types.Authentication` if the authentication is finished
        or :class:`~galaxy.api.types.NextStep` if it requires going to another url.
        This method is called by the GOG Galaxy Client.

        :param stored_credentials: If the client received any credentials to store locally
         in the previous session they will be passed here as a parameter.


        Example of possible override of the method:

        .. code-block:: python
            :linenos:

            async def authenticate(self, stored_credentials=None):
                if not stored_credentials:
                    return NextStep("web_session", PARAMS, cookies=COOKIES)
                else:
                    try:
                        user_data = self._authenticate(stored_credentials)
                    except AccessDenied:
                        raise InvalidCredentials()
                return Authentication(user_data['userId'], user_data['username'])

        """
        raise NotImplementedError()

    async def pass_login_credentials(self, step: str, credentials: Dict[str, str], cookies: List[Dict[str, str]]) \
        -> Union[NextStep, Authentication]:
        """This method is called if we return galaxy.api.types.NextStep from authenticate or from pass_login_credentials.
        This method's parameters provide the data extracted from the web page navigation that previous NextStep finished on.
        This method should either return galaxy.api.types.Authentication if the authentication is finished
        or galaxy.api.types.NextStep if it requires going to another cef url.
        This method is called by the GOG Galaxy Client.

        :param step: deprecated.
        :param credentials: end_uri previous NextStep finished on.
        :param cookies: cookies extracted from the end_uri site.

        Example of possible override of the method:

        .. code-block:: python
            :linenos:

            async def pass_login_credentials(self, step, credentials, cookies):
                if self.got_everything(credentials,cookies):
                    user_data = await self.parse_credentials(credentials,cookies)
                else:
                    next_params = self.get_next_params(credentials,cookies)
                    next_cookies = self.get_next_cookies(credentials,cookies)
                    return NextStep("web_session", next_params, cookies=next_cookies)
                self.store_credentials(user_data['credentials'])
                return Authentication(user_data['userId'], user_data['username'])

        """
        raise NotImplementedError()

    async def get_owned_games(self) -> List[Game]:
        """Override this method to return owned games for currently logged in user.
        This method is called by the GOG Galaxy Client.

        Example of possible override of the method:

        .. code-block:: python
            :linenos:

            async def get_owned_games(self):
                if not self.authenticated():
                    raise AuthenticationRequired()

                games = self.retrieve_owned_games()
                return games

        """
        raise NotImplementedError()

    async def _start_achievements_import(self, game_ids: List[str]) -> None:
        if self._achievements_import_in_progress:
            raise ImportInProgress()

        context = await self.prepare_achievements_context(game_ids)

        async def import_game_achievements(game_id, context_):
            try:
                achievements = await self.get_unlocked_achievements(
                    game_id, context_)
                self._game_achievements_import_success(game_id, achievements)
            except ApplicationError as error:
                self._game_achievements_import_failure(game_id, error)
            except Exception:
                logging.exception(
                    "Unexpected exception raised in import_game_achievements")
                self._game_achievements_import_failure(game_id, UnknownError())

        async def import_games_achievements(game_ids_, context_):
            try:
                imports = [
                    import_game_achievements(game_id, context_)
                    for game_id in game_ids_
                ]
                await asyncio.gather(*imports)
            finally:
                self._achievements_import_finished()
                self._achievements_import_in_progress = False
                self.achievements_import_complete()

        self._external_task_manager.create_task(import_games_achievements(
            game_ids, context),
                                                "unlocked achievements import",
                                                handle_exceptions=False)
        self._achievements_import_in_progress = True

    async def prepare_achievements_context(self, game_ids: List[str]) -> Any:
        """Override this method to prepare context for get_unlocked_achievements.
        This allows for optimizations like batch requests to platform API.
        Default implementation returns None.

        :param game_ids: the ids of the games for which achievements are imported
        :return: context
        """
        return None

    async def get_unlocked_achievements(self, game_id: str,
                                        context: Any) -> List[Achievement]:
        """Override this method to return list of unlocked achievements
        for the game identified by the provided game_id.
        This method is called by import task initialized by GOG Galaxy Client.

        :param game_id: the id of the game for which the achievements are returned
        :param context: the value returned from :meth:`prepare_achievements_context`
        :return: list of Achievement objects
        """
        raise NotImplementedError()

    def achievements_import_complete(self):
        """Override this method to handle operations after achievements import is finished
        (like updating cache).
        """

    async def get_local_games(self) -> List[LocalGame]:
        """Override this method to return the list of
        games present locally on the users pc.
        This method is called by the GOG Galaxy Client.

        Example of possible override of the method:

        .. code-block:: python
            :linenos:

            async def get_local_games(self):
                local_games = []
                for game in self.games_present_on_user_pc:
                    local_game = LocalGame()
                    local_game.game_id = game.id
                    local_game.local_game_state = game.get_installation_status()
                    local_games.append(local_game)
                return local_games

        """
        raise NotImplementedError()

    async def launch_game(self, game_id: str) -> None:
        """Override this method to launch the game
        identified by the provided game_id.
        This method is called by the GOG Galaxy Client.

        :param str game_id: the id of the game to launch

        Example of possible override of the method:

        .. code-block:: python
            :linenos:

            async def launch_game(self, game_id):
                await self.open_uri(f"start client://launchgame/{game_id}")

        """
        raise NotImplementedError()

    async def install_game(self, game_id: str) -> None:
        """Override this method to install the game
        identified by the provided game_id.
        This method is called by the GOG Galaxy Client.

        :param str game_id: the id of the game to install

        Example of possible override of the method:

        .. code-block:: python
            :linenos:

            async def install_game(self, game_id):
                await self.open_uri(f"start client://installgame/{game_id}")

        """
        raise NotImplementedError()

    async def uninstall_game(self, game_id: str) -> None:
        """Override this method to uninstall the game
        identified by the provided game_id.
        This method is called by the GOG Galaxy Client.

        :param str game_id: the id of the game to uninstall

        Example of possible override of the method:

        .. code-block:: python
            :linenos:

            async def uninstall_game(self, game_id):
                await self.open_uri(f"start client://uninstallgame/{game_id}")

        """
        raise NotImplementedError()

    async def shutdown_platform_client(self) -> None:
        """Override this method to gracefully terminate platform client.
        This method is called by the GOG Galaxy Client."""
        raise NotImplementedError()

    async def launch_platform_client(self) -> None:
        """Override this method to launch platform client. Preferably minimized to tray.
        This method is called by the GOG Galaxy Client."""
        raise NotImplementedError()

    async def get_friends(self) -> List[FriendInfo]:
        """Override this method to return the friends list
        of the currently authenticated user.
        This method is called by the GOG Galaxy Client.

        Example of possible override of the method:

        .. code-block:: python
            :linenos:

            async def get_friends(self):
                if not self._http_client.is_authenticated():
                    raise AuthenticationRequired()

                friends = self.retrieve_friends()
                return friends

        """
        raise NotImplementedError()

    async def _start_game_times_import(self, game_ids: List[str]) -> None:
        if self._game_times_import_in_progress:
            raise ImportInProgress()

        context = await self.prepare_game_times_context(game_ids)

        async def import_game_time(game_id, context_):
            try:
                game_time = await self.get_game_time(game_id, context_)
                self._game_time_import_success(game_time)
            except ApplicationError as error:
                self._game_time_import_failure(game_id, error)
            except Exception:
                logging.exception(
                    "Unexpected exception raised in import_game_time")
                self._game_time_import_failure(game_id, UnknownError())

        async def import_game_times(game_ids_, context_):
            try:
                imports = [
                    import_game_time(game_id, context_)
                    for game_id in game_ids_
                ]
                await asyncio.gather(*imports)
            finally:
                self._game_times_import_finished()
                self._game_times_import_in_progress = False
                self.game_times_import_complete()

        self._external_task_manager.create_task(import_game_times(
            game_ids, context),
                                                "game times import",
                                                handle_exceptions=False)
        self._game_times_import_in_progress = True

    async def prepare_game_times_context(self, game_ids: List[str]) -> Any:
        """Override this method to prepare context for get_game_time.
        This allows for optimizations like batch requests to platform API.
        Default implementation returns None.

        :param game_ids: the ids of the games for which game time are imported
        :return: context
        """
        return None

    async def get_game_time(self, game_id: str, context: Any) -> GameTime:
        """Override this method to return the game time for the game
        identified by the provided game_id.
        This method is called by import task initialized by GOG Galaxy Client.

        :param game_id: the id of the game for which the game time is returned
        :param context: the value returned from :meth:`prepare_game_times_context`
        :return: GameTime object
        """
        raise NotImplementedError()

    def game_times_import_complete(self) -> None:
        """Override this method to handle operations after game times import is finished
Пример #6
0
class Connection():
    def __init__(self, reader, writer, encoder=json.JSONEncoder()):
        self._active = True
        self._reader = StreamLineReader(reader)
        self._writer = writer
        self._encoder = encoder
        self._methods = {}
        self._notifications = {}
        self._task_manager = TaskManager("jsonrpc server")
        self._last_request_id = 0
        self._requests_futures = {}

    def register_method(self,
                        name,
                        callback,
                        immediate,
                        sensitive_params=False):
        """
        Register method

        :param name:
        :param callback:
        :param internal: if True the callback will be processed immediately (synchronously)
        :param sensitive_params: list of parameters that are anonymized before logging; \
            if False - no params are considered sensitive, if True - all params are considered sensitive
        """
        self._methods[name] = Method(callback, inspect.signature(callback),
                                     immediate, sensitive_params)

    def register_notification(self,
                              name,
                              callback,
                              immediate,
                              sensitive_params=False):
        """
        Register notification

        :param name:
        :param callback:
        :param internal: if True the callback will be processed immediately (synchronously)
        :param sensitive_params: list of parameters that are anonymized before logging; \
            if False - no params are considered sensitive, if True - all params are considered sensitive
        """
        self._notifications[name] = Method(callback,
                                           inspect.signature(callback),
                                           immediate, sensitive_params)

    async def send_request(self, method, params, sensitive_params):
        """
        Send request

        :param method:
        :param params:
        :param sensitive_params: list of parameters that are anonymized before logging; \
            if False - no params are considered sensitive, if True - all params are considered sensitive
        """
        self._last_request_id += 1
        request_id = str(self._last_request_id)

        loop = asyncio.get_running_loop()
        future = loop.create_future()
        self._requests_futures[self._last_request_id] = (future,
                                                         sensitive_params)

        logger.info("Sending request: id=%s, method=%s, params=%s",
                    request_id, method,
                    anonymise_sensitive_params(params, sensitive_params))

        self._send_request(request_id, method, params)
        return await future

    def send_notification(self, method, params, sensitive_params=False):
        """
        Send notification

        :param method:
        :param params:
        :param sensitive_params: list of parameters that are anonymized before logging; \
            if False - no params are considered sensitive, if True - all params are considered sensitive
        """

        logger.info("Sending notification: method=%s, params=%s", method,
                    anonymise_sensitive_params(params, sensitive_params))

        self._send_notification(method, params)

    async def run(self):
        while self._active:
            try:
                data = await self._reader.readline()
                if not data:
                    self._eof()
                    continue
            except:
                self._eof()
                continue
            data = data.strip()
            logger.debug("Received %d bytes of data", len(data))
            self._handle_input(data)
            await asyncio.sleep(0)  # To not starve task queue

    def close(self):
        if self._active:
            logger.info(
                "Closing JSON-RPC server - not more messages will be read")
            self._active = False

    async def wait_closed(self):
        await self._task_manager.wait()

    def _eof(self):
        logger.info("Received EOF")
        self.close()

    def _handle_input(self, data):
        try:
            message = self._parse_message(data)
        except JsonRpcError as error:
            self._send_error(None, error)
            return

        if isinstance(message, Request):
            if message.id is not None:
                self._handle_request(message)
            else:
                self._handle_notification(message)
        elif isinstance(message, Response):
            self._handle_response(message)

    def _handle_response(self, response):
        request_future = self._requests_futures.get(int(response.id))
        if request_future is None:
            response_type = "response" if response.result is not None else "error"
            logger.warning("Received %s for unknown request: %s",
                           response_type, response.id)
            return

        future, sensitive_params = request_future

        if response.error:
            error = JsonRpcError(response.error.setdefault("code", 0),
                                 response.error.setdefault("message", ""),
                                 response.error.setdefault("data", None))
            self._log_error(response, error, sensitive_params)
            future.set_exception(error)
            return

        self._log_response(response, sensitive_params)
        future.set_result(response.result)

    def _handle_notification(self, request):
        method = self._notifications.get(request.method)
        if not method:
            logger.error("Received unknown notification: %s", request.method)
            return

        callback, signature, immediate, sensitive_params = method
        self._log_request(request, sensitive_params)

        try:
            bound_args = signature.bind(**request.params)
        except TypeError:
            self._send_error(request.id, InvalidParams())

        if immediate:
            callback(*bound_args.args, **bound_args.kwargs)
        else:
            try:
                self._task_manager.create_task(
                    callback(*bound_args.args, **bound_args.kwargs),
                    request.method)
            except Exception:
                logger.exception(
                    "Unexpected exception raised in notification handler")

    def _handle_request(self, request):
        method = self._methods.get(request.method)
        if not method:
            logger.error("Received unknown request: %s", request.method)
            self._send_error(request.id, MethodNotFound())
            return

        callback, signature, immediate, sensitive_params = method
        self._log_request(request, sensitive_params)

        try:
            bound_args = signature.bind(**request.params)
        except TypeError:
            self._send_error(request.id, InvalidParams())

        if immediate:
            response = callback(*bound_args.args, **bound_args.kwargs)
            self._send_response(request.id, response)
        else:

            async def handle():
                try:
                    result = await callback(*bound_args.args,
                                            **bound_args.kwargs)
                    self._send_response(request.id, result)
                except NotImplementedError:
                    self._send_error(request.id, MethodNotFound())
                except JsonRpcError as error:
                    self._send_error(request.id, error)
                except asyncio.CancelledError:
                    self._send_error(request.id, Aborted())
                except Exception as e:  #pylint: disable=broad-except
                    logger.exception(
                        "Unexpected exception raised in plugin handler")
                    self._send_error(request.id, UnknownError(str(e)))

            self._task_manager.create_task(handle(), request.method)

    @staticmethod
    def _parse_message(data):
        try:
            jsonrpc_message = json.loads(data, encoding="utf-8")
            if jsonrpc_message.get("jsonrpc") != "2.0":
                raise InvalidRequest()
            del jsonrpc_message["jsonrpc"]
            if "result" in jsonrpc_message.keys(
            ) or "error" in jsonrpc_message.keys():
                return Response(**jsonrpc_message)
            else:
                return Request(**jsonrpc_message)

        except json.JSONDecodeError:
            raise ParseError()
        except TypeError:
            raise InvalidRequest()

    def _send(self, data, sensitive=True):
        try:
            line = self._encoder.encode(data)
            data = (line + "\n").encode("utf-8")
            if sensitive:
                logger.debug("Sending %d bytes of data", len(data))
            else:
                logger.debug("Sending data: %s", line)
            self._writer.write(data)
        except TypeError as error:
            logger.error(str(error))

    def _send_response(self, request_id, result):
        response = {"jsonrpc": "2.0", "id": request_id, "result": result}
        self._send(response, sensitive=False)

    def _send_error(self, request_id, error):
        response = {"jsonrpc": "2.0", "id": request_id, "error": error.json()}

        self._send(response, sensitive=False)

    def _send_request(self, request_id, method, params):
        request = {
            "jsonrpc": "2.0",
            "method": method,
            "id": request_id,
            "params": params
        }
        self._send(request, sensitive=True)

    def _send_notification(self, method, params):
        notification = {"jsonrpc": "2.0", "method": method, "params": params}
        self._send(notification, sensitive=True)

    @staticmethod
    def _log_request(request, sensitive_params):
        params = anonymise_sensitive_params(request.params, sensitive_params)
        if request.id is not None:
            logger.info("Handling request: id=%s, method=%s, params=%s",
                        request.id, request.method, params)
        else:
            logger.info("Handling notification: method=%s, params=%s",
                        request.method, params)

    @staticmethod
    def _log_response(response, sensitive_params):
        result = anonymise_sensitive_params(response.result, sensitive_params)
        logger.info("Handling response: id=%s, result=%s", response.id, result)

    @staticmethod
    def _log_error(response, error, sensitive_params):
        params = error.data if error.data is not None else {}
        data = anonymise_sensitive_params(params, sensitive_params)
        logger.info("Handling error: id=%s, code=%s, description=%s, data=%s",
                    response.id, error.code, error.message, data)
Пример #7
0
    def __init__(self, platform, version, reader, writer, handshake_token):
        logger.info("Creating plugin for platform %s, version %s",
                    platform.value, version)
        self._platform = platform
        self._version = version

        self._features: Set[Feature] = set()
        self._active = True

        self._reader, self._writer = reader, writer
        self._handshake_token = handshake_token

        encoder = JSONEncoder()
        self._connection = Connection(self._reader, self._writer, encoder)

        self._persistent_cache = dict()

        self._internal_task_manager = TaskManager("plugin internal")
        self._external_task_manager = TaskManager("plugin external")

        self._achievements_importer = Importer(
            self._external_task_manager, "achievements",
            self.get_unlocked_achievements, self.prepare_achievements_context,
            self._game_achievements_import_success,
            self._game_achievements_import_failure,
            self._achievements_import_finished,
            self.achievements_import_complete)
        self._game_time_importer = Importer(
            self._external_task_manager, "game times", self.get_game_time,
            self.prepare_game_times_context, self._game_time_import_success,
            self._game_time_import_failure, self._game_times_import_finished,
            self.game_times_import_complete)
        self._game_library_settings_importer = Importer(
            self._external_task_manager, "game library settings",
            self.get_game_library_settings,
            self.prepare_game_library_settings_context,
            self._game_library_settings_import_success,
            self._game_library_settings_import_failure,
            self._game_library_settings_import_finished,
            self.game_library_settings_import_complete)
        self._os_compatibility_importer = Importer(
            self._external_task_manager, "os compatibility",
            self.get_os_compatibility, self.prepare_os_compatibility_context,
            self._os_compatibility_import_success,
            self._os_compatibility_import_failure,
            self._os_compatibility_import_finished,
            self.os_compatibility_import_complete)
        self._user_presence_importer = Importer(
            self._external_task_manager, "users presence",
            self.get_user_presence, self.prepare_user_presence_context,
            self._user_presence_import_success,
            self._user_presence_import_failure,
            self._user_presence_import_finished,
            self.user_presence_import_complete)
        self._local_size_importer = SynchroneousImporter(
            self._external_task_manager, "local size", self.get_local_size,
            self.prepare_local_size_context, self._local_size_import_success,
            self._local_size_import_failure, self._local_size_import_finished,
            self.local_size_import_complete)
        self._subscription_games_importer = CollectionImporter(
            self._subscriptions_games_partial_import_finished,
            self._external_task_manager, "subscription games",
            self.get_subscription_games,
            self.prepare_subscription_games_context,
            self._subscription_games_import_success,
            self._subscription_games_import_failure,
            self._subscription_games_import_finished,
            self.subscription_games_import_complete)

        # internal
        self._register_method("shutdown", self._shutdown, internal=True)
        self._register_method("get_capabilities",
                              self._get_capabilities,
                              internal=True,
                              immediate=True)
        self._register_method("initialize_cache",
                              self._initialize_cache,
                              internal=True,
                              immediate=True,
                              sensitive_params="data")
        self._register_method("ping",
                              self._ping,
                              internal=True,
                              immediate=True)

        # implemented by developer
        self._register_method("init_authentication",
                              self.authenticate,
                              sensitive_params=["stored_credentials"])
        self._register_method("pass_login_credentials",
                              self.pass_login_credentials,
                              sensitive_params=["cookies", "credentials"])
        self._register_method("import_owned_games",
                              self.get_owned_games,
                              result_name="owned_games")
        self._detect_feature(Feature.ImportOwnedGames, ["get_owned_games"])

        self._register_method("start_achievements_import",
                              self._start_achievements_import)
        self._detect_feature(Feature.ImportAchievements,
                             ["get_unlocked_achievements"])

        self._register_method("import_local_games",
                              self.get_local_games,
                              result_name="local_games")
        self._detect_feature(Feature.ImportInstalledGames, ["get_local_games"])

        self._register_notification("launch_game", self.launch_game)
        self._detect_feature(Feature.LaunchGame, ["launch_game"])

        self._register_notification("install_game", self.install_game)
        self._detect_feature(Feature.InstallGame, ["install_game"])

        self._register_notification("uninstall_game", self.uninstall_game)
        self._detect_feature(Feature.UninstallGame, ["uninstall_game"])

        self._register_notification("shutdown_platform_client",
                                    self.shutdown_platform_client)
        self._detect_feature(Feature.ShutdownPlatformClient,
                             ["shutdown_platform_client"])

        self._register_notification("launch_platform_client",
                                    self.launch_platform_client)
        self._detect_feature(Feature.LaunchPlatformClient,
                             ["launch_platform_client"])

        self._register_method("import_friends",
                              self.get_friends,
                              result_name="friend_info_list")
        self._detect_feature(Feature.ImportFriends, ["get_friends"])

        self._register_method("start_game_times_import",
                              self._start_game_times_import)
        self._detect_feature(Feature.ImportGameTime, ["get_game_time"])

        self._register_method("start_game_library_settings_import",
                              self._start_game_library_settings_import)
        self._detect_feature(Feature.ImportGameLibrarySettings,
                             ["get_game_library_settings"])

        self._register_method("start_os_compatibility_import",
                              self._start_os_compatibility_import)
        self._detect_feature(Feature.ImportOSCompatibility,
                             ["get_os_compatibility"])

        self._register_method("start_user_presence_import",
                              self._start_user_presence_import)
        self._detect_feature(Feature.ImportUserPresence, ["get_user_presence"])

        self._register_method("start_local_size_import",
                              self._start_local_size_import)
        self._detect_feature(Feature.ImportLocalSize, ["get_local_size"])

        self._register_method("import_subscriptions",
                              self.get_subscriptions,
                              result_name="subscriptions")
        self._detect_feature(Feature.ImportSubscriptions,
                             ["get_subscriptions"])

        self._register_method("start_subscription_games_import",
                              self._start_subscription_games_import)
        self._detect_feature(Feature.ImportSubscriptionGames,
                             ["get_subscription_games"])
Пример #8
0
class Server():
    def __init__(self, reader, writer, encoder=json.JSONEncoder()):
        self._active = True
        self._reader = StreamLineReader(reader)
        self._writer = writer
        self._encoder = encoder
        self._methods = {}
        self._notifications = {}
        self._task_manager = TaskManager("jsonrpc server")

    def register_method(self, name, callback, immediate, sensitive_params=False):
        """
        Register method

        :param name:
        :param callback:
        :param internal: if True the callback will be processed immediately (synchronously)
        :param sensitive_params: list of parameters that are anonymized before logging; \
            if False - no params are considered sensitive, if True - all params are considered sensitive
        """
        self._methods[name] = Method(callback, inspect.signature(callback), immediate, sensitive_params)

    def register_notification(self, name, callback, immediate, sensitive_params=False):
        """
        Register notification

        :param name:
        :param callback:
        :param internal: if True the callback will be processed immediately (synchronously)
        :param sensitive_params: list of parameters that are anonymized before logging; \
            if False - no params are considered sensitive, if True - all params are considered sensitive
        """
        self._notifications[name] = Method(callback, inspect.signature(callback), immediate, sensitive_params)

    async def run(self):
        while self._active:
            try:
                data = await self._reader.readline()
                if not data:
                    self._eof()
                    continue
            except:
                self._eof()
                continue
            data = data.strip()
            logging.debug("Received %d bytes of data", len(data))
            self._handle_input(data)
            await asyncio.sleep(0) # To not starve task queue

    def close(self):
        logging.info("Closing JSON-RPC server - not more messages will be read")
        self._active = False

    async def wait_closed(self):
        await self._task_manager.wait()

    def _eof(self):
        logging.info("Received EOF")
        self.close()

    def _handle_input(self, data):
        try:
            request = self._parse_request(data)
        except JsonRpcError as error:
            self._send_error(None, error)
            return

        if request.id is not None:
            self._handle_request(request)
        else:
            self._handle_notification(request)

    def _handle_notification(self, request):
        method = self._notifications.get(request.method)
        if not method:
            logging.error("Received unknown notification: %s", request.method)
            return

        callback, signature, immediate, sensitive_params = method
        self._log_request(request, sensitive_params)

        try:
            bound_args = signature.bind(**request.params)
        except TypeError:
            self._send_error(request.id, InvalidParams())

        if immediate:
            callback(*bound_args.args, **bound_args.kwargs)
        else:
            try:
                self._task_manager.create_task(callback(*bound_args.args, **bound_args.kwargs), request.method)
            except Exception:
                logging.exception("Unexpected exception raised in notification handler")

    def _handle_request(self, request):
        method = self._methods.get(request.method)
        if not method:
            logging.error("Received unknown request: %s", request.method)
            self._send_error(request.id, MethodNotFound())
            return

        callback, signature, immediate, sensitive_params = method
        self._log_request(request, sensitive_params)

        try:
            bound_args = signature.bind(**request.params)
        except TypeError:
            self._send_error(request.id, InvalidParams())

        if immediate:
            response = callback(*bound_args.args, **bound_args.kwargs)
            self._send_response(request.id, response)
        else:
            async def handle():
                try:
                    result = await callback(*bound_args.args, **bound_args.kwargs)
                    self._send_response(request.id, result)
                except NotImplementedError:
                    self._send_error(request.id, MethodNotFound())
                except JsonRpcError as error:
                    self._send_error(request.id, error)
                except asyncio.CancelledError:
                    self._send_error(request.id, Aborted())
                except Exception as e:  #pylint: disable=broad-except
                    logging.exception("Unexpected exception raised in plugin handler")
                    self._send_error(request.id, UnknownError(str(e)))

            self._task_manager.create_task(handle(), request.method)

    @staticmethod
    def _parse_request(data):
        try:
            jsonrpc_request = json.loads(data, encoding="utf-8")
            if jsonrpc_request.get("jsonrpc") != "2.0":
                raise InvalidRequest()
            del jsonrpc_request["jsonrpc"]
            return Request(**jsonrpc_request)
        except json.JSONDecodeError:
            raise ParseError()
        except TypeError:
            raise InvalidRequest()

    def _send(self, data):
        try:
            line = self._encoder.encode(data)
            logging.debug("Sending data: %s", line)
            data = (line + "\n").encode("utf-8")
            self._writer.write(data)
            self._task_manager.create_task(self._writer.drain(), "drain")
        except TypeError as error:
            logging.error(str(error))

    def _send_response(self, request_id, result):
        response = {
            "jsonrpc": "2.0",
            "id": request_id,
            "result": result
        }
        self._send(response)

    def _send_error(self, request_id, error):
        response = {
            "jsonrpc": "2.0",
            "id": request_id,
            "error": {
                "code": error.code,
                "message": error.message
            }
        }

        if error.data is not None:
            response["error"]["data"] = error.data

        self._send(response)

    @staticmethod
    def _log_request(request, sensitive_params):
        params = anonymise_sensitive_params(request.params, sensitive_params)
        if request.id is not None:
            logging.info("Handling request: id=%s, method=%s, params=%s", request.id, request.method, params)
        else:
            logging.info("Handling notification: method=%s, params=%s", request.method, params)
Пример #9
0
 def __init__(self, writer, encoder=json.JSONEncoder()):
     self._writer = writer
     self._encoder = encoder
     self._methods = {}
     self._task_manager = TaskManager("notification client")