def anonymous_login(self): """Login as anonymous user :return: logon result, see `CMsgClientLogonResponse.eresult <https://github.com/ValvePython/steam/blob/513c68ca081dc9409df932ad86c66100164380a6/protobufs/steammessages_clientserver.proto#L95-L118>`_ :rtype: :class:`.EResult` """ self._LOG.debug("Attempting Anonymous login") self._pre_login() self.username = None self.login_key = None message = MsgProto(EMsg.ClientLogon) message.header.steamid = SteamID(type='AnonUser', universe='Public') message.body.protocol_version = 65579 self.send(message) resp = self.wait_msg(EMsg.ClientLogOnResponse, timeout=30) return EResult(resp.body.eresult) if resp else EResult.Fail
def request_free_license(self, app_ids): """ Request license for free app(s) :param app_ids: list of app ids :type app_ids: :class:`list` :return: format (:class:`.EResult`, result_details, receipt_info) :rtype: :class:`tuple` """ resp = self.send_job_and_wait( MsgProto(EMsg.ClientRequestFreeLicense), {'appids': map(int, app_ids)}, timeout=10, ) if resp: return EResult( resp.eresult), resp.granted_appids, resp.granted_packageids else: return EResult.Timeout, None, None
def get_cdn_auth_token(self, depot_id, hostname): """Get CDN authentication token .. note:: This token is no longer needed for access to CDN files :param depot_id: depot id :type depot_id: :class:`int` :param hostname: cdn hostname :type hostname: :class:`str` :return: `CMsgClientGetCDNAuthTokenResponse <https://github.com/ValvePython/steam/blob/39627fe883feeed2206016bacd92cf0e4580ead6/protobufs/steammessages_clientserver_2.proto#L585-L589>`_ :rtype: proto message """ return self.send_job_and_wait(MsgProto(EMsg.ClientGetCDNAuthToken), { 'depot_id': depot_id, 'host_name': hostname, }, timeout=10 )
def send_message(self, message): """Send chat message to this steam user :param message: message to send :type message: str """ # new chat if self._steam.chat_mode == 2: self._steam.send_um("FriendMessages.SendMessage#1", { 'steamid': self.steam_id, 'message': message, 'chat_entry_type': EChatEntryType.ChatMsg, }) # old chat else: self._steam.send(MsgProto(EMsg.ClientFriendMsg), { 'steamid': self.steam_id, 'chat_entry_type': EChatEntryType.ChatMsg, 'message': message.encode('utf8'), })
def get_changes_since(self, change_number, app_changes=True, package_changes=False): """Get changes since a change number :param change_number: change number to use as stating point :type change_number: :class:`int` :param app_changes: whether to inclued app changes :type app_changes: :class:`bool` :param package_changes: whether to inclued package changes :type package_changes: :class:`bool` :return: `CMsgClientPICSChangesSinceResponse <https://github.com/ValvePython/steam/blob/39627fe883feeed2206016bacd92cf0e4580ead6/protobufs/steammessages_clientserver.proto#L1171-L1191>`_ :rtype: proto message instance, or :class:`None` on timeout """ return self.send_job_and_wait(MsgProto(EMsg.ClientPICSChangesSinceRequest), { 'since_change_number': change_number, 'send_app_info_changes': app_changes, 'send_package_info_changes': package_changes, }, timeout=10 )
def _handle_friends_list(self, message): incremental = message.body.bincremental if incremental == False: self._fr.clear() pstate_check = set() # update internal friends list for friend in message.body.friends: steamid = friend.ulfriendid rel = EFriendRelationship(friend.efriendrelationship) if steamid not in self._fr: suser = SteamUser(steamid, rel) self._fr[suser] = suser if rel in (2, 4): if incremental == False: pstate_check.add(steamid) if rel == EFriendRelationship.RequestRecipient: self.emit('friend_invite', suser) else: oldrel = self._fr[steamid]._data['relationship'] self._fr[steamid]._data['relationship'] = rel if rel == EFriendRelationship.No: self.emit('friend_removed', self._fr.pop(steamid)) elif oldrel in (2, 4) and rel == EFriendRelationship.Friend: self.emit('friend_new', self._fr[steamid]) # request persona state for any new entries if pstate_check: m = MsgProto(EMsg.ClientRequestFriendData) m.body.persona_state_requested = 4294967295 # request all possible flags m.body.friends.extend(pstate_check) self._steam.send(m) if not self.ready: self.ready = True self.emit('ready')
def _parse_message(self, message): emsg_id, = struct.unpack_from("<I", message) emsg = EMsg(clear_proto_bit(emsg_id)) if not self.connected and emsg != EMsg.ClientLogOnResponse: self._LOG.debug("Dropped unexpected message: %s (is_proto: %s)", repr(emsg), is_proto(emsg_id), ) return if emsg in (EMsg.ChannelEncryptRequest, EMsg.ChannelEncryptResponse, EMsg.ChannelEncryptResult, ): msg = Msg(emsg, message, parse=False) else: try: if is_proto(emsg_id): msg = MsgProto(emsg, message, parse=False) else: msg = Msg(emsg, message, extended=True, parse=False) except Exception as e: self._LOG.fatal("Failed to deserialize message: %s (is_proto: %s)", repr(emsg), is_proto(emsg_id) ) self._LOG.exception(e) return if self.count_listeners(emsg) or self.verbose_debug: msg.parse() if self.verbose_debug: self._LOG.debug("Incoming: %s\n%s" % (repr(msg), str(msg))) else: self._LOG.debug("Incoming: %s", repr(msg)) self.emit(emsg, msg) return emsg, msg
def add(self, steamid_or_accountname_or_email): """ Add/Accept a steam user to be your friend. When someone sends you an invite, use this method to accept it. :param steamid_or_accountname_or_email: steamid, account name, or email :type steamid_or_accountname_or_email: :class:`int`, :class:`.SteamID`, :class:`.SteamUser`, :class:`str` .. note:: Adding by email doesn't not work. It's only mentioned for the sake of completeness. """ m = MsgProto(EMsg.ClientAddFriend) if isinstance(steamid_or_accountname_or_email, (intBase, int)): m.body.steamid_to_add = steamid_or_accountname_or_email elif isinstance(steamid_or_accountname_or_email, SteamUser): m.body.steamid_to_add = steamid_or_accountname_or_email.steam_id else: m.body.accountname_or_email_to_add = steamid_or_accountname_or_email self._steam.send_job(m)
def change_status(self, **kwargs): """ Set name, persona state, flags .. note:: Changing persona state will also change :attr:`persona_state` :param persona_state: persona state (Online/Offline/Away/etc) :type persona_state: :class:`.EPersonaState` :param player_name: profile name :type player_name: :class:`str` :param persona_state_flags: persona state flags :type persona_state_flags: :class:`.EPersonaStateFlag` """ if not kwargs: return self.persona_state = kwargs.get('persona_state', self.persona_state) message = MsgProto(EMsg.ClientChangeStatus) proto_fill_from_dict(message.body, kwargs) self.send(message)
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 send(self, message): """Send service method request :param message: proto message instance (use :meth:`SteamUnifiedMessages.get`) :return: ``jobid`` event identifier :rtype: :class:`str` Listen for ``jobid`` on this object to catch the response. .. note:: If you listen for ``jobid`` on the client instance you will get the encapsulated message """ if message not in self._data: raise ValueError( "Supplied message seems to be invalid. Did you use 'get' method?" ) capsule = MsgProto(EMsg.ClientServiceMethod) capsule.body.method_name = self._data[message] capsule.body.serialized_method = message.SerializeToString() return self._steam.send_job(capsule)
def get_access_tokens(self, app_ids=[], package_ids=[]): """Get access tokens :param app_ids: list of app ids :type app_ids: :class:`list` :param package_ids: list of package ids :type package_ids: :class:`list` :return: dict with ``apps`` and ``packages`` containing their access tokens, see example below :rtype: :class:`dict`, :class:`None` .. code:: python {'apps': {123: 8888888886, ...}, 'packages': {456: 6666666666, ...} } """ if not app_ids and not package_ids: return resp = self.send_job_and_wait(MsgProto( EMsg.ClientPICSAccessTokenRequest), { 'appids': map(int, app_ids), 'packageids': map(int, package_ids), }, timeout=10) if resp: return { 'apps': dict( map(lambda app: (app.appid, app.access_token), resp.app_access_tokens)), 'packages': dict( map(lambda pkg: (pkg.packageid, pkg.access_token), resp.package_access_tokens)), }
def get_entries(self, start=0, end=0, data_request=None, steam_ids=None): """Get leaderboard entries. :param start: start entry, not index (e.g. rank 1 is ``start=1``) :type start: :class:`int` :param end: end entry, not index (e.g. only one entry then ``start=1,end=1``) :type end: :class:`int` :param data_request: data being requested :type data_request: :class:`steam.enums.common.ELeaderboardDataRequest` :param steam_ids: list of steam ids when using :attr:`.ELeaderboardDataRequest.Users` :type steamids: :class:`list` :return: a list of entries, see ``CMsgClientLBSGetLBEntriesResponse`` :rtype: :class:`list` :raises: :class:`LookupError` on message timeout or error """ message = MsgProto(EMsg.ClientLBSGetLBEntries) message.body.app_id = self.app_id message.body.leaderboard_id = self.id message.body.range_start = start message.body.range_end = end message.body.leaderboard_data_request = self.data_request if data_request is None else data_request if steam_ids: message.body.steamids.extend(steam_ids) resp = self._steam.send_job_and_wait(message, timeout=15) if not resp: raise LookupError("Didn't receive response within 15seconds :(") if resp.eresult != EResult.OK: raise LookupError(EResult(resp.eresult)) if resp.HasField('leaderboard_entry_count'): self.entry_count = resp.leaderboard_entry_count return resp.entries
def login(self, username, password='', login_key=None, auth_code=None, two_factor_code=None, login_id=None): """Login as a specific user :param username: username :type username: :class:`str` :param password: password :type password: :class:`str` :param login_key: login key, instead of password :type login_key: :class:`str` :param auth_code: email authentication code :type auth_code: :class:`str` :param two_factor_code: 2FA authentication code :type two_factor_code: :class:`str` :param login_id: number used for identifying logon session :type login_id: :class:`int` :return: logon result, see `CMsgClientLogonResponse.eresult <https://github.com/ValvePython/steam/blob/513c68ca081dc9409df932ad86c66100164380a6/protobufs/steammessages_clientserver.proto#L95-L118>`_ :rtype: :class:`.EResult` .. note:: Failure to login will result in the server dropping the connection, ``error`` event is fired ``auth_code_required`` event is fired when 2FA or Email code is needed. Here is example code of how to handle the situation. .. code:: python @steamclient.on(steamclient.EVENT_AUTH_CODE_REQUIRED) def auth_code_prompt(is_2fa, code_mismatch): if is_2fa: code = input("Enter 2FA Code: ") steamclient.login(username, password, two_factor_code=code) else: code = input("Enter Email Code: ") steamclient.login(username, password, auth_code=code) Codes are required every time a user logins if sentry is not setup. See :meth:`set_credential_location` """ self._LOG.debug("Attempting login") eresult = self._pre_login() if eresult != EResult.OK: return eresult self.username = username message = MsgProto(EMsg.ClientLogon) message.header.steamid = SteamID(type='Individual', universe='Public') message.body.protocol_version = 65580 message.body.client_package_version = 1561159470 message.body.client_os_type = EOSType.Windows10 message.body.client_language = "english" message.body.should_remember_password = True message.body.supports_rate_limit_response = True message.body.chat_mode = self.chat_mode if login_id is None: message.body.obfuscated_private_ip.v4 = ip_to_int( self.connection.local_address) ^ 0xF00DBAAD else: message.body.obfuscated_private_ip.v4 = login_id message.body.account_name = username if login_key: message.body.login_key = login_key else: message.body.password = password sentry = self.get_sentry(username) if sentry is None: message.body.eresult_sentryfile = EResult.FileNotFound else: message.body.eresult_sentryfile = EResult.OK message.body.sha_sentryfile = sha1_hash(sentry) if auth_code: message.body.auth_code = auth_code if two_factor_code: message.body.two_factor_code = two_factor_code self.send(message) resp = self.wait_msg(EMsg.ClientLogOnResponse, timeout=30) if resp and resp.body.eresult == EResult.OK: self.sleep(0.5) return EResult(resp.body.eresult) if resp else EResult.Fail
def get_player_count(self, appid): resp = self.steam.send_job_and_wait(MsgProto( EMsg.ClientGetNumberOfCurrentPlayersDP), {'appid': appid}, timeout=10) return proto_to_dict(resp) or {}
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 __heartbeat(self, interval): message = MsgProto(EMsg.ClientHeartBeat) while True: self.sleep(interval) self.send(message)
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 _handle_login_key(self, message): self.login_key = message.body.login_key resp = MsgProto(EMsg.ClientNewLoginKeyAccepted) resp.body.unique_id = message.body.unique_id self.send(resp)
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 login(self, username, password='', login_key=None, auth_code=None, two_factor_code=None): """ Login as a specific user :param username: username :type username: :class:`str` :param password: password :type password: :class:`str` :param login_key: login key, instead of password :type login_key: :class:`str` :param auth_code: email authentication code :type auth_code: :class:`str` :param two_factor_code: 2FA authentication code :type two_factor_code: :class:`str` .. note:: Failure to login will result in the server dropping the connection, ``error`` event is fired ``auth_code_required`` event is fired when 2FA or Email code is needed. Here is example code of how to handle the situation. .. code:: python @steamclient.on('auth_code_required') def auth_code_prompt(is_2fa, code_mismatch): if is_2fa: code = raw_input("Enter 2FA Code: ") steamclient.login(username, password, two_factor_code=code) else: code = raw_input("Enter Email Code: ") steamclient.login(username, password, auth_code=code) Codes are required every time a user logins if sentry is not setup. See :meth:`set_credential_location` """ self._LOG.debug("Attempting login") self._pre_login() self.username = username message = MsgProto(EMsg.ClientLogon) message.header.steamid = SteamID(type='Individual', universe='Public') message.body.protocol_version = 65579 message.body.client_package_version = 1771 message.body.client_os_type = EOSType.Win10 message.body.client_language = "english" message.body.should_remember_password = True message.body.account_name = username if login_key: message.body.login_key = login_key else: message.body.password = password sentry = self.get_sentry(username) if sentry is None: message.body.eresult_sentryfile = EResult.FileNotFound else: message.body.eresult_sentryfile = EResult.OK message.body.sha_sentryfile = sha1_hash(sentry) if auth_code: message.body.auth_code = auth_code if two_factor_code: message.body.two_factor_code = two_factor_code self.send(message)