Skip to content

Commit 062d6a9

Browse files
committed
Add user presence import feature
1 parent c874bc1 commit 062d6a9

File tree

7 files changed

+355
-7
lines changed

7 files changed

+355
-7
lines changed

.gitignore

+1
Original file line numberDiff line numberDiff line change
@@ -7,3 +7,4 @@ docs/build/
77
Pipfile
88
.idea
99
docs/source/_build
10+
.mypy_cache

src/galaxy/api/consts.py

+9
Original file line numberDiff line numberDiff line change
@@ -113,6 +113,7 @@ class Feature(Enum):
113113
LaunchPlatformClient = "LaunchPlatformClient"
114114
ImportGameLibrarySettings = "ImportGameLibrarySettings"
115115
ImportOSCompatibility = "ImportOSCompatibility"
116+
ImportUserPresence = "ImportUserPresence"
116117

117118

118119
class LicenseType(Enum):
@@ -140,3 +141,11 @@ class OSCompatibility(Flag):
140141
Windows = 0b001
141142
MacOS = 0b010
142143
Linux = 0b100
144+
145+
146+
class PresenceState(Enum):
147+
""""Possible states of a user."""
148+
Unknown = "unknown"
149+
Online = "online"
150+
Offline = "offline"
151+
Away = "away"

src/galaxy/api/plugin.py

+85-2
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,9 @@
1010
from galaxy.api.consts import Feature, OSCompatibility
1111
from galaxy.api.errors import ImportInProgress, UnknownError
1212
from galaxy.api.jsonrpc import ApplicationError, NotificationClient, Server
13-
from galaxy.api.types import Achievement, Authentication, FriendInfo, Game, GameTime, LocalGame, NextStep, GameLibrarySettings
13+
from galaxy.api.types import (
14+
Achievement, Authentication, FriendInfo, Game, GameLibrarySettings, GameTime, LocalGame, NextStep, UserPresence
15+
)
1416
from galaxy.task_manager import TaskManager
1517

1618

@@ -49,6 +51,7 @@ def __init__(self, platform, version, reader, writer, handshake_token):
4951
self._game_times_import_in_progress = False
5052
self._game_library_settings_import_in_progress = False
5153
self._os_compatibility_import_in_progress = False
54+
self._user_presence_import_in_progress = False
5255

5356
self._persistent_cache = dict()
5457

@@ -118,6 +121,9 @@ def __init__(self, platform, version, reader, writer, handshake_token):
118121
self._register_method("start_os_compatibility_import", self._start_os_compatibility_import)
119122
self._detect_feature(Feature.ImportOSCompatibility, ["get_os_compatibility"])
120123

124+
self._register_method("start_user_presence_import", self._start_user_presence_import)
125+
self._detect_feature(Feature.ImportUserPresence, ["get_user_presence"])
126+
121127
async def __aenter__(self):
122128
return self
123129

@@ -265,7 +271,7 @@ async def pass_login_credentials(self, step, credentials, cookies):
265271
266272
"""
267273
# temporary solution for persistent_cache vs credentials issue
268-
self.persistent_cache['credentials'] = credentials # type: ignore
274+
self.persistent_cache["credentials"] = credentials # type: ignore
269275

270276
self._notification_client.notify("store_credentials", credentials, sensitive_params=True)
271277

@@ -450,6 +456,27 @@ def _os_compatibility_import_failure(self, game_id: str, error: ApplicationError
450456
def _os_compatibility_import_finished(self) -> None:
451457
self._notification_client.notify("os_compatibility_import_finished", None)
452458

459+
def _user_presence_import_success(self, user_id: str, user_presence: UserPresence) -> None:
460+
self._notification_client.notify(
461+
"user_presence_import_success",
462+
{
463+
"user_id": user_id,
464+
"presence": user_presence
465+
}
466+
)
467+
468+
def _user_presence_import_failure(self, user_id: str, error: ApplicationError) -> None:
469+
self._notification_client.notify(
470+
"user_presence_import_failure",
471+
{
472+
"user_id": user_id,
473+
"error": error.json()
474+
}
475+
)
476+
477+
def _user_presence_import_finished(self) -> None:
478+
self._notification_client.notify("user_presence_import_finished", None)
479+
453480
def lost_authentication(self) -> None:
454481
"""Notify the client that integration has lost authentication for the
455482
current user and is unable to perform actions which would require it.
@@ -912,6 +939,62 @@ async def get_os_compatibility(self, game_id: str, context: Any) -> Optional[OSC
912939
def os_compatibility_import_complete(self) -> None:
913940
"""Override this method to handle operations after OS compatibility import is finished (like updating cache)."""
914941

942+
async def _start_user_presence_import(self, user_ids: List[str]) -> None:
943+
if self._user_presence_import_in_progress:
944+
raise ImportInProgress()
945+
946+
context = await self.prepare_user_presence_context(user_ids)
947+
948+
async def import_user_presence(user_id, context_) -> None:
949+
try:
950+
self._user_presence_import_success(user_id, await self.get_user_presence(user_id, context_))
951+
except ApplicationError as error:
952+
self._user_presence_import_failure(user_id, error)
953+
except Exception:
954+
logging.exception("Unexpected exception raised in import_user_presence")
955+
self._user_presence_import_failure(user_id, UnknownError())
956+
957+
async def import_user_presence_set(user_ids_, context_) -> None:
958+
try:
959+
await asyncio.gather(*[
960+
import_user_presence(user_id, context_)
961+
for user_id in user_ids_
962+
])
963+
finally:
964+
self._user_presence_import_finished()
965+
self._user_presence_import_in_progress = False
966+
self.user_presence_import_complete()
967+
968+
self._external_task_manager.create_task(
969+
import_user_presence_set(user_ids, context),
970+
"user presence import",
971+
handle_exceptions=False
972+
)
973+
self._user_presence_import_in_progress = True
974+
975+
async def prepare_user_presence_context(self, user_ids: List[str]) -> Any:
976+
"""Override this method to prepare context for get_user_presence.
977+
This allows for optimizations like batch requests to platform API.
978+
Default implementation returns None.
979+
980+
:param user_ids: the ids of the users for whom presence information is imported
981+
:return: context
982+
"""
983+
return None
984+
985+
async def get_user_presence(self, user_id: str, context: Any) -> UserPresence:
986+
"""Override this method to return presence information for the user with the provided user_id.
987+
This method is called by import task initialized by GOG Galaxy Client.
988+
989+
:param user_id: the id of the user for whom presence information is imported
990+
:param context: the value returned from :meth:`prepare_user_presence_context`
991+
:return: UserPresence presence information of the provided user
992+
"""
993+
raise NotImplementedError()
994+
995+
def user_presence_import_complete(self) -> None:
996+
"""Override this method to handle operations after presence import is finished (like updating cache)."""
997+
915998

916999
def create_and_run_plugin(plugin_class, argv):
9171000
"""Call this method as an entry point for the implemented integration.

src/galaxy/api/types.py

+16-1
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
from dataclasses import dataclass
22
from typing import Dict, List, Optional
33

4-
from galaxy.api.consts import LicenseType, LocalGameState
4+
from galaxy.api.consts import LicenseType, LocalGameState, PresenceState
55

66

77
@dataclass
@@ -174,3 +174,18 @@ class GameLibrarySettings:
174174
game_id: str
175175
tags: Optional[List[str]]
176176
hidden: Optional[bool]
177+
178+
179+
@dataclass
180+
class UserPresence:
181+
"""Presence information of a user.
182+
183+
:param presence_state: the state of the user
184+
:param game_id: id of the game a user is currently in
185+
:param game_title: name of the game a user is currently in
186+
:param presence_status: detailed user's presence description
187+
"""
188+
presence_state: PresenceState
189+
game_id: Optional[str] = None
190+
game_title: Optional[str] = None
191+
presence_status: Optional[str] = None

tests/conftest.py

+11-3
Original file line numberDiff line numberDiff line change
@@ -1,33 +1,38 @@
1-
from contextlib import ExitStack
21
import logging
3-
from unittest.mock import patch, MagicMock
2+
from contextlib import ExitStack
3+
from unittest.mock import MagicMock, patch
44

55
import pytest
66

7-
from galaxy.api.plugin import Plugin
87
from galaxy.api.consts import Platform
8+
from galaxy.api.plugin import Plugin
99
from galaxy.unittest.mock import async_return_value
1010

11+
1112
@pytest.fixture()
1213
def reader():
1314
stream = MagicMock(name="stream_reader")
1415
stream.read = MagicMock()
1516
yield stream
1617

18+
1719
@pytest.fixture()
1820
async def writer():
1921
stream = MagicMock(name="stream_writer")
2022
stream.drain.side_effect = lambda: async_return_value(None)
2123
yield stream
2224

25+
2326
@pytest.fixture()
2427
def read(reader):
2528
yield reader.read
2629

30+
2731
@pytest.fixture()
2832
def write(writer):
2933
yield writer.write
3034

35+
3136
@pytest.fixture()
3237
async def plugin(reader, writer):
3338
"""Return plugin instance with all feature methods mocked"""
@@ -56,6 +61,9 @@ async def plugin(reader, writer):
5661
"get_os_compatibility",
5762
"prepare_os_compatibility_context",
5863
"os_compatibility_import_complete",
64+
"get_user_presence",
65+
"prepare_user_presence_context",
66+
"user_presence_import_complete",
5967
)
6068

6169
with ExitStack() as stack:

tests/test_features.py

+2-1
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,8 @@ def test_base_class():
1616
Feature.ShutdownPlatformClient,
1717
Feature.LaunchPlatformClient,
1818
Feature.ImportGameLibrarySettings,
19-
Feature.ImportOSCompatibility
19+
Feature.ImportOSCompatibility,
20+
Feature.ImportUserPresence
2021
}
2122

2223

0 commit comments

Comments
 (0)