mirror of
https://github.com/gogcom/galaxy-integrations-python-api.git
synced 2026-01-01 03:18:25 -05:00
Compare commits
7 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
49ae2beab9 | ||
|
|
c9e190772c | ||
|
|
789415d31b | ||
|
|
223adf6a38 | ||
|
|
bfb63a42bd | ||
|
|
53b3062719 | ||
|
|
49eb10ac8a |
21
LICENSE
Normal file
21
LICENSE
Normal file
@@ -0,0 +1,21 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2019 GOG sp. z o.o.
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
@@ -5,4 +5,5 @@ 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
|
||||
psutil==5.6.3; sys_platform == 'darwin'
|
||||
|
||||
2
setup.py
2
setup.py
@@ -2,7 +2,7 @@ from setuptools import setup, find_packages
|
||||
|
||||
setup(
|
||||
name="galaxy.plugin.api",
|
||||
version="0.42",
|
||||
version="0.44",
|
||||
description="GOG Galaxy Integrations Python API",
|
||||
author='Galaxy team',
|
||||
author_email='galaxy@gog.com',
|
||||
|
||||
@@ -117,11 +117,3 @@ class LocalGameState(Flag):
|
||||
None_ = 0
|
||||
Installed = 1
|
||||
Running = 2
|
||||
|
||||
|
||||
class PresenceState(Enum):
|
||||
""""Possible states that a user can be in."""
|
||||
Unknown = "Unknown"
|
||||
Online = "online"
|
||||
Offline = "offline"
|
||||
Away = "away"
|
||||
|
||||
@@ -1,20 +1,18 @@
|
||||
import asyncio
|
||||
import dataclasses
|
||||
import json
|
||||
import logging
|
||||
import logging.handlers
|
||||
import dataclasses
|
||||
from enum import Enum
|
||||
from collections import OrderedDict
|
||||
import sys
|
||||
from collections import OrderedDict
|
||||
from enum import Enum
|
||||
from itertools import count
|
||||
from typing import Any, Dict, List, Optional, Set, Union
|
||||
|
||||
from typing import Any, List, Dict, Optional, Union
|
||||
|
||||
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
|
||||
from galaxy.api.types import Authentication, NextStep, Message
|
||||
from galaxy.api.errors import ImportInProgress, UnknownError
|
||||
from galaxy.api.jsonrpc import ApplicationError, NotificationClient, Server
|
||||
from galaxy.api.types import Achievement, Authentication, FriendInfo, Game, GameTime, LocalGame, NextStep
|
||||
|
||||
|
||||
class JSONEncoder(json.JSONEncoder):
|
||||
@@ -23,6 +21,7 @@ class JSONEncoder(json.JSONEncoder):
|
||||
# filter None values
|
||||
def dict_factory(elements):
|
||||
return {k: v for k, v in elements if v is not None}
|
||||
|
||||
return dataclasses.asdict(o, dict_factory=dict_factory)
|
||||
if isinstance(o, Enum):
|
||||
return o.value
|
||||
@@ -31,12 +30,13 @@ class JSONEncoder(json.JSONEncoder):
|
||||
|
||||
class Plugin:
|
||||
"""Use and override methods of this class to create a new platform integration."""
|
||||
|
||||
def __init__(self, platform, version, reader, writer, handshake_token):
|
||||
logging.info("Creating plugin for platform %s, version %s", platform.value, version)
|
||||
self._platform = platform
|
||||
self._version = version
|
||||
|
||||
self._feature_methods = OrderedDict()
|
||||
self._features: Set[Feature] = set()
|
||||
self._active = True
|
||||
self._pass_control_task = None
|
||||
|
||||
@@ -49,6 +49,7 @@ class Plugin:
|
||||
|
||||
def eof_handler():
|
||||
self._shutdown()
|
||||
|
||||
self._server.register_eof(eof_handler)
|
||||
|
||||
self._achievements_import_in_progress = False
|
||||
@@ -56,6 +57,9 @@ class Plugin:
|
||||
|
||||
self._persistent_cache = dict()
|
||||
|
||||
self._tasks = OrderedDict()
|
||||
self._task_counter = count()
|
||||
|
||||
# internal
|
||||
self._register_method("shutdown", self._shutdown, internal=True)
|
||||
self._register_method("get_capabilities", self._get_capabilities, internal=True)
|
||||
@@ -81,97 +85,47 @@ class Plugin:
|
||||
self._register_method(
|
||||
"import_owned_games",
|
||||
self.get_owned_games,
|
||||
result_name="owned_games",
|
||||
feature=Feature.ImportOwnedGames
|
||||
result_name="owned_games"
|
||||
)
|
||||
self._detect_feature(Feature.ImportOwnedGames, ["get_owned_games"])
|
||||
|
||||
self._register_method(
|
||||
"import_unlocked_achievements",
|
||||
self.get_unlocked_achievements,
|
||||
result_name="unlocked_achievements",
|
||||
feature=Feature.ImportAchievements
|
||||
)
|
||||
self._register_method(
|
||||
"start_achievements_import",
|
||||
self.start_achievements_import,
|
||||
)
|
||||
self._register_method(
|
||||
"import_local_games",
|
||||
self.get_local_games,
|
||||
result_name="local_games",
|
||||
feature=Feature.ImportInstalledGames
|
||||
)
|
||||
self._register_notification("launch_game", self.launch_game, feature=Feature.LaunchGame)
|
||||
self._register_notification("install_game", self.install_game, feature=Feature.InstallGame)
|
||||
self._register_notification(
|
||||
"uninstall_game",
|
||||
self.uninstall_game,
|
||||
feature=Feature.UninstallGame
|
||||
)
|
||||
self._register_notification(
|
||||
"shutdown_platform_client",
|
||||
self.shutdown_platform_client,
|
||||
feature=Feature.ShutdownPlatformClient
|
||||
)
|
||||
self._register_method(
|
||||
"import_friends",
|
||||
self.get_friends,
|
||||
result_name="friend_info_list",
|
||||
feature=Feature.ImportFriends
|
||||
)
|
||||
self._register_method(
|
||||
"import_user_infos",
|
||||
self.get_users,
|
||||
result_name="user_info_list",
|
||||
feature=Feature.ImportUsers
|
||||
)
|
||||
self._register_method(
|
||||
"send_message",
|
||||
self.send_message,
|
||||
feature=Feature.Chat
|
||||
)
|
||||
self._register_method(
|
||||
"mark_as_read",
|
||||
self.mark_as_read,
|
||||
feature=Feature.Chat
|
||||
)
|
||||
self._register_method(
|
||||
"import_rooms",
|
||||
self.get_rooms,
|
||||
result_name="rooms",
|
||||
feature=Feature.Chat
|
||||
)
|
||||
self._register_method(
|
||||
"import_room_history_from_message",
|
||||
self.get_room_history_from_message,
|
||||
result_name="messages",
|
||||
feature=Feature.Chat
|
||||
)
|
||||
self._register_method(
|
||||
"import_room_history_from_timestamp",
|
||||
self.get_room_history_from_timestamp,
|
||||
result_name="messages",
|
||||
feature=Feature.Chat
|
||||
)
|
||||
self._register_method(
|
||||
"import_game_times",
|
||||
self.get_game_times,
|
||||
result_name="game_times",
|
||||
feature=Feature.ImportGameTime
|
||||
)
|
||||
self._register_method(
|
||||
"start_game_times_import",
|
||||
self.start_game_times_import,
|
||||
result_name="unlocked_achievements"
|
||||
)
|
||||
self._detect_feature(Feature.ImportAchievements, ["get_unlocked_achievements"])
|
||||
|
||||
self._register_method("start_achievements_import", self.start_achievements_import)
|
||||
self._detect_feature(Feature.ImportAchievements, ["import_games_achievements"])
|
||||
|
||||
self._register_method("import_local_games", self.get_local_games, result_name="local_games")
|
||||
self._detect_feature(Feature.ImportInstalledGames, ["get_local_games"])
|
||||
|
||||
self._register_notification("launch_game", self.launch_game)
|
||||
self._detect_feature(Feature.LaunchGame, ["launch_game"])
|
||||
|
||||
self._register_notification("install_game", self.install_game)
|
||||
self._detect_feature(Feature.InstallGame, ["install_game"])
|
||||
|
||||
self._register_notification("uninstall_game", self.uninstall_game)
|
||||
self._detect_feature(Feature.UninstallGame, ["uninstall_game"])
|
||||
|
||||
self._register_notification("shutdown_platform_client", self.shutdown_platform_client)
|
||||
self._detect_feature(Feature.ShutdownPlatformClient, ["shutdown_platform_client"])
|
||||
|
||||
self._register_method("import_friends", self.get_friends, result_name="friend_info_list")
|
||||
self._detect_feature(Feature.ImportFriends, ["get_friends"])
|
||||
|
||||
self._register_method("import_game_times", self.get_game_times, result_name="game_times")
|
||||
self._detect_feature(Feature.ImportGameTime, ["get_game_times"])
|
||||
|
||||
self._register_method("start_game_times_import", self.start_game_times_import)
|
||||
self._detect_feature(Feature.ImportGameTime, ["import_game_times"])
|
||||
|
||||
@property
|
||||
def features(self):
|
||||
features = []
|
||||
if self.__class__ != Plugin:
|
||||
for feature, handlers in self._feature_methods.items():
|
||||
if self._implements(handlers):
|
||||
features.append(feature)
|
||||
|
||||
return features
|
||||
def features(self) -> List[Feature]:
|
||||
return list(self._features)
|
||||
|
||||
@property
|
||||
def persistent_cache(self) -> Dict:
|
||||
@@ -179,13 +133,17 @@ class Plugin:
|
||||
"""
|
||||
return self._persistent_cache
|
||||
|
||||
def _implements(self, handlers):
|
||||
for handler in handlers:
|
||||
if handler.__name__ not in self.__class__.__dict__:
|
||||
def _implements(self, methods: List[str]) -> bool:
|
||||
for method in methods:
|
||||
if method not in self.__class__.__dict__:
|
||||
return False
|
||||
return True
|
||||
|
||||
def _register_method(self, name, handler, result_name=None, internal=False, sensitive_params=False, feature=None):
|
||||
def _detect_feature(self, feature: Feature, methods: List[str]):
|
||||
if self._implements(methods):
|
||||
self._features.add(feature)
|
||||
|
||||
def _register_method(self, name, handler, result_name=None, internal=False, sensitive_params=False):
|
||||
if internal:
|
||||
def method(*args, **kwargs):
|
||||
result = handler(*args, **kwargs)
|
||||
@@ -194,6 +152,7 @@ class Plugin:
|
||||
result_name: result
|
||||
}
|
||||
return result
|
||||
|
||||
self._server.register_method(name, method, True, sensitive_params)
|
||||
else:
|
||||
async def method(*args, **kwargs):
|
||||
@@ -203,23 +162,37 @@ class Plugin:
|
||||
result_name: result
|
||||
}
|
||||
return result
|
||||
|
||||
self._server.register_method(name, method, False, sensitive_params)
|
||||
|
||||
if feature is not None:
|
||||
self._feature_methods.setdefault(feature, []).append(handler)
|
||||
|
||||
def _register_notification(self, name, handler, internal=False, sensitive_params=False, feature=None):
|
||||
def _register_notification(self, name, handler, internal=False, sensitive_params=False):
|
||||
self._server.register_notification(name, handler, internal, sensitive_params)
|
||||
|
||||
if feature is not None:
|
||||
self._feature_methods.setdefault(feature, []).append(handler)
|
||||
|
||||
async def run(self):
|
||||
"""Plugin's main coroutine."""
|
||||
await self._server.run()
|
||||
if self._pass_control_task is not None:
|
||||
await self._pass_control_task
|
||||
|
||||
def create_task(self, coro, description):
|
||||
"""Wrapper around asyncio.create_task - takes care of canceling tasks on shutdown"""
|
||||
|
||||
async def task_wrapper(task_id):
|
||||
try:
|
||||
return await coro
|
||||
except asyncio.CancelledError:
|
||||
logging.debug("Canceled task %d (%s)", task_id, description)
|
||||
except Exception:
|
||||
logging.exception("Exception raised in task %d (%s)", task_id, description)
|
||||
finally:
|
||||
del self._tasks[task_id]
|
||||
|
||||
task_id = next(self._task_counter)
|
||||
logging.debug("Creating task %d (%s)", task_id, description)
|
||||
task = asyncio.create_task(task_wrapper(task_id))
|
||||
self._tasks[task_id] = task
|
||||
return task
|
||||
|
||||
async def _pass_control(self):
|
||||
while self._active:
|
||||
try:
|
||||
@@ -233,6 +206,8 @@ class Plugin:
|
||||
self._server.stop()
|
||||
self._active = False
|
||||
self.shutdown()
|
||||
for task in self._tasks.values():
|
||||
task.cancel()
|
||||
|
||||
def _get_capabilities(self):
|
||||
return {
|
||||
@@ -417,26 +392,6 @@ class Plugin:
|
||||
params = {"user_id": user_id}
|
||||
self._notification_client.notify("friend_removed", params)
|
||||
|
||||
def update_room(
|
||||
self,
|
||||
room_id: str,
|
||||
unread_message_count: Optional[int]=None,
|
||||
new_messages: Optional[List[Message]]=None
|
||||
) -> 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
|
||||
if new_messages is not None:
|
||||
params["messages"] = new_messages
|
||||
self._notification_client.notify("chat_room_updated", params)
|
||||
|
||||
def update_game_time(self, game_time: GameTime) -> None:
|
||||
"""Notify the client to update game time for a game.
|
||||
|
||||
@@ -554,7 +509,7 @@ class Plugin:
|
||||
raise NotImplementedError()
|
||||
|
||||
async def pass_login_credentials(self, step: str, credentials: Dict[str, str], cookies: List[Dict[str, str]]) \
|
||||
-> Union[NextStep, Authentication]:
|
||||
-> Union[NextStep, Authentication]:
|
||||
"""This method is called if we return galaxy.api.types.NextStep from authenticate or from pass_login_credentials.
|
||||
This method'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
|
||||
@@ -637,6 +592,7 @@ class Plugin:
|
||||
|
||||
:param game_ids: 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)
|
||||
@@ -748,57 +704,6 @@ class Plugin:
|
||||
"""
|
||||
raise NotImplementedError()
|
||||
|
||||
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: str, message_text: str) -> None:
|
||||
"""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: str, last_message_id: str) -> None:
|
||||
"""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) -> 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: str, message_id: str) -> List[Message]:
|
||||
"""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: str, from_timestamp: int) -> List[Message]:
|
||||
"""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) -> List[GameTime]:
|
||||
"""
|
||||
.. deprecated:: 0.33
|
||||
@@ -894,6 +799,9 @@ def create_and_run_plugin(plugin_class, argv):
|
||||
await plugin.run()
|
||||
|
||||
try:
|
||||
if sys.platform == "win32":
|
||||
asyncio.set_event_loop_policy(asyncio.WindowsProactorEventLoopPolicy())
|
||||
|
||||
asyncio.run(coroutine())
|
||||
except Exception:
|
||||
logging.exception("Error while running plugin")
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
from dataclasses import dataclass
|
||||
from typing import List, Dict, Optional
|
||||
|
||||
from galaxy.api.consts import LicenseType, LocalGameState, PresenceState
|
||||
from galaxy.api.consts import LicenseType, LocalGameState
|
||||
|
||||
@dataclass
|
||||
class Authentication():
|
||||
@@ -61,7 +61,6 @@ class NextStep():
|
||||
: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]
|
||||
@@ -130,34 +129,6 @@ class LocalGame():
|
||||
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
|
||||
avatar_url: str
|
||||
presence: Presence
|
||||
|
||||
@dataclass
|
||||
class FriendInfo():
|
||||
"""Information about a friend of the currently authenticated user.
|
||||
@@ -168,32 +139,6 @@ class FriendInfo():
|
||||
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
|
||||
message_text: str
|
||||
|
||||
@dataclass
|
||||
class GameTime():
|
||||
"""Game time of a game, defines the total time spent in the game
|
||||
|
||||
91
src/galaxy/proc_tools.py
Normal file
91
src/galaxy/proc_tools.py
Normal file
@@ -0,0 +1,91 @@
|
||||
import platform
|
||||
from dataclasses import dataclass
|
||||
from typing import Iterable, NewType, Optional, Set
|
||||
|
||||
|
||||
def is_windows():
|
||||
return platform.system() == "Windows"
|
||||
|
||||
|
||||
ProcessId = NewType("ProcessId", int)
|
||||
|
||||
|
||||
@dataclass
|
||||
class ProcessInfo:
|
||||
pid: ProcessId
|
||||
binary_path: Optional[str]
|
||||
|
||||
|
||||
if is_windows():
|
||||
from ctypes import byref, sizeof, windll, create_unicode_buffer, FormatError, WinError
|
||||
from ctypes.wintypes import DWORD
|
||||
|
||||
|
||||
def pids() -> Iterable[ProcessId]:
|
||||
_PROC_ID_T = DWORD
|
||||
list_size = 4096
|
||||
|
||||
def try_get_pids(list_size: int) -> Set[ProcessId]:
|
||||
result_size = DWORD()
|
||||
proc_id_list = (_PROC_ID_T * list_size)()
|
||||
|
||||
if not windll.psapi.EnumProcesses(byref(proc_id_list), sizeof(proc_id_list), byref(result_size)):
|
||||
raise WinError(descr="Failed to get process ID list: %s" % FormatError())
|
||||
|
||||
return proc_id_list[:int(result_size.value / sizeof(_PROC_ID_T()))]
|
||||
|
||||
while True:
|
||||
proc_ids = try_get_pids(list_size)
|
||||
if len(proc_ids) < list_size:
|
||||
return proc_ids
|
||||
|
||||
list_size *= 2
|
||||
|
||||
|
||||
def get_process_info(pid: ProcessId) -> Optional[ProcessInfo]:
|
||||
_PROC_QUERY_LIMITED_INFORMATION = 0x1000
|
||||
|
||||
process_info = ProcessInfo(pid=pid, binary_path=None)
|
||||
|
||||
h_process = windll.kernel32.OpenProcess(_PROC_QUERY_LIMITED_INFORMATION, False, pid)
|
||||
if not h_process:
|
||||
return process_info
|
||||
|
||||
try:
|
||||
def get_exe_path() -> Optional[str]:
|
||||
_MAX_PATH = 260
|
||||
_WIN32_PATH_FORMAT = 0x0000
|
||||
|
||||
exe_path_buffer = create_unicode_buffer(_MAX_PATH)
|
||||
exe_path_len = DWORD(len(exe_path_buffer))
|
||||
|
||||
return exe_path_buffer[:exe_path_len.value] if windll.kernel32.QueryFullProcessImageNameW(
|
||||
h_process, _WIN32_PATH_FORMAT, exe_path_buffer, byref(exe_path_len)
|
||||
) else None
|
||||
|
||||
process_info.binary_path = get_exe_path()
|
||||
finally:
|
||||
windll.kernel32.CloseHandle(h_process)
|
||||
return process_info
|
||||
else:
|
||||
import psutil
|
||||
|
||||
|
||||
def pids() -> Iterable[ProcessId]:
|
||||
for pid in psutil.pids():
|
||||
yield pid
|
||||
|
||||
|
||||
def get_process_info(pid: ProcessId) -> Optional[ProcessInfo]:
|
||||
process_info = ProcessInfo(pid=pid, binary_path=None)
|
||||
try:
|
||||
process_info.binary_path = psutil.Process(pid=pid).as_dict(attrs=["exe"])["exe"]
|
||||
except psutil.NoSuchProcess:
|
||||
pass
|
||||
finally:
|
||||
return process_info
|
||||
|
||||
|
||||
def process_iter() -> Iterable[ProcessInfo]:
|
||||
for pid in pids():
|
||||
yield get_process_info(pid)
|
||||
@@ -3,6 +3,7 @@ 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]
|
||||
@@ -14,6 +15,7 @@ def zip_folder(folder):
|
||||
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:
|
||||
|
||||
@@ -42,12 +42,6 @@ def plugin(reader, writer):
|
||||
"install_game",
|
||||
"uninstall_game",
|
||||
"get_friends",
|
||||
"get_users",
|
||||
"send_message",
|
||||
"mark_as_read",
|
||||
"get_rooms",
|
||||
"get_room_history_from_message",
|
||||
"get_room_history_from_timestamp",
|
||||
"get_game_times",
|
||||
"shutdown_platform_client"
|
||||
)
|
||||
@@ -64,6 +58,7 @@ def plugin(reader, writer):
|
||||
stack.enter_context(patch.object(Plugin, method))
|
||||
yield Plugin(Platform.Generic, "0.1", reader, writer, "token")
|
||||
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
def my_caplog(caplog):
|
||||
caplog.set_level(logging.DEBUG)
|
||||
|
||||
@@ -1,354 +0,0 @@
|
||||
import asyncio
|
||||
import json
|
||||
|
||||
import pytest
|
||||
|
||||
from galaxy.api.types import Room, Message
|
||||
from galaxy.api.errors import (
|
||||
UnknownError, AuthenticationRequired, BackendNotAvailable, BackendTimeout, BackendError,
|
||||
TooManyMessagesSent, IncoherentLastMessage, MessageNotFound
|
||||
)
|
||||
|
||||
def test_send_message_success(plugin, read, write):
|
||||
request = {
|
||||
"jsonrpc": "2.0",
|
||||
"id": "3",
|
||||
"method": "send_message",
|
||||
"params": {
|
||||
"room_id": "14",
|
||||
"message": "Hello!"
|
||||
}
|
||||
}
|
||||
|
||||
read.side_effect = [json.dumps(request).encode() + b"\n", b""]
|
||||
plugin.send_message.coro.return_value = None
|
||||
asyncio.run(plugin.run())
|
||||
plugin.send_message.assert_called_with(room_id="14", message="Hello!")
|
||||
response = json.loads(write.call_args[0][0])
|
||||
|
||||
assert response == {
|
||||
"jsonrpc": "2.0",
|
||||
"id": "3",
|
||||
"result": None
|
||||
}
|
||||
|
||||
@pytest.mark.parametrize("error,code,message", [
|
||||
pytest.param(UnknownError, 0, "Unknown error", id="unknown_error"),
|
||||
pytest.param(AuthenticationRequired, 1, "Authentication required", id="not_authenticated"),
|
||||
pytest.param(BackendNotAvailable, 2, "Backend not available", id="backend_not_available"),
|
||||
pytest.param(BackendTimeout, 3, "Backend timed out", id="backend_timeout"),
|
||||
pytest.param(BackendError, 4, "Backend error", id="backend_error"),
|
||||
pytest.param(TooManyMessagesSent, 300, "Too many messages sent", id="too_many_messages")
|
||||
])
|
||||
def test_send_message_failure(plugin, read, write, error, code, message):
|
||||
request = {
|
||||
"jsonrpc": "2.0",
|
||||
"id": "6",
|
||||
"method": "send_message",
|
||||
"params": {
|
||||
"room_id": "15",
|
||||
"message": "Bye"
|
||||
}
|
||||
}
|
||||
|
||||
read.side_effect = [json.dumps(request).encode() + b"\n", b""]
|
||||
plugin.send_message.coro.side_effect = error()
|
||||
asyncio.run(plugin.run())
|
||||
plugin.send_message.assert_called_with(room_id="15", message="Bye")
|
||||
response = json.loads(write.call_args[0][0])
|
||||
|
||||
assert response == {
|
||||
"jsonrpc": "2.0",
|
||||
"id": "6",
|
||||
"error": {
|
||||
"code": code,
|
||||
"message": message
|
||||
}
|
||||
}
|
||||
|
||||
def test_mark_as_read_success(plugin, read, write):
|
||||
request = {
|
||||
"jsonrpc": "2.0",
|
||||
"id": "7",
|
||||
"method": "mark_as_read",
|
||||
"params": {
|
||||
"room_id": "14",
|
||||
"last_message_id": "67"
|
||||
}
|
||||
}
|
||||
|
||||
read.side_effect = [json.dumps(request).encode() + b"\n", b""]
|
||||
plugin.mark_as_read.coro.return_value = None
|
||||
asyncio.run(plugin.run())
|
||||
plugin.mark_as_read.assert_called_with(room_id="14", last_message_id="67")
|
||||
response = json.loads(write.call_args[0][0])
|
||||
|
||||
assert response == {
|
||||
"jsonrpc": "2.0",
|
||||
"id": "7",
|
||||
"result": None
|
||||
}
|
||||
|
||||
@pytest.mark.parametrize("error,code,message", [
|
||||
pytest.param(UnknownError, 0, "Unknown error", id="unknown_error"),
|
||||
pytest.param(AuthenticationRequired, 1, "Authentication required", id="not_authenticated"),
|
||||
pytest.param(BackendNotAvailable, 2, "Backend not available", id="backend_not_available"),
|
||||
pytest.param(BackendTimeout, 3, "Backend timed out", id="backend_timeout"),
|
||||
pytest.param(BackendError, 4, "Backend error", id="backend_error"),
|
||||
pytest.param(
|
||||
IncoherentLastMessage,
|
||||
400,
|
||||
"Different last message id on backend",
|
||||
id="incoherent_last_message"
|
||||
)
|
||||
])
|
||||
def test_mark_as_read_failure(plugin, read, write, error, code, message):
|
||||
request = {
|
||||
"jsonrpc": "2.0",
|
||||
"id": "4",
|
||||
"method": "mark_as_read",
|
||||
"params": {
|
||||
"room_id": "18",
|
||||
"last_message_id": "7"
|
||||
}
|
||||
}
|
||||
|
||||
read.side_effect = [json.dumps(request).encode() + b"\n", b""]
|
||||
plugin.mark_as_read.coro.side_effect = error()
|
||||
asyncio.run(plugin.run())
|
||||
plugin.mark_as_read.assert_called_with(room_id="18", last_message_id="7")
|
||||
response = json.loads(write.call_args[0][0])
|
||||
|
||||
assert response == {
|
||||
"jsonrpc": "2.0",
|
||||
"id": "4",
|
||||
"error": {
|
||||
"code": code,
|
||||
"message": message
|
||||
}
|
||||
}
|
||||
|
||||
def test_get_rooms_success(plugin, read, write):
|
||||
request = {
|
||||
"jsonrpc": "2.0",
|
||||
"id": "2",
|
||||
"method": "import_rooms"
|
||||
}
|
||||
|
||||
read.side_effect = [json.dumps(request).encode() + b"\n", b""]
|
||||
plugin.get_rooms.coro.return_value = [
|
||||
Room("13", 0, None),
|
||||
Room("15", 34, "8")
|
||||
]
|
||||
asyncio.run(plugin.run())
|
||||
plugin.get_rooms.assert_called_with()
|
||||
response = json.loads(write.call_args[0][0])
|
||||
|
||||
assert response == {
|
||||
"jsonrpc": "2.0",
|
||||
"id": "2",
|
||||
"result": {
|
||||
"rooms": [
|
||||
{
|
||||
"room_id": "13",
|
||||
"unread_message_count": 0,
|
||||
},
|
||||
{
|
||||
"room_id": "15",
|
||||
"unread_message_count": 34,
|
||||
"last_message_id": "8"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
def test_get_rooms_failure(plugin, read, write):
|
||||
request = {
|
||||
"jsonrpc": "2.0",
|
||||
"id": "9",
|
||||
"method": "import_rooms"
|
||||
}
|
||||
|
||||
read.side_effect = [json.dumps(request).encode() + b"\n", b""]
|
||||
plugin.get_rooms.coro.side_effect = UnknownError()
|
||||
asyncio.run(plugin.run())
|
||||
plugin.get_rooms.assert_called_with()
|
||||
response = json.loads(write.call_args[0][0])
|
||||
|
||||
assert response == {
|
||||
"jsonrpc": "2.0",
|
||||
"id": "9",
|
||||
"error": {
|
||||
"code": 0,
|
||||
"message": "Unknown error"
|
||||
}
|
||||
}
|
||||
|
||||
def test_get_room_history_from_message_success(plugin, read, write):
|
||||
request = {
|
||||
"jsonrpc": "2.0",
|
||||
"id": "2",
|
||||
"method": "import_room_history_from_message",
|
||||
"params": {
|
||||
"room_id": "34",
|
||||
"message_id": "66"
|
||||
}
|
||||
}
|
||||
|
||||
read.side_effect = [json.dumps(request).encode() + b"\n", b""]
|
||||
plugin.get_room_history_from_message.coro.return_value = [
|
||||
Message("13", "149", 1549454837, "Hello"),
|
||||
Message("14", "812", 1549454899, "Hi")
|
||||
]
|
||||
asyncio.run(plugin.run())
|
||||
plugin.get_room_history_from_message.assert_called_with(room_id="34", message_id="66")
|
||||
response = json.loads(write.call_args[0][0])
|
||||
|
||||
assert response == {
|
||||
"jsonrpc": "2.0",
|
||||
"id": "2",
|
||||
"result": {
|
||||
"messages": [
|
||||
{
|
||||
"message_id": "13",
|
||||
"sender_id": "149",
|
||||
"sent_time": 1549454837,
|
||||
"message_text": "Hello"
|
||||
},
|
||||
{
|
||||
"message_id": "14",
|
||||
"sender_id": "812",
|
||||
"sent_time": 1549454899,
|
||||
"message_text": "Hi"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
@pytest.mark.parametrize("error,code,message", [
|
||||
pytest.param(UnknownError, 0, "Unknown error", id="unknown_error"),
|
||||
pytest.param(AuthenticationRequired, 1, "Authentication required", id="not_authenticated"),
|
||||
pytest.param(BackendNotAvailable, 2, "Backend not available", id="backend_not_available"),
|
||||
pytest.param(BackendTimeout, 3, "Backend timed out", id="backend_timeout"),
|
||||
pytest.param(BackendError, 4, "Backend error", id="backend_error"),
|
||||
pytest.param(MessageNotFound, 500, "Message not found", id="message_not_found")
|
||||
])
|
||||
def test_get_room_history_from_message_failure(plugin, read, write, error, code, message):
|
||||
request = {
|
||||
"jsonrpc": "2.0",
|
||||
"id": "7",
|
||||
"method": "import_room_history_from_message",
|
||||
"params": {
|
||||
"room_id": "33",
|
||||
"message_id": "88"
|
||||
}
|
||||
}
|
||||
|
||||
read.side_effect = [json.dumps(request).encode() + b"\n", b""]
|
||||
plugin.get_room_history_from_message.coro.side_effect = error()
|
||||
asyncio.run(plugin.run())
|
||||
plugin.get_room_history_from_message.assert_called_with(room_id="33", message_id="88")
|
||||
response = json.loads(write.call_args[0][0])
|
||||
|
||||
assert response == {
|
||||
"jsonrpc": "2.0",
|
||||
"id": "7",
|
||||
"error": {
|
||||
"code": code,
|
||||
"message": message
|
||||
}
|
||||
}
|
||||
|
||||
def test_get_room_history_from_timestamp_success(plugin, read, write):
|
||||
request = {
|
||||
"jsonrpc": "2.0",
|
||||
"id": "7",
|
||||
"method": "import_room_history_from_timestamp",
|
||||
"params": {
|
||||
"room_id": "12",
|
||||
"from_timestamp": 1549454835
|
||||
}
|
||||
}
|
||||
|
||||
read.side_effect = [json.dumps(request).encode() + b"\n", b""]
|
||||
plugin.get_room_history_from_timestamp.coro.return_value = [
|
||||
Message("12", "155", 1549454836, "Bye")
|
||||
]
|
||||
asyncio.run(plugin.run())
|
||||
plugin.get_room_history_from_timestamp.assert_called_with(
|
||||
room_id="12",
|
||||
from_timestamp=1549454835
|
||||
)
|
||||
response = json.loads(write.call_args[0][0])
|
||||
|
||||
assert response == {
|
||||
"jsonrpc": "2.0",
|
||||
"id": "7",
|
||||
"result": {
|
||||
"messages": [
|
||||
{
|
||||
"message_id": "12",
|
||||
"sender_id": "155",
|
||||
"sent_time": 1549454836,
|
||||
"message_text": "Bye"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
def test_get_room_history_from_timestamp_failure(plugin, read, write):
|
||||
request = {
|
||||
"jsonrpc": "2.0",
|
||||
"id": "3",
|
||||
"method": "import_room_history_from_timestamp",
|
||||
"params": {
|
||||
"room_id": "10",
|
||||
"from_timestamp": 1549454800
|
||||
}
|
||||
}
|
||||
|
||||
read.side_effect = [json.dumps(request).encode() + b"\n", b""]
|
||||
plugin.get_room_history_from_timestamp.coro.side_effect = UnknownError()
|
||||
asyncio.run(plugin.run())
|
||||
plugin.get_room_history_from_timestamp.assert_called_with(
|
||||
room_id="10",
|
||||
from_timestamp=1549454800
|
||||
)
|
||||
response = json.loads(write.call_args[0][0])
|
||||
|
||||
assert response == {
|
||||
"jsonrpc": "2.0",
|
||||
"id": "3",
|
||||
"error": {
|
||||
"code": 0,
|
||||
"message": "Unknown error"
|
||||
}
|
||||
}
|
||||
|
||||
def test_update_room(plugin, write):
|
||||
messages = [
|
||||
Message("10", "898", 1549454832, "Hi")
|
||||
]
|
||||
|
||||
async def couritine():
|
||||
plugin.update_room("14", 15, messages)
|
||||
|
||||
asyncio.run(couritine())
|
||||
response = json.loads(write.call_args[0][0])
|
||||
|
||||
assert response == {
|
||||
"jsonrpc": "2.0",
|
||||
"method": "chat_room_updated",
|
||||
"params": {
|
||||
"room_id": "14",
|
||||
"unread_message_count": 15,
|
||||
"messages": [
|
||||
{
|
||||
"message_id": "10",
|
||||
"sender_id": "898",
|
||||
"sent_time": 1549454832,
|
||||
"message_text": "Hi"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -1,45 +1,49 @@
|
||||
from galaxy.api.consts import Feature, Platform
|
||||
from galaxy.api.plugin import Plugin
|
||||
from galaxy.api.consts import Platform, Feature
|
||||
|
||||
|
||||
def test_base_class():
|
||||
plugin = Plugin(Platform.Generic, "0.1", None, None, None)
|
||||
assert plugin.features == []
|
||||
assert set(plugin.features) == {
|
||||
Feature.ImportInstalledGames,
|
||||
Feature.ImportOwnedGames,
|
||||
Feature.LaunchGame,
|
||||
Feature.InstallGame,
|
||||
Feature.UninstallGame,
|
||||
Feature.ImportAchievements,
|
||||
Feature.ImportGameTime,
|
||||
Feature.ImportFriends,
|
||||
Feature.ShutdownPlatformClient
|
||||
}
|
||||
|
||||
|
||||
def test_no_overloads():
|
||||
class PluginImpl(Plugin): #pylint: disable=abstract-method
|
||||
class PluginImpl(Plugin): # pylint: disable=abstract-method
|
||||
pass
|
||||
|
||||
plugin = PluginImpl(Platform.Generic, "0.1", None, None, None)
|
||||
assert plugin.features == []
|
||||
|
||||
|
||||
def test_one_method_feature():
|
||||
class PluginImpl(Plugin): #pylint: disable=abstract-method
|
||||
class PluginImpl(Plugin): # pylint: disable=abstract-method
|
||||
async def get_owned_games(self):
|
||||
pass
|
||||
|
||||
plugin = PluginImpl(Platform.Generic, "0.1", None, None, None)
|
||||
assert plugin.features == [Feature.ImportOwnedGames]
|
||||
|
||||
def test_multiple_methods_feature_all():
|
||||
class PluginImpl(Plugin): #pylint: disable=abstract-method
|
||||
async def send_message(self, room_id, message):
|
||||
|
||||
def test_multi_features():
|
||||
class PluginImpl(Plugin): # pylint: disable=abstract-method
|
||||
async def get_owned_games(self):
|
||||
pass
|
||||
async def mark_as_read(self, room_id, last_message_id):
|
||||
|
||||
async def import_games_achievements(self, game_ids) -> None:
|
||||
pass
|
||||
async def get_rooms(self):
|
||||
pass
|
||||
async def get_room_history_from_message(self, room_id, message_id):
|
||||
pass
|
||||
async def get_room_history_from_timestamp(self, room_id, timestamp):
|
||||
|
||||
async def start_game_times_import(self, game_ids) -> None:
|
||||
pass
|
||||
|
||||
plugin = PluginImpl(Platform.Generic, "0.1", None, None, None)
|
||||
assert plugin.features == [Feature.Chat]
|
||||
|
||||
def test_multiple_methods_feature_not_all():
|
||||
class PluginImpl(Plugin): #pylint: disable=abstract-method
|
||||
async def send_message(self, room_id, message):
|
||||
pass
|
||||
|
||||
plugin = PluginImpl(Platform.Generic, "0.1", None, None, None)
|
||||
assert plugin.features == []
|
||||
assert set(plugin.features) == {Feature.ImportAchievements, Feature.ImportOwnedGames}
|
||||
|
||||
@@ -1,69 +0,0 @@
|
||||
import asyncio
|
||||
import json
|
||||
|
||||
from galaxy.api.types import UserInfo, Presence
|
||||
from galaxy.api.errors import UnknownError
|
||||
from galaxy.api.consts import PresenceState
|
||||
|
||||
|
||||
def test_get_users_success(plugin, read, write):
|
||||
request = {
|
||||
"jsonrpc": "2.0",
|
||||
"id": "8",
|
||||
"method": "import_user_infos",
|
||||
"params": {
|
||||
"user_id_list": ["13"]
|
||||
}
|
||||
}
|
||||
|
||||
read.side_effect = [json.dumps(request).encode() + b"\n", b""]
|
||||
plugin.get_users.coro.return_value = [
|
||||
UserInfo("5", False, "Ula", "http://avatar.png", Presence(PresenceState.Offline))
|
||||
]
|
||||
asyncio.run(plugin.run())
|
||||
plugin.get_users.assert_called_with(user_id_list=["13"])
|
||||
response = json.loads(write.call_args[0][0])
|
||||
|
||||
assert response == {
|
||||
"jsonrpc": "2.0",
|
||||
"id": "8",
|
||||
"result": {
|
||||
"user_info_list": [
|
||||
{
|
||||
"user_id": "5",
|
||||
"is_friend": False,
|
||||
"user_name": "Ula",
|
||||
"avatar_url": "http://avatar.png",
|
||||
"presence": {
|
||||
"presence_state": "offline"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
def test_get_users_failure(plugin, read, write):
|
||||
request = {
|
||||
"jsonrpc": "2.0",
|
||||
"id": "12",
|
||||
"method": "import_user_infos",
|
||||
"params": {
|
||||
"user_id_list": ["10", "11", "12"]
|
||||
}
|
||||
}
|
||||
|
||||
read.side_effect = [json.dumps(request).encode() + b"\n", b""]
|
||||
plugin.get_users.coro.side_effect = UnknownError()
|
||||
asyncio.run(plugin.run())
|
||||
plugin.get_users.assert_called_with(user_id_list=["10", "11", "12"])
|
||||
response = json.loads(write.call_args[0][0])
|
||||
|
||||
assert response == {
|
||||
"jsonrpc": "2.0",
|
||||
"id": "12",
|
||||
"error": {
|
||||
"code": 0,
|
||||
"message": "Unknown error"
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user