def test_simple(self): pairs = [ ('a', 'test'), ('a2', b'\xff\xfe0\x041\x042\x043\x04'.decode('utf-16')), ('bb', 1), ('bb2', -500), ('ccc', 1.0), ('dddd', vdf.POINTER(1234)), ('fffff', vdf.COLOR(1234)), ('gggggg', vdf.UINT_64(1234)), ('hhhhhhh', vdf.INT_64(-1234)), ] data = OrderedDict(pairs) data['level1-1'] = OrderedDict(pairs) data['level1-1']['level2-1'] = OrderedDict(pairs) data['level1-1']['level2-2'] = OrderedDict(pairs) data['level1-2'] = OrderedDict(pairs) result = vdf.binary_loads(vdf.binary_dumps(data), mapper=OrderedDict) self.assertEqual(data, result) result = vdf.binary_loads(vdf.binary_dumps(data, alt_format=True), mapper=OrderedDict, alt_format=True) self.assertEqual(data, result) result = vdf.vbkv_loads(vdf.vbkv_dumps(data), mapper=OrderedDict) self.assertEqual(data, result)
def test_simple(self): pairs = [ ('a', 'test'), ('a2', b'\xd0\xb0\xd0\xb1\xd0\xb2\xd0\xb3'.decode('utf-8')), ('bb', 1), ('bb2', -500), ('ccc', 1.0), ('dddd', vdf.POINTER(1234)), ('fffff', vdf.COLOR(1234)), ('gggggg', vdf.UINT_64(1234)), ('hhhhhhh', vdf.INT_64(-1234)), ] data = OrderedDict(pairs) data['level1-1'] = OrderedDict(pairs) data['level1-1']['level2-1'] = OrderedDict(pairs) data['level1-1']['level2-2'] = OrderedDict(pairs) data['level1-2'] = OrderedDict(pairs) result = vdf.binary_loads(vdf.binary_dumps(data), mapper=OrderedDict) self.assertEqual(data, result) result = vdf.binary_loads(vdf.binary_dumps(data, alt_format=True), mapper=OrderedDict, alt_format=True) self.assertEqual(data, result) result = vdf.vbkv_loads(vdf.vbkv_dumps(data), mapper=OrderedDict) self.assertEqual(data, result)
def test_raise_on_remaining(self): # default binary_loads is to raise with self.assertRaises(SyntaxError): vdf.binary_loads(b'\x01key\x00value\x00\x08' + b'aaaa') # do not raise self.assertEqual( vdf.binary_loads(b'\x01key\x00value\x00\x08' + b'aaaa', raise_on_remaining=False), {'key': 'value'})
def test_merge_multiple_keys_off(self): # VDFDict([('a', VDFDict([('a', '1'), ('b', '2')])), ('a', VDFDict([('a', '3'), ('c', '4')]))]) test = b'\x00a\x00\x01a\x001\x00\x01b\x002\x00\x08\x00a\x00\x01a\x003\x00\x01c\x004\x00\x08\x08' result = {'a': {'a': '3', 'c': '4'}} self.assertEqual(vdf.binary_loads(test, merge_duplicate_keys=False), result)
def appinfo_loads(data): # These should always be present. version, universe = struct.unpack_from("<II",data,0) offset = 8 if version != 0x07564427 and universe != 1: raise ValueError("Invalid appinfo header") result = {} # Parsing applications app_fields = namedtuple("App",'size state last_update access_token checksum change_number') app_struct = struct.Struct("<3IQ20sI") while True: app_id = struct.unpack_from('<I',data,offset)[0] offset += 4 # AppID = 0 marks the last application in the Appinfo if app_id == 0: break # All fields are required. app_info = app_fields._make(app_struct.unpack_from(data,offset)) offset += app_struct.size size = app_info.size + 4 - app_struct.size app = vdf.binary_loads(data[offset:offset+size]) offset += size result[app_id] = app['appinfo'] return result
async def _process_package_info_response(self, body): logger.info("Processing message PICSProductInfoResponse") message = steammessages_clientserver_pb2.CMsgClientPICSProductInfoResponse() message.ParseFromString(body) apps_to_parse = [] for info in message.packages: await self.package_info_handler(str(info.packageid)) if info.packageid == 0: # Packageid 0 contains trash entries for every user logger.info("Skipping packageid 0 ") continue package_content = vdf.binary_loads(info.buffer[4:]) for app in package_content[str(info.packageid)]['appids']: await self.app_info_handler(mother_appid=str(info.packageid), appid=str(package_content[str(info.packageid)]['appids'][app])) apps_to_parse.append(package_content[str(info.packageid)]['appids'][app]) for info in message.apps: app_content = vdf.loads(info.buffer[:-1].decode('utf-8', 'replace')) try: if app_content['appinfo']['common']['type'].lower() == 'game': logger.info(f"Retrieved game {app_content['appinfo']['common']['name']}") await self.app_info_handler(appid=str(app_content['appinfo']['appid']), title=app_content['appinfo']['common']['name'], game=True) else: await self.app_info_handler(appid=str(app_content['appinfo']['appid']), game=False) except KeyError: # Unrecognized app type await self.app_info_handler(appid=str(app_content['appinfo']['appid']), game=False) if len(apps_to_parse) > 0: logger.info(f"Apps to parse {apps_to_parse}, {len(apps_to_parse)} entries") await self.get_apps_info(apps_to_parse)
def get_product_info(self, appids=[], packageids=[]): try: resp = self.steam.send_job_and_wait(MsgProto(EMsg.ClientPICSProductInfoRequest), { 'apps': map(lambda x: {'appid': x}, appids), 'packages': map(lambda x: {'packageid': x}, packageids), }, timeout=10 ) if not resp: return {} resp = proto_to_dict(resp) for app in resp.get('apps', []): app['appinfo'] = vdf.loads( app.pop('buffer')[:-1].decode('utf-8', 'replace'))['appinfo'] app['sha'] = hexlify(app['sha']).decode('utf-8') for pkg in resp.get('packages', []): pkg['appinfo'] = vdf.binary_loads(pkg.pop('buffer')[4:])[ str(pkg['packageid'])] pkg['sha'] = hexlify(pkg['sha']).decode('utf-8') return resp except Exception as e: print('get_product_info() exception: ' + str(e)) return {}
def get_change_number(self, appids=None): if not appids: appids = [] else: appids = [appids] packageids = [] resp = self.steam.send_job_and_wait( MsgProto(EMsg.ClientPICSProductInfoRequest), { 'apps': map(lambda x: {'appid': x}, appids), 'packages': map(lambda x: {'packageid': x}, packageids), }, timeout=10) if not resp: return {} resp = proto_to_dict(resp) for app in resp.get('apps', []): app['appinfo'] = vdf.loads( app.pop('buffer').rstrip('\x00'))['appinfo'] app['sha'] = hexlify(app['sha']) for pkg in resp.get('packages', []): pkg['appinfo'] = vdf.binary_loads(pkg.pop('buffer')[4:])[str( pkg['packageid'])] pkg['sha'] = hexlify(pkg['sha']) return resp['apps'][0]['change_number']
def get_product_info(self, apps=[], packages=[], timeout=15): """Get product info for apps and packages :param apps: items in the list should be either just ``app_id``, or ``(app_id, access_token)`` :type apps: :class:`list` :param packages: items in the list should be either just ``package_id``, or ``(package_id, access_token)`` :type packages: :class:`list` :return: dict with ``apps`` and ``packages`` containing their info, see example below :rtype: :class:`dict`, :class:`None` .. code:: python {'apps': {570: {...}, ...}, 'packages': {123: {...}, ...} } """ if not apps and not packages: return message = MsgProto(EMsg.ClientPICSProductInfoRequest) for app in apps: app_info = message.body.apps.add() app_info.only_public = False if isinstance(app, tuple): app_info.appid, app_info.access_token = app else: app_info.appid = app for package in packages: package_info = message.body.packages.add() if isinstance(package, tuple): package_info.appid, package_info.access_token = package else: package_info.packageid = package message.body.meta_data_only = False job_id = self.send_job(message) data = dict(apps={}, packages={}) while True: chunk = self.wait_event(job_id, timeout=timeout) if chunk is None: return chunk = chunk[0].body for app in chunk.apps: data['apps'][app.appid] = vdf.loads(app.buffer[:-1].decode( 'utf-8', 'replace'))['appinfo'] for pkg in chunk.packages: data['packages'][pkg.packageid] = vdf.binary_loads( pkg.buffer[4:])[str(pkg.packageid)] if not chunk.response_pending: break return data
def get_product_info(self, apps=[], packages=[], timeout=15): """Get product info for apps and packages :param apps: items in the list should be either just ``app_id``, or ``(app_id, access_token)`` :type apps: :class:`list` :param packages: items in the list should be either just ``package_id``, or ``(package_id, access_token)`` :type packages: :class:`list` :return: dict with ``apps`` and ``packages`` containing their info, see example below :rtype: :class:`dict`, :class:`None` .. code:: python {'apps': {570: {...}, ...}, 'packages': {123: {...}, ...} } """ if not apps and not packages: return message = MsgProto(EMsg.ClientPICSProductInfoRequest) for app in apps: app_info = message.body.apps.add() app_info.only_public = False if isinstance(app, tuple): app_info.appid, app_info.access_token = app else: app_info.appid = app for package in packages: package_info = message.body.packages.add() if isinstance(package, tuple): package_info.appid, package_info.access_token = package else: package_info.packageid = package message.body.meta_data_only = False job_id = self.send_job(message) data = dict(apps={}, packages={}) while True: chunk = self.wait_event(job_id, timeout=timeout) if chunk is None: return chunk = chunk[0].body for app in chunk.apps: data['apps'][app.appid] = vdf.loads(app.buffer[:-1].decode('utf-8', 'replace'))['appinfo'] for pkg in chunk.packages: data['packages'][pkg.packageid] = vdf.binary_loads(pkg.buffer[4:])[str(pkg.packageid)] if not chunk.response_pending: break return data
def get_appinfo_sections(path): """ Parse an appinfo.vdf file and return all the deserialized binary VDF objects inside it """ # appinfo.vdf is not actually a (binary) VDF file, but a binary file # containing multiple binary VDF sections. # File structure based on comment from vdf developer: # https://github.com/ValvePython/vdf/issues/13#issuecomment-321700244 with open(path, "rb") as f: data = f.read() i = 0 # Parse the header header_size = struct.calcsize(APPINFO_STRUCT_HEADER) magic, universe = struct.unpack( APPINFO_STRUCT_HEADER, data[0:header_size] ) i += header_size if magic != b"'DV\x07": raise SyntaxError("Invalid file magic number") sections = [] section_size = struct.calcsize(APPINFO_STRUCT_SECTION) while True: # We don't need any of the fields besides 'entry_size', # which is used to determine the length of the variable-length VDF # field. # Still, here they are for posterity's sake. (appid, entry_size, infostate, last_updated, access_token, sha_hash, change_number) = struct.unpack( APPINFO_STRUCT_SECTION, data[i:i+section_size]) vdf_section_size = entry_size - 40 i += section_size try: vdf_d = vdf.binary_loads(data[i:i+vdf_section_size]) sections.append(vdf_d) except UnicodeDecodeError: # vdf is unable to decode binary VDF objects containing # invalid UTF-8 strings. # Since we're only interested in the SteamPlay manifests, # we can skip those faulty sections. # # TODO: Remove this once the upstream bug at # https://github.com/ValvePython/vdf/issues/20 # is fixed pass i += vdf_section_size if i == len(data) - 4: return sections
def get_custom_windows_shortcuts(steam_path): """ Get a list of custom shortcuts for Windows applications as a list of SteamApp objects """ # Get the Steam ID3 for the currently logged-in user steamid3 = find_current_steamid3(steam_path) shortcuts_path = os.path.join( steam_path, "userdata", str(steamid3), "config", "shortcuts.vdf" ) try: with open(shortcuts_path, "rb") as f: content = f.read() vdf_data = vdf.binary_loads(content) except IOError: logger.info( "Couldn't find custom shortcuts. Maybe none have been created yet?" ) return [] steam_apps = [] for shortcut_id, shortcut_data in vdf_data["shortcuts"].items(): # The "exe" field can also be "Exe". Account for this by making # all field names lowercase shortcut_data = {k.lower(): v for k, v in shortcut_data.items()} shortcut_id = int(shortcut_id) appid = get_appid_from_shortcut( target=shortcut_data["exe"], name=shortcut_data["appname"] ) prefix_path = os.path.join( steam_path, "steamapps", "compatdata", str(appid), "pfx" ) install_path = shortcut_data["startdir"].strip('"') if not os.path.isdir(prefix_path): continue steam_apps.append( SteamApp( appid=appid, name="Non-Steam shortcut: {}".format(shortcut_data["appname"]), prefix_path=prefix_path, install_path=install_path ) ) logger.info( "Found %d Steam shortcuts running under Proton", len(steam_apps) ) return steam_apps
def get_custom_windows_shortcuts(steam_path): """ Get a list of custom shortcuts for Windows applications as a list of SteamApp objects """ # Get the Steam ID3 for the currently logged-in user steamid3 = find_current_steamid3(steam_path) shortcuts_path = \ steam_path / "userdata" / str(steamid3) / "config" / "shortcuts.vdf" try: content = shortcuts_path.read_bytes() vdf_data = lower_dict(vdf.binary_loads(content)) except IOError: logger.info( "Couldn't find custom shortcuts. Maybe none have been created yet?" ) return [] steam_apps = [] for shortcut_id, shortcut_data in vdf_data["shortcuts"].items(): # The "exe" field can also be "Exe". Account for this by making # all field names lowercase shortcut_data = lower_dict(shortcut_data) shortcut_id = int(shortcut_id) if "appid" in shortcut_data: appid = shortcut_data["appid"] & 0xffffffff else: appid = get_appid_from_shortcut(target=shortcut_data["exe"], name=shortcut_data["appname"]) prefix_path = \ steam_path / "steamapps" / "compatdata" / str(appid) / "pfx" install_path = Path(shortcut_data["startdir"].strip('"')) if not prefix_path.is_dir(): continue steam_apps.append( SteamApp(appid=appid, name="Non-Steam shortcut: {}".format( shortcut_data["appname"]), prefix_path=prefix_path, install_path=install_path)) logger.info( "Found %d Steam shortcuts running using Steam compatibility tools", len(steam_apps)) return steam_apps
def load(self, data): buf, self.memberList = StructReader(data), list() (self.steamIdChat, self.steamIdFriend, self.chatRoomType, self.steamIdOwner, self.steamIdClan, self.chatFlags, self.enterResponse, self.numMembers ) = buf.unpack("<QQIQQ?II") self.chatRoomName = buf.read_cstring().decode('utf-8') for _ in range(self.numMembers): self.memberList.append(vdf.binary_loads(buf.read(64))['MessageObject']) self.UNKNOWN1, = buf.unpack("<I")
def load(self, data): buf, self.memberList = StructReader(data), list() (self.steamIdChat, self.steamIdFriend, self.chatRoomType, self.steamIdOwner, self.steamIdClan, self.chatFlags, self.enterResponse, self.numMembers) = buf.unpack("<QQIQQ?II") self.chatRoomName = buf.read_cstring().decode('utf-8') for _ in range(self.numMembers): self.memberList.append( vdf.binary_loads(buf.read(64))['MessageObject']) self.UNKNOWN1, = buf.unpack("<I")
async def _process_user_stats_response(self, body): logger.debug("Processing message ClientGetUserStatsResponse") message = steammessages_clientserver_pb2.CMsgClientGetUserStatsResponse( ) message.ParseFromString(body) game_id = message.game_id stats = message.stats achievs = message.achievement_blocks logger.debug(f"Processing user stats response for {message.game_id}") achievements_schema = vdf.binary_loads(message.schema, merge_duplicate_keys=False) achievements_unlocked = [] for achievement_block in achievs: achi_block_enum = 32 * (achievement_block.achievement_id - 1) for index, unlock_time in enumerate(achievement_block.unlock_time): if unlock_time > 0: if str(achievement_block.achievement_id) not in achievements_schema[str(game_id)]['stats'] or \ str(index) not in achievements_schema[str(game_id)]['stats'][str(achievement_block.achievement_id)]['bits']: logger.warning("Non existent achievement unlocked") continue try: if 'english' in achievements_schema[str( game_id)]['stats'][str( achievement_block.achievement_id)]['bits'][ str(index)]['display']['name']: name = achievements_schema[str(game_id)]['stats'][ str(achievement_block.achievement_id)]['bits'][ str(index)]['display']['name']['english'] else: name = achievements_schema[str(game_id)]['stats'][ str(achievement_block.achievement_id)]['bits'][ str(index)]['display']['name'] achievements_unlocked.append({ 'id': achi_block_enum + index, 'unlock_time': unlock_time, 'name': name }) except Exception as e: logger.error( "Unable to parse achievement %d from block %s : %s", index, str(achievement_block.achievement_id), repr(e)) logger.info(achievs) logger.info(achievements_schema) logger.info(message.schema) raise UnknownBackendResponse( f"Achievements parser error: {e.__class__}") await self.stats_handler(game_id, stats, achievements_unlocked)
def test_loads_params_invalid(self): with self.assertRaises(TypeError): vdf.binary_loads([]) with self.assertRaises(TypeError): vdf.binary_loads(11111) with self.assertRaises(TypeError): vdf.binary_loads(BytesIO()) with self.assertRaises(TypeError): vdf.binary_load(b'', b'bbbb')
async def _process_package_info_response(self, body): logger.info("Processing message PICSProductInfoResponse") message = steammessages_clientserver_pb2.CMsgClientPICSProductInfoResponse( ) message.ParseFromString(body) apps_to_parse = [] for info in message.packages: await self.package_info_handler() package_id = str(info.packageid) package_content = vdf.binary_loads(info.buffer[4:]) package = package_content.get(package_id) if package is None: continue for app in package['appids'].values(): appid = str(app) await self.app_info_handler(package_id=package_id, appid=appid) apps_to_parse.append(app) for info in message.apps: app_content = vdf.loads(info.buffer[:-1].decode( 'utf-8', 'replace')) appid = str(app_content['appinfo']['appid']) try: type_ = app_content['appinfo']['common']['type'].lower() title = app_content['appinfo']['common']['name'] parent = None if 'extended' in app_content['appinfo'] and type_ == 'dlc': parent = app_content['appinfo']['extended']['dlcforappid'] logger.info(f"Retrieved dlc {title} for {parent}") if type == 'game': logger.info(f"Retrieved game {title}") await self.app_info_handler(appid=appid, title=title, type=type_, parent=parent) except KeyError: logger.info(f"Unrecognized app structure {app_content}") await self.app_info_handler(appid=appid, title='unknown', type='unknown', parent=None) if len(apps_to_parse) > 0: logger.info( f"Apps to parse {apps_to_parse}, {len(apps_to_parse)} entries") await self.get_apps_info(apps_to_parse)
def fetch_party(): global steam3id global oldID message = MsgProto(EMsg.ClientRichPresenceRequest) message.body.steamid_request.extend([theSteamID]) message.header.routing_appid=570 resp = client.send_message_and_wait(message, EMsg.ClientRichPresenceInfo) try: rp = vdf.binary_loads(resp.rich_presence[0].rich_presence_kv)['RP'] if rp['WatchableGameID'] == oldID or 'param0' not in rp: return except KeyError: return oldID = rp['WatchableGameID'] steamIDList = re.findall(r'steam_id: (\d*)', str(rp)) messageSend = '{}: with '.format(game_modes[rp['param0']]) if len(steamIDList) == 1 or not steamIDList: messageSend = '{}: Solo queue'.format(game_modes[rp['param0']]) sendMessage(messageSend) return num = 0 for steamID in steamIDList: num += 1 userName = '' if int(steamID) == theSteamID: continue if steamID in knownUsers: userName = knownUsers[steamID] else: userName = client.get_user(steamID).name if len(steamIDList) == 2: messageSend += userName break if num == len(steamIDList): messageSend = messageSend[:-2] messageSend += ' and {}'.format(userName) break messageSend += '{}, '.format(userName) gevent.spawn(sendMessage, messageSend)
def get_appinfo_sections(path): """ Parse an appinfo.vdf file and return all the deserialized binary VDF objects inside it """ # appinfo.vdf is not actually a (binary) VDF file, but a binary file # containing multiple binary VDF sections. # File structure based on comment from vdf developer: # https://github.com/ValvePython/vdf/issues/13#issuecomment-321700244 data = path.read_bytes() i = 0 # Parse the header header_size = struct.calcsize(APPINFO_STRUCT_HEADER) magic, universe = struct.unpack( APPINFO_STRUCT_HEADER, data[0:header_size] ) i += header_size if magic != b"'DV\x07": raise SyntaxError("Invalid file magic number") sections = [] section_size = struct.calcsize(APPINFO_STRUCT_SECTION) while True: # We don't need any of the fields besides 'entry_size', # which is used to determine the length of the variable-length VDF # field. # Still, here they are for posterity's sake. (appid, entry_size, infostate, last_updated, access_token, sha_hash, change_number) = struct.unpack( APPINFO_STRUCT_SECTION, data[i:i+section_size]) vdf_section_size = entry_size - 40 i += section_size vdf_d = vdf.binary_loads(data[i:i+vdf_section_size]) sections.append(vdf_d) i += vdf_section_size if i == len(data) - 4: return sections
def register_product_key(self, key): """Register/Redeem a CD-Key :param key: CD-Key :type key: :class:`str` :return: format ``(eresult, result_details, receipt_info)`` :rtype: :class:`tuple` Example ``receipt_info``: .. code:: python {'BasePrice': 0, 'CurrencyCode': 0, 'ErrorHeadline': '', 'ErrorLinkText': '', 'ErrorLinkURL': '', 'ErrorString': '', 'LineItemCount': 1, 'PaymentMethod': 1, 'PurchaseStatus': 1, 'ResultDetail': 0, 'Shipping': 0, 'Tax': 0, 'TotalDiscount': 0, 'TransactionID': UINT_64(111111111111111111), 'TransactionTime': 1473000000, 'lineitems': {'0': {'ItemDescription': 'Half-Life 3', 'TransactionID': UINT_64(11111111111111111), 'packageid': 1234}}, 'packageid': -1} """ resp = self.send_job_and_wait( MsgProto(EMsg.ClientRegisterKey), {'key': key}, timeout=30, ) if resp: details = vdf.binary_loads(resp.purchase_receipt_info).get( 'MessageObject', None) return EResult(resp.eresult), resp.purchase_result_details, details else: return EResult.Timeout, None, None
def register_product_key(self, key): """Register/Redeem a CD-Key :param key: CD-Key :type key: :class:`str` :return: format ``(eresult, result_details, receipt_info)`` :rtype: :class:`tuple` Example ``receipt_info``: .. code:: python {'BasePrice': 0, 'CurrencyCode': 0, 'ErrorHeadline': '', 'ErrorLinkText': '', 'ErrorLinkURL': '', 'ErrorString': '', 'LineItemCount': 1, 'PaymentMethod': 1, 'PurchaseStatus': 1, 'ResultDetail': 0, 'Shipping': 0, 'Tax': 0, 'TotalDiscount': 0, 'TransactionID': UINT_64(111111111111111111), 'TransactionTime': 1473000000, 'lineitems': {'0': {'ItemDescription': 'Half-Life 3', 'TransactionID': UINT_64(11111111111111111), 'packageid': 1234}}, 'packageid': -1} """ resp = self.send_job_and_wait(MsgProto(EMsg.ClientRegisterKey), {'key': key}, timeout=30, ) if resp: details = vdf.binary_loads(resp.purchase_receipt_info).get('MessageObject', None) return EResult(resp.eresult), resp.purchase_result_details, details else: return EResult.Timeout, None, None
def get_product_info(self, appids=[], packageids=[]): resp = self.steam.send_job_and_wait(MsgProto(EMsg.ClientPICSProductInfoRequest), { 'apps': map(lambda x: {'appid': x}, appids), 'packages': map(lambda x: {'packageid': x}, packageids), }, timeout=10 ) if not resp: return {} resp = proto_to_dict(resp) for app in resp.get('apps', []): app['appinfo'] = vdf.loads(app.pop('buffer')[:-1].decode('utf-8', 'replace'))['appinfo'] app['sha'] = hexlify(app['sha']).decode('utf-8') for pkg in resp.get('packages', []): pkg['appinfo'] = vdf.binary_loads(pkg.pop('buffer')[4:])[str(pkg['packageid'])] pkg['sha'] = hexlify(pkg['sha']).decode('utf-8') return resp
def product_info_handler(packages, apps): for info in packages: self.package_info_handler() package_id = str(info.packageid) package_content = vdf.binary_loads(info.buffer[4:]) package = package_content.get(package_id) if package is None: continue for app in package['appids'].values(): appid = str(app) self.app_info_handler(package_id=package_id, appid=appid) apps_to_parse.append(app) for info in apps: app_content = vdf.loads(info.buffer[:-1].decode( 'utf-8', 'replace')) appid = str(app_content['appinfo']['appid']) try: type_ = app_content['appinfo']['common']['type'].lower() title = app_content['appinfo']['common']['name'] parent = None if 'extended' in app_content['appinfo'] and type_ == 'dlc': parent = app_content['appinfo']['extended'][ 'dlcforappid'] logger.debug(f"Retrieved dlc {title} for {parent}") if type == 'game': logger.debug(f"Retrieved game {title}") self.app_info_handler(appid=appid, title=title, type=type_, parent=parent) except KeyError: logger.warning(f"Unrecognized app structure {app_content}") self.app_info_handler(appid=appid, title='unknown', type='unknown', parent=None)
def test_loads_unterminated_string(self): with self.assertRaises(SyntaxError): vdf.binary_loads(b'\x01abbbb')
def get_product_info(self, apps=[], packages=[], timeout=15): """Get product info for apps and packages :param apps: items in the list should be either just ``app_id``, or :class:`dict` :type apps: :class:`list` :param packages: items in the list should be either just ``package_id``, or :class:`dict` :type packages: :class:`list` :return: dict with ``apps`` and ``packages`` containing their info, see example below :rtype: :class:`dict`, :class:`None` .. code:: python {'apps': {570: {...}, ...}, 'packages': {123: {...}, ...} } Access token is needed to access full information for certain apps, and also package info. Each app and package has its' own access token. If a token is required then ``_missing_token=True`` in the response. App access tokens are obtained by calling :meth:`get_access_tokens`, and are returned only when the account has a license for the specified app. Example code: .. code:: python result = client.get_product_info(apps=[123]) if result['apps'][123]['_missing_token']: tokens = client.get_access_token(apps=[123]) result = client.get_product_info(apps=[{'appid': 123, 'access_token': tokens['apps'][123] }]) .. note:: It is best to just request access token for all apps, before sending a product info request. Package tokens are located in the account license list. See :attr:`.licenses` .. code:: python result = client.get_product_info(packages=[{'packageid': 123, 'access_token': client.licenses[123].access_token, }]) """ if not apps and not packages: return message = MsgProto(EMsg.ClientPICSProductInfoRequest) for app in apps: app_info = message.body.apps.add() app_info.only_public = False if isinstance(app, dict): proto_fill_from_dict(app_info, app) else: app_info.appid = app for package in packages: package_info = message.body.packages.add() if isinstance(package, dict): proto_fill_from_dict(package_info, package) else: package_info.packageid = package message.body.meta_data_only = False job_id = self.send_job(message) data = dict(apps={}, packages={}) while True: chunk = self.wait_event(job_id, timeout=timeout, raises=True) chunk = chunk[0].body for app in chunk.apps: data['apps'][app.appid] = vdf.loads(app.buffer[:-1].decode('utf-8', 'replace'))['appinfo'] data['apps'][app.appid]['_missing_token'] = app.missing_token for pkg in chunk.packages: data['packages'][pkg.packageid] = vdf.binary_loads(pkg.buffer[4:]).get(str(pkg.packageid), {}) data['packages'][pkg.packageid]['_missing_token'] = pkg.missing_token if not chunk.response_pending: break return data
def test_loads_unknown_type(self): with self.assertRaises(SyntaxError): vdf.binary_loads(b'\x33a\x00\x08')
def test_alternative_format(self): with self.assertRaises(SyntaxError): vdf.binary_loads(b'\x00a\x00\x00b\x00\x0b\x0b') with self.assertRaises(SyntaxError): vdf.binary_loads(b'\x00a\x00\x00b\x00\x08\x08', alt_format=True)
def test_loads_unbalanced_nesting(self): with self.assertRaises(SyntaxError): vdf.binary_loads(b'\x00a\x00\x00b\x00\x08') with self.assertRaises(SyntaxError): vdf.binary_loads(b'\x00a\x00\x00b\x00\x08\x08\x08\x08')
def get_product_info(self, apps=[], packages=[], timeout=15): """Get product info for apps and packages :param apps: items in the list should be either just ``app_id``, or :class:`dict` :type apps: :class:`list` :param packages: items in the list should be either just ``package_id``, or :class:`dict` :type packages: :class:`list` :return: dict with ``apps`` and ``packages`` containing their info, see example below :rtype: :class:`dict`, :class:`None` .. code:: python {'apps': {570: {...}, ...}, 'packages': {123: {...}, ...} } When a token is needed to access the full info (e.g. branches and depots) the ``_missing_token`` will be set to ``True``. The token can be obtained by calling :meth:`get_access_tokens` if the account has a license. .. code:: python result = client.get_product_info(apps=[123]) if result['apps'][123]['_missing_token']: tokens = client.get_access_token(apps=[123]) result = client.get_product_info(apps={'appid': 123, 'access_token': tokens['apps'][123] }) """ if not apps and not packages: return message = MsgProto(EMsg.ClientPICSProductInfoRequest) for app in apps: app_info = message.body.apps.add() app_info.only_public = False if isinstance(app, dict): proto_fill_from_dict(app_info, app) else: app_info.appid = app for package in packages: package_info = message.body.packages.add() if isinstance(package, dict): proto_fill_from_dict(package_info, package) else: package_info.packageid = package message.body.meta_data_only = False job_id = self.send_job(message) data = dict(apps={}, packages={}) while True: chunk = self.wait_event(job_id, timeout=timeout, raises=True) chunk = chunk[0].body for app in chunk.apps: data['apps'][app.appid] = vdf.loads(app.buffer[:-1].decode('utf-8', 'replace'))['appinfo'] data['apps'][app.appid]['_missing_token'] = app.missing_token for pkg in chunk.packages: data['packages'][pkg.packageid] = vdf.binary_loads(pkg.buffer[4:])[str(pkg.packageid)] data['packages'][pkg.packageid]['_missing_token'] = pkg.missing_token if not chunk.response_pending: break return data
def test_loads_empty(self): self.assertEqual(vdf.binary_loads(b''), {})
import vdf import sys import os import json if len(sys.argv) < 2: print("format: {} UserGameStatsSchema_480.bin".format(sys.argv[0])) exit(0) with open(sys.argv[1], 'rb') as f: schema = vdf.binary_loads(f.read()) language = 'english' STAT_TYPE_INT = '1' STAT_TYPE_FLOAT = '2' STAT_TYPE_AVGRATE = '3' STAT_TYPE_BITS = '4' achievements_out = [] stats_out = [] for appid in schema: sch = schema[appid] stat_info = sch['stats'] for s in stat_info: stat = stat_info[s] if stat['type'] == STAT_TYPE_BITS: achs = stat['bits']
def get_product_info(self, apps=[], packages=[], meta_data_only=False, raw=False, auto_access_tokens=True, timeout=15): """Get product info for apps and packages :param apps: items in the list should be either just ``app_id``, or :class:`dict` :type apps: :class:`list` :param packages: items in the list should be either just ``package_id``, or :class:`dict` :type packages: :class:`list` :param meta_data_only: only meta data will be returned in the reponse (e.g. change number, missing_token, sha1) :type meta_data_only: :class:`bool` :param raw: Data buffer for each app is returned as bytes in its' original form. Apps buffer is text VDF, and package buffer is binary VDF :type raw: :class:`bool` :param auto_access_token: automatically request and fill access tokens :type auto_access_token: :class:`bool` :return: dict with ``apps`` and ``packages`` containing their info, see example below :rtype: :class:`dict`, :class:`None` .. code:: python {'apps': {570: {...}, ...}, 'packages': {123: {...}, ...} } Access token is needed to access full information for certain apps, and also package info. Each app and package has its' own access token. If a token is required then ``_missing_token=True`` in the response. App access tokens are obtained by calling :meth:`get_access_tokens`, and are returned only when the account has a license for the specified app. Example code: .. code:: python result = client.get_product_info(apps=[123]) if result['apps'][123]['_missing_token']: tokens = client.get_access_token(apps=[123]) result = client.get_product_info(apps=[{'appid': 123, 'access_token': tokens['apps'][123] }]) .. note:: It is best to just request access token for all apps, before sending a product info request. Package tokens are located in the account license list. See :attr:`.licenses` .. code:: python result = client.get_product_info(packages=[{'packageid': 123, 'access_token': client.licenses[123].access_token, }]) """ if not apps and not packages: return if auto_access_tokens: tokens = self.get_access_tokens( app_ids=list( map( lambda app: app['appid'] if isinstance(app, dict) else app, apps)), package_ids=list( map( lambda pkg: pkg['packageid'] if isinstance(pkg, dict) else pkg, packages))) else: tokens = None message = MsgProto(EMsg.ClientPICSProductInfoRequest) for app in apps: app_info = message.body.apps.add() if tokens: app_info.access_token = tokens['apps'].get( app['appid'] if isinstance(app, dict) else app, 0) if isinstance(app, dict): proto_fill_from_dict(app_info, app) else: app_info.appid = app for package in packages: package_info = message.body.packages.add() if tokens: package_info.access_token = tokens['packages'].get( package['packageid'] if isinstance(package, dict) else package, 0) if isinstance(package, dict): proto_fill_from_dict(package_info, package) else: package_info.packageid = package message.body.meta_data_only = meta_data_only message.body.num_prev_failed = 0 message.body.supports_package_tokens = 1 job_id = self.send_job(message) data = dict(apps={}, packages={}) while True: chunk = self.wait_event(job_id, timeout=timeout, raises=True) chunk = chunk[0].body for app in chunk.apps: if app.buffer and not raw: data['apps'][app.appid] = vdf.loads(app.buffer[:-1].decode( 'utf-8', 'replace'))['appinfo'] else: data['apps'][app.appid] = {} data['apps'][app.appid]['_missing_token'] = app.missing_token data['apps'][app.appid]['_change_number'] = app.change_number data['apps'][app.appid]['_sha'] = hexlify( app.sha).decode('ascii') data['apps'][app.appid]['_size'] = app.size if app.buffer and raw: data['apps'][app.appid]['_buffer'] = app.buffer for pkg in chunk.packages: if pkg.buffer and not raw: data['packages'][pkg.packageid] = vdf.binary_loads( pkg.buffer[4:]).get(str(pkg.packageid), {}) else: data['packages'][pkg.packageid] = {} data['packages'][ pkg.packageid]['_missing_token'] = pkg.missing_token data['packages'][ pkg.packageid]['_change_number'] = pkg.change_number data['packages'][pkg.packageid]['_sha'] = hexlify( pkg.sha).decode('ascii') data['packages'][pkg.packageid]['_size'] = pkg.size if pkg.buffer and raw: data['packages'][pkg.packageid]['_buffer'] = pkg.buffer if not chunk.response_pending: break return data
def test_merge_multiple_keys_on(self): # VDFDict([('a', VDFDict([('a', '1'), ('b', '2')])), ('a', VDFDict([('a', '3'), ('c', '4')]))]) test = b'\x00a\x00\x01a\x001\x00\x01b\x002\x00\x08\x00a\x00\x01a\x003\x00\x01c\x004\x00\x08\x08' result = {'a': {'a': '3', 'b': '2', 'c': '4'}} self.assertEqual(vdf.binary_loads(test, merge_duplicate_keys=True), result)
def test_loads_type_checks(self): with self.assertRaises(TypeError): vdf.binary_loads(None) with self.assertRaises(TypeError): vdf.binary_loads(b'', mapper=list)
def get_shortcuts(user_context): with open(paths.shortcuts_path(user_context), 'rb') as fp: vdf_dict = vdf.binary_loads(fp.read()) return list(vdf_dict.get('shortcuts').values())