Skip to content
Draft
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
75 changes: 32 additions & 43 deletions src/bnet_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -132,35 +132,36 @@ async def _send_message(self, service_name, method_id, body, callback=None):
await self.writer.drain()

# @todo
async def _on_resource__content_handle(self, entity_id, account_info, future: asyncio.Future, header, body):
async def _on_resource__content_handle(self, future: asyncio.Future, header, body):
response = content_handle_pb2.ContentHandle()
response.ParseFromString(body)
# log.debug(f"fetched content:content_handle token: {header.token} body: {response}")

future.set_result(account_info)
future.set_result({})

async def _on_presence__query_game_account(self, entity_id, account_info, future: asyncio.Future, header, body):
async def _on_presence__query_game_account(self, future: asyncio.Future, header, body):
if not body:
log.warning("failed presence:query_response")
future.set_result(account_info)
future.set_result({})
return

response = presence_service_pb2.QueryResponse()
response.ParseFromString(body)
# log.debug(f"fetched presence:query_response token: {header.token} body: {response}")

game_account_info = {}
for f in response.field:
if f.key.group != 2:
continue

if f.key.field == 1:
account_info["game_account_is_online"] = f.value.bool_value
game_account_info["is_online"] = f.value.bool_value
elif f.key.field == 2:
account_info["game_account_is_busy"] = f.value.bool_value
game_account_info["is_busy"] = f.value.bool_value
elif f.key.field == 3:
account_info["game_account_program"] = f.value.fourcc_value # e.g. "App", "BSAp" or "Pro" (for Overwatch)
game_account_info["program"] = f.value.fourcc_value # e.g. "App", "BSAp" or "Pro" (for Overwatch)
elif f.key.field == 4:
account_info["game_account_last_online"] = datetime.fromtimestamp(f.value.int_value / 1000 / 1000) # e.g. 1584194739362351 (microseconds timestamp)
game_account_info["last_online"] = datetime.fromtimestamp(f.value.int_value / 1000 / 1000) # e.g. 1584194739362351 (microseconds timestamp)
elif f.key.field == 5:
continue
elif f.key.field == 6:
Expand All @@ -170,21 +171,17 @@ async def _on_presence__query_game_account(self, entity_id, account_info, future
elif f.key.field == 8:
rich_presence = presence_types_pb2.RichPresence()
rich_presence.ParseFromString(f.value.message_value) # e.g. "\rorP\000\025aorp\030\025"
account_info["game_account_rich_presence"] = rich_presence
game_account_info["rich_presence"] = rich_presence
elif f.key.field == 9:
continue
elif f.key.field == 10:
account_info["game_account_is_away"] = f.value.bool_value
game_account_info["is_away"] = f.value.bool_value
elif f.key.field == 11:
continue

if "game_account_rich_presence" not in account_info:
future.set_result(account_info)
return
future.set_result(game_account_info)

await self.fetch_friend_presence_game_presence_details(entity_id, account_info, future)

async def _on_presence__query_account(self, entity_id, account_info, future: asyncio.Future, header, body):
async def _on_presence__query_account(self, future: asyncio.Future, header, body):
if not body:
log.error("failed presence:query_response")
future.set_result({})
Expand All @@ -194,19 +191,17 @@ async def _on_presence__query_account(self, entity_id, account_info, future: asy
response.ParseFromString(body)
# log.debug(f"fetched presence:query_response token: {header.token} body: {response}")

num_game_accounts = 0

account_info = {}
for f in response.field:
if f.key.group != 1:
continue

if f.key.field == 1:
account_info["full_name"] = f.value.string_value.encode('utf-8') # e.g. "Firstname Lastname"
elif f.key.field == 3:
num_game_accounts += 1
if f"game_account_{num_game_accounts}" not in account_info:
account_info[f"game_account_{num_game_accounts}"] = {}
account_info[f"game_account_{num_game_accounts}"]["id"] = f.value.entityid_value # e.g. high: 144115197778542960 low: 131237370
if "game_accounts" not in account_info:
account_info["game_accounts"] = []
account_info["game_accounts"].append({"id": f.value.entityid_value}) # e.g. high: 144115197778542960 low: 131237370
elif f.key.field == 4:
account_info["battle_tag"] = f.value.string_value # e.g. "Username#1234"
elif f.key.field == 6:
Expand All @@ -218,13 +213,7 @@ async def _on_presence__query_account(self, entity_id, account_info, future: asy
elif f.key.field == 11:
account_info["is_busy"] = f.value.bool_value

if num_game_accounts == 0:
future.set_result(account_info)
return

for i in range(num_game_accounts):
game_account_id = account_info[f"game_account_{i + 1}"]["id"]
await self.fetch_friend_presence_game_account_details(entity_id, game_account_id, account_info, future)
future.set_result(account_info)

async def _on_friends__subscribe_to_friends(self, future: asyncio.Future, header, body):
if not body:
Expand Down Expand Up @@ -382,12 +371,12 @@ async def fetch_friend_battle_tag(self, entity_id, future: asyncio.Future):
key.group = 1 # account
key.field = 4 # battle_tag

await self._send_message(self._PRESENCE_SERVICE, 4, request, functools.partial(self._on_presence__query_account, entity_id, {}, future))
await self._send_message(self._PRESENCE_SERVICE, 4, request, functools.partial(self._on_presence__query_account, future))

async def fetch_friend_presence_account_details(self, entity_id, future: asyncio.Future):
async def fetch_friend_presence_account_details(self, account_id, future: asyncio.Future):
request = presence_service_pb2.QueryRequest()
request.entity_id.high = entity_id.high
request.entity_id.low = entity_id.low
request.entity_id.high = account_id.high
request.entity_id.low = account_id.low
for i in [3, 4]:
key = request.key.add()
key.program = 0x424e
Expand All @@ -404,9 +393,9 @@ async def fetch_friend_presence_account_details(self, entity_id, future: asyncio
# key.field = 8 (away_time) return e.g. 1583607638026991
# key.field = 11 (is_busy) always returns false

await self._send_message(self._PRESENCE_SERVICE, 4, request, functools.partial(self._on_presence__query_account, entity_id, {}, future))
await self._send_message(self._PRESENCE_SERVICE, 4, request, functools.partial(self._on_presence__query_account, future))

async def fetch_friend_presence_game_account_details(self, entity_id, game_account_id, account_info, future: asyncio.Future):
async def fetch_friend_presence_game_account_details(self, game_account_id, future: asyncio.Future):
request = presence_service_pb2.QueryRequest()
request.entity_id.high = game_account_id.high
request.entity_id.low = game_account_id.low
Expand All @@ -416,7 +405,7 @@ async def fetch_friend_presence_game_account_details(self, entity_id, game_accou
key.group = 2 # 2 game account
key.field = i

# key.field = 1 (is online) works for all not-offline friends and always returns true
# key.field = 1 (is online) works for all not-offline friends and returns true
# key.field = 2 (is_busy???)
# key.field = 3 (program_id) works for all not-offline friends and returns e.g. "BSAp", "Pro" (for Overwatch)
# key.field = 4 (last_online???) returns e.g. 1584194739362351
Expand All @@ -425,18 +414,18 @@ async def fetch_friend_presence_game_account_details(self, entity_id, game_accou
# key.field = 7 (account_id???) returns e.g. high: 72057594037927936 low: 101974425
# key.field = 8 (rich_presence) only returns when user is in-game, e.g. "\rorP\000\025aorp\030\025"
# key.field = 9 (???) returns e.g. 1584228809551117
# key.field = 10 (is_away???)
# key.field = 10 (is_away) works for all not-offline friends and returns true/false
# key.field = 11 (last_online???)

await self._send_message(self._PRESENCE_SERVICE, 4, request, functools.partial(self._on_presence__query_game_account, entity_id, account_info, future))
await self._send_message(self._PRESENCE_SERVICE, 4, request, functools.partial(self._on_presence__query_game_account, future))

async def fetch_friend_presence_game_presence_details(self, entity_id, account_info, future: asyncio.Future):
async def fetch_friend_presence_game_presence_details(self, rich_presence, future: asyncio.Future):
request = resource_service_pb2.ContentHandleRequest()
request.program_id = account_info["game_account_rich_presence"].program_id
request.stream_id = account_info["game_account_rich_presence"].stream_id
# message_id = account_info["game_account_rich_presence"].index
request.program_id = rich_presence.program_id
request.stream_id = rich_presence.stream_id
# message_id = rich_presence.index

await self._send_message(self._RESOURCES_SERVICE, 1, request, functools.partial(self._on_resource__content_handle, entity_id, account_info, future))
await self._send_message(self._RESOURCES_SERVICE, 1, request, functools.partial(self._on_resource__content_handle, future))

async def fetch_friends_list(self, future: asyncio.Future):
request = friends_service_pb2.SubscribeToFriendsRequest()
Expand Down
35 changes: 13 additions & 22 deletions src/plugin.py
Original file line number Diff line number Diff line change
Expand Up @@ -291,39 +291,30 @@ async def get_user_presence(self, user_id: str, context: Any) -> UserPresence:
raise BackendError("not authenticated with battle.net")

friend_presence = await self.social_features.get_friend_presence(user_id)
# user_info = context.get(user_id)
# if user_info is None:
# raise UnknownError(
# "User {} not in friend list (plugin only supports fetching presence for friends)".format(user_id)
# )

if friend_presence is None or "game_account_is_online" not in friend_presence:
if friend_presence is None or "game_accounts" not in friend_presence:
return UserPresence(presence_state=PresenceState.Offline)

# collect relevant info from all game_accounts (program, state)
_programs = [game_account.get('program') for game_account in friend_presence["game_accounts"]]
_away_states = [game_account.get('is_away', False) for game_account in friend_presence["game_accounts"]]

game_id = None
game_title = None
if "game_account_program" in friend_presence:
for game in Blizzard.BATTLENET_GAMES:
if game.family == friend_presence["game_account_program"]: # can be a game family, e.g. "Pro" for Overwatch
game_id = game.uid
game_title = game.name
break

in_game_status = None
# if "rich_presence" in friend_presence:
# in_game_status = None # friend_presence["rich_presence"] # .program_id .stream_id
for game in Blizzard.BATTLENET_GAMES:
if game.family in _programs: # program can be a game family, e.g. "Pro" for Overwatch
game_id = game.uid
game_title = game.name

state = PresenceState.Online
if "game_account_is_away" in friend_presence and friend_presence["game_account_is_away"]:
state = PresenceState.Away
if "game_account_is_busy" in friend_presence and friend_presence["game_account_is_busy"]:
state = PresenceState.Away
state = PresenceState.Away
if False in _away_states: # if one game_account says is_away = False, than the user is online. yeah.
state = PresenceState.Online

return UserPresence(
presence_state=state,
game_id=game_id, # e.g. "5272175" for Overwatch
game_title=game_title,
in_game_status=in_game_status,
in_game_status=None,
full_status=None
)

Expand Down
18 changes: 14 additions & 4 deletions src/social.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@ class SocialFeatures(object):
def __init__(self, _bnet_client):
self.bnet_client = _bnet_client
self._friends = {}
self._friends_presence = {}

async def get_friends(self):
_friends_future = asyncio.get_running_loop().create_future()
Expand All @@ -28,12 +27,23 @@ async def get_friends(self):
return self._friends

async def get_friend_presence(self, user_id):
if user_id not in self._friends:
return None

_friend_presence_future = asyncio.get_running_loop().create_future()
await self.bnet_client.fetch_friend_presence_account_details(self._friends[user_id].id, _friend_presence_future)
await _friend_presence_future

self._friends_presence[user_id] = _friend_presence_future.result()
account_info = _friend_presence_future.result()

if "game_accounts" in account_info:
for game_account in account_info["game_accounts"]:
_game_account_future = asyncio.get_running_loop().create_future()
await self.bnet_client.fetch_friend_presence_game_account_details(game_account["id"], _game_account_future)
await _game_account_future

game_account.update(_game_account_future.result())

log.debug(f"fetched friend presence [id = user_id]: {json.dumps(self._friends_presence[user_id], default=str)}")
log.debug(f"fetched friend presence ({user_id}): {json.dumps(account_info, default=str)}")

return self._friends_presence[user_id]
return account_info