mirror of
https://github.com/gogcom/galaxy-integrations-python-api.git
synced 2026-01-03 12:28:14 -05:00
Compare commits
87 Commits
0.6
...
deployed_0
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ed91fd582c | ||
|
|
bd393a96f0 | ||
|
|
c53aab1abb | ||
|
|
80f40b1971 | ||
|
|
0da0296154 | ||
|
|
9a115557b3 | ||
|
|
14c2d7d9e8 | ||
|
|
4a7a759cea | ||
|
|
da8da24b01 | ||
|
|
ccbb13e685 | ||
|
|
a3ca815975 | ||
|
|
f2d4127a31 | ||
|
|
07b6edce12 | ||
|
|
ef7f9ccca1 | ||
|
|
3b296cbcc9 | ||
|
|
f5361cd5ab | ||
|
|
758909efba | ||
|
|
0bc8000f14 | ||
|
|
e62e7e0e6e | ||
|
|
be6c0eb03e | ||
|
|
0ee56193de | ||
|
|
6bc91a12fa | ||
|
|
6d513d86bf | ||
|
|
bdd2225262 | ||
|
|
68fdc4d188 | ||
|
|
f283c10a95 | ||
|
|
453734cefe | ||
|
|
85f1d83c28 | ||
|
|
701d3cf522 | ||
|
|
c8083b9006 | ||
|
|
0608ade6d3 | ||
|
|
c349a3df8e | ||
|
|
1fd959a665 | ||
|
|
234a21d085 | ||
|
|
90835ece58 | ||
|
|
9e1c8cfddd | ||
|
|
f7f170b9ca | ||
|
|
8ad5ed76b7 | ||
|
|
7727098c6f | ||
|
|
e53dc8f2c6 | ||
|
|
527fd034bf | ||
|
|
6e251c6eb9 | ||
|
|
dc9fc2cc5d | ||
|
|
1fb79eb21a | ||
|
|
7b9bcf86a1 | ||
|
|
30b3533e1d | ||
|
|
92b1d8e4df | ||
|
|
4adef2dace | ||
|
|
1430fe39d7 | ||
|
|
c591efc493 | ||
|
|
7c4f3fba5b | ||
|
|
f2e2e41d04 | ||
|
|
25b850d8bb | ||
|
|
403736612a | ||
|
|
3071c2e771 | ||
|
|
23ef34bed5 | ||
|
|
a4b08f8105 | ||
|
|
4d62b8ccb8 | ||
|
|
d759b4aa85 | ||
|
|
9b33397827 | ||
|
|
e09e443064 | ||
|
|
00ed52384a | ||
|
|
958d9bc0e6 | ||
|
|
d73d048ff7 | ||
|
|
e06e40f845 | ||
|
|
833e6999d7 | ||
|
|
ca778e2cdb | ||
|
|
9a06428fc0 | ||
|
|
f9eaeaf726 | ||
|
|
f09171672f | ||
|
|
ca8d0dfaf4 | ||
|
|
73bc9aa8ec | ||
|
|
52273e2f8c | ||
|
|
bda867473c | ||
|
|
6885cdc439 | ||
|
|
88e25a93be | ||
|
|
67e7a4c0b2 | ||
|
|
788d2550e6 | ||
|
|
059a1ea343 | ||
|
|
300ade5d43 | ||
|
|
43556a0470 | ||
|
|
e244d3bb44 | ||
|
|
d6e6efc633 | ||
|
|
a114c9721c | ||
|
|
6c0389834b | ||
|
|
bc7d1c2914 | ||
|
|
d69e1aaa08 |
50
README.md
50
README.md
@@ -1,24 +1,52 @@
|
|||||||
# Galaxy python plugin API
|
# GOG Galaxy - Community Integration - Python API
|
||||||
|
|
||||||
## Usage
|
This document is still work in progress.
|
||||||
|
|
||||||
Implement plugin:
|
## Basic Usage
|
||||||
|
|
||||||
|
Basic implementation:
|
||||||
|
|
||||||
```python
|
```python
|
||||||
import asyncio
|
import sys
|
||||||
from galaxy.api.plugin import Plugin
|
from galaxy.api.plugin import Plugin, create_and_run_plugin
|
||||||
|
from galaxy.api.consts import Platform
|
||||||
|
|
||||||
class PluginExample(Plugin):
|
class PluginExample(Plugin):
|
||||||
|
def __init__(self, reader, writer, token):
|
||||||
|
super().__init__(
|
||||||
|
Platform.Generic, # Choose platform from available list
|
||||||
|
"0.1", # Version
|
||||||
|
reader,
|
||||||
|
writer,
|
||||||
|
token
|
||||||
|
)
|
||||||
|
|
||||||
# implement methods
|
# implement methods
|
||||||
async def authenticate(self, stored_credentials=None):
|
async def authenticate(self, stored_credentials=None):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
def main():
|
||||||
|
create_and_run_plugin(PluginExample, sys.argv)
|
||||||
|
|
||||||
# run plugin event loop
|
# run plugin event loop
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
asyncio.run(MockPlugin().run())
|
main()
|
||||||
```
|
```
|
||||||
|
|
||||||
Use [pyinstaller](https://www.pyinstaller.org/) to create plugin executbale.
|
Plugin should be deployed with manifest:
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"name": "Example plugin",
|
||||||
|
"platform": "generic",
|
||||||
|
"guid": "UNIQUE-GUID",
|
||||||
|
"version": "0.1",
|
||||||
|
"description": "Example plugin",
|
||||||
|
"author": "Name",
|
||||||
|
"email": "author@email.com",
|
||||||
|
"url": "https://github.com/user/galaxy-plugin-example",
|
||||||
|
"script": "plugin.py"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
## Development
|
## Development
|
||||||
|
|
||||||
@@ -30,4 +58,10 @@ pip install -r requirements.txt
|
|||||||
Run tests:
|
Run tests:
|
||||||
```bash
|
```bash
|
||||||
pytest
|
pytest
|
||||||
```
|
```
|
||||||
|
## Methods Documentation
|
||||||
|
TODO
|
||||||
|
|
||||||
|
## Legal Notice
|
||||||
|
|
||||||
|
By integrating or attempting to integrate any applications or content with or into GOG Galaxy® 2.0. you represent that such application or content is your original creation (other than any software made available by GOG) and/or that you have all necessary rights to grant such applicable rights to the relevant community integration to GOG and to GOG Galaxy 2.0 end users for the purpose of use of such community integration and that such community integration comply with any third party license and other requirements including compliance with applicable laws.
|
||||||
|
|||||||
14
jenkins/release.groovy
Normal file
14
jenkins/release.groovy
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
stage('Upload to github')
|
||||||
|
{
|
||||||
|
node('ActiveClientMacosxBuilder') {
|
||||||
|
deleteDir()
|
||||||
|
checkout scm
|
||||||
|
withPythonEnv('/usr/local/bin/python3.7') {
|
||||||
|
withCredentials([string(credentialsId: 'github_goggalaxy', variable: 'GITHUB_TOKEN')]) {
|
||||||
|
sh 'pip install -r jenkins/requirements.txt'
|
||||||
|
def version = sh(returnStdout: true, script: 'python setup.py --version').trim()
|
||||||
|
sh "python jenkins/release.py $version"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
26
jenkins/release.py
Normal file
26
jenkins/release.py
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
import os
|
||||||
|
import sys
|
||||||
|
from galaxy.github.exporter import transfer_repo
|
||||||
|
|
||||||
|
GITHUB_USERNAME = "goggalaxy"
|
||||||
|
GITHUB_EMAIL = "galaxy-sdk@gog.com"
|
||||||
|
GITHUB_TOKEN = os.environ["GITHUB_TOKEN"]
|
||||||
|
GITHUB_REPO_NAME = "galaxy-integrations-python-api"
|
||||||
|
SOURCE_BRANCH = os.environ["GIT_REFSPEC"]
|
||||||
|
|
||||||
|
GITLAB_USERNAME = "galaxy-client"
|
||||||
|
GITLAB_REPO_NAME = "galaxy-plugin-api"
|
||||||
|
|
||||||
|
def version_provider(_):
|
||||||
|
return sys.argv[1]
|
||||||
|
|
||||||
|
gh_version = transfer_repo(
|
||||||
|
version_provider=version_provider,
|
||||||
|
source_repo_spec="git@gitlab.gog.com:{}/{}.git".format(GITLAB_USERNAME, GITLAB_REPO_NAME),
|
||||||
|
source_include_elements=["src", "tests", "requirements.txt", ".gitignore", "*.md", "pytest.ini", "setup.py"],
|
||||||
|
source_branch=SOURCE_BRANCH,
|
||||||
|
dest_repo_spec="https://{}:{}@github.com/{}/{}.git".format(GITHUB_USERNAME, GITHUB_TOKEN, "gogcom", GITHUB_REPO_NAME),
|
||||||
|
dest_branch="master",
|
||||||
|
dest_user_email=GITHUB_EMAIL,
|
||||||
|
dest_user_name="GOG Galaxy SDK Team"
|
||||||
|
)
|
||||||
1
jenkins/requirements.txt
Normal file
1
jenkins/requirements.txt
Normal file
@@ -0,0 +1 @@
|
|||||||
|
git+ssh://git@gitlab.gog.com/galaxy-client/github-exporter.git@v0.1
|
||||||
@@ -1,2 +1,8 @@
|
|||||||
|
-e .
|
||||||
pytest==4.2.0
|
pytest==4.2.0
|
||||||
|
pytest-asyncio==0.10.0
|
||||||
|
pytest-mock==1.10.3
|
||||||
pytest-flakes==4.0.0
|
pytest-flakes==4.0.0
|
||||||
|
# because of pip bug https://github.com/pypa/pip/issues/4780
|
||||||
|
aiohttp==3.5.4
|
||||||
|
certifi==2019.3.9
|
||||||
|
|||||||
9
setup.py
9
setup.py
@@ -2,9 +2,14 @@ from setuptools import setup, find_packages
|
|||||||
|
|
||||||
setup(
|
setup(
|
||||||
name="galaxy.plugin.api",
|
name="galaxy.plugin.api",
|
||||||
version="0.6",
|
version="0.31.3",
|
||||||
description="Galaxy python plugin API",
|
description="Galaxy python plugin API",
|
||||||
author='Galaxy team',
|
author='Galaxy team',
|
||||||
author_email='galaxy@gog.com',
|
author_email='galaxy@gog.com',
|
||||||
packages=find_packages()
|
packages=find_packages("src"),
|
||||||
|
package_dir={'': 'src'},
|
||||||
|
install_requires=[
|
||||||
|
"aiohttp==3.5.4",
|
||||||
|
"certifi==2019.3.9"
|
||||||
|
]
|
||||||
)
|
)
|
||||||
|
|||||||
1
src/galaxy/__init__.py
Normal file
1
src/galaxy/__init__.py
Normal file
@@ -0,0 +1 @@
|
|||||||
|
__path__ = __import__('pkgutil').extend_path(__path__, __name__)
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
from enum import Enum
|
from enum import Enum, Flag
|
||||||
|
|
||||||
class Platform(Enum):
|
class Platform(Enum):
|
||||||
Unknown = "unknown"
|
Unknown = "unknown"
|
||||||
@@ -10,8 +10,10 @@ class Platform(Enum):
|
|||||||
Origin = "origin"
|
Origin = "origin"
|
||||||
Uplay = "uplay"
|
Uplay = "uplay"
|
||||||
Battlenet = "battlenet"
|
Battlenet = "battlenet"
|
||||||
|
Epic = "epic"
|
||||||
|
|
||||||
class Feature(Enum):
|
class Feature(Enum):
|
||||||
|
Unknown = "Unknown"
|
||||||
ImportInstalledGames = "ImportInstalledGames"
|
ImportInstalledGames = "ImportInstalledGames"
|
||||||
ImportOwnedGames = "ImportOwnedGames"
|
ImportOwnedGames = "ImportOwnedGames"
|
||||||
LaunchGame = "LaunchGame"
|
LaunchGame = "LaunchGame"
|
||||||
@@ -22,12 +24,21 @@ class Feature(Enum):
|
|||||||
Chat = "Chat"
|
Chat = "Chat"
|
||||||
ImportUsers = "ImportUsers"
|
ImportUsers = "ImportUsers"
|
||||||
VerifyGame = "VerifyGame"
|
VerifyGame = "VerifyGame"
|
||||||
|
ImportFriends = "ImportFriends"
|
||||||
|
|
||||||
class LocalGameState(Enum):
|
class LicenseType(Enum):
|
||||||
Installed = "Installed"
|
Unknown = "Unknown"
|
||||||
Running = "Running"
|
SinglePurchase = "SinglePurchase"
|
||||||
|
FreeToPlay = "FreeToPlay"
|
||||||
|
OtherUserLicense = "OtherUserLicense"
|
||||||
|
|
||||||
|
class LocalGameState(Flag):
|
||||||
|
None_ = 0
|
||||||
|
Installed = 1
|
||||||
|
Running = 2
|
||||||
|
|
||||||
class PresenceState(Enum):
|
class PresenceState(Enum):
|
||||||
|
Unknown = "Unknown"
|
||||||
Online = "online"
|
Online = "online"
|
||||||
Offline = "offline"
|
Offline = "offline"
|
||||||
Away = "away"
|
Away = "away"
|
||||||
@@ -1,8 +1,6 @@
|
|||||||
from galaxy.api.jsonrpc import ApplicationError
|
from galaxy.api.jsonrpc import ApplicationError, UnknownError
|
||||||
|
|
||||||
class UnknownError(ApplicationError):
|
UnknownError = UnknownError
|
||||||
def __init__(self, data=None):
|
|
||||||
super().__init__(0, "Unknown error", data)
|
|
||||||
|
|
||||||
class AuthenticationRequired(ApplicationError):
|
class AuthenticationRequired(ApplicationError):
|
||||||
def __init__(self, data=None):
|
def __init__(self, data=None):
|
||||||
@@ -20,6 +18,10 @@ class BackendError(ApplicationError):
|
|||||||
def __init__(self, data=None):
|
def __init__(self, data=None):
|
||||||
super().__init__(4, "Backend error", data)
|
super().__init__(4, "Backend error", data)
|
||||||
|
|
||||||
|
class UnknownBackendResponse(ApplicationError):
|
||||||
|
def __init__(self, data=None):
|
||||||
|
super().__init__(4, "Backend responded in uknown way", data)
|
||||||
|
|
||||||
class InvalidCredentials(ApplicationError):
|
class InvalidCredentials(ApplicationError):
|
||||||
def __init__(self, data=None):
|
def __init__(self, data=None):
|
||||||
super().__init__(100, "Invalid credentials", data)
|
super().__init__(100, "Invalid credentials", data)
|
||||||
@@ -75,3 +77,7 @@ class IncoherentLastMessage(ApplicationError):
|
|||||||
class MessageNotFound(ApplicationError):
|
class MessageNotFound(ApplicationError):
|
||||||
def __init__(self, data=None):
|
def __init__(self, data=None):
|
||||||
super().__init__(500, "Message not found", data)
|
super().__init__(500, "Message not found", data)
|
||||||
|
|
||||||
|
class ImportInProgress(ApplicationError):
|
||||||
|
def __init__(self, data=None):
|
||||||
|
super().__init__(600, "Import already in progress", data)
|
||||||
@@ -1,6 +1,8 @@
|
|||||||
import asyncio
|
import asyncio
|
||||||
from collections import namedtuple
|
from collections import namedtuple
|
||||||
|
from collections.abc import Iterable
|
||||||
import logging
|
import logging
|
||||||
|
import inspect
|
||||||
import json
|
import json
|
||||||
|
|
||||||
class JsonRpcError(Exception):
|
class JsonRpcError(Exception):
|
||||||
@@ -10,6 +12,9 @@ class JsonRpcError(Exception):
|
|||||||
self.data = data
|
self.data = data
|
||||||
super().__init__()
|
super().__init__()
|
||||||
|
|
||||||
|
def __eq__(self, other):
|
||||||
|
return self.code == other.code and self.message == other.message and self.data == other.data
|
||||||
|
|
||||||
class ParseError(JsonRpcError):
|
class ParseError(JsonRpcError):
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
super().__init__(-32700, "Parse error")
|
super().__init__(-32700, "Parse error")
|
||||||
@@ -24,7 +29,7 @@ class MethodNotFound(JsonRpcError):
|
|||||||
|
|
||||||
class InvalidParams(JsonRpcError):
|
class InvalidParams(JsonRpcError):
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
super().__init__(-32601, "Invalid params")
|
super().__init__(-32602, "Invalid params")
|
||||||
|
|
||||||
class Timeout(JsonRpcError):
|
class Timeout(JsonRpcError):
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
@@ -40,8 +45,26 @@ class ApplicationError(JsonRpcError):
|
|||||||
raise ValueError("The error code in reserved range")
|
raise ValueError("The error code in reserved range")
|
||||||
super().__init__(code, message, data)
|
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])
|
Request = namedtuple("Request", ["method", "params", "id"], defaults=[{}, None])
|
||||||
Method = namedtuple("Method", ["callback", "internal"])
|
Method = namedtuple("Method", ["callback", "signature", "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():
|
class Server():
|
||||||
def __init__(self, reader, writer, encoder=json.JSONEncoder()):
|
def __init__(self, reader, writer, encoder=json.JSONEncoder()):
|
||||||
@@ -53,11 +76,27 @@ class Server():
|
|||||||
self._notifications = {}
|
self._notifications = {}
|
||||||
self._eof_listeners = []
|
self._eof_listeners = []
|
||||||
|
|
||||||
def register_method(self, name, callback, internal):
|
def register_method(self, name, callback, internal, sensitive_params=False):
|
||||||
self._methods[name] = Method(callback, internal)
|
"""
|
||||||
|
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, inspect.signature(callback), internal, sensitive_params)
|
||||||
|
|
||||||
def register_notification(self, name, callback, internal):
|
def register_notification(self, name, callback, internal, sensitive_params=False):
|
||||||
self._notifications[name] = Method(callback, internal)
|
"""
|
||||||
|
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, inspect.signature(callback), internal, sensitive_params)
|
||||||
|
|
||||||
def register_eof(self, callback):
|
def register_eof(self, callback):
|
||||||
self._eof_listeners.append(callback)
|
self._eof_listeners.append(callback)
|
||||||
@@ -73,7 +112,7 @@ class Server():
|
|||||||
self._eof()
|
self._eof()
|
||||||
continue
|
continue
|
||||||
data = data.strip()
|
data = data.strip()
|
||||||
logging.debug("Received data: %s", data)
|
logging.debug("Received %d bytes of data", len(data))
|
||||||
self._handle_input(data)
|
self._handle_input(data)
|
||||||
|
|
||||||
def stop(self):
|
def stop(self):
|
||||||
@@ -92,59 +131,65 @@ class Server():
|
|||||||
self._send_error(None, error)
|
self._send_error(None, error)
|
||||||
return
|
return
|
||||||
|
|
||||||
logging.debug("Parsed input: %s", request)
|
|
||||||
|
|
||||||
if request.id is not None:
|
if request.id is not None:
|
||||||
self._handle_request(request)
|
self._handle_request(request)
|
||||||
else:
|
else:
|
||||||
self._handle_notification(request)
|
self._handle_notification(request)
|
||||||
|
|
||||||
def _handle_notification(self, request):
|
def _handle_notification(self, request):
|
||||||
logging.debug("Handling notification %s", request)
|
|
||||||
method = self._notifications.get(request.method)
|
method = self._notifications.get(request.method)
|
||||||
if not method:
|
if not method:
|
||||||
logging.error("Received uknown notification: %s", request.method)
|
logging.error("Received unknown notification: %s", request.method)
|
||||||
|
return
|
||||||
|
|
||||||
|
callback, signature, internal, sensitive_params = method
|
||||||
|
self._log_request(request, sensitive_params)
|
||||||
|
|
||||||
|
try:
|
||||||
|
bound_args = signature.bind(**request.params)
|
||||||
|
except TypeError:
|
||||||
|
self._send_error(request.id, InvalidParams())
|
||||||
|
|
||||||
callback, internal = method
|
|
||||||
if internal:
|
if internal:
|
||||||
# internal requests are handled immediately
|
# internal requests are handled immediately
|
||||||
callback(**request.params)
|
callback(*bound_args.args, **bound_args.kwargs)
|
||||||
else:
|
else:
|
||||||
try:
|
try:
|
||||||
asyncio.create_task(callback(**request.params))
|
asyncio.create_task(callback(*bound_args.args, **bound_args.kwargs))
|
||||||
except Exception as error: #pylint: disable=broad-except
|
except Exception:
|
||||||
logging.error(
|
logging.exception("Unexpected exception raised in notification handler")
|
||||||
"Unexpected exception raised in notification handler: %s",
|
|
||||||
repr(error)
|
|
||||||
)
|
|
||||||
|
|
||||||
def _handle_request(self, request):
|
def _handle_request(self, request):
|
||||||
logging.debug("Handling request %s", request)
|
|
||||||
method = self._methods.get(request.method)
|
method = self._methods.get(request.method)
|
||||||
|
|
||||||
if not 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())
|
self._send_error(request.id, MethodNotFound())
|
||||||
return
|
return
|
||||||
|
|
||||||
callback, internal = method
|
callback, signature, internal, 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 internal:
|
if internal:
|
||||||
# internal requests are handled immediately
|
# internal requests are handled immediately
|
||||||
response = callback(request.params)
|
response = callback(*bound_args.args, **bound_args.kwargs)
|
||||||
self._send_response(request.id, response)
|
self._send_response(request.id, response)
|
||||||
else:
|
else:
|
||||||
async def handle():
|
async def handle():
|
||||||
try:
|
try:
|
||||||
result = await callback(request.params)
|
result = await callback(*bound_args.args, **bound_args.kwargs)
|
||||||
self._send_response(request.id, result)
|
self._send_response(request.id, result)
|
||||||
except TypeError:
|
|
||||||
self._send_error(request.id, InvalidParams())
|
|
||||||
except NotImplementedError:
|
except NotImplementedError:
|
||||||
self._send_error(request.id, MethodNotFound())
|
self._send_error(request.id, MethodNotFound())
|
||||||
except JsonRpcError as error:
|
except JsonRpcError as error:
|
||||||
self._send_error(request.id, error)
|
self._send_error(request.id, error)
|
||||||
except Exception as error: #pylint: disable=broad-except
|
except Exception as e: #pylint: disable=broad-except
|
||||||
logging.error("Unexpected exception raised in plugin handler: %s", repr(error))
|
logging.exception("Unexpected exception raised in plugin handler")
|
||||||
|
self._send_error(request.id, UnknownError(str(e)))
|
||||||
|
|
||||||
asyncio.create_task(handle())
|
asyncio.create_task(handle())
|
||||||
|
|
||||||
@@ -165,7 +210,8 @@ class Server():
|
|||||||
try:
|
try:
|
||||||
line = self._encoder.encode(data)
|
line = self._encoder.encode(data)
|
||||||
logging.debug("Sending data: %s", line)
|
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())
|
asyncio.create_task(self._writer.drain())
|
||||||
except TypeError as error:
|
except TypeError as error:
|
||||||
logging.error(str(error))
|
logging.error(str(error))
|
||||||
@@ -193,25 +239,47 @@ class Server():
|
|||||||
|
|
||||||
self._send(response)
|
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():
|
class NotificationClient():
|
||||||
def __init__(self, writer, encoder=json.JSONEncoder()):
|
def __init__(self, writer, encoder=json.JSONEncoder()):
|
||||||
self._writer = writer
|
self._writer = writer
|
||||||
self._encoder = encoder
|
self._encoder = encoder
|
||||||
self._methods = {}
|
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 = {
|
notification = {
|
||||||
"jsonrpc": "2.0",
|
"jsonrpc": "2.0",
|
||||||
"method": method,
|
"method": method,
|
||||||
"params": params
|
"params": params
|
||||||
}
|
}
|
||||||
|
self._log(method, params, sensitive_params)
|
||||||
self._send(notification)
|
self._send(notification)
|
||||||
|
|
||||||
def _send(self, data):
|
def _send(self, data):
|
||||||
try:
|
try:
|
||||||
line = self._encoder.encode(data)
|
line = self._encoder.encode(data)
|
||||||
logging.debug("Sending data: %s", line)
|
data = (line + "\n").encode("utf-8")
|
||||||
self._writer.write((line + "\n").encode("utf-8"))
|
logging.debug("Sending %d byte of data", len(data))
|
||||||
|
self._writer.write(data)
|
||||||
asyncio.create_task(self._writer.drain())
|
asyncio.create_task(self._writer.drain())
|
||||||
except TypeError as error:
|
except TypeError as error:
|
||||||
logging.error("Failed to parse outgoing message: %s", str(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)
|
||||||
@@ -1,12 +1,15 @@
|
|||||||
import asyncio
|
import asyncio
|
||||||
import json
|
import json
|
||||||
import logging
|
import logging
|
||||||
|
import logging.handlers
|
||||||
import dataclasses
|
import dataclasses
|
||||||
from enum import Enum
|
from enum import Enum
|
||||||
from collections import OrderedDict
|
from collections import OrderedDict
|
||||||
|
import sys
|
||||||
|
|
||||||
from galaxy.api.jsonrpc import Server, NotificationClient
|
from galaxy.api.jsonrpc import Server, NotificationClient
|
||||||
from galaxy.api.consts import Feature
|
from galaxy.api.consts import Feature
|
||||||
|
from galaxy.api.errors import UnknownError, ImportInProgress
|
||||||
|
|
||||||
class JSONEncoder(json.JSONEncoder):
|
class JSONEncoder(json.JSONEncoder):
|
||||||
def default(self, o): # pylint: disable=method-hidden
|
def default(self, o): # pylint: disable=method-hidden
|
||||||
@@ -20,8 +23,10 @@ class JSONEncoder(json.JSONEncoder):
|
|||||||
return super().default(o)
|
return super().default(o)
|
||||||
|
|
||||||
class Plugin():
|
class Plugin():
|
||||||
def __init__(self, platform, 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)
|
||||||
self._platform = platform
|
self._platform = platform
|
||||||
|
self._version = version
|
||||||
|
|
||||||
self._feature_methods = OrderedDict()
|
self._feature_methods = OrderedDict()
|
||||||
self._active = True
|
self._active = True
|
||||||
@@ -34,16 +39,28 @@ class Plugin():
|
|||||||
self._notification_client = NotificationClient(self._writer, encoder)
|
self._notification_client = NotificationClient(self._writer, encoder)
|
||||||
|
|
||||||
def eof_handler():
|
def eof_handler():
|
||||||
self._active = False
|
self._shutdown()
|
||||||
self._server.register_eof(eof_handler)
|
self._server.register_eof(eof_handler)
|
||||||
|
|
||||||
|
self._achievements_import_in_progress = False
|
||||||
|
self._game_times_import_in_progress = False
|
||||||
|
|
||||||
# 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)
|
self._register_method("get_capabilities", self._get_capabilities, internal=True)
|
||||||
self._register_method("ping", self._ping, internal=True)
|
self._register_method("ping", self._ping, internal=True)
|
||||||
|
|
||||||
# implemented by developer
|
# 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,
|
||||||
|
sensitive_params=["cookies", "credentials"]
|
||||||
|
)
|
||||||
self._register_method(
|
self._register_method(
|
||||||
"import_owned_games",
|
"import_owned_games",
|
||||||
self.get_owned_games,
|
self.get_owned_games,
|
||||||
@@ -56,6 +73,10 @@ class Plugin():
|
|||||||
result_name="unlocked_achievements",
|
result_name="unlocked_achievements",
|
||||||
feature=Feature.ImportAchievements
|
feature=Feature.ImportAchievements
|
||||||
)
|
)
|
||||||
|
self._register_method(
|
||||||
|
"start_achievements_import",
|
||||||
|
self.start_achievements_import,
|
||||||
|
)
|
||||||
self._register_method(
|
self._register_method(
|
||||||
"import_local_games",
|
"import_local_games",
|
||||||
self.get_local_games,
|
self.get_local_games,
|
||||||
@@ -72,8 +93,8 @@ class Plugin():
|
|||||||
self._register_method(
|
self._register_method(
|
||||||
"import_friends",
|
"import_friends",
|
||||||
self.get_friends,
|
self.get_friends,
|
||||||
result_name="user_info_list",
|
result_name="friend_info_list",
|
||||||
feature=Feature.ImportUsers
|
feature=Feature.ImportFriends
|
||||||
)
|
)
|
||||||
self._register_method(
|
self._register_method(
|
||||||
"import_user_infos",
|
"import_user_infos",
|
||||||
@@ -109,13 +130,16 @@ class Plugin():
|
|||||||
result_name="messages",
|
result_name="messages",
|
||||||
feature=Feature.Chat
|
feature=Feature.Chat
|
||||||
)
|
)
|
||||||
|
|
||||||
self._register_method(
|
self._register_method(
|
||||||
"import_game_times",
|
"import_game_times",
|
||||||
self.get_game_times,
|
self.get_game_times,
|
||||||
result_name="game_times",
|
result_name="game_times",
|
||||||
feature=Feature.ImportGameTime
|
feature=Feature.ImportGameTime
|
||||||
)
|
)
|
||||||
|
self._register_method(
|
||||||
|
"start_game_times_import",
|
||||||
|
self.start_game_times_import,
|
||||||
|
)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def features(self):
|
def features(self):
|
||||||
@@ -133,31 +157,31 @@ class Plugin():
|
|||||||
return False
|
return False
|
||||||
return True
|
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:
|
if internal:
|
||||||
def method(params):
|
def method(*args, **kwargs):
|
||||||
result = handler(**params)
|
result = handler(*args, **kwargs)
|
||||||
if result_name:
|
if result_name:
|
||||||
result = {
|
result = {
|
||||||
result_name: result
|
result_name: result
|
||||||
}
|
}
|
||||||
return result
|
return result
|
||||||
self._server.register_method(name, method, True)
|
self._server.register_method(name, method, True, sensitive_params)
|
||||||
else:
|
else:
|
||||||
async def method(params):
|
async def method(*args, **kwargs):
|
||||||
result = await handler(**params)
|
result = await handler(*args, **kwargs)
|
||||||
if result_name:
|
if result_name:
|
||||||
result = {
|
result = {
|
||||||
result_name: result
|
result_name: result
|
||||||
}
|
}
|
||||||
return result
|
return result
|
||||||
self._server.register_method(name, method, False)
|
self._server.register_method(name, method, False, sensitive_params)
|
||||||
|
|
||||||
if feature is not None:
|
if feature is not None:
|
||||||
self._feature_methods.setdefault(feature, []).append(handler)
|
self._feature_methods.setdefault(feature, []).append(handler)
|
||||||
|
|
||||||
def _register_notification(self, name, handler, internal=False, feature=None):
|
def _register_notification(self, name, handler, internal=False, sensitive_params=False, feature=None):
|
||||||
self._server.register_notification(name, handler, internal)
|
self._server.register_notification(name, handler, internal, sensitive_params)
|
||||||
|
|
||||||
if feature is not None:
|
if feature is not None:
|
||||||
self._feature_methods.setdefault(feature, []).append(handler)
|
self._feature_methods.setdefault(feature, []).append(handler)
|
||||||
@@ -166,8 +190,10 @@ class Plugin():
|
|||||||
"""Plugin main coorutine"""
|
"""Plugin main coorutine"""
|
||||||
async def pass_control():
|
async def pass_control():
|
||||||
while self._active:
|
while self._active:
|
||||||
logging.debug("Passing control to plugin")
|
try:
|
||||||
self.tick()
|
self.tick()
|
||||||
|
except Exception:
|
||||||
|
logging.exception("Unexpected exception raised in plugin tick")
|
||||||
await asyncio.sleep(1)
|
await asyncio.sleep(1)
|
||||||
|
|
||||||
await asyncio.gather(pass_control(), self._server.run())
|
await asyncio.gather(pass_control(), self._server.run())
|
||||||
@@ -194,7 +220,7 @@ class Plugin():
|
|||||||
"""Notify client to store plugin credentials.
|
"""Notify client to store plugin credentials.
|
||||||
They will be pass to next authencicate calls.
|
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):
|
def add_game(self, game):
|
||||||
params = {"owned_game" : game}
|
params = {"owned_game" : game}
|
||||||
@@ -215,22 +241,38 @@ class Plugin():
|
|||||||
}
|
}
|
||||||
self._notification_client.notify("achievement_unlocked", params)
|
self._notification_client.notify("achievement_unlocked", params)
|
||||||
|
|
||||||
|
def game_achievements_import_success(self, game_id, achievements):
|
||||||
|
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, error):
|
||||||
|
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):
|
||||||
|
self._notification_client.notify("achievements_import_finished", None)
|
||||||
|
|
||||||
def update_local_game_status(self, local_game):
|
def update_local_game_status(self, local_game):
|
||||||
params = {"local_game" : local_game}
|
params = {"local_game" : local_game}
|
||||||
self._notification_client.notify("local_game_status_changed", params)
|
self._notification_client.notify("local_game_status_changed", params)
|
||||||
|
|
||||||
def add_friend(self, user):
|
def add_friend(self, user):
|
||||||
params = {"user_info" : user}
|
params = {"friend_info" : user}
|
||||||
self._notification_client.notify("friend_added", params)
|
self._notification_client.notify("friend_added", params)
|
||||||
|
|
||||||
def remove_friend(self, user_id):
|
def remove_friend(self, user_id):
|
||||||
params = {"user_id" : user_id}
|
params = {"user_id" : user_id}
|
||||||
self._notification_client.notify("friend_removed", params)
|
self._notification_client.notify("friend_removed", params)
|
||||||
|
|
||||||
def update_friend(self, user):
|
|
||||||
params = {"user_info" : user}
|
|
||||||
self._notification_client.notify("friend_updated", params)
|
|
||||||
|
|
||||||
def update_room(self, room_id, unread_message_count=None, new_messages=None):
|
def update_room(self, room_id, unread_message_count=None, new_messages=None):
|
||||||
params = {"room_id": room_id}
|
params = {"room_id": room_id}
|
||||||
if unread_message_count is not None:
|
if unread_message_count is not None:
|
||||||
@@ -243,6 +285,23 @@ class Plugin():
|
|||||||
params = {"game_time" : game_time}
|
params = {"game_time" : game_time}
|
||||||
self._notification_client.notify("game_time_updated", params)
|
self._notification_client.notify("game_time_updated", params)
|
||||||
|
|
||||||
|
def game_time_import_success(self, game_time):
|
||||||
|
params = {"game_time" : game_time}
|
||||||
|
self._notification_client.notify("game_time_import_success", params)
|
||||||
|
|
||||||
|
def game_time_import_failure(self, game_id, error):
|
||||||
|
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):
|
||||||
|
self._notification_client.notify("game_times_import_finished", None)
|
||||||
|
|
||||||
def lost_authentication(self):
|
def lost_authentication(self):
|
||||||
self._notification_client.notify("authentication_lost", None)
|
self._notification_client.notify("authentication_lost", None)
|
||||||
|
|
||||||
@@ -267,12 +326,41 @@ class Plugin():
|
|||||||
"""
|
"""
|
||||||
raise NotImplementedError()
|
raise NotImplementedError()
|
||||||
|
|
||||||
|
async def pass_login_credentials(self, step, credentials, cookies):
|
||||||
|
raise NotImplementedError()
|
||||||
|
|
||||||
async def get_owned_games(self):
|
async def get_owned_games(self):
|
||||||
raise NotImplementedError()
|
raise NotImplementedError()
|
||||||
|
|
||||||
async def get_unlocked_achievements(self, game_id):
|
async def get_unlocked_achievements(self, game_id):
|
||||||
raise NotImplementedError()
|
raise NotImplementedError()
|
||||||
|
|
||||||
|
async def start_achievements_import(self, game_ids):
|
||||||
|
if self._achievements_import_in_progress:
|
||||||
|
raise ImportInProgress()
|
||||||
|
|
||||||
|
async def import_games_achievements_task(game_ids):
|
||||||
|
try:
|
||||||
|
await self.import_games_achievements(game_ids)
|
||||||
|
finally:
|
||||||
|
self.achievements_import_finished()
|
||||||
|
self._achievements_import_in_progress = False
|
||||||
|
|
||||||
|
asyncio.create_task(import_games_achievements_task(game_ids))
|
||||||
|
self._achievements_import_in_progress = True
|
||||||
|
|
||||||
|
async def import_games_achievements(self, game_ids):
|
||||||
|
"""Call game_achievements_import_success/game_achievements_import_failure for each game_id on the list"""
|
||||||
|
async def import_game_achievements(game_id):
|
||||||
|
try:
|
||||||
|
achievements = await self.get_unlocked_achievements(game_id)
|
||||||
|
self.game_achievements_import_success(game_id, achievements)
|
||||||
|
except Exception as error:
|
||||||
|
self.game_achievements_import_failure(game_id, error)
|
||||||
|
|
||||||
|
imports = [import_game_achievements(game_id) for game_id in game_ids]
|
||||||
|
await asyncio.gather(*imports)
|
||||||
|
|
||||||
async def get_local_games(self):
|
async def get_local_games(self):
|
||||||
raise NotImplementedError()
|
raise NotImplementedError()
|
||||||
|
|
||||||
@@ -291,7 +379,7 @@ class Plugin():
|
|||||||
async def get_users(self, user_id_list):
|
async def get_users(self, user_id_list):
|
||||||
raise NotImplementedError()
|
raise NotImplementedError()
|
||||||
|
|
||||||
async def send_message(self, room_id, message):
|
async def send_message(self, room_id, message_text):
|
||||||
raise NotImplementedError()
|
raise NotImplementedError()
|
||||||
|
|
||||||
async def mark_as_read(self, room_id, last_message_id):
|
async def mark_as_read(self, room_id, last_message_id):
|
||||||
@@ -309,22 +397,66 @@ class Plugin():
|
|||||||
async def get_game_times(self):
|
async def get_game_times(self):
|
||||||
raise NotImplementedError()
|
raise NotImplementedError()
|
||||||
|
|
||||||
|
async def start_game_times_import(self, game_ids):
|
||||||
|
if self._game_times_import_in_progress:
|
||||||
|
raise ImportInProgress()
|
||||||
|
|
||||||
|
async def import_game_times_task(game_ids):
|
||||||
|
try:
|
||||||
|
await self.import_game_times(game_ids)
|
||||||
|
finally:
|
||||||
|
self.game_times_import_finished()
|
||||||
|
self._game_times_import_in_progress = False
|
||||||
|
|
||||||
|
asyncio.create_task(import_game_times_task(game_ids))
|
||||||
|
self._game_times_import_in_progress = True
|
||||||
|
|
||||||
|
async def import_game_times(self, game_ids):
|
||||||
|
"""Call game_time_import_success/game_time_import_failure for each game_id on the list"""
|
||||||
|
try:
|
||||||
|
game_times = await self.get_game_times()
|
||||||
|
game_ids_set = set(game_ids)
|
||||||
|
for game_time in game_times:
|
||||||
|
if game_time.game_id not in game_ids_set:
|
||||||
|
continue
|
||||||
|
self.game_time_import_success(game_time)
|
||||||
|
game_ids_set.discard(game_time.game_id)
|
||||||
|
for game_id in game_ids_set:
|
||||||
|
self.game_time_import_failure(game_id, UnknownError())
|
||||||
|
except Exception as error:
|
||||||
|
for game_id in game_ids:
|
||||||
|
self.game_time_import_failure(game_id, error)
|
||||||
|
|
||||||
def create_and_run_plugin(plugin_class, argv):
|
def create_and_run_plugin(plugin_class, argv):
|
||||||
if not issubclass(plugin_class, Plugin):
|
|
||||||
raise TypeError("plugin_class must be subclass of Plugin")
|
|
||||||
if len(argv) < 3:
|
if len(argv) < 3:
|
||||||
raise ValueError("Not enough parameters, required: token, port")
|
logging.critical("Not enough parameters, required: token, port")
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
token = argv[1]
|
token = argv[1]
|
||||||
|
|
||||||
try:
|
try:
|
||||||
port = int(argv[2])
|
port = int(argv[2])
|
||||||
except ValueError as e:
|
except ValueError:
|
||||||
raise ValueError("Failed to parse port value, {}".format(e))
|
logging.critical("Failed to parse port value: %s", argv[2])
|
||||||
|
sys.exit(2)
|
||||||
|
|
||||||
if not (1 <= port <= 65535):
|
if not (1 <= port <= 65535):
|
||||||
raise ValueError("Port value out of range (1, 65535)")
|
logging.critical("Port value out of range (1, 65535)")
|
||||||
|
sys.exit(3)
|
||||||
|
|
||||||
|
if not issubclass(plugin_class, Plugin):
|
||||||
|
logging.critical("plugin_class must be subclass of Plugin")
|
||||||
|
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')
|
||||||
|
logging.info("Using local address: %s:%u", *extra_info)
|
||||||
plugin = plugin_class(reader, writer, token)
|
plugin = plugin_class(reader, writer, token)
|
||||||
await plugin.run()
|
await plugin.run()
|
||||||
asyncio.run(coroutine())
|
|
||||||
|
try:
|
||||||
|
asyncio.run(coroutine())
|
||||||
|
except Exception:
|
||||||
|
logging.exception("Error while running plugin")
|
||||||
|
sys.exit(5)
|
||||||
@@ -1,17 +1,31 @@
|
|||||||
from dataclasses import dataclass
|
from dataclasses import dataclass
|
||||||
from typing import List
|
from typing import List, Dict, Optional
|
||||||
|
|
||||||
from galaxy.api.consts import LocalGameState, PresenceState
|
from galaxy.api.consts import LicenseType, LocalGameState, PresenceState
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
class Authentication():
|
class Authentication():
|
||||||
user_id: str
|
user_id: str
|
||||||
user_name: str
|
user_name: str
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class Cookie():
|
||||||
|
name: str
|
||||||
|
value: str
|
||||||
|
domain: Optional[str] = None
|
||||||
|
path: Optional[str] = None
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class NextStep():
|
||||||
|
next_step: str
|
||||||
|
auth_params: Dict[str, str]
|
||||||
|
cookies: Optional[List[Cookie]] = None
|
||||||
|
js: Optional[Dict[str, List[str]]] = None
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
class LicenseInfo():
|
class LicenseInfo():
|
||||||
license_type: str
|
license_type: LicenseType
|
||||||
owner: str = None
|
owner: Optional[str] = None
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
class Dlc():
|
class Dlc():
|
||||||
@@ -23,13 +37,18 @@ class Dlc():
|
|||||||
class Game():
|
class Game():
|
||||||
game_id: str
|
game_id: str
|
||||||
game_title: str
|
game_title: str
|
||||||
dlcs: List[Dlc]
|
dlcs: Optional[List[Dlc]]
|
||||||
license_info: LicenseInfo
|
license_info: LicenseInfo
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
class Achievement():
|
class Achievement():
|
||||||
achievement_id: str
|
|
||||||
unlock_time: int
|
unlock_time: int
|
||||||
|
achievement_id: Optional[str] = None
|
||||||
|
achievement_name: Optional[str] = None
|
||||||
|
|
||||||
|
def __post_init__(self):
|
||||||
|
assert self.achievement_id or self.achievement_name, \
|
||||||
|
"One of achievement_id or achievement_name is required"
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
class LocalGame():
|
class LocalGame():
|
||||||
@@ -39,8 +58,8 @@ class LocalGame():
|
|||||||
@dataclass
|
@dataclass
|
||||||
class Presence():
|
class Presence():
|
||||||
presence_state: PresenceState
|
presence_state: PresenceState
|
||||||
game_id: str = None
|
game_id: Optional[str] = None
|
||||||
presence_status: str = None
|
presence_status: Optional[str] = None
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
class UserInfo():
|
class UserInfo():
|
||||||
@@ -50,6 +69,11 @@ class UserInfo():
|
|||||||
avatar_url: str
|
avatar_url: str
|
||||||
presence: Presence
|
presence: Presence
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class FriendInfo():
|
||||||
|
user_id: str
|
||||||
|
user_name: str
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
class Room():
|
class Room():
|
||||||
room_id: str
|
room_id: str
|
||||||
47
src/galaxy/http.py
Normal file
47
src/galaxy/http.py
Normal file
@@ -0,0 +1,47 @@
|
|||||||
|
import asyncio
|
||||||
|
import ssl
|
||||||
|
from http import HTTPStatus
|
||||||
|
|
||||||
|
import aiohttp
|
||||||
|
import certifi
|
||||||
|
|
||||||
|
from galaxy.api.errors import (
|
||||||
|
AccessDenied, AuthenticationRequired,
|
||||||
|
BackendTimeout, BackendNotAvailable, BackendError, NetworkError, UnknownBackendResponse, UnknownError
|
||||||
|
)
|
||||||
|
|
||||||
|
class HttpClient:
|
||||||
|
def __init__(self, limit=20, timeout=aiohttp.ClientTimeout(total=60), cookie_jar=None):
|
||||||
|
ssl_context = ssl.SSLContext(ssl.PROTOCOL_TLS_CLIENT)
|
||||||
|
ssl_context.load_verify_locations(certifi.where())
|
||||||
|
connector = aiohttp.TCPConnector(limit=limit, ssl=ssl_context)
|
||||||
|
self._session = aiohttp.ClientSession(connector=connector, timeout=timeout, cookie_jar=cookie_jar)
|
||||||
|
|
||||||
|
async def close(self):
|
||||||
|
await self._session.close()
|
||||||
|
|
||||||
|
async def request(self, method, *args, **kwargs):
|
||||||
|
try:
|
||||||
|
response = await self._session.request(method, *args, **kwargs)
|
||||||
|
except asyncio.TimeoutError:
|
||||||
|
raise BackendTimeout()
|
||||||
|
except aiohttp.ServerDisconnectedError:
|
||||||
|
raise BackendNotAvailable()
|
||||||
|
except aiohttp.ClientConnectionError:
|
||||||
|
raise NetworkError()
|
||||||
|
except aiohttp.ContentTypeError:
|
||||||
|
raise UnknownBackendResponse()
|
||||||
|
except aiohttp.ClientError:
|
||||||
|
raise UnknownError()
|
||||||
|
if response.status == HTTPStatus.UNAUTHORIZED:
|
||||||
|
raise AuthenticationRequired()
|
||||||
|
if response.status == HTTPStatus.FORBIDDEN:
|
||||||
|
raise AccessDenied()
|
||||||
|
if response.status == HTTPStatus.SERVICE_UNAVAILABLE:
|
||||||
|
raise BackendNotAvailable()
|
||||||
|
if response.status >= 500:
|
||||||
|
raise BackendError()
|
||||||
|
if response.status >= 400:
|
||||||
|
raise UnknownError()
|
||||||
|
|
||||||
|
return response
|
||||||
20
src/galaxy/tools.py
Normal file
20
src/galaxy/tools.py
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
import io
|
||||||
|
import os
|
||||||
|
import zipfile
|
||||||
|
from glob import glob
|
||||||
|
|
||||||
|
def zip_folder(folder):
|
||||||
|
files = glob(os.path.join(folder, "**"), recursive=True)
|
||||||
|
files = [file.replace(folder + os.sep, "") for file in files]
|
||||||
|
files = [file for file in files if file]
|
||||||
|
|
||||||
|
zip_buffer = io.BytesIO()
|
||||||
|
with zipfile.ZipFile(zip_buffer, mode="w", compression=zipfile.ZIP_DEFLATED) as zipf:
|
||||||
|
for file in files:
|
||||||
|
zipf.write(os.path.join(folder, file), arcname=file)
|
||||||
|
return zip_buffer
|
||||||
|
|
||||||
|
def zip_folder_to_file(folder, filename):
|
||||||
|
zip_content = zip_folder(folder).getbuffer()
|
||||||
|
with open(filename, "wb") as archive:
|
||||||
|
archive.write(zip_content)
|
||||||
12
src/galaxy/unittest/mock.py
Normal file
12
src/galaxy/unittest/mock.py
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
from asyncio import coroutine
|
||||||
|
from unittest.mock import MagicMock
|
||||||
|
|
||||||
|
class AsyncMock(MagicMock):
|
||||||
|
async def __call__(self, *args, **kwargs):
|
||||||
|
return super(AsyncMock, self).__call__(*args, **kwargs)
|
||||||
|
|
||||||
|
def coroutine_mock():
|
||||||
|
coro = MagicMock(name="CoroutineResult")
|
||||||
|
corofunc = MagicMock(name="CoroutineFunction", side_effect=coroutine(coro))
|
||||||
|
corofunc.coro = coro
|
||||||
|
return corofunc
|
||||||
@@ -1,6 +0,0 @@
|
|||||||
from unittest.mock import MagicMock
|
|
||||||
|
|
||||||
class AsyncMock(MagicMock):
|
|
||||||
async def __call__(self, *args, **kwargs):
|
|
||||||
# pylint: disable=useless-super-delegation
|
|
||||||
return super(AsyncMock, self).__call__(*args, **kwargs)
|
|
||||||
@@ -6,7 +6,7 @@ import pytest
|
|||||||
|
|
||||||
from galaxy.api.plugin import Plugin
|
from galaxy.api.plugin import Plugin
|
||||||
from galaxy.api.consts import Platform
|
from galaxy.api.consts import Platform
|
||||||
from tests.async_mock import AsyncMock
|
from galaxy.unittest.mock import AsyncMock, coroutine_mock
|
||||||
|
|
||||||
@pytest.fixture()
|
@pytest.fixture()
|
||||||
def reader():
|
def reader():
|
||||||
@@ -57,10 +57,10 @@ def plugin(reader, writer):
|
|||||||
|
|
||||||
with ExitStack() as stack:
|
with ExitStack() as stack:
|
||||||
for method in async_methods:
|
for method in async_methods:
|
||||||
stack.enter_context(patch.object(Plugin, method, new_callable=AsyncMock))
|
stack.enter_context(patch.object(Plugin, method, new_callable=coroutine_mock))
|
||||||
for method in methods:
|
for method in methods:
|
||||||
stack.enter_context(patch.object(Plugin, method))
|
stack.enter_context(patch.object(Plugin, method))
|
||||||
yield Plugin(Platform.Generic, reader, writer, "token")
|
yield Plugin(Platform.Generic, "0.1", reader, writer, "token")
|
||||||
|
|
||||||
@pytest.fixture(autouse=True)
|
@pytest.fixture(autouse=True)
|
||||||
def my_caplog(caplog):
|
def my_caplog(caplog):
|
||||||
|
|||||||
@@ -1,8 +1,20 @@
|
|||||||
import asyncio
|
import asyncio
|
||||||
import json
|
import json
|
||||||
|
from unittest.mock import call
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
from pytest import raises
|
||||||
|
|
||||||
from galaxy.api.types import Achievement
|
from galaxy.api.types import Achievement
|
||||||
from galaxy.api.errors import UnknownError
|
from galaxy.api.errors import UnknownError, ImportInProgress, BackendError
|
||||||
|
|
||||||
|
def test_initialization_no_unlock_time():
|
||||||
|
with raises(Exception):
|
||||||
|
Achievement(achievement_id="lvl30", achievement_name="Got level 30")
|
||||||
|
|
||||||
|
def test_initialization_no_id_nor_name():
|
||||||
|
with raises(AssertionError):
|
||||||
|
Achievement(unlock_time=1234567890)
|
||||||
|
|
||||||
def test_success(plugin, readline, write):
|
def test_success(plugin, readline, write):
|
||||||
request = {
|
request = {
|
||||||
@@ -14,9 +26,10 @@ def test_success(plugin, readline, write):
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
readline.side_effect = [json.dumps(request), ""]
|
readline.side_effect = [json.dumps(request), ""]
|
||||||
plugin.get_unlocked_achievements.return_value = [
|
plugin.get_unlocked_achievements.coro.return_value = [
|
||||||
Achievement("lvl10", 1548421241),
|
Achievement(achievement_id="lvl10", unlock_time=1548421241),
|
||||||
Achievement("lvl20", 1548422395)
|
Achievement(achievement_name="Got level 20", unlock_time=1548422395),
|
||||||
|
Achievement(achievement_id="lvl30", achievement_name="Got level 30", unlock_time=1548495633)
|
||||||
]
|
]
|
||||||
asyncio.run(plugin.run())
|
asyncio.run(plugin.run())
|
||||||
plugin.get_unlocked_achievements.assert_called_with(game_id="14")
|
plugin.get_unlocked_achievements.assert_called_with(game_id="14")
|
||||||
@@ -32,8 +45,13 @@ def test_success(plugin, readline, write):
|
|||||||
"unlock_time": 1548421241
|
"unlock_time": 1548421241
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"achievement_id": "lvl20",
|
"achievement_name": "Got level 20",
|
||||||
"unlock_time": 1548422395
|
"unlock_time": 1548422395
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"achievement_id": "lvl30",
|
||||||
|
"achievement_name": "Got level 30",
|
||||||
|
"unlock_time": 1548495633
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
@@ -50,7 +68,7 @@ def test_failure(plugin, readline, write):
|
|||||||
}
|
}
|
||||||
|
|
||||||
readline.side_effect = [json.dumps(request), ""]
|
readline.side_effect = [json.dumps(request), ""]
|
||||||
plugin.get_unlocked_achievements.side_effect = UnknownError()
|
plugin.get_unlocked_achievements.coro.side_effect = UnknownError()
|
||||||
asyncio.run(plugin.run())
|
asyncio.run(plugin.run())
|
||||||
plugin.get_unlocked_achievements.assert_called()
|
plugin.get_unlocked_achievements.assert_called()
|
||||||
response = json.loads(write.call_args[0][0])
|
response = json.loads(write.call_args[0][0])
|
||||||
@@ -65,7 +83,7 @@ def test_failure(plugin, readline, write):
|
|||||||
}
|
}
|
||||||
|
|
||||||
def test_unlock_achievement(plugin, write):
|
def test_unlock_achievement(plugin, write):
|
||||||
achievement = Achievement("lvl20", 1548422395)
|
achievement = Achievement(achievement_id="lvl20", unlock_time=1548422395)
|
||||||
|
|
||||||
async def couritine():
|
async def couritine():
|
||||||
plugin.unlock_achievement("14", achievement)
|
plugin.unlock_achievement("14", achievement)
|
||||||
@@ -84,3 +102,92 @@ def test_unlock_achievement(plugin, write):
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_game_achievements_import_success(plugin, write):
|
||||||
|
achievements = [
|
||||||
|
Achievement(achievement_id="lvl10", unlock_time=1548421241),
|
||||||
|
Achievement(achievement_name="Got level 20", unlock_time=1548422395)
|
||||||
|
]
|
||||||
|
plugin.game_achievements_import_success("134", achievements)
|
||||||
|
response = json.loads(write.call_args[0][0])
|
||||||
|
|
||||||
|
assert response == {
|
||||||
|
"jsonrpc": "2.0",
|
||||||
|
"method": "game_achievements_import_success",
|
||||||
|
"params": {
|
||||||
|
"game_id": "134",
|
||||||
|
"unlocked_achievements": [
|
||||||
|
{
|
||||||
|
"achievement_id": "lvl10",
|
||||||
|
"unlock_time": 1548421241
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"achievement_name": "Got level 20",
|
||||||
|
"unlock_time": 1548422395
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_game_achievements_import_failure(plugin, write):
|
||||||
|
plugin.game_achievements_import_failure("134", ImportInProgress())
|
||||||
|
response = json.loads(write.call_args[0][0])
|
||||||
|
|
||||||
|
assert response == {
|
||||||
|
"jsonrpc": "2.0",
|
||||||
|
"method": "game_achievements_import_failure",
|
||||||
|
"params": {
|
||||||
|
"game_id": "134",
|
||||||
|
"error": {
|
||||||
|
"code": 600,
|
||||||
|
"message": "Import already in progress"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_achievements_import_finished(plugin, write):
|
||||||
|
plugin.achievements_import_finished()
|
||||||
|
response = json.loads(write.call_args[0][0])
|
||||||
|
|
||||||
|
assert response == {
|
||||||
|
"jsonrpc": "2.0",
|
||||||
|
"method": "achievements_import_finished",
|
||||||
|
"params": None
|
||||||
|
}
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_start_achievements_import(plugin, write, mocker):
|
||||||
|
game_achievements_import_success = mocker.patch.object(plugin, "game_achievements_import_success")
|
||||||
|
game_achievements_import_failure = mocker.patch.object(plugin, "game_achievements_import_failure")
|
||||||
|
achievements_import_finished = mocker.patch.object(plugin, "achievements_import_finished")
|
||||||
|
|
||||||
|
game_ids = ["1", "5", "9"]
|
||||||
|
error = BackendError()
|
||||||
|
achievements = [
|
||||||
|
Achievement(achievement_id="lvl10", unlock_time=1548421241),
|
||||||
|
Achievement(achievement_name="Got level 20", unlock_time=1548422395)
|
||||||
|
]
|
||||||
|
plugin.get_unlocked_achievements.coro.side_effect = [
|
||||||
|
achievements,
|
||||||
|
[],
|
||||||
|
error
|
||||||
|
]
|
||||||
|
await plugin.start_achievements_import(game_ids)
|
||||||
|
|
||||||
|
with pytest.raises(ImportInProgress):
|
||||||
|
await plugin.start_achievements_import(["4", "8"])
|
||||||
|
|
||||||
|
# wait until all tasks are finished
|
||||||
|
for _ in range(4):
|
||||||
|
await asyncio.sleep(0)
|
||||||
|
|
||||||
|
plugin.get_unlocked_achievements.coro.assert_has_calls([call("1"), call("5"), call("9")])
|
||||||
|
game_achievements_import_success.assert_has_calls([
|
||||||
|
call("1", achievements),
|
||||||
|
call("5", [])
|
||||||
|
])
|
||||||
|
game_achievements_import_failure.assert_called_once_with("9", error)
|
||||||
|
achievements_import_finished.assert_called_once_with()
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ def test_success(plugin, readline, write):
|
|||||||
}
|
}
|
||||||
|
|
||||||
readline.side_effect = [json.dumps(request), ""]
|
readline.side_effect = [json.dumps(request), ""]
|
||||||
plugin.authenticate.return_value = Authentication("132", "Zenek")
|
plugin.authenticate.coro.return_value = Authentication("132", "Zenek")
|
||||||
asyncio.run(plugin.run())
|
asyncio.run(plugin.run())
|
||||||
plugin.authenticate.assert_called_with()
|
plugin.authenticate.assert_called_with()
|
||||||
response = json.loads(write.call_args[0][0])
|
response = json.loads(write.call_args[0][0])
|
||||||
@@ -56,7 +56,7 @@ def test_failure(plugin, readline, write, error, code, message):
|
|||||||
}
|
}
|
||||||
|
|
||||||
readline.side_effect = [json.dumps(request), ""]
|
readline.side_effect = [json.dumps(request), ""]
|
||||||
plugin.authenticate.side_effect = error()
|
plugin.authenticate.coro.side_effect = error()
|
||||||
asyncio.run(plugin.run())
|
asyncio.run(plugin.run())
|
||||||
plugin.authenticate.assert_called_with()
|
plugin.authenticate.assert_called_with()
|
||||||
response = json.loads(write.call_args[0][0])
|
response = json.loads(write.call_args[0][0])
|
||||||
@@ -82,7 +82,7 @@ def test_stored_credentials(plugin, readline, write):
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
readline.side_effect = [json.dumps(request), ""]
|
readline.side_effect = [json.dumps(request), ""]
|
||||||
plugin.authenticate.return_value = Authentication("132", "Zenek")
|
plugin.authenticate.coro.return_value = Authentication("132", "Zenek")
|
||||||
asyncio.run(plugin.run())
|
asyncio.run(plugin.run())
|
||||||
plugin.authenticate.assert_called_with(stored_credentials={"token": "ABC"})
|
plugin.authenticate.assert_called_with(stored_credentials={"token": "ABC"})
|
||||||
write.assert_called()
|
write.assert_called()
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ def test_send_message_success(plugin, readline, write):
|
|||||||
}
|
}
|
||||||
|
|
||||||
readline.side_effect = [json.dumps(request), ""]
|
readline.side_effect = [json.dumps(request), ""]
|
||||||
plugin.send_message.return_value = None
|
plugin.send_message.coro.return_value = None
|
||||||
asyncio.run(plugin.run())
|
asyncio.run(plugin.run())
|
||||||
plugin.send_message.assert_called_with(room_id="14", message="Hello!")
|
plugin.send_message.assert_called_with(room_id="14", message="Hello!")
|
||||||
response = json.loads(write.call_args[0][0])
|
response = json.loads(write.call_args[0][0])
|
||||||
@@ -52,7 +52,7 @@ def test_send_message_failure(plugin, readline, write, error, code, message):
|
|||||||
}
|
}
|
||||||
|
|
||||||
readline.side_effect = [json.dumps(request), ""]
|
readline.side_effect = [json.dumps(request), ""]
|
||||||
plugin.send_message.side_effect = error()
|
plugin.send_message.coro.side_effect = error()
|
||||||
asyncio.run(plugin.run())
|
asyncio.run(plugin.run())
|
||||||
plugin.send_message.assert_called_with(room_id="15", message="Bye")
|
plugin.send_message.assert_called_with(room_id="15", message="Bye")
|
||||||
response = json.loads(write.call_args[0][0])
|
response = json.loads(write.call_args[0][0])
|
||||||
@@ -78,7 +78,7 @@ def test_mark_as_read_success(plugin, readline, write):
|
|||||||
}
|
}
|
||||||
|
|
||||||
readline.side_effect = [json.dumps(request), ""]
|
readline.side_effect = [json.dumps(request), ""]
|
||||||
plugin.mark_as_read.return_value = None
|
plugin.mark_as_read.coro.return_value = None
|
||||||
asyncio.run(plugin.run())
|
asyncio.run(plugin.run())
|
||||||
plugin.mark_as_read.assert_called_with(room_id="14", last_message_id="67")
|
plugin.mark_as_read.assert_called_with(room_id="14", last_message_id="67")
|
||||||
response = json.loads(write.call_args[0][0])
|
response = json.loads(write.call_args[0][0])
|
||||||
@@ -114,7 +114,7 @@ def test_mark_as_read_failure(plugin, readline, write, error, code, message):
|
|||||||
}
|
}
|
||||||
|
|
||||||
readline.side_effect = [json.dumps(request), ""]
|
readline.side_effect = [json.dumps(request), ""]
|
||||||
plugin.mark_as_read.side_effect = error()
|
plugin.mark_as_read.coro.side_effect = error()
|
||||||
asyncio.run(plugin.run())
|
asyncio.run(plugin.run())
|
||||||
plugin.mark_as_read.assert_called_with(room_id="18", last_message_id="7")
|
plugin.mark_as_read.assert_called_with(room_id="18", last_message_id="7")
|
||||||
response = json.loads(write.call_args[0][0])
|
response = json.loads(write.call_args[0][0])
|
||||||
@@ -136,7 +136,7 @@ def test_get_rooms_success(plugin, readline, write):
|
|||||||
}
|
}
|
||||||
|
|
||||||
readline.side_effect = [json.dumps(request), ""]
|
readline.side_effect = [json.dumps(request), ""]
|
||||||
plugin.get_rooms.return_value = [
|
plugin.get_rooms.coro.return_value = [
|
||||||
Room("13", 0, None),
|
Room("13", 0, None),
|
||||||
Room("15", 34, "8")
|
Room("15", 34, "8")
|
||||||
]
|
]
|
||||||
@@ -170,7 +170,7 @@ def test_get_rooms_failure(plugin, readline, write):
|
|||||||
}
|
}
|
||||||
|
|
||||||
readline.side_effect = [json.dumps(request), ""]
|
readline.side_effect = [json.dumps(request), ""]
|
||||||
plugin.get_rooms.side_effect = UnknownError()
|
plugin.get_rooms.coro.side_effect = UnknownError()
|
||||||
asyncio.run(plugin.run())
|
asyncio.run(plugin.run())
|
||||||
plugin.get_rooms.assert_called_with()
|
plugin.get_rooms.assert_called_with()
|
||||||
response = json.loads(write.call_args[0][0])
|
response = json.loads(write.call_args[0][0])
|
||||||
@@ -196,7 +196,7 @@ def test_get_room_history_from_message_success(plugin, readline, write):
|
|||||||
}
|
}
|
||||||
|
|
||||||
readline.side_effect = [json.dumps(request), ""]
|
readline.side_effect = [json.dumps(request), ""]
|
||||||
plugin.get_room_history_from_message.return_value = [
|
plugin.get_room_history_from_message.coro.return_value = [
|
||||||
Message("13", "149", 1549454837, "Hello"),
|
Message("13", "149", 1549454837, "Hello"),
|
||||||
Message("14", "812", 1549454899, "Hi")
|
Message("14", "812", 1549454899, "Hi")
|
||||||
]
|
]
|
||||||
@@ -245,7 +245,7 @@ def test_get_room_history_from_message_failure(plugin, readline, write, error, c
|
|||||||
}
|
}
|
||||||
|
|
||||||
readline.side_effect = [json.dumps(request), ""]
|
readline.side_effect = [json.dumps(request), ""]
|
||||||
plugin.get_room_history_from_message.side_effect = error()
|
plugin.get_room_history_from_message.coro.side_effect = error()
|
||||||
asyncio.run(plugin.run())
|
asyncio.run(plugin.run())
|
||||||
plugin.get_room_history_from_message.assert_called_with(room_id="33", message_id="88")
|
plugin.get_room_history_from_message.assert_called_with(room_id="33", message_id="88")
|
||||||
response = json.loads(write.call_args[0][0])
|
response = json.loads(write.call_args[0][0])
|
||||||
@@ -271,7 +271,7 @@ def test_get_room_history_from_timestamp_success(plugin, readline, write):
|
|||||||
}
|
}
|
||||||
|
|
||||||
readline.side_effect = [json.dumps(request), ""]
|
readline.side_effect = [json.dumps(request), ""]
|
||||||
plugin.get_room_history_from_timestamp.return_value = [
|
plugin.get_room_history_from_timestamp.coro.return_value = [
|
||||||
Message("12", "155", 1549454836, "Bye")
|
Message("12", "155", 1549454836, "Bye")
|
||||||
]
|
]
|
||||||
asyncio.run(plugin.run())
|
asyncio.run(plugin.run())
|
||||||
@@ -308,7 +308,7 @@ def test_get_room_history_from_timestamp_failure(plugin, readline, write):
|
|||||||
}
|
}
|
||||||
|
|
||||||
readline.side_effect = [json.dumps(request), ""]
|
readline.side_effect = [json.dumps(request), ""]
|
||||||
plugin.get_room_history_from_timestamp.side_effect = UnknownError()
|
plugin.get_room_history_from_timestamp.coro.side_effect = UnknownError()
|
||||||
asyncio.run(plugin.run())
|
asyncio.run(plugin.run())
|
||||||
plugin.get_room_history_from_timestamp.assert_called_with(
|
plugin.get_room_history_from_timestamp.assert_called_with(
|
||||||
room_id="10",
|
room_id="10",
|
||||||
|
|||||||
@@ -2,14 +2,14 @@ from galaxy.api.plugin import Plugin
|
|||||||
from galaxy.api.consts import Platform, Feature
|
from galaxy.api.consts import Platform, Feature
|
||||||
|
|
||||||
def test_base_class():
|
def test_base_class():
|
||||||
plugin = Plugin(Platform.Generic, None, None, None)
|
plugin = Plugin(Platform.Generic, "0.1", None, None, None)
|
||||||
assert plugin.features == []
|
assert plugin.features == []
|
||||||
|
|
||||||
def test_no_overloads():
|
def test_no_overloads():
|
||||||
class PluginImpl(Plugin): #pylint: disable=abstract-method
|
class PluginImpl(Plugin): #pylint: disable=abstract-method
|
||||||
pass
|
pass
|
||||||
|
|
||||||
plugin = PluginImpl(Platform.Generic, None, None, None)
|
plugin = PluginImpl(Platform.Generic, "0.1", None, None, None)
|
||||||
assert plugin.features == []
|
assert plugin.features == []
|
||||||
|
|
||||||
def test_one_method_feature():
|
def test_one_method_feature():
|
||||||
@@ -17,7 +17,7 @@ def test_one_method_feature():
|
|||||||
async def get_owned_games(self):
|
async def get_owned_games(self):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
plugin = PluginImpl(Platform.Generic, None, None, None)
|
plugin = PluginImpl(Platform.Generic, "0.1", None, None, None)
|
||||||
assert plugin.features == [Feature.ImportOwnedGames]
|
assert plugin.features == [Feature.ImportOwnedGames]
|
||||||
|
|
||||||
def test_multiple_methods_feature_all():
|
def test_multiple_methods_feature_all():
|
||||||
@@ -33,7 +33,7 @@ def test_multiple_methods_feature_all():
|
|||||||
async def get_room_history_from_timestamp(self, room_id, timestamp):
|
async def get_room_history_from_timestamp(self, room_id, timestamp):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
plugin = PluginImpl(Platform.Generic, None, None, None)
|
plugin = PluginImpl(Platform.Generic, "0.1", None, None, None)
|
||||||
assert plugin.features == [Feature.Chat]
|
assert plugin.features == [Feature.Chat]
|
||||||
|
|
||||||
def test_multiple_methods_feature_not_all():
|
def test_multiple_methods_feature_not_all():
|
||||||
@@ -41,5 +41,5 @@ def test_multiple_methods_feature_not_all():
|
|||||||
async def send_message(self, room_id, message):
|
async def send_message(self, room_id, message):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
plugin = PluginImpl(Platform.Generic, None, None, None)
|
plugin = PluginImpl(Platform.Generic, "0.1", None, None, None)
|
||||||
assert plugin.features == []
|
assert plugin.features == []
|
||||||
|
|||||||
90
tests/test_friends.py
Normal file
90
tests/test_friends.py
Normal file
@@ -0,0 +1,90 @@
|
|||||||
|
import asyncio
|
||||||
|
import json
|
||||||
|
|
||||||
|
from galaxy.api.types import FriendInfo
|
||||||
|
from galaxy.api.errors import UnknownError
|
||||||
|
|
||||||
|
|
||||||
|
def test_get_friends_success(plugin, readline, write):
|
||||||
|
request = {
|
||||||
|
"jsonrpc": "2.0",
|
||||||
|
"id": "3",
|
||||||
|
"method": "import_friends"
|
||||||
|
}
|
||||||
|
|
||||||
|
readline.side_effect = [json.dumps(request), ""]
|
||||||
|
plugin.get_friends.coro.return_value = [
|
||||||
|
FriendInfo("3", "Jan"),
|
||||||
|
FriendInfo("5", "Ola")
|
||||||
|
]
|
||||||
|
asyncio.run(plugin.run())
|
||||||
|
plugin.get_friends.assert_called_with()
|
||||||
|
response = json.loads(write.call_args[0][0])
|
||||||
|
|
||||||
|
assert response == {
|
||||||
|
"jsonrpc": "2.0",
|
||||||
|
"id": "3",
|
||||||
|
"result": {
|
||||||
|
"friend_info_list": [
|
||||||
|
{"user_id": "3", "user_name": "Jan"},
|
||||||
|
{"user_id": "5", "user_name": "Ola"}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def test_get_friends_failure(plugin, readline, write):
|
||||||
|
request = {
|
||||||
|
"jsonrpc": "2.0",
|
||||||
|
"id": "3",
|
||||||
|
"method": "import_friends"
|
||||||
|
}
|
||||||
|
|
||||||
|
readline.side_effect = [json.dumps(request), ""]
|
||||||
|
plugin.get_friends.coro.side_effect = UnknownError()
|
||||||
|
asyncio.run(plugin.run())
|
||||||
|
plugin.get_friends.assert_called_with()
|
||||||
|
response = json.loads(write.call_args[0][0])
|
||||||
|
|
||||||
|
assert response == {
|
||||||
|
"jsonrpc": "2.0",
|
||||||
|
"id": "3",
|
||||||
|
"error": {
|
||||||
|
"code": 0,
|
||||||
|
"message": "Unknown error",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def test_add_friend(plugin, write):
|
||||||
|
friend = FriendInfo("7", "Kuba")
|
||||||
|
|
||||||
|
async def couritine():
|
||||||
|
plugin.add_friend(friend)
|
||||||
|
|
||||||
|
asyncio.run(couritine())
|
||||||
|
response = json.loads(write.call_args[0][0])
|
||||||
|
|
||||||
|
assert response == {
|
||||||
|
"jsonrpc": "2.0",
|
||||||
|
"method": "friend_added",
|
||||||
|
"params": {
|
||||||
|
"friend_info": {"user_id": "7", "user_name": "Kuba"}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def test_remove_friend(plugin, write):
|
||||||
|
async def couritine():
|
||||||
|
plugin.remove_friend("5")
|
||||||
|
|
||||||
|
asyncio.run(couritine())
|
||||||
|
response = json.loads(write.call_args[0][0])
|
||||||
|
|
||||||
|
assert response == {
|
||||||
|
"jsonrpc": "2.0",
|
||||||
|
"method": "friend_removed",
|
||||||
|
"params": {
|
||||||
|
"user_id": "5"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,8 +1,10 @@
|
|||||||
import asyncio
|
import asyncio
|
||||||
import json
|
import json
|
||||||
|
from unittest.mock import call
|
||||||
|
|
||||||
|
import pytest
|
||||||
from galaxy.api.types import GameTime
|
from galaxy.api.types import GameTime
|
||||||
from galaxy.api.errors import UnknownError
|
from galaxy.api.errors import UnknownError, ImportInProgress, BackendError
|
||||||
|
|
||||||
def test_success(plugin, readline, write):
|
def test_success(plugin, readline, write):
|
||||||
request = {
|
request = {
|
||||||
@@ -12,7 +14,7 @@ def test_success(plugin, readline, write):
|
|||||||
}
|
}
|
||||||
|
|
||||||
readline.side_effect = [json.dumps(request), ""]
|
readline.side_effect = [json.dumps(request), ""]
|
||||||
plugin.get_game_times.return_value = [
|
plugin.get_game_times.coro.return_value = [
|
||||||
GameTime("3", 60, 1549550504),
|
GameTime("3", 60, 1549550504),
|
||||||
GameTime("5", 10, 1549550502)
|
GameTime("5", 10, 1549550502)
|
||||||
]
|
]
|
||||||
@@ -47,7 +49,7 @@ def test_failure(plugin, readline, write):
|
|||||||
}
|
}
|
||||||
|
|
||||||
readline.side_effect = [json.dumps(request), ""]
|
readline.side_effect = [json.dumps(request), ""]
|
||||||
plugin.get_game_times.side_effect = UnknownError()
|
plugin.get_game_times.coro.side_effect = UnknownError()
|
||||||
asyncio.run(plugin.run())
|
asyncio.run(plugin.run())
|
||||||
plugin.get_game_times.assert_called_with()
|
plugin.get_game_times.assert_called_with()
|
||||||
response = json.loads(write.call_args[0][0])
|
response = json.loads(write.call_args[0][0])
|
||||||
@@ -81,3 +83,93 @@ def test_update_game(plugin, write):
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_game_time_import_success(plugin, write):
|
||||||
|
plugin.game_time_import_success(GameTime("3", 60, 1549550504))
|
||||||
|
response = json.loads(write.call_args[0][0])
|
||||||
|
|
||||||
|
assert response == {
|
||||||
|
"jsonrpc": "2.0",
|
||||||
|
"method": "game_time_import_success",
|
||||||
|
"params": {
|
||||||
|
"game_time": {
|
||||||
|
"game_id": "3",
|
||||||
|
"time_played": 60,
|
||||||
|
"last_played_time": 1549550504
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_game_time_import_failure(plugin, write):
|
||||||
|
plugin.game_time_import_failure("134", ImportInProgress())
|
||||||
|
response = json.loads(write.call_args[0][0])
|
||||||
|
|
||||||
|
assert response == {
|
||||||
|
"jsonrpc": "2.0",
|
||||||
|
"method": "game_time_import_failure",
|
||||||
|
"params": {
|
||||||
|
"game_id": "134",
|
||||||
|
"error": {
|
||||||
|
"code": 600,
|
||||||
|
"message": "Import already in progress"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_game_times_import_finished(plugin, write):
|
||||||
|
plugin.game_times_import_finished()
|
||||||
|
response = json.loads(write.call_args[0][0])
|
||||||
|
|
||||||
|
assert response == {
|
||||||
|
"jsonrpc": "2.0",
|
||||||
|
"method": "game_times_import_finished",
|
||||||
|
"params": None
|
||||||
|
}
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_start_game_times_import(plugin, write, mocker):
|
||||||
|
game_time_import_success = mocker.patch.object(plugin, "game_time_import_success")
|
||||||
|
game_time_import_failure = mocker.patch.object(plugin, "game_time_import_failure")
|
||||||
|
game_times_import_finished = mocker.patch.object(plugin, "game_times_import_finished")
|
||||||
|
|
||||||
|
game_ids = ["1", "5"]
|
||||||
|
game_time = GameTime("1", 10, 1549550502)
|
||||||
|
plugin.get_game_times.coro.return_value = [
|
||||||
|
game_time
|
||||||
|
]
|
||||||
|
await plugin.start_game_times_import(game_ids)
|
||||||
|
|
||||||
|
with pytest.raises(ImportInProgress):
|
||||||
|
await plugin.start_game_times_import(["4", "8"])
|
||||||
|
|
||||||
|
# wait until all tasks are finished
|
||||||
|
for _ in range(4):
|
||||||
|
await asyncio.sleep(0)
|
||||||
|
|
||||||
|
plugin.get_game_times.coro.assert_called_once_with()
|
||||||
|
game_time_import_success.assert_called_once_with(game_time)
|
||||||
|
game_time_import_failure.assert_called_once_with("5", UnknownError())
|
||||||
|
game_times_import_finished.assert_called_once_with()
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_start_game_times_import_failure(plugin, write, mocker):
|
||||||
|
game_time_import_failure = mocker.patch.object(plugin, "game_time_import_failure")
|
||||||
|
game_times_import_finished = mocker.patch.object(plugin, "game_times_import_finished")
|
||||||
|
|
||||||
|
game_ids = ["1", "5"]
|
||||||
|
error = BackendError()
|
||||||
|
plugin.get_game_times.coro.side_effect = error
|
||||||
|
|
||||||
|
await plugin.start_game_times_import(game_ids)
|
||||||
|
|
||||||
|
# wait until all tasks are finished
|
||||||
|
for _ in range(4):
|
||||||
|
await asyncio.sleep(0)
|
||||||
|
|
||||||
|
plugin.get_game_times.coro.assert_called_once_with()
|
||||||
|
|
||||||
|
assert game_time_import_failure.mock_calls == [call("1", error), call("5", error)]
|
||||||
|
game_times_import_finished.assert_called_once_with()
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ def test_get_capabilites(reader, writer, readline, write):
|
|||||||
"method": "get_capabilities"
|
"method": "get_capabilities"
|
||||||
}
|
}
|
||||||
token = "token"
|
token = "token"
|
||||||
plugin = PluginImpl(Platform.Generic, reader, writer, token)
|
plugin = PluginImpl(Platform.Generic, "0.1", reader, writer, token)
|
||||||
readline.side_effect = [json.dumps(request), ""]
|
readline.side_effect = [json.dumps(request), ""]
|
||||||
asyncio.run(plugin.run())
|
asyncio.run(plugin.run())
|
||||||
response = json.loads(write.call_args[0][0])
|
response = json.loads(write.call_args[0][0])
|
||||||
|
|||||||
@@ -16,9 +16,10 @@ def test_success(plugin, readline, write):
|
|||||||
|
|
||||||
readline.side_effect = [json.dumps(request), ""]
|
readline.side_effect = [json.dumps(request), ""]
|
||||||
|
|
||||||
plugin.get_local_games.return_value = [
|
plugin.get_local_games.coro.return_value = [
|
||||||
LocalGame("1", "Running"),
|
LocalGame("1", LocalGameState.Running),
|
||||||
LocalGame("2", "Installed")
|
LocalGame("2", LocalGameState.Installed),
|
||||||
|
LocalGame("3", LocalGameState.Installed | LocalGameState.Running)
|
||||||
]
|
]
|
||||||
asyncio.run(plugin.run())
|
asyncio.run(plugin.run())
|
||||||
plugin.get_local_games.assert_called_with()
|
plugin.get_local_games.assert_called_with()
|
||||||
@@ -31,11 +32,15 @@ def test_success(plugin, readline, write):
|
|||||||
"local_games" : [
|
"local_games" : [
|
||||||
{
|
{
|
||||||
"game_id": "1",
|
"game_id": "1",
|
||||||
"local_game_state": "Running"
|
"local_game_state": LocalGameState.Running.value
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"game_id": "2",
|
"game_id": "2",
|
||||||
"local_game_state": "Installed"
|
"local_game_state": LocalGameState.Installed.value
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"game_id": "3",
|
||||||
|
"local_game_state": (LocalGameState.Installed | LocalGameState.Running).value
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
@@ -56,7 +61,7 @@ def test_failure(plugin, readline, write, error, code, message):
|
|||||||
}
|
}
|
||||||
|
|
||||||
readline.side_effect = [json.dumps(request), ""]
|
readline.side_effect = [json.dumps(request), ""]
|
||||||
plugin.get_local_games.side_effect = error()
|
plugin.get_local_games.coro.side_effect = error()
|
||||||
asyncio.run(plugin.run())
|
asyncio.run(plugin.run())
|
||||||
plugin.get_local_games.assert_called_with()
|
plugin.get_local_games.assert_called_with()
|
||||||
response = json.loads(write.call_args[0][0])
|
response = json.loads(write.call_args[0][0])
|
||||||
@@ -85,7 +90,7 @@ def test_local_game_state_update(plugin, write):
|
|||||||
"params": {
|
"params": {
|
||||||
"local_game": {
|
"local_game": {
|
||||||
"game_id": "1",
|
"game_id": "1",
|
||||||
"local_game_state": "Running"
|
"local_game_state": LocalGameState.Running.value
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import asyncio
|
|||||||
import json
|
import json
|
||||||
|
|
||||||
from galaxy.api.types import Game, Dlc, LicenseInfo
|
from galaxy.api.types import Game, Dlc, LicenseInfo
|
||||||
|
from galaxy.api.consts import LicenseType
|
||||||
from galaxy.api.errors import UnknownError
|
from galaxy.api.errors import UnknownError
|
||||||
|
|
||||||
def test_success(plugin, readline, write):
|
def test_success(plugin, readline, write):
|
||||||
@@ -12,16 +13,16 @@ def test_success(plugin, readline, write):
|
|||||||
}
|
}
|
||||||
|
|
||||||
readline.side_effect = [json.dumps(request), ""]
|
readline.side_effect = [json.dumps(request), ""]
|
||||||
plugin.get_owned_games.return_value = [
|
plugin.get_owned_games.coro.return_value = [
|
||||||
Game("3", "Doom", None, LicenseInfo("SinglePurchase", None)),
|
Game("3", "Doom", None, LicenseInfo(LicenseType.SinglePurchase, None)),
|
||||||
Game(
|
Game(
|
||||||
"5",
|
"5",
|
||||||
"Witcher 3",
|
"Witcher 3",
|
||||||
[
|
[
|
||||||
Dlc("7", "Hearts of Stone", LicenseInfo("SinglePurchase", None)),
|
Dlc("7", "Hearts of Stone", LicenseInfo(LicenseType.SinglePurchase, None)),
|
||||||
Dlc("8", "Temerian Armor Set", LicenseInfo("FreeToPlay", None)),
|
Dlc("8", "Temerian Armor Set", LicenseInfo(LicenseType.FreeToPlay, None)),
|
||||||
],
|
],
|
||||||
LicenseInfo("SinglePurchase", None))
|
LicenseInfo(LicenseType.SinglePurchase, None))
|
||||||
]
|
]
|
||||||
asyncio.run(plugin.run())
|
asyncio.run(plugin.run())
|
||||||
plugin.get_owned_games.assert_called_with()
|
plugin.get_owned_games.assert_called_with()
|
||||||
@@ -74,7 +75,7 @@ def test_failure(plugin, readline, write):
|
|||||||
}
|
}
|
||||||
|
|
||||||
readline.side_effect = [json.dumps(request), ""]
|
readline.side_effect = [json.dumps(request), ""]
|
||||||
plugin.get_owned_games.side_effect = UnknownError()
|
plugin.get_owned_games.coro.side_effect = UnknownError()
|
||||||
asyncio.run(plugin.run())
|
asyncio.run(plugin.run())
|
||||||
plugin.get_owned_games.assert_called_with()
|
plugin.get_owned_games.assert_called_with()
|
||||||
response = json.loads(write.call_args[0][0])
|
response = json.loads(write.call_args[0][0])
|
||||||
@@ -89,7 +90,7 @@ def test_failure(plugin, readline, write):
|
|||||||
}
|
}
|
||||||
|
|
||||||
def test_add_game(plugin, write):
|
def test_add_game(plugin, write):
|
||||||
game = Game("3", "Doom", None, LicenseInfo("SinglePurchase", None))
|
game = Game("3", "Doom", None, LicenseInfo(LicenseType.SinglePurchase, None))
|
||||||
|
|
||||||
async def couritine():
|
async def couritine():
|
||||||
plugin.add_game(game)
|
plugin.add_game(game)
|
||||||
@@ -127,7 +128,7 @@ def test_remove_game(plugin, write):
|
|||||||
}
|
}
|
||||||
|
|
||||||
def test_update_game(plugin, write):
|
def test_update_game(plugin, write):
|
||||||
game = Game("3", "Doom", None, LicenseInfo("SinglePurchase", None))
|
game = Game("3", "Doom", None, LicenseInfo(LicenseType.SinglePurchase, None))
|
||||||
|
|
||||||
async def couritine():
|
async def couritine():
|
||||||
plugin.update_game(game)
|
plugin.update_game(game)
|
||||||
|
|||||||
@@ -5,153 +5,6 @@ from galaxy.api.types import UserInfo, Presence
|
|||||||
from galaxy.api.errors import UnknownError
|
from galaxy.api.errors import UnknownError
|
||||||
from galaxy.api.consts import PresenceState
|
from galaxy.api.consts import PresenceState
|
||||||
|
|
||||||
def test_get_friends_success(plugin, readline, write):
|
|
||||||
request = {
|
|
||||||
"jsonrpc": "2.0",
|
|
||||||
"id": "3",
|
|
||||||
"method": "import_friends"
|
|
||||||
}
|
|
||||||
|
|
||||||
readline.side_effect = [json.dumps(request), ""]
|
|
||||||
plugin.get_friends.return_value = [
|
|
||||||
UserInfo(
|
|
||||||
"3",
|
|
||||||
True,
|
|
||||||
"Jan",
|
|
||||||
"http://avatar1.png",
|
|
||||||
Presence(
|
|
||||||
PresenceState.Online,
|
|
||||||
"123",
|
|
||||||
"Main menu"
|
|
||||||
)
|
|
||||||
),
|
|
||||||
UserInfo(
|
|
||||||
"5",
|
|
||||||
True,
|
|
||||||
"Ola",
|
|
||||||
"http://avatar2.png",
|
|
||||||
Presence(PresenceState.Offline)
|
|
||||||
)
|
|
||||||
]
|
|
||||||
asyncio.run(plugin.run())
|
|
||||||
plugin.get_friends.assert_called_with()
|
|
||||||
response = json.loads(write.call_args[0][0])
|
|
||||||
|
|
||||||
assert response == {
|
|
||||||
"jsonrpc": "2.0",
|
|
||||||
"id": "3",
|
|
||||||
"result": {
|
|
||||||
"user_info_list": [
|
|
||||||
{
|
|
||||||
"user_id": "3",
|
|
||||||
"is_friend": True,
|
|
||||||
"user_name": "Jan",
|
|
||||||
"avatar_url": "http://avatar1.png",
|
|
||||||
"presence": {
|
|
||||||
"presence_state": "online",
|
|
||||||
"game_id": "123",
|
|
||||||
"presence_status": "Main menu"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"user_id": "5",
|
|
||||||
"is_friend": True,
|
|
||||||
"user_name": "Ola",
|
|
||||||
"avatar_url": "http://avatar2.png",
|
|
||||||
"presence": {
|
|
||||||
"presence_state": "offline"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
def test_get_friends_failure(plugin, readline, write):
|
|
||||||
request = {
|
|
||||||
"jsonrpc": "2.0",
|
|
||||||
"id": "3",
|
|
||||||
"method": "import_friends"
|
|
||||||
}
|
|
||||||
|
|
||||||
readline.side_effect = [json.dumps(request), ""]
|
|
||||||
plugin.get_friends.side_effect = UnknownError()
|
|
||||||
asyncio.run(plugin.run())
|
|
||||||
plugin.get_friends.assert_called_with()
|
|
||||||
response = json.loads(write.call_args[0][0])
|
|
||||||
|
|
||||||
assert response == {
|
|
||||||
"jsonrpc": "2.0",
|
|
||||||
"id": "3",
|
|
||||||
"error": {
|
|
||||||
"code": 0,
|
|
||||||
"message": "Unknown error",
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
def test_add_friend(plugin, write):
|
|
||||||
friend = UserInfo("7", True, "Kuba", "http://avatar.png", Presence(PresenceState.Offline))
|
|
||||||
|
|
||||||
async def couritine():
|
|
||||||
plugin.add_friend(friend)
|
|
||||||
|
|
||||||
asyncio.run(couritine())
|
|
||||||
response = json.loads(write.call_args[0][0])
|
|
||||||
|
|
||||||
assert response == {
|
|
||||||
"jsonrpc": "2.0",
|
|
||||||
"method": "friend_added",
|
|
||||||
"params": {
|
|
||||||
"user_info": {
|
|
||||||
"user_id": "7",
|
|
||||||
"is_friend": True,
|
|
||||||
"user_name": "Kuba",
|
|
||||||
"avatar_url": "http://avatar.png",
|
|
||||||
"presence": {
|
|
||||||
"presence_state": "offline"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
def test_remove_friend(plugin, write):
|
|
||||||
async def couritine():
|
|
||||||
plugin.remove_friend("5")
|
|
||||||
|
|
||||||
asyncio.run(couritine())
|
|
||||||
response = json.loads(write.call_args[0][0])
|
|
||||||
|
|
||||||
assert response == {
|
|
||||||
"jsonrpc": "2.0",
|
|
||||||
"method": "friend_removed",
|
|
||||||
"params": {
|
|
||||||
"user_id": "5"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
def test_update_friend(plugin, write):
|
|
||||||
friend = UserInfo("9", True, "Anna", "http://avatar.png", Presence(PresenceState.Offline))
|
|
||||||
|
|
||||||
async def couritine():
|
|
||||||
plugin.update_friend(friend)
|
|
||||||
|
|
||||||
asyncio.run(couritine())
|
|
||||||
response = json.loads(write.call_args[0][0])
|
|
||||||
|
|
||||||
assert response == {
|
|
||||||
"jsonrpc": "2.0",
|
|
||||||
"method": "friend_updated",
|
|
||||||
"params": {
|
|
||||||
"user_info": {
|
|
||||||
"user_id": "9",
|
|
||||||
"is_friend": True,
|
|
||||||
"user_name": "Anna",
|
|
||||||
"avatar_url": "http://avatar.png",
|
|
||||||
"presence": {
|
|
||||||
"presence_state": "offline"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
def test_get_users_success(plugin, readline, write):
|
def test_get_users_success(plugin, readline, write):
|
||||||
request = {
|
request = {
|
||||||
@@ -164,7 +17,7 @@ def test_get_users_success(plugin, readline, write):
|
|||||||
}
|
}
|
||||||
|
|
||||||
readline.side_effect = [json.dumps(request), ""]
|
readline.side_effect = [json.dumps(request), ""]
|
||||||
plugin.get_users.return_value = [
|
plugin.get_users.coro.return_value = [
|
||||||
UserInfo("5", False, "Ula", "http://avatar.png", Presence(PresenceState.Offline))
|
UserInfo("5", False, "Ula", "http://avatar.png", Presence(PresenceState.Offline))
|
||||||
]
|
]
|
||||||
asyncio.run(plugin.run())
|
asyncio.run(plugin.run())
|
||||||
@@ -189,6 +42,7 @@ def test_get_users_success(plugin, readline, write):
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
def test_get_users_failure(plugin, readline, write):
|
def test_get_users_failure(plugin, readline, write):
|
||||||
request = {
|
request = {
|
||||||
"jsonrpc": "2.0",
|
"jsonrpc": "2.0",
|
||||||
@@ -200,7 +54,7 @@ def test_get_users_failure(plugin, readline, write):
|
|||||||
}
|
}
|
||||||
|
|
||||||
readline.side_effect = [json.dumps(request), ""]
|
readline.side_effect = [json.dumps(request), ""]
|
||||||
plugin.get_users.side_effect = UnknownError()
|
plugin.get_users.coro.side_effect = UnknownError()
|
||||||
asyncio.run(plugin.run())
|
asyncio.run(plugin.run())
|
||||||
plugin.get_users.assert_called_with(user_id_list=["10", "11", "12"])
|
plugin.get_users.assert_called_with(user_id_list=["10", "11", "12"])
|
||||||
response = json.loads(write.call_args[0][0])
|
response = json.loads(write.call_args[0][0])
|
||||||
|
|||||||
Reference in New Issue
Block a user