Compare commits

..

4 Commits

Author SHA1 Message Date
Mieszko Banczerowski
5ca9254d2a GPI-438: Add link to docs in readme.md; fix make.py 2019-06-10 18:53:02 +02:00
Mieszko Banczerowski
79808e49f7 Bump version after adding documentation 2019-06-07 14:02:23 +02:00
Mateusz Silaczewski
7099cf3195 Merge branch 'documentation' of https://gitlab.gog.com/galaxy-client/galaxy-plugin-api into documentation 2019-06-06 15:56:01 +02:00
Rafal Makagon
ed91fd582c Deploy setup py 2019-05-31 12:08:49 +02:00
16 changed files with 777 additions and 83 deletions

7
.gitignore vendored
View File

@@ -1,2 +1,9 @@
# pytest
__pycache__/
.vscode/
.venv/
src/galaxy.plugin.api.egg-info/
docs/build/
Pipfile
.idea
docs/source/_build

View File

@@ -1,10 +1,30 @@
# GOG Galaxy - Community Integration - Python API
# GOG Galaxy Integrations Python API
This document is still work in progress.
This Python library allows to easily build community integrations for various gaming platforms with GOG Galaxy 2.0.
## Basic Usage
- refer to our <a href='https://galaxy-integrations-python-api.readthedocs.io'>documentation</a>
Basic implementation:
## Features
Each integration in GOG Galaxy 2.0 comes as a separate Python script, and is launched as a separate process, that which needs to communicate with main instance of GOG Galaxy 2.0.
The provided features are:
- multistep authorisation using a browser built into GOG Galaxy 2.0
- support for GOG Galaxy 2.0 features:
- importing owned and detecting installed games
- installing and launching games
- importing achievements and game time
- importing friends lists and statuses
- importing friends recomendations list
- receiving and sending chat messages
- cache storage
## Basic usage
Eeach integration should inherit from the :class:`~galaxy.api.plugin.Plugin` class. Supported methods like :meth:`~galaxy.api.plugin.Plugin.get_owned_games` should be overwritten - they are called from the GOG Galaxy client in the appropriate times.
Each of those method can raise exceptions inherited from the :exc:`~galaxy.api.jsonrpc.ApplicationError`.
Communication between an integration and the client is also possible with the use of notifications, for example: :meth:`~galaxy.api.plugin.Plugin.update_local_game_status`.
```python
import sys
@@ -33,7 +53,11 @@ if __name__ == "__main__":
main()
```
Plugin should be deployed with manifest:
## Deployment
The client has a built-in Python 3.7 interpreter, so the integrations are delivered as `.py` files.
The additional `manifest.json` file is required:
```json
{
"name": "Example plugin",
@@ -47,21 +71,6 @@ Plugin should be deployed with manifest:
"script": "plugin.py"
}
```
## Development
Install required packages:
```bash
pip install -r requirements.txt
```
Run tests:
```bash
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.
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.

16
docs/make.py Normal file
View File

@@ -0,0 +1,16 @@
"""Builds documentation locally. Use for preview only"""
import pathlib
import subprocess
import webbrowser
source = pathlib.Path("docs", "source")
build = pathlib.Path("docs", "build")
master_doc = 'index.html'
subprocess.run(['sphinx-build', '-M', 'clean', str(source), str(build)])
subprocess.run(['sphinx-build', '-M', 'html', str(source), str(build)])
master_path = build / 'html' / master_doc
webbrowser.open(f'file://{master_path.resolve()}')

4
docs/requirements.txt Normal file
View File

@@ -0,0 +1,4 @@
Sphinx==2.0.1
sphinx-rtd-theme==0.4.3
sphinx-autodoc-typehints==1.6.0
m2r==0.2.1

74
docs/source/conf.py Normal file
View File

@@ -0,0 +1,74 @@
# Configuration file for the Sphinx documentation builder.
# Documentation:
# http://www.sphinx-doc.org/en/master/config
import os
import sys
import subprocess
# -- Path setup --------------------------------------------------------------
_ROOT = os.path.join('..', '..')
sys.path.append(os.path.abspath(os.path.join(_ROOT, 'src')))
# -- Project information -----------------------------------------------------
project = 'GOG Galaxy Integrations API'
copyright = '2019, GOG.com'
_author, _version = subprocess.check_output(
['python', os.path.join(_ROOT, 'setup.py'), '--author', '--version'],
universal_newlines=True).strip().split('\n')
author = _author
version = _version
release = _version
# -- General configuration ---------------------------------------------------
# Add any Sphinx extension module names here, as strings. They can be
# extensions coming with Sphinx (named 'sphinx.ext.*') or your custom
# ones.
extensions = [
'sphinx.ext.autodoc',
'sphinx_autodoc_typehints',
'm2r' # mdinclude directive for makrdown files
]
autodoc_member_order = 'bysource'
autodoc_inherit_docstrings = False
autodoc_mock_imports = ["galaxy.http"]
set_type_checking_flag = True
# Add any paths that contain templates here, relative to this directory.
templates_path = ['_templates']
# List of patterns, relative to source directory, that match files and
# directories to ignore when looking for source files.
# This pattern also affects html_static_path and html_extra_path.
exclude_patterns = []
# -- Options for HTML output -------------------------------------------------
# The theme to use for HTML and HTML Help pages. See the documentation for
# a list of builtin themes.
#
html_theme = "sphinx_rtd_theme"
html_theme_options = {
# 'canonical_url': '', # main page to be serach in google with trailing slash
'display_version': True,
'style_external_links': True,
# Toc options
'collapse_navigation': False,
'sticky_navigation': True,
'navigation_depth': 4,
}
# Add any paths that contain custom static files (such as style sheets) here,
# relative to this directory. They are copied after the builtin static files,
# so a file named "default.css" will overwrite the builtin "default.css".
html_static_path = ['_static']
master_doc = 'index'

View File

@@ -0,0 +1,40 @@
galaxy.api
=================
plugin
------------------------
.. automodule:: galaxy.api.plugin
:members:
:undoc-members:
:exclude-members: JSONEncoder, features, achievements_import_finished, game_times_import_finished, start_achievements_import, start_game_times_import, get_game_times, get_unlocked_achievements
types
-----------------------
.. automodule:: galaxy.api.types
:members:
:undoc-members:
consts
------------------------
.. automodule:: galaxy.api.consts
:members:
:undoc-members:
:show-inheritance:
:exclude-members: Feature
errors
------------------------
.. autoexception:: galaxy.api.jsonrpc.ApplicationError
:show-inheritance:
.. autoexception:: galaxy.api.jsonrpc.UnknownError
:show-inheritance:
.. automodule:: galaxy.api.errors
:members:
:undoc-members:
:show-inheritance:

14
docs/source/index.rst Normal file
View File

@@ -0,0 +1,14 @@
GOG Galaxy Integrations Python API
=================================================
.. toctree::
:maxdepth: 2
:includehidden:
Overview <overview>
API <galaxy.api>
Index
-------------------
* :ref:`genindex`

7
docs/source/overview.rst Normal file
View File

@@ -0,0 +1,7 @@
.. mdinclude:: ../../README.md
:end-line: 4
.. excluding self-pointing documentation link
.. mdinclude:: ../../README.md
:start-line: 6

View File

@@ -17,7 +17,7 @@ def version_provider(_):
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"],
source_include_elements=["src", "docs", "tests", "requirements.txt", ".readthedocs.yml" ".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",

View File

@@ -5,4 +5,4 @@ pytest-mock==1.10.3
pytest-flakes==4.0.0
# because of pip bug https://github.com/pypa/pip/issues/4780
aiohttp==3.5.4
certifi==2019.3.9
certifi==2019.3.9

View File

@@ -2,8 +2,8 @@ from setuptools import setup, find_packages
setup(
name="galaxy.plugin.api",
version="0.31.2",
description="Galaxy python plugin API",
version="0.32.1",
description="GOG Galaxy Integrations Python API",
author='Galaxy team',
author_email='galaxy@gog.com',
packages=find_packages("src"),

12
src/.readthedocs.yml Normal file
View File

@@ -0,0 +1,12 @@
version: 2
sphinx:
configuration: docs/source/conf.py
formats: all
python:
version: 3.7
install:
- requirements: requirements.txt
- requirements: docs/requirements.txt

View File

@@ -1,6 +1,8 @@
from enum import Enum, Flag
class Platform(Enum):
"""Supported gaming platforms"""
Unknown = "unknown"
Gog = "gog"
Steam = "steam"
@@ -12,7 +14,11 @@ class Platform(Enum):
Battlenet = "battlenet"
Epic = "epic"
class Feature(Enum):
"""Possible features that can be implemented by an integration.
It does not have to support all or any specific features from the list.
"""
Unknown = "Unknown"
ImportInstalledGames = "ImportInstalledGames"
ImportOwnedGames = "ImportOwnedGames"
@@ -26,18 +32,27 @@ class Feature(Enum):
VerifyGame = "VerifyGame"
ImportFriends = "ImportFriends"
class LicenseType(Enum):
"""Possible game license types, understandable for the GOG Galaxy client."""
Unknown = "Unknown"
SinglePurchase = "SinglePurchase"
FreeToPlay = "FreeToPlay"
OtherUserLicense = "OtherUserLicense"
class LocalGameState(Flag):
"""Possible states that a local game can be in.
For example a game which is both installed and currently running should have its state set as a "bitwise or" of Running and Installed flags:
``local_game_state=<LocalGameState.Running|Installed: 3>``
"""
None_ = 0
Installed = 1
Running = 2
class PresenceState(Enum):
""""Possible states that a user can be in."""
Unknown = "Unknown"
Online = "online"
Offline = "offline"

View File

@@ -79,22 +79,24 @@ class Server():
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
:param sensitive_params: list of parameters that are anonymized before logging; \
if False - no params are considered sensitive, if True - all params are considered sensitive
"""
self._methods[name] = Method(callback, inspect.signature(callback), internal, sensitive_params)
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
:param sensitive_params: list of parameters that are anonymized before logging; \
if False - no params are considered sensitive, if True - all params are considered sensitive
"""
self._notifications[name] = Method(callback, inspect.signature(callback), internal, sensitive_params)
@@ -187,7 +189,7 @@ class Server():
self._send_error(request.id, MethodNotFound())
except JsonRpcError as error:
self._send_error(request.id, error)
except Exception as e: #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)))
@@ -256,10 +258,11 @@ class NotificationClient():
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
:param sensitive_params: list of parameters that are anonymized before logging; \
if False - no params are considered sensitive, if True - all params are considered sensitive
"""
notification = {
"jsonrpc": "2.0",

View File

@@ -7,7 +7,11 @@ from enum import Enum
from collections import OrderedDict
import sys
from galaxy.api.jsonrpc import Server, NotificationClient
from typing import List, Dict
from galaxy.api.types import Achievement, Game, LocalGame, FriendInfo, GameTime, UserInfo, Room
from galaxy.api.jsonrpc import Server, NotificationClient, ApplicationError
from galaxy.api.consts import Feature
from galaxy.api.errors import UnknownError, ImportInProgress
@@ -23,6 +27,7 @@ class JSONEncoder(json.JSONEncoder):
return super().default(o)
class Plugin():
"""Use and override methods of this class to create a new platform integration."""
def __init__(self, platform, version, reader, writer, handshake_token):
logging.info("Creating plugin for platform %s, version %s", platform.value, version)
self._platform = platform
@@ -187,7 +192,7 @@ class Plugin():
self._feature_methods.setdefault(feature, []).append(handler)
async def run(self):
"""Plugin main coorutine"""
"""Plugin main coroutine."""
async def pass_control():
while self._active:
try:
@@ -199,7 +204,7 @@ class Plugin():
await asyncio.gather(pass_control(), self._server.run())
def _shutdown(self):
logging.info("Shuting down")
logging.info("Shutting down")
self._server.stop()
self._active = False
self.shutdown()
@@ -216,39 +221,115 @@ class Plugin():
pass
# notifications
def store_credentials(self, credentials):
"""Notify client to store plugin credentials.
They will be pass to next authencicate calls.
"""
def store_credentials(self, credentials: dict):
"""Notify the client to store authentication credentials.
Credentials are passed on the next authenticate call.
:param credentials: credentials that client will store; they are stored locally on a user pc
Example use case of store_credentials:
.. code-block:: python
:linenos:
async def pass_login_credentials(self, step, credentials, cookies):
if self.got_everything(credentials,cookies):
user_data = await self.parse_credentials(credentials,cookies)
else:
next_params = self.get_next_params(credentials,cookies)
next_cookies = self.get_next_cookies(credentials,cookies)
return NextStep("web_session", next_params, cookies=next_cookies)
self.store_credentials(user_data['credentials'])
return Authentication(user_data['userId'], user_data['username'])
"""
self._notification_client.notify("store_credentials", credentials, sensitive_params=True)
def add_game(self, game):
def add_game(self, game: Game):
"""Notify the client to add game to the list of owned games
of the currently authenticated user.
:param game: Game to add to the list of owned games
Example use case of add_game:
.. code-block:: python
:linenos:
async def check_for_new_games(self):
games = await self.get_owned_games()
for game in games:
if game not in self.owned_games_cache:
self.owned_games_cache.append(game)
self.add_game(game)
"""
params = {"owned_game" : game}
self._notification_client.notify("owned_game_added", params)
def remove_game(self, game_id):
def remove_game(self, game_id: str):
"""Notify the client to remove game from the list of owned games
of the currently authenticated user.
:param game_id: game id of the game to remove from the list of owned games
Example use case of remove_game:
.. code-block:: python
:linenos:
async def check_for_removed_games(self):
games = await self.get_owned_games()
for game in self.owned_games_cache:
if game not in games:
self.owned_games_cache.remove(game)
self.remove_game(game.game_id)
"""
params = {"game_id" : game_id}
self._notification_client.notify("owned_game_removed", params)
def update_game(self, game):
def update_game(self, game: Game):
"""Notify the client to update the status of a game
owned by the currently authenticated user.
:param game: Game to update
"""
params = {"owned_game" : game}
self._notification_client.notify("owned_game_updated", params)
def unlock_achievement(self, game_id, achievement):
def unlock_achievement(self, game_id: str, achievement: Achievement):
"""Notify the client to unlock an achievement for a specific game.
:param game_id: game_id of the game for which to unlock an achievement.
:param achievement: achievement to unlock.
"""
params = {
"game_id": game_id,
"achievement": achievement
}
self._notification_client.notify("achievement_unlocked", params)
def game_achievements_import_success(self, game_id, achievements):
def game_achievements_import_success(self, game_id: str, achievements):
"""Notify the client that import of achievements for a given game has succeeded.
This method is called by import_games_achievements.
:param game_id: id of the game for which the achievements were imported
:param achievements: list of imported 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):
def game_achievements_import_failure(self, game_id: str, error: ApplicationError):
"""Notify the client that import of achievements for a given game has failed.
This method is called by import_games_achievements.
:param game_id: id of the game for which the achievements import failed
:param error: error which prevented the achievements import
"""
params = {
"game_id": game_id,
"error": {
@@ -259,21 +340,60 @@ class Plugin():
self._notification_client.notify("game_achievements_import_failure", params)
def achievements_import_finished(self):
"""Notify the client that importing achievements has finished.
This method is called by import_games_achievements_task"""
self._notification_client.notify("achievements_import_finished", None)
def update_local_game_status(self, local_game):
def update_local_game_status(self, local_game: LocalGame):
"""Notify the client to update the status of a local game.
:param local_game: the LocalGame to update
Example use case triggered by the :meth:`.tick` method:
.. code-block:: python
:linenos:
:emphasize-lines: 5
async def _check_statuses(self):
for game in await self._get_local_games():
if game.status == self._cached_game_statuses.get(game.id):
continue
self.update_local_game_status(LocalGame(game.id, game.status))
self._cached_games_statuses[game.id] = game.status
asyncio.sleep(5) # interval
def tick(self):
if self._check_statuses_task is None or self._check_statuses_task.done():
self._check_statuses_task = asyncio.create_task(self._check_statuses())
"""
params = {"local_game" : local_game}
self._notification_client.notify("local_game_status_changed", params)
def add_friend(self, user):
def add_friend(self, user: FriendInfo):
"""Notify the client to add a user to friends list of the currently authenticated user.
:param user: FriendInfo of a user that the client will add to friends list
"""
params = {"friend_info" : user}
self._notification_client.notify("friend_added", params)
def remove_friend(self, user_id):
def remove_friend(self, user_id: str):
"""Notify the client to remove a user from friends list of the currently authenticated user.
:param user_id: id of the user to remove from friends list
"""
params = {"user_id" : user_id}
self._notification_client.notify("friend_removed", params)
def update_room(self, room_id, unread_message_count=None, new_messages=None):
def update_room(self, room_id: str, unread_message_count=None, new_messages=None):
"""WIP, Notify the client to update the information regarding
a chat room that the currently authenticated user is in.
:param room_id: id of the room to update
:param unread_message_count: information about the new unread message count in the room
:param new_messages: list of new messages that the user received
"""
params = {"room_id": room_id}
if unread_message_count is not None:
params["unread_message_count"] = unread_message_count
@@ -281,15 +401,30 @@ class Plugin():
params["messages"] = new_messages
self._notification_client.notify("chat_room_updated", params)
def update_game_time(self, game_time):
def update_game_time(self, game_time: GameTime):
"""Notify the client to update game time for a game.
:param game_time: game time to update
"""
params = {"game_time" : game_time}
self._notification_client.notify("game_time_updated", params)
def game_time_import_success(self, game_time):
def game_time_import_success(self, game_time: GameTime):
"""Notify the client that import of a given game_time has succeeded.
This method is called by import_game_times.
:param game_time: game_time which was imported
"""
params = {"game_time" : game_time}
self._notification_client.notify("game_time_import_success", params)
def game_time_import_failure(self, game_id, error):
def game_time_import_failure(self, game_id: str, error: ApplicationError):
"""Notify the client that import of a game time for a given game has failed.
This method is called by import_game_times.
:param game_id: id of the game for which the game time could not be imported
:param error: error which prevented the game time import
"""
params = {
"game_id": game_id,
"error": {
@@ -300,42 +435,133 @@ class Plugin():
self._notification_client.notify("game_time_import_failure", params)
def game_times_import_finished(self):
"""Notify the client that importing game times has finished.
This method is called by :meth:`~.import_game_times_task`.
"""
self._notification_client.notify("game_times_import_finished", None)
def lost_authentication(self):
"""Notify the client that integration has lost authentication for the
current user and is unable to perform actions which would require it.
"""
self._notification_client.notify("authentication_lost", None)
# handlers
def tick(self):
"""This method is called periodicaly.
Override it to implement periodical tasks like refreshing cache.
This method should not be blocking - any longer actions should be
handled by asycio tasks.
"""This method is called periodically.
Override it to implement periodical non-blocking tasks.
This method is called internally.
Example of possible override of the method:
.. code-block:: python
:linenos:
def tick(self):
if not self.checking_for_new_games:
asyncio.create_task(self.check_for_new_games())
if not self.checking_for_removed_games:
asyncio.create_task(self.check_for_removed_games())
if not self.updating_game_statuses:
asyncio.create_task(self.update_game_statuses())
"""
def shutdown(self):
"""This method is called on plugin shutdown.
"""This method is called on integration shutdown.
Override it to implement tear down.
"""
This method is called by the GOG Galaxy client."""
# methods
async def authenticate(self, stored_credentials=None):
"""Overide this method to handle plugin authentication.
The method should return galaxy.api.types.Authentication
or raise galaxy.api.types.LoginError on authentication failure.
async def authenticate(self, stored_credentials:dict=None):
"""Override this method to handle user authentication.
This method should either return :class:`~galaxy.api.types.Authentication` if the authentication is finished
or :class:`~galaxy.api.types.NextStep` if it requires going to another url.
This method is called by the GOG Galaxy client.
:param stored_credentials: If the client received any credentials to store locally
in the previous session they will be passed here as a parameter.
Example of possible override of the method:
.. code-block:: python
:linenos:
async def authenticate(self, stored_credentials=None):
if not stored_credentials:
return NextStep("web_session", PARAMS, cookies=COOKIES)
else:
try:
user_data = self._authenticate(stored_credentials)
except AccessDenied:
raise InvalidCredentials()
return Authentication(user_data['userId'], user_data['username'])
"""
raise NotImplementedError()
async def pass_login_credentials(self, step, credentials, cookies):
async def pass_login_credentials(self, step: str, credentials: Dict[str, str], cookies: List[Dict[str, str]]):
"""This method is called if we return galaxy.api.types.NextStep from authenticate or from pass_login_credentials.
This method's parameters provide the data extracted from the web page navigation that previous NextStep finished on.
This method should either return galaxy.api.types.Authentication if the authentication is finished
or galaxy.api.types.NextStep if it requires going to another cef url.
This method is called by the GOG Galaxy client.
:param step: deprecated.
:param credentials: end_uri previous NextStep finished on.
:param cookies: cookies extracted from the end_uri site.
Example of possible override of the method:
.. code-block:: python
:linenos:
async def pass_login_credentials(self, step, credentials, cookies):
if self.got_everything(credentials,cookies):
user_data = await self.parse_credentials(credentials,cookies)
else:
next_params = self.get_next_params(credentials,cookies)
next_cookies = self.get_next_cookies(credentials,cookies)
return NextStep("web_session", next_params, cookies=next_cookies)
self.store_credentials(user_data['credentials'])
return Authentication(user_data['userId'], user_data['username'])
"""
raise NotImplementedError()
async def get_owned_games(self):
async def get_owned_games(self) -> List[Game]:
"""Override this method to return owned games for currenly logged in user.
This method is called by the GOG Galaxy client.
Example of possible override of the method:
.. code-block:: python
:linenos:
async def get_owned_games(self):
if not self.authenticated():
raise AuthenticationRequired()
games = self.retrieve_owned_games()
return games
"""
raise NotImplementedError()
async def get_unlocked_achievements(self, game_id):
async def get_unlocked_achievements(self, game_id: str) -> List[Achievement]:
"""
.. deprecated:: 0.33
Use :meth:`~.import_games_achievements`.
"""
raise NotImplementedError()
async def start_achievements_import(self, game_ids):
async def start_achievements_import(self, game_ids: List[str]):
"""Starts the task of importing achievements.
This method is called by the GOG Galaxy client.
:param game_ids: ids of the games for which the achievements are imported
"""
if self._achievements_import_in_progress:
raise ImportInProgress()
@@ -349,8 +575,15 @@ class Plugin():
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_games_achievements(self, game_ids: List[str]):
"""
Override this method to return the unlocked achievements
of the user that is currently logged in to the plugin.
Call game_achievements_import_success/game_achievements_import_failure for each game_id on the list.
This method is called by the GOG Galaxy client.
:param game_id: ids of the games for which to import unlocked achievements
"""
async def import_game_achievements(game_id):
try:
achievements = await self.get_unlocked_achievements(game_id)
@@ -361,43 +594,165 @@ class Plugin():
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) -> List[LocalGame]:
"""Override this method to return the list of
games present locally on the users pc.
This method is called by the GOG Galaxy client.
Example of possible override of the method:
.. code-block:: python
:linenos:
async def get_local_games(self):
local_games = []
for game in self.games_present_on_user_pc:
local_game = LocalGame()
local_game.game_id = game.id
local_game.local_game_state = game.get_installation_status()
local_games.append(local_game)
return local_games
"""
raise NotImplementedError()
async def launch_game(self, game_id):
async def launch_game(self, game_id: str):
"""Override this method to launch the game
identified by the provided game_id.
This method is called by the GOG Galaxy client.
:param str game_id: id of the game to launch
Example of possible override of the method:
.. code-block:: python
:linenos:
async def launch_game(self, game_id):
await self.open_uri(f"start client://launchgame/{game_id}")
"""
raise NotImplementedError()
async def install_game(self, game_id):
async def install_game(self, game_id: str):
"""Override this method to install the game
identified by the provided game_id.
This method is called by the GOG Galaxy client.
:param str game_id: id of the game to install
Example of possible override of the method:
.. code-block:: python
:linenos:
async def install_game(self, game_id):
await self.open_uri(f"start client://installgame/{game_id}")
"""
raise NotImplementedError()
async def uninstall_game(self, game_id):
async def uninstall_game(self, game_id: str):
"""Override this method to uninstall the game
identified by the provided game_id.
This method is called by the GOG Galaxy client.
:param str game_id: id of the game to uninstall
Example of possible override of the method:
.. code-block:: python
:linenos:
async def uninstall_game(self, game_id):
await self.open_uri(f"start client://uninstallgame/{game_id}")
"""
raise NotImplementedError()
async def get_friends(self):
async def get_friends(self) -> List[FriendInfo]:
"""Override this method to return the friends list
of the currently authenticated user.
This method is called by the GOG Galaxy client.
Example of possible override of the method:
.. code-block:: python
:linenos:
async def get_friends(self):
if not self._http_client.is_authenticated():
raise AuthenticationRequired()
friends = self.retrieve_friends()
return friends
"""
raise NotImplementedError()
async def get_users(self, user_id_list):
async def get_users(self, user_id_list: List[str]) -> List[UserInfo]:
"""WIP, Override this method to return the list of users matching the provided ids.
This method is called by the GOG Galaxy client.
:param user_id_list: list of user ids
"""
raise NotImplementedError()
async def send_message(self, room_id, message_text):
async def send_message(self, room_id: str, message_text: str):
"""WIP, Override this method to send message to a chat room.
This method is called by the GOG Galaxy client.
:param room_id: id of the room to which the message should be sent
:param message_text: text which should be sent in the message
"""
raise NotImplementedError()
async def mark_as_read(self, room_id, last_message_id):
async def mark_as_read(self, room_id: str, last_message_id: str):
"""WIP, Override this method to mark messages in a chat room as read up to the id provided in the parameter.
This method is called by the GOG Galaxy client.
:param room_id: id of the room
:param last_message_id: id of the last message; room is marked as read only if this id matches the last message id known to the client
"""
raise NotImplementedError()
async def get_rooms(self):
async def get_rooms(self) -> List[Room]:
"""WIP, Override this method to return the chat rooms in which the user is currently in.
This method is called by the GOG Galaxy client
"""
raise NotImplementedError()
async def get_room_history_from_message(self, room_id, message_id):
async def get_room_history_from_message(self, room_id: str, message_id: str):
"""WIP, Override this method to return the chat room history since the message provided in parameter.
This method is called by the GOG Galaxy client.
:param room_id: id of the room
:param message_id: id of the message since which the history should be retrieved
"""
raise NotImplementedError()
async def get_room_history_from_timestamp(self, room_id, from_timestamp):
async def get_room_history_from_timestamp(self, room_id: str, from_timestamp: int):
"""WIP, Override this method to return the chat room history since the timestamp provided in parameter.
This method is called by the GOG Galaxy client.
:param room_id: id of the room
:param from_timestamp: timestamp since which the history should be retrieved
"""
raise NotImplementedError()
async def get_game_times(self):
async def get_game_times(self) -> List[GameTime]:
"""
.. deprecated:: 0.33
Use :meth:`~.import_game_times`.
"""
raise NotImplementedError()
async def start_game_times_import(self, game_ids):
async def start_game_times_import(self, game_ids: List[str]):
"""Starts the task of importing game times
This method is called by the GOG Galaxy client.
:param game_ids: ids of the games for which the game time is imported
"""
if self._game_times_import_in_progress:
raise ImportInProgress()
@@ -411,8 +766,15 @@ class Plugin():
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"""
async def import_game_times(self, game_ids: List[str]):
"""
Override this method to return game times for
games owned by the currently authenticated user.
Call game_time_import_success/game_time_import_failure for each game_id on the list.
This method is called by GOG Galaxy Client.
:param game_ids: ids of the games for which the game time is imported
"""
try:
game_times = await self.get_game_times()
game_ids_set = set(game_ids)
@@ -427,7 +789,24 @@ class Plugin():
for game_id in game_ids:
self.game_time_import_failure(game_id, error)
def create_and_run_plugin(plugin_class, argv):
"""Call this method as an entry point for the implemented integration.
:param plugin_class: your plugin class.
:param argv: command line arguments with which the script was started.
Example of possible use of the method:
.. code-block:: python
:linenos:
def main():
create_and_run_plugin(PlatformPlugin, sys.argv)
if __name__ == "__main__":
main()
"""
if len(argv) < 3:
logging.critical("Not enough parameters, required: token, port")
sys.exit(1)

View File

@@ -5,11 +5,24 @@ from galaxy.api.consts import LicenseType, LocalGameState, PresenceState
@dataclass
class Authentication():
"""Return this from :meth:`.authenticate` or :meth:`.pass_login_credentials`
to inform the client that authentication has successfully finished.
:param user_id: id of the authenticated user
:param user_name: username of the authenticated user
"""
user_id: str
user_name: str
@dataclass
class Cookie():
"""Cookie
:param name: name of the cookie
:param value: value of the cookie
:param domain: optional domain of the cookie
:param path: optional path of the cookie
"""
name: str
value: str
domain: Optional[str] = None
@@ -17,6 +30,39 @@ class Cookie():
@dataclass
class NextStep():
"""Return this from :meth:`.authenticate` or :meth:`.pass_login_credentials` to open client built-in browser with given url.
For example:
.. code-block:: python
:linenos:
PARAMS = {
"window_title": "Login to platform",
"window_width": 800,
"window_height": 600,
"start_uri": URL,
"end_uri_regex": r"^https://platform_website\.com/.*"
}
JS = {r"^https://platform_website\.com/.*": [
r'''
location.reload();
'''
]}
COOKIES = [Cookie("Cookie1", "ok", ".platform.com"),
Cookie("Cookie2", "ok", ".platform.com")
]
async def authenticate(self, stored_credentials=None):
if not stored_credentials:
return NextStep("web_session", PARAMS, cookies=COOKIES, js=JS)
:param auth_params: configuration options: {"window_title": :class:`str`, "window_width": :class:`str`, "window_height": :class:`int`, "start_uri": :class:`int`, "end_uri_regex": :class:`str`}
:param cookies: browser initial set of cookies
:param js: a map of the url regex patterns into the list of *js* scripts that should be executed on every document at given step of internal browser authentication.
"""
next_step: str
auth_params: Dict[str, str]
cookies: Optional[List[Cookie]] = None
@@ -24,17 +70,35 @@ class NextStep():
@dataclass
class LicenseInfo():
"""Information about the license of related product.
:param license_type: type of license
:param owner: optional owner of the related product, defaults to currently authenticated user
"""
license_type: LicenseType
owner: Optional[str] = None
@dataclass
class Dlc():
"""Downloadable content object.
:param dlc_id: id of the dlc
:param dlc_title: title of the dlc
:param license_info: information about the license attached to the dlc
"""
dlc_id: str
dlc_title: str
license_info: LicenseInfo
@dataclass
class Game():
"""Game object.
:param game_id: unique identifier of the game, this will be passed as parameter for methods such as launch_game
:param game_title: title of the game
:param dlcs: list of dlcs available for the game
:param license_info: information about the license attached to the game
"""
game_id: str
game_title: str
dlcs: Optional[List[Dlc]]
@@ -42,6 +106,12 @@ class Game():
@dataclass
class Achievement():
"""Achievement, has to be initialized with either id or name.
:param unlock_time: unlock time of the achievement
:param achievement_id: optional id of the achievement
:param achievement_name: optional name of the achievement
"""
unlock_time: int
achievement_id: Optional[str] = None
achievement_name: Optional[str] = None
@@ -52,17 +122,36 @@ class Achievement():
@dataclass
class LocalGame():
"""Game locally present on the authenticated user's computer.
:param game_id: id of the game
:param local_game_state: state of the game
"""
game_id: str
local_game_state: LocalGameState
@dataclass
class Presence():
"""Information about a presence of a user.
:param presence_state: the state in which the user's presence is
:param game_id: id of the game which the user is currently playing
:param presence_status: optional attached string with the detailed description of the user's presence
"""
presence_state: PresenceState
game_id: Optional[str] = None
presence_status: Optional[str] = None
@dataclass
class UserInfo():
"""Detailed information about a user.
:param user_id: of the user
:param is_friend: whether the user is a friend of the currently authenticated user
:param user_name: of the user
:param avatar_url: to the avatar of the user
:param presence: about the users presence
"""
user_id: str
is_friend: bool
user_name: str
@@ -71,17 +160,35 @@ class UserInfo():
@dataclass
class FriendInfo():
"""Information about a friend of the currently authenticated user.
:param user_id: id of the user
:param user_name: username of the user
"""
user_id: str
user_name: str
@dataclass
class Room():
"""WIP, Chatroom.
:param room_id: id of the room
:param unread_message_count: number of unread messages in the room
:param last_message_id: id of the last message in the room
"""
room_id: str
unread_message_count: int
last_message_id: str
@dataclass
class Message():
"""WIP, A chatroom message.
:param message_id: id of the message
:param sender_id: id of the sender of the message
:param sent_time: time at which the message was sent
:param message_text: text attached to the message
"""
message_id: str
sender_id: str
sent_time: int
@@ -89,6 +196,13 @@ class Message():
@dataclass
class GameTime():
"""Game time of a game, defines the total time spent in the game
and the last time the game was played.
:param game_id: id of the related game
:param time_played: the total time spent in the game in **minutes**
:param last_time_played: last time the game was played (**unix timestamp**)
"""
game_id: str
time_played: int
last_played_time: int