mirror of
https://github.com/gogcom/galaxy-integrations-python-api.git
synced 2026-01-01 11:28:12 -05:00
Compare commits
25 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
26102dd832 | ||
|
|
cdcebda529 | ||
|
|
a83f348d7d | ||
|
|
1c196d60d5 | ||
|
|
deb125ec48 | ||
|
|
4cc0055119 | ||
|
|
00164fab67 | ||
|
|
453cd1cc70 | ||
|
|
1f55253fd7 | ||
|
|
7aa3b01abd | ||
|
|
bd14d58bad | ||
|
|
274b9a2c18 | ||
|
|
75e5a66fbe | ||
|
|
2a9ec3067d | ||
|
|
69532a5ba9 | ||
|
|
f5d47b0167 | ||
|
|
02f4faa432 | ||
|
|
3d3922c965 | ||
|
|
b695cdfc78 | ||
|
|
66ab1809b8 | ||
|
|
8bf367d0f9 | ||
|
|
2cf83395fa | ||
|
|
4aa76b6e3d | ||
|
|
c03465e8f2 | ||
|
|
810a87718d |
7
setup.py
7
setup.py
@@ -2,14 +2,15 @@ from setuptools import setup, find_packages
|
|||||||
|
|
||||||
setup(
|
setup(
|
||||||
name="galaxy.plugin.api",
|
name="galaxy.plugin.api",
|
||||||
version="0.56",
|
version="0.63",
|
||||||
description="GOG Galaxy Integrations Python API",
|
description="GOG Galaxy Integrations Python API",
|
||||||
author='Galaxy team',
|
author='Galaxy team',
|
||||||
author_email='galaxy@gog.com',
|
author_email='galaxy@gog.com',
|
||||||
packages=find_packages("src"),
|
packages=find_packages("src"),
|
||||||
package_dir={'': 'src'},
|
package_dir={'': 'src'},
|
||||||
install_requires=[
|
install_requires=[
|
||||||
"aiohttp==3.5.4",
|
"aiohttp>=3.5.4",
|
||||||
"certifi==2019.3.9"
|
"certifi>=2019.3.9",
|
||||||
|
"psutil>=5.6.3; sys_platform == 'darwin'"
|
||||||
]
|
]
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -8,6 +8,10 @@ import json
|
|||||||
from galaxy.reader import StreamLineReader
|
from galaxy.reader import StreamLineReader
|
||||||
from galaxy.task_manager import TaskManager
|
from galaxy.task_manager import TaskManager
|
||||||
|
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
class JsonRpcError(Exception):
|
class JsonRpcError(Exception):
|
||||||
def __init__(self, code, message, data=None):
|
def __init__(self, code, message, data=None):
|
||||||
self.code = code
|
self.code = code
|
||||||
@@ -25,7 +29,7 @@ class JsonRpcError(Exception):
|
|||||||
}
|
}
|
||||||
|
|
||||||
if self.data is not None:
|
if self.data is not None:
|
||||||
obj["error"]["data"] = self.data
|
obj["data"] = self.data
|
||||||
|
|
||||||
return obj
|
return obj
|
||||||
|
|
||||||
@@ -89,7 +93,6 @@ class Connection():
|
|||||||
self._methods = {}
|
self._methods = {}
|
||||||
self._notifications = {}
|
self._notifications = {}
|
||||||
self._task_manager = TaskManager("jsonrpc server")
|
self._task_manager = TaskManager("jsonrpc server")
|
||||||
self._write_lock = asyncio.Lock()
|
|
||||||
self._last_request_id = 0
|
self._last_request_id = 0
|
||||||
self._requests_futures = {}
|
self._requests_futures = {}
|
||||||
|
|
||||||
@@ -133,7 +136,7 @@ class Connection():
|
|||||||
future = loop.create_future()
|
future = loop.create_future()
|
||||||
self._requests_futures[self._last_request_id] = (future, sensitive_params)
|
self._requests_futures[self._last_request_id] = (future, sensitive_params)
|
||||||
|
|
||||||
logging.info(
|
logger.info(
|
||||||
"Sending request: id=%s, method=%s, params=%s",
|
"Sending request: id=%s, method=%s, params=%s",
|
||||||
request_id, method, anonymise_sensitive_params(params, sensitive_params)
|
request_id, method, anonymise_sensitive_params(params, sensitive_params)
|
||||||
)
|
)
|
||||||
@@ -151,7 +154,7 @@ class Connection():
|
|||||||
if False - no params are considered sensitive, if True - all params are considered sensitive
|
if False - no params are considered sensitive, if True - all params are considered sensitive
|
||||||
"""
|
"""
|
||||||
|
|
||||||
logging.info(
|
logger.info(
|
||||||
"Sending notification: method=%s, params=%s",
|
"Sending notification: method=%s, params=%s",
|
||||||
method, anonymise_sensitive_params(params, sensitive_params)
|
method, anonymise_sensitive_params(params, sensitive_params)
|
||||||
)
|
)
|
||||||
@@ -169,20 +172,20 @@ class Connection():
|
|||||||
self._eof()
|
self._eof()
|
||||||
continue
|
continue
|
||||||
data = data.strip()
|
data = data.strip()
|
||||||
logging.debug("Received %d bytes of data", len(data))
|
logger.debug("Received %d bytes of data", len(data))
|
||||||
self._handle_input(data)
|
self._handle_input(data)
|
||||||
await asyncio.sleep(0) # To not starve task queue
|
await asyncio.sleep(0) # To not starve task queue
|
||||||
|
|
||||||
def close(self):
|
def close(self):
|
||||||
if self._active:
|
if self._active:
|
||||||
logging.info("Closing JSON-RPC server - not more messages will be read")
|
logger.info("Closing JSON-RPC server - not more messages will be read")
|
||||||
self._active = False
|
self._active = False
|
||||||
|
|
||||||
async def wait_closed(self):
|
async def wait_closed(self):
|
||||||
await self._task_manager.wait()
|
await self._task_manager.wait()
|
||||||
|
|
||||||
def _eof(self):
|
def _eof(self):
|
||||||
logging.info("Received EOF")
|
logger.info("Received EOF")
|
||||||
self.close()
|
self.close()
|
||||||
|
|
||||||
def _handle_input(self, data):
|
def _handle_input(self, data):
|
||||||
@@ -204,7 +207,7 @@ class Connection():
|
|||||||
request_future = self._requests_futures.get(int(response.id))
|
request_future = self._requests_futures.get(int(response.id))
|
||||||
if request_future is None:
|
if request_future is None:
|
||||||
response_type = "response" if response.result is not None else "error"
|
response_type = "response" if response.result is not None else "error"
|
||||||
logging.warning("Received %s for unknown request: %s", response_type, response.id)
|
logger.warning("Received %s for unknown request: %s", response_type, response.id)
|
||||||
return
|
return
|
||||||
|
|
||||||
future, sensitive_params = request_future
|
future, sensitive_params = request_future
|
||||||
@@ -225,7 +228,7 @@ class Connection():
|
|||||||
def _handle_notification(self, request):
|
def _handle_notification(self, request):
|
||||||
method = self._notifications.get(request.method)
|
method = self._notifications.get(request.method)
|
||||||
if not method:
|
if not method:
|
||||||
logging.error("Received unknown notification: %s", request.method)
|
logger.error("Received unknown notification: %s", request.method)
|
||||||
return
|
return
|
||||||
|
|
||||||
callback, signature, immediate, sensitive_params = method
|
callback, signature, immediate, sensitive_params = method
|
||||||
@@ -242,12 +245,12 @@ class Connection():
|
|||||||
try:
|
try:
|
||||||
self._task_manager.create_task(callback(*bound_args.args, **bound_args.kwargs), request.method)
|
self._task_manager.create_task(callback(*bound_args.args, **bound_args.kwargs), request.method)
|
||||||
except Exception:
|
except Exception:
|
||||||
logging.exception("Unexpected exception raised in notification handler")
|
logger.exception("Unexpected exception raised in notification handler")
|
||||||
|
|
||||||
def _handle_request(self, request):
|
def _handle_request(self, request):
|
||||||
method = self._methods.get(request.method)
|
method = self._methods.get(request.method)
|
||||||
if not method:
|
if not method:
|
||||||
logging.error("Received unknown request: %s", request.method)
|
logger.error("Received unknown request: %s", request.method)
|
||||||
self._send_error(request.id, MethodNotFound())
|
self._send_error(request.id, MethodNotFound())
|
||||||
return
|
return
|
||||||
|
|
||||||
@@ -274,7 +277,7 @@ class Connection():
|
|||||||
except asyncio.CancelledError:
|
except asyncio.CancelledError:
|
||||||
self._send_error(request.id, Aborted())
|
self._send_error(request.id, Aborted())
|
||||||
except Exception as e: #pylint: disable=broad-except
|
except Exception as e: #pylint: disable=broad-except
|
||||||
logging.exception("Unexpected exception raised in plugin handler")
|
logger.exception("Unexpected exception raised in plugin handler")
|
||||||
self._send_error(request.id, UnknownError(str(e)))
|
self._send_error(request.id, UnknownError(str(e)))
|
||||||
|
|
||||||
self._task_manager.create_task(handle(), request.method)
|
self._task_manager.create_task(handle(), request.method)
|
||||||
@@ -296,19 +299,17 @@ class Connection():
|
|||||||
except TypeError:
|
except TypeError:
|
||||||
raise InvalidRequest()
|
raise InvalidRequest()
|
||||||
|
|
||||||
def _send(self, data):
|
def _send(self, data, sensitive=True):
|
||||||
async def send_task(data_):
|
|
||||||
async with self._write_lock:
|
|
||||||
self._writer.write(data_)
|
|
||||||
await self._writer.drain()
|
|
||||||
|
|
||||||
try:
|
try:
|
||||||
line = self._encoder.encode(data)
|
line = self._encoder.encode(data)
|
||||||
logging.debug("Sending data: %s", line)
|
|
||||||
data = (line + "\n").encode("utf-8")
|
data = (line + "\n").encode("utf-8")
|
||||||
self._task_manager.create_task(send_task(data), "send")
|
if sensitive:
|
||||||
|
logger.debug("Sending %d bytes of data", len(data))
|
||||||
|
else:
|
||||||
|
logging.debug("Sending data: %s", line)
|
||||||
|
self._writer.write(data)
|
||||||
except TypeError as error:
|
except TypeError as error:
|
||||||
logging.error(str(error))
|
logger.error(str(error))
|
||||||
|
|
||||||
def _send_response(self, request_id, result):
|
def _send_response(self, request_id, result):
|
||||||
response = {
|
response = {
|
||||||
@@ -316,7 +317,7 @@ class Connection():
|
|||||||
"id": request_id,
|
"id": request_id,
|
||||||
"result": result
|
"result": result
|
||||||
}
|
}
|
||||||
self._send(response)
|
self._send(response, sensitive=False)
|
||||||
|
|
||||||
def _send_error(self, request_id, error):
|
def _send_error(self, request_id, error):
|
||||||
response = {
|
response = {
|
||||||
@@ -325,7 +326,7 @@ class Connection():
|
|||||||
"error": error.json()
|
"error": error.json()
|
||||||
}
|
}
|
||||||
|
|
||||||
self._send(response)
|
self._send(response, sensitive=False)
|
||||||
|
|
||||||
def _send_request(self, request_id, method, params):
|
def _send_request(self, request_id, method, params):
|
||||||
request = {
|
request = {
|
||||||
@@ -334,7 +335,7 @@ class Connection():
|
|||||||
"id": request_id,
|
"id": request_id,
|
||||||
"params": params
|
"params": params
|
||||||
}
|
}
|
||||||
self._send(request)
|
self._send(request, sensitive=True)
|
||||||
|
|
||||||
def _send_notification(self, method, params):
|
def _send_notification(self, method, params):
|
||||||
notification = {
|
notification = {
|
||||||
@@ -342,24 +343,24 @@ class Connection():
|
|||||||
"method": method,
|
"method": method,
|
||||||
"params": params
|
"params": params
|
||||||
}
|
}
|
||||||
self._send(notification)
|
self._send(notification, sensitive=True)
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def _log_request(request, sensitive_params):
|
def _log_request(request, sensitive_params):
|
||||||
params = anonymise_sensitive_params(request.params, sensitive_params)
|
params = anonymise_sensitive_params(request.params, sensitive_params)
|
||||||
if request.id is not None:
|
if request.id is not None:
|
||||||
logging.info("Handling request: id=%s, method=%s, params=%s", request.id, request.method, params)
|
logger.info("Handling request: id=%s, method=%s, params=%s", request.id, request.method, params)
|
||||||
else:
|
else:
|
||||||
logging.info("Handling notification: method=%s, params=%s", request.method, params)
|
logger.info("Handling notification: method=%s, params=%s", request.method, params)
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def _log_response(response, sensitive_params):
|
def _log_response(response, sensitive_params):
|
||||||
result = anonymise_sensitive_params(response.result, sensitive_params)
|
result = anonymise_sensitive_params(response.result, sensitive_params)
|
||||||
logging.info("Handling response: id=%s, result=%s", response.id, result)
|
logger.info("Handling response: id=%s, result=%s", response.id, result)
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def _log_error(response, error, sensitive_params):
|
def _log_error(response, error, sensitive_params):
|
||||||
data = anonymise_sensitive_params(error.data, sensitive_params)
|
data = anonymise_sensitive_params(error.data, sensitive_params)
|
||||||
logging.info("Handling error: id=%s, code=%s, description=%s, data=%s",
|
logger.info("Handling error: id=%s, code=%s, description=%s, data=%s",
|
||||||
response.id, error.code, error.message, data
|
response.id, error.code, error.message, data
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -2,7 +2,6 @@ import asyncio
|
|||||||
import dataclasses
|
import dataclasses
|
||||||
import json
|
import json
|
||||||
import logging
|
import logging
|
||||||
import logging.handlers
|
|
||||||
import sys
|
import sys
|
||||||
from enum import Enum
|
from enum import Enum
|
||||||
from typing import Any, Dict, List, Optional, Set, Union
|
from typing import Any, Dict, List, Optional, Set, Union
|
||||||
@@ -16,6 +15,9 @@ from galaxy.api.types import (
|
|||||||
from galaxy.task_manager import TaskManager
|
from galaxy.task_manager import TaskManager
|
||||||
|
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
class JSONEncoder(json.JSONEncoder):
|
class JSONEncoder(json.JSONEncoder):
|
||||||
def default(self, o): # pylint: disable=method-hidden
|
def default(self, o): # pylint: disable=method-hidden
|
||||||
if dataclasses.is_dataclass(o):
|
if dataclasses.is_dataclass(o):
|
||||||
@@ -29,11 +31,74 @@ class JSONEncoder(json.JSONEncoder):
|
|||||||
return super().default(o)
|
return super().default(o)
|
||||||
|
|
||||||
|
|
||||||
|
class Importer:
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
task_manger,
|
||||||
|
name,
|
||||||
|
get,
|
||||||
|
prepare_context,
|
||||||
|
notification_success,
|
||||||
|
notification_failure,
|
||||||
|
notification_finished,
|
||||||
|
complete
|
||||||
|
):
|
||||||
|
self._task_manager = task_manger
|
||||||
|
self._name = name
|
||||||
|
self._get = get
|
||||||
|
self._prepare_context = prepare_context
|
||||||
|
self._notification_success = notification_success
|
||||||
|
self._notification_failure = notification_failure
|
||||||
|
self._notification_finished = notification_finished
|
||||||
|
self._complete = complete
|
||||||
|
|
||||||
|
self._import_in_progress = False
|
||||||
|
|
||||||
|
async def start(self, ids):
|
||||||
|
if self._import_in_progress:
|
||||||
|
raise ImportInProgress()
|
||||||
|
|
||||||
|
async def import_element(id_, context_):
|
||||||
|
try:
|
||||||
|
element = await self._get(id_, context_)
|
||||||
|
self._notification_success(id_, element)
|
||||||
|
except ApplicationError as error:
|
||||||
|
self._notification_failure(id_, error)
|
||||||
|
except asyncio.CancelledError:
|
||||||
|
pass
|
||||||
|
except Exception:
|
||||||
|
logger.exception("Unexpected exception raised in %s importer", self._name)
|
||||||
|
self._notification_failure(id_, UnknownError())
|
||||||
|
|
||||||
|
async def import_elements(ids_, context_):
|
||||||
|
try:
|
||||||
|
imports = [import_element(id_, context_) for id_ in ids_]
|
||||||
|
await asyncio.gather(*imports)
|
||||||
|
self._notification_finished()
|
||||||
|
self._complete()
|
||||||
|
except asyncio.CancelledError:
|
||||||
|
logger.debug("Importing %s cancelled", self._name)
|
||||||
|
finally:
|
||||||
|
self._import_in_progress = False
|
||||||
|
|
||||||
|
self._import_in_progress = True
|
||||||
|
try:
|
||||||
|
context = await self._prepare_context(ids)
|
||||||
|
self._task_manager.create_task(
|
||||||
|
import_elements(ids, context),
|
||||||
|
"{} import".format(self._name),
|
||||||
|
handle_exceptions=False
|
||||||
|
)
|
||||||
|
except:
|
||||||
|
self._import_in_progress = False
|
||||||
|
raise
|
||||||
|
|
||||||
|
|
||||||
class Plugin:
|
class Plugin:
|
||||||
"""Use and override methods of this class to create a new platform integration."""
|
"""Use and override methods of this class to create a new platform integration."""
|
||||||
|
|
||||||
def __init__(self, platform, version, reader, writer, handshake_token):
|
def __init__(self, platform, version, reader, writer, handshake_token):
|
||||||
logging.info("Creating plugin for platform %s, version %s", platform.value, version)
|
logger.info("Creating plugin for platform %s, version %s", platform.value, version)
|
||||||
self._platform = platform
|
self._platform = platform
|
||||||
self._version = version
|
self._version = version
|
||||||
|
|
||||||
@@ -46,17 +111,62 @@ class Plugin:
|
|||||||
encoder = JSONEncoder()
|
encoder = JSONEncoder()
|
||||||
self._connection = Connection(self._reader, self._writer, encoder)
|
self._connection = Connection(self._reader, self._writer, encoder)
|
||||||
|
|
||||||
self._achievements_import_in_progress = False
|
|
||||||
self._game_times_import_in_progress = False
|
|
||||||
self._game_library_settings_import_in_progress = False
|
|
||||||
self._os_compatibility_import_in_progress = False
|
|
||||||
self._user_presence_import_in_progress = False
|
|
||||||
|
|
||||||
self._persistent_cache = dict()
|
self._persistent_cache = dict()
|
||||||
|
|
||||||
self._internal_task_manager = TaskManager("plugin internal")
|
self._internal_task_manager = TaskManager("plugin internal")
|
||||||
self._external_task_manager = TaskManager("plugin external")
|
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
|
||||||
|
)
|
||||||
|
|
||||||
# internal
|
# internal
|
||||||
self._register_method("shutdown", self._shutdown, internal=True)
|
self._register_method("shutdown", self._shutdown, internal=True)
|
||||||
self._register_method("get_capabilities", self._get_capabilities, internal=True, immediate=True)
|
self._register_method("get_capabilities", self._get_capabilities, internal=True, immediate=True)
|
||||||
@@ -189,24 +299,31 @@ class Plugin:
|
|||||||
async def run(self):
|
async def run(self):
|
||||||
"""Plugin's main coroutine."""
|
"""Plugin's main coroutine."""
|
||||||
await self._connection.run()
|
await self._connection.run()
|
||||||
logging.debug("Plugin run loop finished")
|
logger.debug("Plugin run loop finished")
|
||||||
|
|
||||||
def close(self) -> None:
|
def close(self) -> None:
|
||||||
if not self._active:
|
if not self._active:
|
||||||
return
|
return
|
||||||
|
|
||||||
logging.info("Closing plugin")
|
logger.info("Closing plugin")
|
||||||
self._connection.close()
|
self._connection.close()
|
||||||
self._external_task_manager.cancel()
|
self._external_task_manager.cancel()
|
||||||
self._internal_task_manager.create_task(self.shutdown(), "shutdown")
|
|
||||||
|
async def shutdown():
|
||||||
|
try:
|
||||||
|
await asyncio.wait_for(self.shutdown(), 30)
|
||||||
|
except asyncio.TimeoutError:
|
||||||
|
logging.warning("Plugin shutdown timed out")
|
||||||
|
|
||||||
|
self._internal_task_manager.create_task(shutdown(), "shutdown")
|
||||||
self._active = False
|
self._active = False
|
||||||
|
|
||||||
async def wait_closed(self) -> None:
|
async def wait_closed(self) -> None:
|
||||||
logging.debug("Waiting for plugin to close")
|
logger.debug("Waiting for plugin to close")
|
||||||
await self._external_task_manager.wait()
|
await self._external_task_manager.wait()
|
||||||
await self._internal_task_manager.wait()
|
await self._internal_task_manager.wait()
|
||||||
await self._connection.wait_closed()
|
await self._connection.wait_closed()
|
||||||
logging.debug("Plugin closed")
|
logger.debug("Plugin closed")
|
||||||
|
|
||||||
def create_task(self, coro, description):
|
def create_task(self, coro, description):
|
||||||
"""Wrapper around asyncio.create_task - takes care of canceling tasks on shutdown"""
|
"""Wrapper around asyncio.create_task - takes care of canceling tasks on shutdown"""
|
||||||
@@ -217,11 +334,11 @@ class Plugin:
|
|||||||
try:
|
try:
|
||||||
self.tick()
|
self.tick()
|
||||||
except Exception:
|
except Exception:
|
||||||
logging.exception("Unexpected exception raised in plugin tick")
|
logger.exception("Unexpected exception raised in plugin tick")
|
||||||
await asyncio.sleep(1)
|
await asyncio.sleep(1)
|
||||||
|
|
||||||
async def _shutdown(self):
|
async def _shutdown(self):
|
||||||
logging.info("Shutting down")
|
logger.info("Shutting down")
|
||||||
self.close()
|
self.close()
|
||||||
await self._external_task_manager.wait()
|
await self._external_task_manager.wait()
|
||||||
await self._internal_task_manager.wait()
|
await self._internal_task_manager.wait()
|
||||||
@@ -238,7 +355,7 @@ class Plugin:
|
|||||||
try:
|
try:
|
||||||
self.handshake_complete()
|
self.handshake_complete()
|
||||||
except Exception:
|
except Exception:
|
||||||
logging.exception("Unhandled exception during `handshake_complete` step")
|
logger.exception("Unhandled exception during `handshake_complete` step")
|
||||||
self._internal_task_manager.create_task(self._pass_control(), "tick")
|
self._internal_task_manager.create_task(self._pass_control(), "tick")
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
@@ -397,6 +514,13 @@ class Plugin:
|
|||||||
params = {"user_id": user_id}
|
params = {"user_id": user_id}
|
||||||
self._connection.send_notification("friend_removed", params)
|
self._connection.send_notification("friend_removed", params)
|
||||||
|
|
||||||
|
def update_friend_info(self, user: UserInfo) -> None:
|
||||||
|
"""Notify the client about the updated friend information.
|
||||||
|
|
||||||
|
:param user: UserInfo of a friend whose info was updated
|
||||||
|
"""
|
||||||
|
self._connection.send_notification("friend_updated", params={"friend_info": user})
|
||||||
|
|
||||||
def update_game_time(self, game_time: GameTime) -> None:
|
def update_game_time(self, game_time: GameTime) -> None:
|
||||||
"""Notify the client to update game time for a game.
|
"""Notify the client to update game time for a game.
|
||||||
|
|
||||||
@@ -405,7 +529,21 @@ class Plugin:
|
|||||||
params = {"game_time": game_time}
|
params = {"game_time": game_time}
|
||||||
self._connection.send_notification("game_time_updated", params)
|
self._connection.send_notification("game_time_updated", params)
|
||||||
|
|
||||||
def _game_time_import_success(self, game_time: GameTime) -> None:
|
def update_user_presence(self, user_id: str, user_presence: UserPresence) -> None:
|
||||||
|
"""Notify the client about the updated user presence information.
|
||||||
|
|
||||||
|
:param user_id: the id of the user whose presence information is updated
|
||||||
|
:param user_presence: presence information of the specified user
|
||||||
|
"""
|
||||||
|
self._connection.send_notification(
|
||||||
|
"user_presence_updated",
|
||||||
|
{
|
||||||
|
"user_id": user_id,
|
||||||
|
"presence": user_presence
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
def _game_time_import_success(self, game_id: str, game_time: GameTime) -> None:
|
||||||
params = {"game_time": game_time}
|
params = {"game_time": game_time}
|
||||||
self._connection.send_notification("game_time_import_success", params)
|
self._connection.send_notification("game_time_import_success", params)
|
||||||
|
|
||||||
@@ -419,7 +557,7 @@ class Plugin:
|
|||||||
def _game_times_import_finished(self) -> None:
|
def _game_times_import_finished(self) -> None:
|
||||||
self._connection.send_notification("game_times_import_finished", None)
|
self._connection.send_notification("game_times_import_finished", None)
|
||||||
|
|
||||||
def _game_library_settings_import_success(self, game_library_settings: GameLibrarySettings) -> None:
|
def _game_library_settings_import_success(self, game_id: str, game_library_settings: GameLibrarySettings) -> None:
|
||||||
params = {"game_library_settings": game_library_settings}
|
params = {"game_library_settings": game_library_settings}
|
||||||
self._connection.send_notification("game_library_settings_import_success", params)
|
self._connection.send_notification("game_library_settings_import_success", params)
|
||||||
|
|
||||||
@@ -557,10 +695,11 @@ class Plugin:
|
|||||||
|
|
||||||
async def pass_login_credentials(self, step: str, credentials: Dict[str, str], cookies: List[Dict[str, str]]) \
|
async def pass_login_credentials(self, step: str, credentials: Dict[str, str], cookies: List[Dict[str, str]]) \
|
||||||
-> Union[NextStep, Authentication]:
|
-> Union[NextStep, Authentication]:
|
||||||
"""This method is called if we return galaxy.api.types.NextStep from authenticate or from pass_login_credentials.
|
"""This method is called if we return :class:`~galaxy.api.types.NextStep` from :meth:`.authenticate`
|
||||||
|
or :meth:`.pass_login_credentials`.
|
||||||
This method's parameters provide the data extracted from the web page navigation that previous NextStep finished on.
|
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
|
This method should either return :class:`~galaxy.api.types.Authentication` if the authentication is finished
|
||||||
or galaxy.api.types.NextStep if it requires going to another cef url.
|
or :class:`~galaxy.api.types.NextStep` if it requires going to another cef url.
|
||||||
This method is called by the GOG Galaxy Client.
|
This method is called by the GOG Galaxy Client.
|
||||||
|
|
||||||
:param step: deprecated.
|
:param step: deprecated.
|
||||||
@@ -605,36 +744,7 @@ class Plugin:
|
|||||||
raise NotImplementedError()
|
raise NotImplementedError()
|
||||||
|
|
||||||
async def _start_achievements_import(self, game_ids: List[str]) -> None:
|
async def _start_achievements_import(self, game_ids: List[str]) -> None:
|
||||||
if self._achievements_import_in_progress:
|
await self._achievements_importer.start(game_ids)
|
||||||
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:
|
async def prepare_achievements_context(self, game_ids: List[str]) -> Any:
|
||||||
"""Override this method to prepare context for get_unlocked_achievements.
|
"""Override this method to prepare context for get_unlocked_achievements.
|
||||||
@@ -769,36 +879,7 @@ class Plugin:
|
|||||||
raise NotImplementedError()
|
raise NotImplementedError()
|
||||||
|
|
||||||
async def _start_game_times_import(self, game_ids: List[str]) -> None:
|
async def _start_game_times_import(self, game_ids: List[str]) -> None:
|
||||||
if self._game_times_import_in_progress:
|
await self._game_time_importer.start(game_ids)
|
||||||
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:
|
async def prepare_game_times_context(self, game_ids: List[str]) -> Any:
|
||||||
"""Override this method to prepare context for get_game_time.
|
"""Override this method to prepare context for get_game_time.
|
||||||
@@ -827,36 +908,7 @@ class Plugin:
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
async def _start_game_library_settings_import(self, game_ids: List[str]) -> None:
|
async def _start_game_library_settings_import(self, game_ids: List[str]) -> None:
|
||||||
if self._game_library_settings_import_in_progress:
|
await self._game_library_settings_importer.start(game_ids)
|
||||||
raise ImportInProgress()
|
|
||||||
|
|
||||||
context = await self.prepare_game_library_settings_context(game_ids)
|
|
||||||
|
|
||||||
async def import_game_library_settings(game_id, context_):
|
|
||||||
try:
|
|
||||||
game_library_settings = await self.get_game_library_settings(game_id, context_)
|
|
||||||
self._game_library_settings_import_success(game_library_settings)
|
|
||||||
except ApplicationError as error:
|
|
||||||
self._game_library_settings_import_failure(game_id, error)
|
|
||||||
except Exception:
|
|
||||||
logging.exception("Unexpected exception raised in import_game_library_settings")
|
|
||||||
self._game_library_settings_import_failure(game_id, UnknownError())
|
|
||||||
|
|
||||||
async def import_game_library_settings_set(game_ids_, context_):
|
|
||||||
try:
|
|
||||||
imports = [import_game_library_settings(game_id, context_) for game_id in game_ids_]
|
|
||||||
await asyncio.gather(*imports)
|
|
||||||
finally:
|
|
||||||
self._game_library_settings_import_finished()
|
|
||||||
self._game_library_settings_import_in_progress = False
|
|
||||||
self.game_library_settings_import_complete()
|
|
||||||
|
|
||||||
self._external_task_manager.create_task(
|
|
||||||
import_game_library_settings_set(game_ids, context),
|
|
||||||
"game library settings import",
|
|
||||||
handle_exceptions=False
|
|
||||||
)
|
|
||||||
self._game_library_settings_import_in_progress = True
|
|
||||||
|
|
||||||
async def prepare_game_library_settings_context(self, game_ids: List[str]) -> Any:
|
async def prepare_game_library_settings_context(self, game_ids: List[str]) -> Any:
|
||||||
"""Override this method to prepare context for get_game_library_settings.
|
"""Override this method to prepare context for get_game_library_settings.
|
||||||
@@ -885,37 +937,7 @@ class Plugin:
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
async def _start_os_compatibility_import(self, game_ids: List[str]) -> None:
|
async def _start_os_compatibility_import(self, game_ids: List[str]) -> None:
|
||||||
if self._os_compatibility_import_in_progress:
|
await self._os_compatibility_importer.start(game_ids)
|
||||||
raise ImportInProgress()
|
|
||||||
|
|
||||||
context = await self.prepare_os_compatibility_context(game_ids)
|
|
||||||
|
|
||||||
async def import_os_compatibility(game_id, context_):
|
|
||||||
try:
|
|
||||||
os_compatibility = await self.get_os_compatibility(game_id, context_)
|
|
||||||
self._os_compatibility_import_success(game_id, os_compatibility)
|
|
||||||
except ApplicationError as error:
|
|
||||||
self._os_compatibility_import_failure(game_id, error)
|
|
||||||
except Exception:
|
|
||||||
logging.exception("Unexpected exception raised in import_os_compatibility")
|
|
||||||
self._os_compatibility_import_failure(game_id, UnknownError())
|
|
||||||
|
|
||||||
async def import_os_compatibility_set(game_ids_, context_):
|
|
||||||
try:
|
|
||||||
await asyncio.gather(*[
|
|
||||||
import_os_compatibility(game_id, context_) for game_id in game_ids_
|
|
||||||
])
|
|
||||||
finally:
|
|
||||||
self._os_compatibility_import_finished()
|
|
||||||
self._os_compatibility_import_in_progress = False
|
|
||||||
self.os_compatibility_import_complete()
|
|
||||||
|
|
||||||
self._external_task_manager.create_task(
|
|
||||||
import_os_compatibility_set(game_ids, context),
|
|
||||||
"game OS compatibility import",
|
|
||||||
handle_exceptions=False
|
|
||||||
)
|
|
||||||
self._os_compatibility_import_in_progress = True
|
|
||||||
|
|
||||||
async def prepare_os_compatibility_context(self, game_ids: List[str]) -> Any:
|
async def prepare_os_compatibility_context(self, game_ids: List[str]) -> Any:
|
||||||
"""Override this method to prepare context for get_os_compatibility.
|
"""Override this method to prepare context for get_os_compatibility.
|
||||||
@@ -940,45 +962,15 @@ class Plugin:
|
|||||||
def os_compatibility_import_complete(self) -> None:
|
def os_compatibility_import_complete(self) -> None:
|
||||||
"""Override this method to handle operations after OS compatibility import is finished (like updating cache)."""
|
"""Override this method to handle operations after OS compatibility import is finished (like updating cache)."""
|
||||||
|
|
||||||
async def _start_user_presence_import(self, user_ids: List[str]) -> None:
|
async def _start_user_presence_import(self, user_id_list: List[str]) -> None:
|
||||||
if self._user_presence_import_in_progress:
|
await self._user_presence_importer.start(user_id_list)
|
||||||
raise ImportInProgress()
|
|
||||||
|
|
||||||
context = await self.prepare_user_presence_context(user_ids)
|
async def prepare_user_presence_context(self, user_id_list: List[str]) -> Any:
|
||||||
|
|
||||||
async def import_user_presence(user_id, context_) -> None:
|
|
||||||
try:
|
|
||||||
self._user_presence_import_success(user_id, await self.get_user_presence(user_id, context_))
|
|
||||||
except ApplicationError as error:
|
|
||||||
self._user_presence_import_failure(user_id, error)
|
|
||||||
except Exception:
|
|
||||||
logging.exception("Unexpected exception raised in import_user_presence")
|
|
||||||
self._user_presence_import_failure(user_id, UnknownError())
|
|
||||||
|
|
||||||
async def import_user_presence_set(user_ids_, context_) -> None:
|
|
||||||
try:
|
|
||||||
await asyncio.gather(*[
|
|
||||||
import_user_presence(user_id, context_)
|
|
||||||
for user_id in user_ids_
|
|
||||||
])
|
|
||||||
finally:
|
|
||||||
self._user_presence_import_finished()
|
|
||||||
self._user_presence_import_in_progress = False
|
|
||||||
self.user_presence_import_complete()
|
|
||||||
|
|
||||||
self._external_task_manager.create_task(
|
|
||||||
import_user_presence_set(user_ids, context),
|
|
||||||
"user presence import",
|
|
||||||
handle_exceptions=False
|
|
||||||
)
|
|
||||||
self._user_presence_import_in_progress = True
|
|
||||||
|
|
||||||
async def prepare_user_presence_context(self, user_ids: List[str]) -> Any:
|
|
||||||
"""Override this method to prepare context for get_user_presence.
|
"""Override this method to prepare context for get_user_presence.
|
||||||
This allows for optimizations like batch requests to platform API.
|
This allows for optimizations like batch requests to platform API.
|
||||||
Default implementation returns None.
|
Default implementation returns None.
|
||||||
|
|
||||||
:param user_ids: the ids of the users for whom presence information is imported
|
:param user_id_list: the ids of the users for whom presence information is imported
|
||||||
:return: context
|
:return: context
|
||||||
"""
|
"""
|
||||||
return None
|
return None
|
||||||
@@ -1015,7 +1007,7 @@ def create_and_run_plugin(plugin_class, argv):
|
|||||||
main()
|
main()
|
||||||
"""
|
"""
|
||||||
if len(argv) < 3:
|
if len(argv) < 3:
|
||||||
logging.critical("Not enough parameters, required: token, port")
|
logger.critical("Not enough parameters, required: token, port")
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
|
|
||||||
token = argv[1]
|
token = argv[1]
|
||||||
@@ -1023,23 +1015,28 @@ def create_and_run_plugin(plugin_class, argv):
|
|||||||
try:
|
try:
|
||||||
port = int(argv[2])
|
port = int(argv[2])
|
||||||
except ValueError:
|
except ValueError:
|
||||||
logging.critical("Failed to parse port value: %s", argv[2])
|
logger.critical("Failed to parse port value: %s", argv[2])
|
||||||
sys.exit(2)
|
sys.exit(2)
|
||||||
|
|
||||||
if not (1 <= port <= 65535):
|
if not (1 <= port <= 65535):
|
||||||
logging.critical("Port value out of range (1, 65535)")
|
logger.critical("Port value out of range (1, 65535)")
|
||||||
sys.exit(3)
|
sys.exit(3)
|
||||||
|
|
||||||
if not issubclass(plugin_class, Plugin):
|
if not issubclass(plugin_class, Plugin):
|
||||||
logging.critical("plugin_class must be subclass of Plugin")
|
logger.critical("plugin_class must be subclass of Plugin")
|
||||||
sys.exit(4)
|
sys.exit(4)
|
||||||
|
|
||||||
async def coroutine():
|
async def coroutine():
|
||||||
reader, writer = await asyncio.open_connection("127.0.0.1", port)
|
reader, writer = await asyncio.open_connection("127.0.0.1", port)
|
||||||
extra_info = writer.get_extra_info("sockname")
|
try:
|
||||||
logging.info("Using local address: %s:%u", *extra_info)
|
extra_info = writer.get_extra_info("sockname")
|
||||||
async with plugin_class(reader, writer, token) as plugin:
|
logger.info("Using local address: %s:%u", *extra_info)
|
||||||
await plugin.run()
|
async with plugin_class(reader, writer, token) as plugin:
|
||||||
|
await plugin.run()
|
||||||
|
finally:
|
||||||
|
writer.close()
|
||||||
|
await writer.wait_closed()
|
||||||
|
|
||||||
|
|
||||||
try:
|
try:
|
||||||
if sys.platform == "win32":
|
if sys.platform == "win32":
|
||||||
@@ -1047,5 +1044,5 @@ def create_and_run_plugin(plugin_class, argv):
|
|||||||
|
|
||||||
asyncio.run(coroutine())
|
asyncio.run(coroutine())
|
||||||
except Exception:
|
except Exception:
|
||||||
logging.exception("Error while running plugin")
|
logger.exception("Error while running plugin")
|
||||||
sys.exit(5)
|
sys.exit(5)
|
||||||
|
|||||||
@@ -61,9 +61,11 @@ class NextStep:
|
|||||||
if not stored_credentials:
|
if not stored_credentials:
|
||||||
return NextStep("web_session", PARAMS, cookies=COOKIES, js=JS)
|
return NextStep("web_session", PARAMS, cookies=COOKIES, js=JS)
|
||||||
|
|
||||||
:param auth_params: configuration options: {"window_title": :class:`str`, "window_width": :class:`str`, "window_height": :class:`int`, "start_uri": :class:`int`, "end_uri_regex": :class:`str`}
|
:param auth_params: configuration options: {"window_title": :class:`str`, "window_width": :class:`str`,
|
||||||
|
"window_height": :class:`int`, "start_uri": :class:`int`, "end_uri_regex": :class:`str`}
|
||||||
:param cookies: browser initial set of cookies
|
:param cookies: browser initial set of cookies
|
||||||
:param js: a map of the url regex patterns into the list of *js* scripts that should be executed on every document at given step of internal browser authentication.
|
:param js: a map of the url regex patterns into the list of *js* scripts that should be executed
|
||||||
|
on every document at given step of internal browser authentication.
|
||||||
"""
|
"""
|
||||||
next_step: str
|
next_step: str
|
||||||
auth_params: Dict[str, str]
|
auth_params: Dict[str, str]
|
||||||
@@ -142,7 +144,8 @@ class LocalGame:
|
|||||||
class FriendInfo:
|
class FriendInfo:
|
||||||
"""
|
"""
|
||||||
.. deprecated:: 0.56
|
.. deprecated:: 0.56
|
||||||
Use: :class:`UserInfo`.
|
Use :class:`UserInfo`.
|
||||||
|
|
||||||
Information about a friend of the currently authenticated user.
|
Information about a friend of the currently authenticated user.
|
||||||
|
|
||||||
:param user_id: id of the user
|
:param user_id: id of the user
|
||||||
@@ -158,9 +161,14 @@ class UserInfo:
|
|||||||
|
|
||||||
:param user_id: id of the user
|
:param user_id: id of the user
|
||||||
:param user_name: username of the user
|
:param user_name: username of the user
|
||||||
|
:param avatar_url: the URL of the user avatar
|
||||||
|
:param profile_url: the URL of the user profile
|
||||||
"""
|
"""
|
||||||
user_id: str
|
user_id: str
|
||||||
user_name: str
|
user_name: str
|
||||||
|
avatar_url: Optional[str]
|
||||||
|
profile_url: Optional[str]
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
class GameTime:
|
class GameTime:
|
||||||
@@ -169,7 +177,7 @@ class GameTime:
|
|||||||
|
|
||||||
:param game_id: id of the related game
|
:param game_id: id of the related game
|
||||||
:param time_played: the total time spent in the game in **minutes**
|
:param time_played: the total time spent in the game in **minutes**
|
||||||
:param last_time_played: last time the game was played (**unix timestamp**)
|
:param last_played_time: last time the game was played (**unix timestamp**)
|
||||||
"""
|
"""
|
||||||
game_id: str
|
game_id: str
|
||||||
time_played: Optional[int]
|
time_played: Optional[int]
|
||||||
@@ -182,7 +190,7 @@ class GameLibrarySettings:
|
|||||||
|
|
||||||
:param game_id: id of the related game
|
:param game_id: id of the related game
|
||||||
:param tags: collection of tags assigned to the game
|
:param tags: collection of tags assigned to the game
|
||||||
:param hidden: indicates if the game should be hidden in GOG Galaxy application
|
:param hidden: indicates if the game should be hidden in GOG Galaxy client
|
||||||
"""
|
"""
|
||||||
game_id: str
|
game_id: str
|
||||||
tags: Optional[List[str]]
|
tags: Optional[List[str]]
|
||||||
@@ -193,12 +201,18 @@ class GameLibrarySettings:
|
|||||||
class UserPresence:
|
class UserPresence:
|
||||||
"""Presence information of a user.
|
"""Presence information of a user.
|
||||||
|
|
||||||
|
The GOG Galaxy client will prefer to generate user status basing on `game_id` (or `game_title`)
|
||||||
|
and `in_game_status` fields but if plugin is not capable of delivering it then the `full_status` will be used if
|
||||||
|
available
|
||||||
|
|
||||||
:param presence_state: the state of the user
|
:param presence_state: the state of the user
|
||||||
:param game_id: id of the game a user is currently in
|
:param game_id: id of the game a user is currently in
|
||||||
:param game_title: name of the game a user is currently in
|
:param game_title: name of the game a user is currently in
|
||||||
:param presence_status: detailed user's presence description
|
:param in_game_status: status set by the game itself e.x. "In Main Menu"
|
||||||
|
:param full_status: full user status e.x. "Playing <title_name>: <in_game_status>"
|
||||||
"""
|
"""
|
||||||
presence_state: PresenceState
|
presence_state: PresenceState
|
||||||
game_id: Optional[str] = None
|
game_id: Optional[str] = None
|
||||||
game_title: Optional[str] = None
|
game_title: Optional[str] = None
|
||||||
presence_status: Optional[str] = None
|
in_game_status: Optional[str] = None
|
||||||
|
full_status: Optional[str] = None
|
||||||
|
|||||||
@@ -44,6 +44,8 @@ from galaxy.api.errors import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
#: Default limit of the simultaneous connections for ssl connector.
|
#: Default limit of the simultaneous connections for ssl connector.
|
||||||
DEFAULT_LIMIT = 20
|
DEFAULT_LIMIT = 20
|
||||||
#: Default timeout in seconds used for client session.
|
#: Default timeout in seconds used for client session.
|
||||||
@@ -136,11 +138,11 @@ def handle_exception():
|
|||||||
if error.status >= 500:
|
if error.status >= 500:
|
||||||
raise BackendError()
|
raise BackendError()
|
||||||
if error.status >= 400:
|
if error.status >= 400:
|
||||||
logging.warning(
|
logger.warning(
|
||||||
"Got status %d while performing %s request for %s",
|
"Got status %d while performing %s request for %s",
|
||||||
error.status, error.request_info.method, str(error.request_info.url)
|
error.status, error.request_info.method, str(error.request_info.url)
|
||||||
)
|
)
|
||||||
raise UnknownError()
|
raise UnknownError()
|
||||||
except aiohttp.ClientError:
|
except aiohttp.ClientError:
|
||||||
logging.exception("Caught exception while performing request")
|
logger.exception("Caught exception while performing request")
|
||||||
raise UnknownError()
|
raise UnknownError()
|
||||||
|
|||||||
@@ -3,7 +3,6 @@ from dataclasses import dataclass
|
|||||||
from typing import Iterable, NewType, Optional, List, cast
|
from typing import Iterable, NewType, Optional, List, cast
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
ProcessId = NewType("ProcessId", int)
|
ProcessId = NewType("ProcessId", int)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -3,6 +3,10 @@ import logging
|
|||||||
from collections import OrderedDict
|
from collections import OrderedDict
|
||||||
from itertools import count
|
from itertools import count
|
||||||
|
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
class TaskManager:
|
class TaskManager:
|
||||||
def __init__(self, name):
|
def __init__(self, name):
|
||||||
self._name = name
|
self._name = name
|
||||||
@@ -15,23 +19,23 @@ class TaskManager:
|
|||||||
async def task_wrapper(task_id):
|
async def task_wrapper(task_id):
|
||||||
try:
|
try:
|
||||||
result = await coro
|
result = await coro
|
||||||
logging.debug("Task manager %s: finished task %d (%s)", self._name, task_id, description)
|
logger.debug("Task manager %s: finished task %d (%s)", self._name, task_id, description)
|
||||||
return result
|
return result
|
||||||
except asyncio.CancelledError:
|
except asyncio.CancelledError:
|
||||||
if handle_exceptions:
|
if handle_exceptions:
|
||||||
logging.debug("Task manager %s: canceled task %d (%s)", self._name, task_id, description)
|
logger.debug("Task manager %s: canceled task %d (%s)", self._name, task_id, description)
|
||||||
else:
|
else:
|
||||||
raise
|
raise
|
||||||
except Exception:
|
except Exception:
|
||||||
if handle_exceptions:
|
if handle_exceptions:
|
||||||
logging.exception("Task manager %s: exception raised in task %d (%s)", self._name, task_id, description)
|
logger.exception("Task manager %s: exception raised in task %d (%s)", self._name, task_id, description)
|
||||||
else:
|
else:
|
||||||
raise
|
raise
|
||||||
finally:
|
finally:
|
||||||
del self._tasks[task_id]
|
del self._tasks[task_id]
|
||||||
|
|
||||||
task_id = next(self._task_counter)
|
task_id = next(self._task_counter)
|
||||||
logging.debug("Task manager %s: creating task %d (%s)", self._name, task_id, description)
|
logger.debug("Task manager %s: creating task %d (%s)", self._name, task_id, description)
|
||||||
task = asyncio.create_task(task_wrapper(task_id))
|
task = asyncio.create_task(task_wrapper(task_id))
|
||||||
self._tasks[task_id] = task
|
self._tasks[task_id] = task
|
||||||
return task
|
return task
|
||||||
|
|||||||
@@ -21,11 +21,19 @@ def coroutine_mock():
|
|||||||
corofunc.coro = coro
|
corofunc.coro = coro
|
||||||
return corofunc
|
return corofunc
|
||||||
|
|
||||||
|
|
||||||
async def skip_loop(iterations=1):
|
async def skip_loop(iterations=1):
|
||||||
for _ in range(iterations):
|
for _ in range(iterations):
|
||||||
await asyncio.sleep(0)
|
await asyncio.sleep(0)
|
||||||
|
|
||||||
|
|
||||||
async def async_return_value(return_value, loop_iterations_delay=0):
|
async def async_return_value(return_value, loop_iterations_delay=0):
|
||||||
await skip_loop(loop_iterations_delay)
|
if loop_iterations_delay > 0:
|
||||||
|
await skip_loop(loop_iterations_delay)
|
||||||
return return_value
|
return return_value
|
||||||
|
|
||||||
|
|
||||||
|
async def async_raise(error, loop_iterations_delay=0):
|
||||||
|
if loop_iterations_delay > 0:
|
||||||
|
await skip_loop(loop_iterations_delay)
|
||||||
|
raise error
|
||||||
|
|||||||
@@ -17,8 +17,8 @@ async def test_get_friends_success(plugin, read, write):
|
|||||||
|
|
||||||
read.side_effect = [async_return_value(create_message(request)), async_return_value(b"", 10)]
|
read.side_effect = [async_return_value(create_message(request)), async_return_value(b"", 10)]
|
||||||
plugin.get_friends.return_value = async_return_value([
|
plugin.get_friends.return_value = async_return_value([
|
||||||
UserInfo("3", "Jan"),
|
UserInfo("3", "Jan", "https://avatar.url/u3", None),
|
||||||
UserInfo("5", "Ola")
|
UserInfo("5", "Ola", None, "https://profile.url/u5")
|
||||||
])
|
])
|
||||||
await plugin.run()
|
await plugin.run()
|
||||||
plugin.get_friends.assert_called_with()
|
plugin.get_friends.assert_called_with()
|
||||||
@@ -29,8 +29,8 @@ async def test_get_friends_success(plugin, read, write):
|
|||||||
"id": "3",
|
"id": "3",
|
||||||
"result": {
|
"result": {
|
||||||
"friend_info_list": [
|
"friend_info_list": [
|
||||||
{"user_id": "3", "user_name": "Jan"},
|
{"user_id": "3", "user_name": "Jan", "avatar_url": "https://avatar.url/u3"},
|
||||||
{"user_id": "5", "user_name": "Ola"}
|
{"user_id": "5", "user_name": "Ola", "profile_url": "https://profile.url/u5"}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -64,7 +64,7 @@ async def test_get_friends_failure(plugin, read, write):
|
|||||||
|
|
||||||
@pytest.mark.asyncio
|
@pytest.mark.asyncio
|
||||||
async def test_add_friend(plugin, write):
|
async def test_add_friend(plugin, write):
|
||||||
friend = UserInfo("7", "Kuba")
|
friend = UserInfo("7", "Kuba", avatar_url="https://avatar.url/kuba.jpg", profile_url="https://profile.url/kuba")
|
||||||
|
|
||||||
plugin.add_friend(friend)
|
plugin.add_friend(friend)
|
||||||
await skip_loop()
|
await skip_loop()
|
||||||
@@ -74,7 +74,12 @@ async def test_add_friend(plugin, write):
|
|||||||
"jsonrpc": "2.0",
|
"jsonrpc": "2.0",
|
||||||
"method": "friend_added",
|
"method": "friend_added",
|
||||||
"params": {
|
"params": {
|
||||||
"friend_info": {"user_id": "7", "user_name": "Kuba"}
|
"friend_info": {
|
||||||
|
"user_id": "7",
|
||||||
|
"user_name": "Kuba",
|
||||||
|
"avatar_url": "https://avatar.url/kuba.jpg",
|
||||||
|
"profile_url": "https://profile.url/kuba"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
@@ -94,3 +99,26 @@ async def test_remove_friend(plugin, write):
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_update_friend_info(plugin, write):
|
||||||
|
plugin.update_friend_info(
|
||||||
|
UserInfo("7", "Jakub", avatar_url="https://new-avatar.url/kuba2.jpg", profile_url="https://profile.url/kuba")
|
||||||
|
)
|
||||||
|
await skip_loop()
|
||||||
|
|
||||||
|
assert get_messages(write) == [
|
||||||
|
{
|
||||||
|
"jsonrpc": "2.0",
|
||||||
|
"method": "friend_updated",
|
||||||
|
"params": {
|
||||||
|
"friend_info": {
|
||||||
|
"user_id": "7",
|
||||||
|
"user_name": "Jakub",
|
||||||
|
"avatar_url": "https://new-avatar.url/kuba2.jpg",
|
||||||
|
"profile_url": "https://profile.url/kuba"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|||||||
@@ -5,20 +5,20 @@ import pytest
|
|||||||
from galaxy.api.consts import PresenceState
|
from galaxy.api.consts import PresenceState
|
||||||
from galaxy.api.errors import BackendError
|
from galaxy.api.errors import BackendError
|
||||||
from galaxy.api.types import UserPresence
|
from galaxy.api.types import UserPresence
|
||||||
from galaxy.unittest.mock import async_return_value
|
from galaxy.unittest.mock import async_return_value, skip_loop
|
||||||
from tests import create_message, get_messages
|
from tests import create_message, get_messages
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.asyncio
|
@pytest.mark.asyncio
|
||||||
async def test_get_user_presence_success(plugin, read, write):
|
async def test_get_user_presence_success(plugin, read, write):
|
||||||
context = "abc"
|
context = "abc"
|
||||||
user_ids = ["666", "13", "42", "69"]
|
user_id_list = ["666", "13", "42", "69", "22"]
|
||||||
plugin.prepare_user_presence_context.return_value = async_return_value(context)
|
plugin.prepare_user_presence_context.return_value = async_return_value(context)
|
||||||
request = {
|
request = {
|
||||||
"jsonrpc": "2.0",
|
"jsonrpc": "2.0",
|
||||||
"id": "11",
|
"id": "11",
|
||||||
"method": "start_user_presence_import",
|
"method": "start_user_presence_import",
|
||||||
"params": {"user_ids": user_ids}
|
"params": {"user_id_list": user_id_list}
|
||||||
}
|
}
|
||||||
read.side_effect = [async_return_value(create_message(request)), async_return_value(b"", 10)]
|
read.side_effect = [async_return_value(create_message(request)), async_return_value(b"", 10)]
|
||||||
plugin.get_user_presence.side_effect = [
|
plugin.get_user_presence.side_effect = [
|
||||||
@@ -26,30 +26,41 @@ async def test_get_user_presence_success(plugin, read, write):
|
|||||||
PresenceState.Unknown,
|
PresenceState.Unknown,
|
||||||
"game-id1",
|
"game-id1",
|
||||||
None,
|
None,
|
||||||
"unknown state"
|
"unknown state",
|
||||||
|
None
|
||||||
)),
|
)),
|
||||||
async_return_value(UserPresence(
|
async_return_value(UserPresence(
|
||||||
PresenceState.Offline,
|
PresenceState.Offline,
|
||||||
None,
|
None,
|
||||||
None,
|
None,
|
||||||
"Going to grandma's house"
|
"Going to grandma's house",
|
||||||
|
None
|
||||||
)),
|
)),
|
||||||
async_return_value(UserPresence(
|
async_return_value(UserPresence(
|
||||||
PresenceState.Online,
|
PresenceState.Online,
|
||||||
"game-id3",
|
"game-id3",
|
||||||
"game-title3",
|
"game-title3",
|
||||||
"Pew pew"
|
"Pew pew",
|
||||||
|
None
|
||||||
)),
|
)),
|
||||||
async_return_value(UserPresence(
|
async_return_value(UserPresence(
|
||||||
PresenceState.Away,
|
PresenceState.Away,
|
||||||
None,
|
None,
|
||||||
"game-title4",
|
"game-title4",
|
||||||
"AFKKTHXBY"
|
"AFKKTHXBY",
|
||||||
|
None
|
||||||
|
)),
|
||||||
|
async_return_value(UserPresence(
|
||||||
|
PresenceState.Away,
|
||||||
|
None,
|
||||||
|
"game-title5",
|
||||||
|
None,
|
||||||
|
"Playing game-title5: In Menu"
|
||||||
)),
|
)),
|
||||||
]
|
]
|
||||||
await plugin.run()
|
await plugin.run()
|
||||||
plugin.get_user_presence.assert_has_calls([
|
plugin.get_user_presence.assert_has_calls([
|
||||||
call(user_id, context) for user_id in user_ids
|
call(user_id, context) for user_id in user_id_list
|
||||||
])
|
])
|
||||||
plugin.user_presence_import_complete.assert_called_once_with()
|
plugin.user_presence_import_complete.assert_called_once_with()
|
||||||
|
|
||||||
@@ -67,7 +78,7 @@ async def test_get_user_presence_success(plugin, read, write):
|
|||||||
"presence": {
|
"presence": {
|
||||||
"presence_state": PresenceState.Unknown.value,
|
"presence_state": PresenceState.Unknown.value,
|
||||||
"game_id": "game-id1",
|
"game_id": "game-id1",
|
||||||
"presence_status": "unknown state"
|
"in_game_status": "unknown state"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -78,7 +89,7 @@ async def test_get_user_presence_success(plugin, read, write):
|
|||||||
"user_id": "13",
|
"user_id": "13",
|
||||||
"presence": {
|
"presence": {
|
||||||
"presence_state": PresenceState.Offline.value,
|
"presence_state": PresenceState.Offline.value,
|
||||||
"presence_status": "Going to grandma's house"
|
"in_game_status": "Going to grandma's house"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -91,7 +102,7 @@ async def test_get_user_presence_success(plugin, read, write):
|
|||||||
"presence_state": PresenceState.Online.value,
|
"presence_state": PresenceState.Online.value,
|
||||||
"game_id": "game-id3",
|
"game_id": "game-id3",
|
||||||
"game_title": "game-title3",
|
"game_title": "game-title3",
|
||||||
"presence_status": "Pew pew"
|
"in_game_status": "Pew pew"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -103,7 +114,19 @@ async def test_get_user_presence_success(plugin, read, write):
|
|||||||
"presence": {
|
"presence": {
|
||||||
"presence_state": PresenceState.Away.value,
|
"presence_state": PresenceState.Away.value,
|
||||||
"game_title": "game-title4",
|
"game_title": "game-title4",
|
||||||
"presence_status": "AFKKTHXBY"
|
"in_game_status": "AFKKTHXBY"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"jsonrpc": "2.0",
|
||||||
|
"method": "user_presence_import_success",
|
||||||
|
"params": {
|
||||||
|
"user_id": "22",
|
||||||
|
"presence": {
|
||||||
|
"presence_state": PresenceState.Away.value,
|
||||||
|
"game_title": "game-title5",
|
||||||
|
"full_status": "Playing game-title5: In Menu"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -128,7 +151,7 @@ async def test_get_user_presence_error(exception, code, message, plugin, read, w
|
|||||||
"jsonrpc": "2.0",
|
"jsonrpc": "2.0",
|
||||||
"id": request_id,
|
"id": request_id,
|
||||||
"method": "start_user_presence_import",
|
"method": "start_user_presence_import",
|
||||||
"params": {"user_ids": [user_id]}
|
"params": {"user_id_list": [user_id]}
|
||||||
}
|
}
|
||||||
read.side_effect = [async_return_value(create_message(request)), async_return_value(b"", 10)]
|
read.side_effect = [async_return_value(create_message(request)), async_return_value(b"", 10)]
|
||||||
plugin.get_user_presence.side_effect = exception
|
plugin.get_user_presence.side_effect = exception
|
||||||
@@ -169,7 +192,7 @@ async def test_prepare_get_user_presence_context_error(plugin, read, write):
|
|||||||
"jsonrpc": "2.0",
|
"jsonrpc": "2.0",
|
||||||
"id": request_id,
|
"id": request_id,
|
||||||
"method": "start_user_presence_import",
|
"method": "start_user_presence_import",
|
||||||
"params": {"user_ids": ["6"]}
|
"params": {"user_id_list": ["6"]}
|
||||||
}
|
}
|
||||||
read.side_effect = [async_return_value(create_message(request)), async_return_value(b"", 10)]
|
read.side_effect = [async_return_value(create_message(request)), async_return_value(b"", 10)]
|
||||||
await plugin.run()
|
await plugin.run()
|
||||||
@@ -195,7 +218,7 @@ async def test_import_already_in_progress_error(plugin, read, write):
|
|||||||
"id": "3",
|
"id": "3",
|
||||||
"method": "start_user_presence_import",
|
"method": "start_user_presence_import",
|
||||||
"params": {
|
"params": {
|
||||||
"user_ids": ["42"]
|
"user_id_list": ["42"]
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -203,7 +226,7 @@ async def test_import_already_in_progress_error(plugin, read, write):
|
|||||||
"id": "4",
|
"id": "4",
|
||||||
"method": "start_user_presence_import",
|
"method": "start_user_presence_import",
|
||||||
"params": {
|
"params": {
|
||||||
"user_ids": ["666"]
|
"user_id_list": ["666"]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
@@ -229,3 +252,25 @@ async def test_import_already_in_progress_error(plugin, read, write):
|
|||||||
"message": "Import already in progress"
|
"message": "Import already in progress"
|
||||||
}
|
}
|
||||||
} in responses
|
} in responses
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_update_user_presence(plugin, write):
|
||||||
|
plugin.update_user_presence("42", UserPresence(PresenceState.Online, "game-id", "game-title", "Pew pew"))
|
||||||
|
await skip_loop()
|
||||||
|
|
||||||
|
assert get_messages(write) == [
|
||||||
|
{
|
||||||
|
"jsonrpc": "2.0",
|
||||||
|
"method": "user_presence_updated",
|
||||||
|
"params": {
|
||||||
|
"user_id": "42",
|
||||||
|
"presence": {
|
||||||
|
"presence_state": PresenceState.Online.value,
|
||||||
|
"game_id": "game-id",
|
||||||
|
"game_title": "game-title",
|
||||||
|
"in_game_status": "Pew pew"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|||||||
Reference in New Issue
Block a user