Compare commits

..

14 Commits
0.14 ... 0.19

Author SHA1 Message Date
Aliaksei Paulouski
d759b4aa85 Increment version 2019-03-28 14:37:30 +01:00
Romuald Juchnowicz-Bierbasz
9b33397827 Add NextStep and pass_login_credentials 2019-03-28 10:20:16 +01:00
Romuald Juchnowicz-Bierbasz
e09e443064 Increment version 2019-03-27 15:15:25 +01:00
Romuald Juchnowicz-Bierbasz
00ed52384a Exclude tests from package 2019-03-27 15:14:29 +01:00
Aliaksei Paulouski
958d9bc0e6 Fix send_message message param name 2019-03-25 11:46:30 +01:00
Pawel Kierski
d73d048ff7 Increment version to 0.17 2019-03-12 16:10:23 +01:00
Aliaksei Paulouski
e06e40f845 Fix duplicated error code 2019-03-12 15:53:42 +01:00
Paweł Kierski
833e6999d7 Return JSON-RPC reponse on generic Exception 2019-03-12 15:38:22 +01:00
Paweł Kierski
ca778e2cdb SDK-2647 Serializing local game state as integer 2019-03-11 13:47:12 +01:00
Romuald Juchnowicz-Bierbasz
9a06428fc0 Remove log 2019-03-11 12:19:45 +01:00
Romuald Juchnowicz-Bierbasz
f9eaeaf726 Increment version, add changelog 2019-03-11 11:08:28 +01:00
Romuald Juchnowicz-Bierbasz
f09171672f SDK-2636: Do not log sensitive data 2019-03-11 11:07:31 +01:00
Romuald Juchnowicz-Bierbasz
ca8d0dfaf4 Increment version, add changelog 2019-03-08 10:19:43 +01:00
Romuald Juchnowicz-Bierbasz
73bc9aa8ec SDK-2623: Call shutdown on socket close 2019-03-08 10:16:57 +01:00
8 changed files with 124 additions and 53 deletions

View File

@@ -34,5 +34,10 @@ pytest
## Changelog
### 0.16
* Do not log sensitive data.
* Return `LocalGameState` as int (possible combination of flags).
### 0.15
* `shutdown()` is called on socket disconnection.
### 0.14
* Added required version parameter to Plugin constructor.

View File

@@ -1,4 +1,4 @@
from enum import Enum
from enum import Enum, Flag
class Platform(Enum):
Unknown = "unknown"
@@ -30,10 +30,10 @@ class LicenseType(Enum):
FreeToPlay = "FreeToPlay"
OtherUserLicense = "OtherUserLicense"
class LocalGameState(Enum):
None_ = "None"
Installed = "Installed"
Running = "Running"
class LocalGameState(Flag):
None_ = 0
Installed = 1
Running = 2
class PresenceState(Enum):
Unknown = "Unknown"

View File

@@ -1,8 +1,6 @@
from galaxy.api.jsonrpc import ApplicationError
from galaxy.api.jsonrpc import ApplicationError, UnknownError
class UnknownError(ApplicationError):
def __init__(self, data=None):
super().__init__(0, "Unknown error", data)
UnknownError = UnknownError
class AuthenticationRequired(ApplicationError):
def __init__(self, data=None):

View File

@@ -1,5 +1,6 @@
import asyncio
from collections import namedtuple
from collections.abc import Iterable
import logging
import json
@@ -24,7 +25,7 @@ class MethodNotFound(JsonRpcError):
class InvalidParams(JsonRpcError):
def __init__(self):
super().__init__(-32601, "Invalid params")
super().__init__(-32602, "Invalid params")
class Timeout(JsonRpcError):
def __init__(self):
@@ -40,8 +41,26 @@ class ApplicationError(JsonRpcError):
raise ValueError("The error code in reserved range")
super().__init__(code, message, data)
class UnknownError(ApplicationError):
def __init__(self, data=None):
super().__init__(0, "Unknown error", data)
Request = namedtuple("Request", ["method", "params", "id"], defaults=[{}, None])
Method = namedtuple("Method", ["callback", "internal"])
Method = namedtuple("Method", ["callback", "internal", "sensitive_params"])
def anonymise_sensitive_params(params, sensitive_params):
anomized_data = "****"
if not sensitive_params:
return params
if isinstance(sensitive_params, Iterable):
anomized_params = params.copy()
for key in anomized_params.keys():
if key in sensitive_params:
anomized_params[key] = anomized_data
return anomized_params
return anomized_data
class Server():
def __init__(self, reader, writer, encoder=json.JSONEncoder()):
@@ -53,11 +72,27 @@ class Server():
self._notifications = {}
self._eof_listeners = []
def register_method(self, name, callback, internal):
self._methods[name] = Method(callback, internal)
def register_method(self, name, callback, internal, 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 will by anonymized before logging; if False - no params
are considered sensitive, if True - all params are considered sensitive
"""
self._methods[name] = Method(callback, internal, sensitive_params)
def register_notification(self, name, callback, internal):
self._notifications[name] = Method(callback, internal)
def register_notification(self, name, callback, internal, 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 will by anonymized before logging; if False - no params
are considered sensitive, if True - all params are considered sensitive
"""
self._notifications[name] = Method(callback, internal, sensitive_params)
def register_eof(self, callback):
self._eof_listeners.append(callback)
@@ -73,7 +108,7 @@ class Server():
self._eof()
continue
data = data.strip()
logging.debug("Received data: %s", data)
logging.debug("Received %d bytes of data", len(data))
self._handle_input(data)
def stop(self):
@@ -92,43 +127,39 @@ class Server():
self._send_error(None, error)
return
logging.debug("Parsed input: %s", request)
if request.id is not None:
self._handle_request(request)
else:
self._handle_notification(request)
def _handle_notification(self, request):
logging.debug("Handling notification %s", request)
method = self._notifications.get(request.method)
if not method:
logging.error("Received uknown notification: %s", request.method)
logging.error("Received unknown notification: %s", request.method)
return
callback, internal = method
callback, internal, sensitive_params = method
self._log_request(request, sensitive_params)
if internal:
# internal requests are handled immediately
callback(**request.params)
else:
try:
asyncio.create_task(callback(**request.params))
except Exception as error: #pylint: disable=broad-except
logging.error(
"Unexpected exception raised in notification handler: %s",
repr(error)
)
except Exception:
logging.exception("Unexpected exception raised in notification handler")
def _handle_request(self, request):
logging.debug("Handling request %s", request)
method = self._methods.get(request.method)
if not method:
logging.error("Received uknown request: %s", request.method)
logging.error("Received unknown request: %s", request.method)
self._send_error(request.id, MethodNotFound())
return
callback, internal = method
callback, internal, sensitive_params = method
self._log_request(request, sensitive_params)
if internal:
# internal requests are handled immediately
response = callback(request.params)
@@ -144,8 +175,9 @@ class Server():
self._send_error(request.id, MethodNotFound())
except JsonRpcError as error:
self._send_error(request.id, error)
except Exception: #pylint: disable=broad-except
except Exception as e: #pylint: disable=broad-except
logging.exception("Unexpected exception raised in plugin handler")
self._send_error(request.id, UnknownError(str(e)))
asyncio.create_task(handle())
@@ -166,7 +198,8 @@ class Server():
try:
line = self._encoder.encode(data)
logging.debug("Sending data: %s", line)
self._writer.write((line + "\n").encode("utf-8"))
data = (line + "\n").encode("utf-8")
self._writer.write(data)
asyncio.create_task(self._writer.drain())
except TypeError as error:
logging.error(str(error))
@@ -194,25 +227,47 @@ class Server():
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)
class NotificationClient():
def __init__(self, writer, encoder=json.JSONEncoder()):
self._writer = writer
self._encoder = encoder
self._methods = {}
def notify(self, method, params):
def notify(self, method, params, sensitive_params=False):
"""
Send notification
:param method:
:param params:
:param sensitive_params: list of parameters that will by 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)
def _send(self, data):
try:
line = self._encoder.encode(data)
logging.debug("Sending data: %s", line)
self._writer.write((line + "\n").encode("utf-8"))
data = (line + "\n").encode("utf-8")
logging.debug("Sending %d byte of data", len(data))
self._writer.write(data)
asyncio.create_task(self._writer.drain())
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)

View File

@@ -39,7 +39,7 @@ class Plugin():
self._notification_client = NotificationClient(self._writer, encoder)
def eof_handler():
self._active = False
self._shutdown()
self._server.register_eof(eof_handler)
# internal
@@ -48,7 +48,8 @@ class Plugin():
self._register_method("ping", self._ping, internal=True)
# implemented by developer
self._register_method("init_authentication", self.authenticate)
self._register_method("init_authentication", self.authenticate, sensitive_params=["stored_credentials"])
self._register_method("pass_login_credentials", self.pass_login_credentials)
self._register_method(
"import_owned_games",
self.get_owned_games,
@@ -138,7 +139,7 @@ class Plugin():
return False
return True
def _register_method(self, name, handler, result_name=None, internal=False, feature=None):
def _register_method(self, name, handler, result_name=None, internal=False, sensitive_params=False, feature=None):
if internal:
def method(params):
result = handler(**params)
@@ -147,7 +148,7 @@ class Plugin():
result_name: result
}
return result
self._server.register_method(name, method, True)
self._server.register_method(name, method, True, sensitive_params)
else:
async def method(params):
result = await handler(**params)
@@ -156,13 +157,13 @@ class Plugin():
result_name: result
}
return result
self._server.register_method(name, method, False)
self._server.register_method(name, method, False, sensitive_params)
if feature is not None:
self._feature_methods.setdefault(feature, []).append(handler)
def _register_notification(self, name, handler, internal=False, feature=None):
self._server.register_notification(name, handler, internal)
def _register_notification(self, name, handler, internal=False, sensitive_params=False, feature=None):
self._server.register_notification(name, handler, internal, sensitive_params)
if feature is not None:
self._feature_methods.setdefault(feature, []).append(handler)
@@ -171,7 +172,6 @@ class Plugin():
"""Plugin main coorutine"""
async def pass_control():
while self._active:
logging.debug("Passing control to plugin")
try:
self.tick()
except Exception:
@@ -202,7 +202,7 @@ class Plugin():
"""Notify client to store plugin credentials.
They will be pass to next authencicate calls.
"""
self._notification_client.notify("store_credentials", credentials)
self._notification_client.notify("store_credentials", credentials, sensitive_params=True)
def add_game(self, game):
params = {"owned_game" : game}
@@ -275,6 +275,9 @@ class Plugin():
"""
raise NotImplementedError()
async def pass_login_credentials(self, step, credentials, cookies):
raise NotImplementedError()
async def get_owned_games(self):
raise NotImplementedError()
@@ -299,7 +302,7 @@ class Plugin():
async def get_users(self, user_id_list):
raise NotImplementedError()
async def send_message(self, room_id, message):
async def send_message(self, room_id, message_text):
raise NotImplementedError()
async def mark_as_read(self, room_id, last_message_id):

View File

@@ -1,5 +1,5 @@
from dataclasses import dataclass
from typing import List, Optional
from typing import List, Dict, Optional
from galaxy.api.consts import LicenseType, LocalGameState, PresenceState
@@ -8,6 +8,11 @@ class Authentication():
user_id: str
user_name: str
@dataclass
class NextStep():
next_step: str
auth_params: Dict[str, str]
@dataclass
class LicenseInfo():
license_type: LicenseType

View File

@@ -2,9 +2,9 @@ from setuptools import setup, find_packages
setup(
name="galaxy.plugin.api",
version="0.14",
version="0.19",
description="Galaxy python plugin API",
author='Galaxy team',
author_email='galaxy@gog.com',
packages=find_packages()
packages=find_packages(exclude=["tests"])
)

View File

@@ -17,8 +17,9 @@ def test_success(plugin, readline, write):
readline.side_effect = [json.dumps(request), ""]
plugin.get_local_games.return_value = [
LocalGame("1", "Running"),
LocalGame("2", "Installed")
LocalGame("1", LocalGameState.Running),
LocalGame("2", LocalGameState.Installed),
LocalGame("3", LocalGameState.Installed | LocalGameState.Running)
]
asyncio.run(plugin.run())
plugin.get_local_games.assert_called_with()
@@ -31,11 +32,15 @@ def test_success(plugin, readline, write):
"local_games" : [
{
"game_id": "1",
"local_game_state": "Running"
"local_game_state": LocalGameState.Running.value
},
{
"game_id": "2",
"local_game_state": "Installed"
"local_game_state": LocalGameState.Installed.value
},
{
"game_id": "3",
"local_game_state": (LocalGameState.Installed | LocalGameState.Running).value
}
]
}
@@ -85,7 +90,7 @@ def test_local_game_state_update(plugin, write):
"params": {
"local_game": {
"game_id": "1",
"local_game_state": "Running"
"local_game_state": LocalGameState.Running.value
}
}
}