async def get_last_played_game(self, friend_name): headers = { "Authorization": f"Bearer {self._current_sc_token}", "User-Agent": USER_AGENT, "X-Requested-With": "XMLHttpRequest" } try: resp = await self._current_session.get("https://scapi.rockstargames.com/profile/getprofile?nickname=" f"{friend_name}&maxFriends=3", headers=headers) await self._update_cookies_from_response(resp) resp_json = await resp.json() except AssertionError: await self._refresh_credentials_social_club_light() return await self.get_last_played_game(friend_name) try: # The last played game is always listed first in the ownedGames list. last_played_ugc = resp_json['accounts'][0]['rockstarAccount']['gamesOwned'][0]['name'] title_id = get_game_title_id_from_ugc_title_id(last_played_ugc + "_PC") last_played_time = await get_unix_epoch_time_from_date(resp_json['accounts'][0] ['rockstarAccount']['gamesOwned'][0] ['lastSeen']) if LOG_SENSITIVE_DATA: log.debug(f"{friend_name}'s Last Played Game: " f"{games_cache[title_id]['friendlyName'] if title_id else last_played_ugc}") return UserPresence(PresenceState.Online, game_id=str(games_cache[title_id]['rosTitleId']) if title_id else last_played_ugc, in_game_status=f"Last Played {await get_time_passed(last_played_time)}") except IndexError: # If a game is not found in the gamesOwned list, then the user has not played any games. In this case, we # cannot be certain of their presence status. if LOG_SENSITIVE_DATA: log.warning(f"ROCKSTAR_LAST_PLAYED_WARNING: The user {friend_name} has not played any games!") return UserPresence(PresenceState.Unknown)
async def test_get_user_presence_success(authenticated_plugin, steam_client): presence = await authenticated_plugin.get_user_presence( "76561198040630463", CONTEXT) assert presence == UserPresence(presence_state=PresenceState.Offline) presence = await authenticated_plugin.get_user_presence( "76561198053830887", CONTEXT) assert presence == UserPresence(presence_state=PresenceState.Online, game_id="124523113")
async def test_get_user_presence_success(authenticated_plugin, steam_client): presence = await authenticated_plugin.get_user_presence("76561198040630463", CONTEXT) assert presence == UserPresence(presence_state=PresenceState.Offline) presence = await authenticated_plugin.get_user_presence("76561198053830887", CONTEXT) assert presence == UserPresence(presence_state=PresenceState.Online, game_id="124523113") presence = await authenticated_plugin.get_user_presence("76561198053830888", CONTEXT) assert presence == UserPresence(presence_state=PresenceState.Online, game_id="123321", game_title="abc", in_game_status=None) presence = await authenticated_plugin.get_user_presence("76561198053830889", CONTEXT) assert presence == UserPresence(presence_state=PresenceState.Online, game_id="123321", game_title="abc", in_game_status='menuSimple')
async def __xmpp_get_gog_presence(self, xmpp_presence: str) -> UserPresence: xmpp_client = await self.__xmpp_get_client('WOT') presence_state = PresenceState.Unknown game_id = None game_title = None status = None if xmpp_presence == 'online': presence_state = PresenceState.Online game_id = xmpp_client.get_game_full_id() game_title = xmpp_client.get_game_title() elif xmpp_presence == 'mobile': presence_state = PresenceState.Online elif xmpp_presence == 'offline': presence_state = PresenceState.Offline else: self._logger.error( 'plugin/__xmpp_get_gog_presence: unknown presence state %s' % xmpp_presence) return UserPresence(presence_state=presence_state, game_id=game_id, game_title=game_title, in_game_status=status)
def from_user_info(user_info: UserInfo) -> UserPresence: if user_info.state == EPersonaState.Online: state = PresenceState.Online elif user_info.state == EPersonaState.Offline: state = PresenceState.Offline elif user_info.state == EPersonaState.Away: state = PresenceState.Away elif user_info.state == EPersonaState.Busy: state = PresenceState.Away else: state = PresenceState.Unknown game_id = str( user_info.game_id ) if user_info.game_id is not None and user_info.game_id != 0 else None game_title = user_info.game_name if user_info.game_name is not None and user_info.game_name else None status = None if user_info.rich_presence is not None: status = user_info.rich_presence.get("status") if status and status[0] == "#": # TODO: handle it logger.info(f"Skipping not simple rich presence status {status}") status = None return UserPresence(presence_state=state, game_id=game_id, game_title=game_title, in_game_status=status)
async def get_user_presence(self, user_id, context): # For user presence settings 2 and 3, we need to verify that the specified user owns the game to get their # stats. friend_name = self.get_friend_user_name_from_user_id(user_id) if LOG_SENSITIVE_DATA: log.debug(f"ROCKSTAR_PRESENCE_START: Getting user presence for {friend_name} (Rockstar ID: {user_id})...") if context: for player in context['onlineFriends']: if player['userId'] == user_id: # This user owns the specified game, so we can return this information. break else: # The user does not own the specified game, so we need to return their last played game. return await self._http_client.get_last_played_game(friend_name) if CONFIG_OPTIONS['user_presence_mode'] == 0: self.presence_cache[user_id] = UserPresence(presence_state=PresenceState.Unknown) # 0 - Disable User Presence else: switch = { 1: self._http_client.get_last_played_game(friend_name), # 1 - Get Last Played Game 2: self._http_client.get_gta_online_stats(user_id, friend_name), # 2 - Get GTA Online Character Stats 3: self._http_client.get_rdo_stats(user_id, friend_name) # 3 - Get Red Dead Online Character Stats } self.presence_cache[user_id] = await asyncio.create_task(switch[CONFIG_OPTIONS['user_presence_mode']]) return self.presence_cache[user_id]
def from_user_info(user_info: UserInfo) -> UserPresence: if user_info.state == EPersonaState.Online: state = PresenceState.Online elif user_info.state == EPersonaState.Offline: state = PresenceState.Offline elif user_info.state == EPersonaState.Away: state = PresenceState.Away elif user_info.state == EPersonaState.Busy: state = PresenceState.Away else: state = PresenceState.Unknown game_id = str( user_info.game_id ) if user_info.game_id is not None and user_info.game_id != 0 else None game_title = user_info.game_name if user_info.game_name is not None and user_info.game_name else None status = None if user_info.rich_presence is not None: status = user_info.rich_presence.get("status") return UserPresence(presence_state=state, game_id=game_id, game_title=game_title, in_game_status=status)
async def presence_from_user_info(user_info: ProtoUserInfo, translations_cache: dict) -> UserPresence: if user_info.state == EPersonaState.Online: state = PresenceState.Online elif user_info.state == EPersonaState.Snooze: # In game afk, sitting in the main menu etc. Steam chat and others show this as online/in-game state = PresenceState.Online elif user_info.state == EPersonaState.Offline: state = PresenceState.Offline elif user_info.state == EPersonaState.Away: state = PresenceState.Away elif user_info.state == EPersonaState.Busy: state = PresenceState.Away else: state = PresenceState.Unknown game_id = str( user_info.game_id ) if user_info.game_id is not None and user_info.game_id != 0 else None game_title = user_info.game_name if user_info.game_name is not None and user_info.game_name else None status = None if user_info.rich_presence is not None: logger.info(f"Attempting to resolve rich presence from {user_info}") check_for_params = r"%.*%" status = user_info.rich_presence.get("steam_display") if not status: status = user_info.rich_presence.get("status") if status: try: if int(game_id) in translations_cache and translations_cache[ int(game_id)]: try: status = await asyncio.wait_for(_translate_presence( user_info, status, translations_cache[int(game_id)]), timeout=1) except asyncio.TimeoutError: logger.info( f"Timed out translating presence {user_info.rich_presence} using {translations_cache[int(game_id)]}" ) status = None elif "#" in status or re.findall(check_for_params, status): logger.info( f"Skipping not simple rich presence status {status}") status = None except Exception as e: logger.info( f"Unable to translate rich presence {status} due to {repr(e)}" ) status = None return UserPresence(presence_state=state, game_id=game_id, game_title=game_title, in_game_status=status)
async def __xmpp_on_got_offline(self, presence) -> None: xmpp_client = await self.__xmpp_get_client('WOT') user_id = xmpp_client.get_user_name_from_jid(presence['from']) if not user_id: return self.update_user_presence( user_id, UserPresence(presence_state=PresenceState.Offline))
def presence_from_user_info(user_info: ProtoUserInfo, translations_cache: dict) -> UserPresence: if user_info.state == EPersonaState.Online: state = PresenceState.Online elif user_info.state == EPersonaState.Snooze: # In game afk, sitting in the main menu etc. Steam chat and others show this as online/in-game state = PresenceState.Online elif user_info.state == EPersonaState.Offline: state = PresenceState.Offline elif user_info.state == EPersonaState.Away: state = PresenceState.Away elif user_info.state == EPersonaState.Busy: state = PresenceState.Away else: state = PresenceState.Unknown game_id = str( user_info.game_id ) if user_info.game_id is not None and user_info.game_id != 0 else None game_title = user_info.game_name if user_info.game_name is not None and user_info.game_name else None status = None if user_info.rich_presence is not None: status = user_info.rich_presence.get("status") if status and status[0] == "#": if int(game_id) in translations_cache: status = _translate_string(game_id, status, translations_cache) num_params = user_info.rich_presence.get("num_params") if num_params and int(num_params) > 0: for param in range(0, int(num_params)): param_string = user_info.rich_presence.get( f"param{param}") if param_string and param_string[0] == "#": param_string = _translate_string( game_id, param_string, translations_cache) presence_substring = f"%param{param}%" status = status.replace("{" + presence_substring + "}", param_string) status = status.replace(presence_substring, param_string) else: logger.info( f"Skipping not simple rich presence status {status}") status = None return UserPresence(presence_state=state, game_id=game_id, game_title=game_title, in_game_status=status)
def friend_info_parser(profile): if profile["primaryOnlineStatus"] == "online": presence_state = PresenceState.Online else: presence_state = PresenceState.Offline game_title = game_id = None if "presences" in profile: for presence in profile["presences"]: try: if presence["onlineStatus"] == "online" and presence["platform"] == "PS4": game_title = presence["titleName"] game_id = presence["npTitleId"] except: continue return {profile['accountId']: UserPresence(presence_state, game_id, game_title)}
async def test_update_user_presence(plugin, write): plugin.update_user_presence( "42", UserPresence(PresenceState.Online, "game-id", "game-title", "Pew pew")) await skip_loop() assert get_messages(write) == [{ "jsonrpc": "2.0", "method": "user_presence_updated", "params": { "user_id": "42", "presence": { "presence_state": PresenceState.Online.value, "game_id": "game-id", "game_title": "game-title", "in_game_status": "Pew pew" } } }]
async def presence_from_user_info(user_info: ProtoUserInfo, translations_cache: dict) -> UserPresence: if user_info.state == EPersonaState.Online: state = PresenceState.Online elif user_info.state == EPersonaState.Snooze: # In game afk, sitting in the main menu etc. Steam chat and others show this as online/in-game state = PresenceState.Online elif user_info.state == EPersonaState.Offline: state = PresenceState.Offline elif user_info.state == EPersonaState.Away: state = PresenceState.Away elif user_info.state == EPersonaState.Busy: state = PresenceState.Away else: state = PresenceState.Unknown game_id = str( user_info.game_id ) if user_info.game_id is not None and user_info.game_id != 0 else None game_title = user_info.game_name if user_info.game_name is not None and user_info.game_name else None status = None if user_info.rich_presence is not None: check_for_params = r"%.*%" status = user_info.rich_presence.get("steam_display") if not status: status = user_info.rich_presence.get("status") if status: try: if int(game_id) in translations_cache and translations_cache[ int(game_id)]: async def translate_presence(user_info, status): token_list = translations_cache[int(game_id)] replaced = True while replaced: replaced = False params = user_info.rich_presence.keys() for param in params: if "%" + param + "%" in status: status = status.replace( "%" + param + "%", user_info.rich_presence.get(param)) replaced = True for token in token_list.tokens: if token.name.lower() in status.lower(): token_replace = re.compile( re.escape(token.name), re.IGNORECASE) status = token_replace.sub( token.value, status) replaced = True break status = status.replace("{", " ") status = status.replace("}", " ") return status try: status = await asyncio.wait_for( translate_presence(user_info, status), 1) except asyncio.TimeoutError: logger.info( f"Timed out translating presence {user_info.rich_presence} using {translations_cache[int(game_id)]}" ) status = None elif "#" in status or re.findall(check_for_params, status): logger.info( f"Skipping not simple rich presence status {status}") status = None except Exception as e: logger.info( f"Unable to translate rich presence {status} due to {repr(e)}" ) status = None return UserPresence(presence_state=state, game_id=game_id, game_title=game_title, in_game_status=status)
async def get_user_presence(self, user_id: str, context: Any) -> UserPresence: for user in context: if user_id in user: return user[user_id] return UserPresence(PresenceState.Unknown)
async def test_get_user_presence_success(plugin, read, write): context = "abc" user_ids = ["666", "13", "42", "69"] plugin.prepare_user_presence_context.return_value = async_return_value( context) request = { "jsonrpc": "2.0", "id": "11", "method": "start_user_presence_import", "params": { "user_ids": user_ids } } read.side_effect = [ async_return_value(create_message(request)), async_return_value(b"", 10) ] plugin.get_user_presence.side_effect = [ async_return_value( UserPresence(PresenceState.Unknown, "game-id1", None, "unknown state")), async_return_value( UserPresence(PresenceState.Offline, None, None, "Going to grandma's house")), async_return_value( UserPresence(PresenceState.Online, "game-id3", "game-title3", "Pew pew")), async_return_value( UserPresence(PresenceState.Away, None, "game-title4", "AFKKTHXBY")), ] await plugin.run() plugin.get_user_presence.assert_has_calls( [call(user_id, context) for user_id in user_ids]) plugin.user_presence_import_complete.assert_called_once_with() assert get_messages(write) == [{ "jsonrpc": "2.0", "id": "11", "result": None }, { "jsonrpc": "2.0", "method": "user_presence_import_success", "params": { "user_id": "666", "presence": { "presence_state": PresenceState.Unknown.value, "game_id": "game-id1", "presence_status": "unknown state" } } }, { "jsonrpc": "2.0", "method": "user_presence_import_success", "params": { "user_id": "13", "presence": { "presence_state": PresenceState.Offline.value, "presence_status": "Going to grandma's house" } } }, { "jsonrpc": "2.0", "method": "user_presence_import_success", "params": { "user_id": "42", "presence": { "presence_state": PresenceState.Online.value, "game_id": "game-id3", "game_title": "game-title3", "presence_status": "Pew pew" } } }, { "jsonrpc": "2.0", "method": "user_presence_import_success", "params": { "user_id": "69", "presence": { "presence_state": PresenceState.Away.value, "game_title": "game-title4", "presence_status": "AFKKTHXBY" } } }, { "jsonrpc": "2.0", "method": "user_presence_import_finished", "params": None }]
async def get_rdo_stats(self, user_id, friend_name): headers = { 'Authorization': f'Bearer {self._current_sc_token}', 'User-Agent': USER_AGENT, 'X-Requested-With': 'XMLHttpRequest' } try: resp = await self._current_session.get("https://scapi.rockstargames.com/games/rdo/navigationData?platform=pc&" f"rockstarId={user_id}", headers=headers) await self._update_cookies_from_response(resp) resp_json = await resp.json() except AssertionError: await self._refresh_credentials_social_club_light() return await self.get_rdo_stats(user_id, friend_name) try: char_name = resp_json['result']['onlineCharacterName'] char_rank = resp_json['result']['onlineCharacterRank'] except KeyError: if LOG_SENSITIVE_DATA: log.debug(f"ROCKSTAR_RED_DEAD_ONLINE_STATS_MISSING: {friend_name} (Rockstar ID: {user_id}) does not " f"have any character stats for Red Dead Online. Returning default user presence...") return await self.get_last_played_game(friend_name) if LOG_SENSITIVE_DATA: log.debug(f"ROCKSTAR_RED_DEAD_ONLINE_STATS_PARTIAL: {friend_name} (Rockstar ID: {user_id}) has a character " f"named {char_name}, who is at rank {str(char_rank)}.") # As an added bonus, we will find the user's preferred role (bounty hunter, collector, or trader). This is # determined by the acquired rank in each role. resp = await self._current_session.get("https://scapi.rockstargames.com/games/rdo/awards/progress?platform=pc&" f"rockstarId={user_id}", headers=headers) await self._update_cookies_from_response(resp) resp_json = await resp.json() ranks = { "Bounty Hunter": None, "Collector": None, "Trader": None } for goal in resp_json['challengeGoals']: if goal['id'] == "MPAC_Role_BountyHunter_001": ranks['Bounty Hunter'] = goal['goalValue'] elif goal['id'] == "MPAC_Role_Collector_001": ranks['Collector'] = goal['goalValue'] elif goal['id'] == "MPAC_Role_Trader_001": ranks['Trader'] = goal['goalValue'] for rank, val in ranks.items(): if not val: break else: break max_rank = 0 highest_rank = "" for rank, val in ranks.items(): if val > max_rank: max_rank = val highest_rank = rank # If two roles have the same rank, then the character is considered to have a Hybrid role. elif val == max_rank and max_rank != 0: highest_rank = "Hybrid" break if LOG_SENSITIVE_DATA: log.debug(f"ROCKSTAR_RED_DEAD_ONLINE_STATS: [{friend_name}] Red Dead Online: {char_name} - Rank {char_rank}" f" {highest_rank}") return UserPresence(PresenceState.Online, game_id="13", in_game_status=f"Red Dead Online: {char_name} - Rank {char_rank} {highest_rank}")
async def get_gta_online_stats(self, user_id, friend_name): class GTAOnlineStatParser(HTMLParser): char_rank = None char_title = None rank_internal_pos = None def handle_starttag(self, tag, attrs): if not self.rank_internal_pos and tag == "div" and len(attrs) > 0: class_, name = attrs[0] if not re.search(r"^rankHex right-grad .*", name): return self.rank_internal_pos = self.getpos()[0] def handle_data(self, data): if not self.rank_internal_pos: return if not self.char_rank and self.getpos()[0] == (self.rank_internal_pos + 1): self.char_rank = data elif not self.char_title and self.getpos()[0] == (self.rank_internal_pos + 2): # There is a bug in the Social Club API where a user who is past rank 105 no longer has their title # shown. However, they are still a "Kingpin." if int(self.char_rank) >= 105: self.char_title = "Kingpin" else: self.char_title = data def get_stats(self): return self.char_rank, self.char_title url = ("https://socialclub.rockstargames.com/games/gtav/career/overviewAjax?character=Freemode&" f"rockstarIds={user_id}&slot=Freemode&nickname={friend_name}&gamerHandle=&gamerTag=&category=Overview" f"&_={int(time() * 1000)}") headers = { 'Accept': 'text/html, */*', 'Cookie': await self.get_cookies_for_headers(), "RequestVerificationToken": await self._get_request_verification_token( "https://socialclub.rockstargames.com/games/gtav/pc/career/overview/gtaonline", "https://socialclub.rockstargames.com/games"), 'User-Agent': USER_AGENT } while True: try: resp = await self._current_session.get(url, headers=headers) await self._update_cookies_from_response(resp) break except aiohttp.ClientResponseError as e: if e.status == 429: await asyncio.sleep(5) else: raise e except Exception: raise resp_text = await resp.text() parser = GTAOnlineStatParser() parser.feed(resp_text) rank, title = parser.get_stats() parser.close() if rank and title: log.debug(f"ROCKSTAR_GTA_ONLINE_STATS: [{friend_name}] Grand Theft Auto Online: Rank {rank} {title}") return UserPresence(PresenceState.Online, game_id="11", in_game_status=f"Grand Theft Auto Online: Rank {rank} {title}") else: if LOG_SENSITIVE_DATA: log.debug(f"ROCKSTAR_GTA_ONLINE_STATS_MISSING: {friend_name} (Rockstar ID: {user_id}) does not have " f"any character stats for Grand Theft Auto Online. Returning default user presence...") return await self.get_last_played_game(friend_name)
async def get_user_presence(self, user_id, context): state = context.get(user_id, PresenceState.Offline) return UserPresence(state)
@dataclass class token_translations_parametrized_mock_dataclass_EN: name = "#EN" value = "_english" @dataclass class translations_cache_parametrized_mock_dataclass: tokens = [token_translations_parametrized_mock_dataclass_menu(), token_translations_parametrized_mock_dataclass_EN()] @pytest.mark.parametrize( "user_info,user_presence", [ # Empty ( ProtoUserInfo(), UserPresence(presence_state=PresenceState.Unknown) ), # Offline ( ProtoUserInfo(state=EPersonaState.Offline), UserPresence(presence_state=PresenceState.Offline) ), # User online not playing a game ( ProtoUserInfo(state=EPersonaState.Online, game_id=0, game_name="", rich_presence={}), UserPresence(presence_state=PresenceState.Online, game_id=None, game_title=None, in_game_status=None) ), # User online playing a game ( ProtoUserInfo(state=EPersonaState.Online, game_id=1512, game_name="abc", rich_presence={"status": "menu"}), UserPresence(
import pytest from protocol.consts import EPersonaState from protocol.types import UserInfo from presence import from_user_info from galaxy.api.errors import AuthenticationRequired, UnknownError from galaxy.api.consts import PresenceState from galaxy.api.types import UserPresence @pytest.mark.parametrize( "user_info,user_presence", [ # Empty (UserInfo(), UserPresence(presence_state=PresenceState.Unknown)), # Offline (UserInfo(state=EPersonaState.Offline), UserPresence(presence_state=PresenceState.Offline)), # User online not playing a game (UserInfo(state=EPersonaState.Online, game_id=0, game_name="", rich_presence={}), UserPresence(presence_state=PresenceState.Online, game_id=None, game_title=None, in_game_status=None)), # User online playing a game (UserInfo(state=EPersonaState.Online, game_id=1512,
tokens = [TOKEN("#menu", "translated_menu{%param0%}"), TOKEN("#EN", "english")] @dataclass class translations_cache_values_fitting_partially_dataclass: tokens = [TOKEN("#PlayingAs", "Playing %Empire% (%Ethics1% %Ethics2%) in %Year%"), TOKEN("#PlayingAsNew", "Playing %Empire% (%Ethics%) in %Year%"), TOKEN("#PlayingAsNoEthics", "Playing %Empire% in %Year%")] @pytest.mark.parametrize( "user_info,user_presence", [ # Empty ( ProtoUserInfo(), UserPresence(presence_state=PresenceState.Unknown) ), # Offline ( ProtoUserInfo(state=EPersonaState.Offline), UserPresence(presence_state=PresenceState.Offline) ), # User online not playing a game ( ProtoUserInfo(state=EPersonaState.Online, game_id=0, game_name="", rich_presence={}), UserPresence(presence_state=PresenceState.Online, game_id=None, game_title=None, in_game_status=None) ), # User online playing a game ( ProtoUserInfo(state=EPersonaState.Online, game_id=1512, game_name="abc", rich_presence={"status": "menu"}), UserPresence(