Browse Source

refactor unified messages to use the new methods

* Removed SteamClient.unified_messages
* Added steam.exceptions.SteamError
pull/202/head
Rossen Georgiev 6 years ago
parent
commit
91377faa82
  1. 4
      CHANGES.md
  2. 45
      steam/client/__init__.py
  3. 59
      steam/client/builtins/gameservers.py
  4. 160
      steam/client/builtins/unified_messages.py
  5. 10
      steam/client/cdn.py
  6. 3
      steam/core/cm.py
  7. 8
      steam/core/msg/__init__.py
  8. 7
      steam/exceptions.py
  9. 11
      steam/guard.py

4
CHANGES.md

@ -6,6 +6,8 @@ This release brings some breaking changes
- Removed imports from 'steam' namespace - Removed imports from 'steam' namespace
- Replaced builtin CM server list with automatic discovery via WebAPI or DNS - Replaced builtin CM server list with automatic discovery via WebAPI or DNS
- Removed `SteamClient.unifed_messages`
- UM/ServiceMethods are now handled in the `SteamClient` instance. See `SteamClient.send_um()`
- Removed `steam.client.mixins` package - Removed `steam.client.mixins` package
- Renamed `medium` param to `backend` on `SteamAuthenticator` - Renamed `medium` param to `backend` on `SteamAuthenticator`
- Added `WebAuth.cli_login()`, handles all steps of the login process - Added `WebAuth.cli_login()`, handles all steps of the login process
@ -14,11 +16,9 @@ This release brings some breaking changes
- Added `rich_presence` property to `SteamUser` - Added `rich_presence` property to `SteamUser`
- Fixed `create_emergency_codes()` not returning codes - Fixed `create_emergency_codes()` not returning codes
- Fixed `validate_phone_number()` returning no data - Fixed `validate_phone_number()` returning no data
- Added protos for new chat via unified messages
- Updated protobufs - Updated protobufs
- Removed `SteamClient.change_email()` - Removed `SteamClient.change_email()`
- Removed `SteamClient.create_account()` - Removed `SteamClient.create_account()`
- `SteanClient.unified_messages` now expose errors by returning a tuple `(result, error)`
- `get_product_info()` now replaces invalid unicode chars - `get_product_info()` now replaces invalid unicode chars
- Updated `SteamID.is_valid` - Updated `SteamID.is_valid`
- Updated various Enums - Updated various Enums

45
steam/client/__init__.py

@ -21,13 +21,14 @@ from getpass import getpass
import logging import logging
import six import six
from steam.core.crypto import sha1_hash
from eventemitter import EventEmitter from eventemitter import EventEmitter
from steam.enums.emsg import EMsg
from steam.enums import EResult, EOSType, EPersonaState from steam.enums import EResult, EOSType, EPersonaState
from steam.core.msg import MsgProto from steam.enums.emsg import EMsg
from steam.core.cm import CMClient from steam.core.cm import CMClient
from steam.core.msg import MsgProto
from steam.core.crypto import sha1_hash
from steam.steamid import SteamID from steam.steamid import SteamID
from steam.exceptions import SteamError
from steam.client.builtins import BuiltinBase from steam.client.builtins import BuiltinBase
from steam.util import ip_from_int, ip_to_int, proto_fill_from_dict from steam.util import ip_from_int, ip_to_int, proto_fill_from_dict
@ -59,7 +60,6 @@ class SteamClient(CMClient, BuiltinBase):
self._LOG = logging.getLogger("SteamClient") self._LOG = logging.getLogger("SteamClient")
# register listners # register listners
self.on(None, self._handle_jobs)
self.on(self.EVENT_DISCONNECTED, self._handle_disconnect) self.on(self.EVENT_DISCONNECTED, self._handle_disconnect)
self.on(self.EVENT_RECONNECT, self._handle_disconnect) self.on(self.EVENT_RECONNECT, self._handle_disconnect)
self.on(EMsg.ClientNewLoginKey, self._handle_login_key) self.on(EMsg.ClientNewLoginKey, self._handle_login_key)
@ -94,6 +94,27 @@ class SteamClient(CMClient, BuiltinBase):
self.logged_on = False self.logged_on = False
CMClient.disconnect(self, *args, **kwargs) CMClient.disconnect(self, *args, **kwargs)
def _parse_message(self, message):
result = CMClient._parse_message(self, message)
if result is None:
return
emsg, msg = result
# emit job events
if msg.proto:
jobid = msg.header.jobid_target
else:
jobid = msg.header.targetJobID
if jobid not in (-1, 18446744073709551615):
self.emit("job_%d" % jobid, msg)
# emit UMs
if emsg in (EMsg.ServiceMethodResponse, EMsg.ServiceMethodSendToClient):
self.emit(msg.header.target_job_name, msg)
def _bootstrap_cm_list_from_file(self): def _bootstrap_cm_list_from_file(self):
if not self.credential_location or self.cm_servers.last_updated > 0: if not self.credential_location or self.cm_servers.last_updated > 0:
return return
@ -154,18 +175,6 @@ class SteamClient(CMClient, BuiltinBase):
except IOError as e: except IOError as e:
self._LOG.error("saving %s: %s" % (filepath, str(e))) self._LOG.error("saving %s: %s" % (filepath, str(e)))
def _handle_jobs(self, event, *args):
if isinstance(event, EMsg):
message = args[0]
if message.proto:
jobid = message.header.jobid_target
else:
jobid = message.header.targetJobID
if jobid not in (-1, 18446744073709551615):
self.emit("job_%d" % jobid, *args)
def _handle_disconnect(self, *args): def _handle_disconnect(self, *args):
self.logged_on = False self.logged_on = False
self.current_jobid = 0 self.current_jobid = 0
@ -514,7 +523,7 @@ class SteamClient(CMClient, BuiltinBase):
message = MsgProto(EMsg.ClientLogon) message = MsgProto(EMsg.ClientLogon)
message.header.steamid = SteamID(type='Individual', universe='Public') message.header.steamid = SteamID(type='Individual', universe='Public')
message.body.protocol_version = 65580 message.body.protocol_version = 65580
message.body.client_package_version = 1771 message.body.client_package_version = 1561159470
message.body.client_os_type = EOSType.Windows10 message.body.client_os_type = EOSType.Windows10
message.body.client_language = "english" message.body.client_language = "english"
message.body.should_remember_password = True message.body.should_remember_password = True
@ -571,6 +580,7 @@ class SteamClient(CMClient, BuiltinBase):
message = MsgProto(EMsg.ClientLogon) message = MsgProto(EMsg.ClientLogon)
message.header.steamid = SteamID(type='AnonUser', universe='Public') message.header.steamid = SteamID(type='AnonUser', universe='Public')
message.body.client_package_version = 1561159470
message.body.protocol_version = 65580 message.body.protocol_version = 65580
self.send(message) self.send(message)
@ -669,4 +679,3 @@ class SteamClient(CMClient, BuiltinBase):
result = self.login(username, password, None, auth_code, two_factor_code) result = self.login(username, password, None, auth_code, two_factor_code)
return result return result

59
steam/client/builtins/gameservers.py

@ -36,6 +36,7 @@ Filter code What it does
""" """
from steam.steamid import SteamID from steam.steamid import SteamID
from steam.core.msg import MsgProto from steam.core.msg import MsgProto
from steam.enums import EResult
from steam.enums.emsg import EMsg from steam.enums.emsg import EMsg
from steam.util import ip_to_int, ip_from_int, proto_to_dict from steam.util import ip_to_int, ip_from_int, proto_to_dict
@ -50,7 +51,6 @@ class GameServers(object):
class SteamGameServers(object): class SteamGameServers(object):
def __init__(self, steam): def __init__(self, steam):
self._s = steam self._s = steam
self._um = steam.unified_messages
def query(self, filter_text, max_servers=10, timeout=30, **kw): def query(self, filter_text, max_servers=10, timeout=30, **kw):
r""" r"""
@ -117,7 +117,7 @@ class SteamGameServers(object):
:type timeout: int :type timeout: int
:returns: list of servers, see below. (``None`` is returned steam doesn't respond) :returns: list of servers, see below. (``None`` is returned steam doesn't respond)
:rtype: :class:`list`, :class:`None` :rtype: :class:`list`, :class:`None`
:raises: :class:`.UnifiedMessageError` :raises: :class:`.SteamError`
Sample response: Sample response:
@ -143,20 +143,20 @@ class SteamGameServers(object):
'version': '1.35.4.0'} 'version': '1.35.4.0'}
] ]
""" """
resp, error = self._um.send_and_wait("GameServers.GetServerList#1", resp = self._s.send_um_and_wait("GameServers.GetServerList#1",
{ {
"filter": filter_text, "filter": filter_text,
"limit": max_servers, "limit": max_servers,
}, },
timeout=20, timeout=20,
) )
if error:
raise error
if resp is None: if resp is None:
return None return None
if resp.header.eresult != EResult.OK:
raise SteamError(resp.header.error_message, resp.header.eresult)
resp = proto_to_dict(resp) resp = proto_to_dict(resp.body)
if not resp: if not resp:
return [] return []
@ -175,7 +175,7 @@ class SteamGameServers(object):
:type timeout: int :type timeout: int
:return: map of ips to steamids :return: map of ips to steamids
:rtype: dict :rtype: dict
:raises: :class:`.UnifiedMessageError` :raises: :class:`.SteamError`
Sample response: Sample response:
@ -183,16 +183,16 @@ class SteamGameServers(object):
{SteamID(id=123456, type='AnonGameServer', universe='Public', instance=1234): '1.2.3.4:27060'} {SteamID(id=123456, type='AnonGameServer', universe='Public', instance=1234): '1.2.3.4:27060'}
""" """
resp, error = self._um.send_and_wait("GameServers.GetServerIPsBySteamID#1", resp = self._s.send_um_and_wait("GameServers.GetServerIPsBySteamID#1",
{"server_steamids": server_steam_ids}, {"server_steamids": server_steam_ids},
timeout=timeout, timeout=timeout,
) )
if error:
raise error
if resp is None: if resp is None:
return None return None
if resp.header.eresult != EResult.OK:
raise SteamError(resp.header.error_message, resp.header.eresult)
return {SteamID(server.steamid): server.addr for server in resp.servers} return {SteamID(server.steamid): server.addr for server in resp.body.servers}
def get_steamids_from_ips(self, server_ips, timeout=30): def get_steamids_from_ips(self, server_ips, timeout=30):
"""Resolve SteamIDs from IPs """Resolve SteamIDs from IPs
@ -203,7 +203,7 @@ class SteamGameServers(object):
:type timeout: int :type timeout: int
:return: map of steamids to ips :return: map of steamids to ips
:rtype: dict :rtype: dict
:raises: :class:`.UnifiedMessageError` :raises: :class:`.SteamError`
Sample response: Sample response:
@ -211,14 +211,13 @@ class SteamGameServers(object):
{'1.2.3.4:27060': SteamID(id=123456, type='AnonGameServer', universe='Public', instance=1234)} {'1.2.3.4:27060': SteamID(id=123456, type='AnonGameServer', universe='Public', instance=1234)}
""" """
resp, error = self._um.send_and_wait("GameServers.GetServerSteamIDsByIP#1", resp = self._s.send_um_and_wait("GameServers.GetServerSteamIDsByIP#1",
{"server_ips": server_ips}, {"server_ips": server_ips},
timeout=timeout, timeout=timeout,
) )
if error:
raise error
if resp is None: if resp is None:
return None return None
if resp.header.eresult != EResult.OK:
raise SteamError(resp.header.error_message, resp.header.eresult)
return {server.addr: SteamID(server.steamid) for server in resp.servers} return {server.addr: SteamID(server.steamid) for server in resp.body.servers}

160
steam/client/builtins/unified_messages.py

@ -1,161 +1,79 @@
""" """
:class:`SteamUnifiedMessages` provides a simply API to send and receive unified messages. Methods to call service methods, also known as unified messages
Example code: Example code:
.. code:: python .. code:: python
# the easy way # the easy way
response = client.unified_messages.send_and_wait('Player.GetGameBadgeLevels#1', { response = client.send_um_and_wait('Player.GetGameBadgeLevels#1', {
'property': 1, 'property': 1,
'something': 'value', 'something': 'value',
}) })
print(response.body)
# the other way # the other way
jobid = client.unified_message.send('Player.GetGameBadgeLevels#1', {'something': 1}) jobid = client.send_um('Player.GetGameBadgeLevels#1', {'something': 1})
response, = client.unified_message.wait_event(jobid) response = client.wait_event(jobid)
The backend might error out, but we still get response. Here is how to check for error:
.. code:: python
if response.header.eresult != EResult.OK:
print(response.header.error_message)
# i know what im doing, alright?
message = client.unified_message.get('Player.GetGameBadgeLevels#1')
message.something = 1
response = client.unified_message.send_and_wait(message)
""" """
import logging
from eventemitter import EventEmitter
from steam.core.msg import MsgProto, get_um from steam.core.msg import MsgProto, get_um
from steam.enums import EResult
from steam.enums.emsg import EMsg from steam.enums.emsg import EMsg
from steam.util import WeakRefKeyDict, proto_fill_from_dict from steam.util import proto_fill_from_dict
class UnifiedMessages(object): class UnifiedMessages(object):
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
super(UnifiedMessages, self).__init__(*args, **kwargs) super(UnifiedMessages, self).__init__(*args, **kwargs)
name = "%s.unified_messages" % self.__class__.__name__ def send_um(self, method_name, params=None):
self.unified_messages = SteamUnifiedMessages(self, name) #: instance of :class:`SteamUnifiedMessages`
class UnifiedMessageError(Exception):
def __init__(self, message, eresult=EResult.Invalid):
self.eresult = eresult
self.message = message
def __repr__(self):
return "%s(%s, %s)" % (self.__class__.__name__, self.eresult, repr(self.message))
def __str__(self):
return "(%s) %s" % (self.eresult, self.message)
class SteamUnifiedMessages(EventEmitter):
"""Simple API for send/recv of unified messages
Incoming messages are emitted as events once with their ``jobid``
and once with their method name (e.g. ``Player.GetGameBadgeLevels#1``)
"""
def __init__(self, steam, logger_name=None):
self._LOG = logging.getLogger(logger_name if logger_name else self.__class__.__name__)
self._steam = steam
self._data = WeakRefKeyDict()
steam.on(EMsg.ServiceMethod, self._handle_service_method)
steam.on(EMsg.ClientServiceMethodResponse, self._handle_client_service_method)
def emit(self, event, *args):
if event is not None:
self._LOG.debug("Emit event: %s" % repr(event))
EventEmitter.emit(self, event, *args)
def _handle_service_method(self, message):
self.emit(message.header.target_job_name, message.body)
def _handle_client_service_method(self, message):
method_name = message.body.method_name
proto = get_um(method_name, response=True)
if proto is None:
self._LOG.error("Unable to find proto for %s" % repr(method_name))
return
error = None
if message.header.eresult != EResult.OK:
error = UnifiedMessageError(message.header.error_message,
EResult(message.header.eresult),
)
resp = proto()
resp.ParseFromString(message.body.serialized_method_response)
self.emit(method_name, resp, error)
jobid = message.header.jobid_target
if jobid not in (-1, 18446744073709551615):
self.emit("job_%d" % jobid, resp, error)
def get(self, method_name):
"""Get request proto instance for given methed name
:param method_name: name for the method (e.g. ``Player.GetGameBadgeLevels#1``)
:type method_name: :class:`str`
:return: proto message instance, or :class:`None` if not found
"""
proto = get_um(method_name)
if proto is None:
return None
message = proto()
self._data[message] = method_name
return message
def send(self, message, params=None):
"""Send service method request """Send service method request
:param message: :param method_name: method name (e.g. ``Player.GetGameBadgeLevels#1``)
proto message instance (use :meth:`SteamUnifiedMessages.get`) :type method_name: :class:`str`
or method name (e.g. ``Player.GetGameBadgeLevels#1``)
:type message: :class:`str`, proto message instance
:param params: message parameters :param params: message parameters
:type params: :class:`dict` :type params: :class:`dict`
:return: ``jobid`` event identifier :return: ``job_id`` identifier
:rtype: :class:`str` :rtype: :class:`str`
Listen for ``jobid`` on this object to catch the response. 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 isinstance(message, str): proto = get_um(method_name)
message = self.get(message)
if message not in self._data:
raise ValueError("Supplied message is invalid. Use 'get' method.")
if params: if proto is None:
proto_fill_from_dict(message, params) raise ValueError("Failed to find method named: %s" % method_name)
capsule = MsgProto(EMsg.ClientServiceMethod) message = MsgProto(EMsg.ServiceMethodCallFromClient)
capsule.body.method_name = self._data[message] message.header.target_job_name = method_name
capsule.body.serialized_method = message.SerializeToString() message.body = proto()
if params:
proto_fill_from_dict(message.body, params)
return self._steam.send_job(capsule) return self.send_job(message)
def send_and_wait(self, message, params=None, timeout=10, raises=False): def send_um_and_wait(self, method_name, params=None, timeout=10, raises=False):
"""Send service method request and wait for response """Send service method request and wait for response
:param message: :param method_name: method name (e.g. ``Player.GetGameBadgeLevels#1``)
proto message instance (use :meth:`SteamUnifiedMessages.get`) :type method_name: :class:`str`
or method name (e.g. ``Player.GetGameBadgeLevels#1``)
:type message: :class:`str`, proto message instance
:param params: message parameters :param params: message parameters
:type params: :class:`dict` :type params: :class:`dict`
:param timeout: (optional) seconds to wait :param timeout: (optional) seconds to wait
:type timeout: :class:`int` :type timeout: :class:`int`
:param raises: (optional) On timeout if :class:`False` return :class:`None`, else raise :class:`gevent.Timeout` :param raises: (optional) On timeout if :class:`False` return :class:`None`, else raise :class:`gevent.Timeout`
:type raises: :class:`bool` :type raises: :class:`bool`
:return: response proto message instance :return: response message
:rtype: (proto message, :class:`.UnifiedMessageError`) :rtype: proto message instance
:raises: :class:`gevent.Timeout` :raises: :class:`gevent.Timeout`
""" """
job_id = self.send(message, params) job_id = self.send_um(method_name, params)
resp = self.wait_event(job_id, timeout, raises=raises) return self.wait_msg(job_id, timeout, raises=raises)
return (None, None) if resp is None else resp

10
steam/client/cdn.py

@ -562,8 +562,10 @@ class CDNClient(object):
:type item_id: int :type item_id: int
:returns: manifest instance :returns: manifest instance
:rtype: :class:`.CDNDepotManifest` :rtype: :class:`.CDNDepotManifest`
:raises: steam error
:rtype: :class:`.SteamError`
""" """
resp, error = self.steam.unified_messages.send_and_wait('PublishedFile.GetDetails#1', { resp = self.steam.send_um_and_wait('PublishedFile.GetDetails#1', {
'publishedfileids': [item_id], 'publishedfileids': [item_id],
'includetags': False, 'includetags': False,
'includeadditionalpreviews': False, 'includeadditionalpreviews': False,
@ -576,10 +578,10 @@ class CDNClient(object):
'language': 0 'language': 0
}, timeout=7) }, timeout=7)
if error: if resp.header.eresult != EResult.OK:
raise error raise SteamError(resp.header.error_message, resp.header.eresult)
wf = None if resp is None else resp.publishedfiledetails[0] wf = None if resp is None else resp.body.publishedfiledetails[0]
if wf is None or wf.result != EResult.OK: if wf is None or wf.result != EResult.OK:
raise ValueError("Failed getting workshop file info: %s" % repr( raise ValueError("Failed getting workshop file info: %s" % repr(

3
steam/core/cm.py

@ -263,7 +263,7 @@ class CMClient(EventEmitter):
msg = Msg(emsg, message, extended=True) msg = Msg(emsg, message, extended=True)
except Exception as e: except Exception as e:
self._LOG.fatal("Failed to deserialize message: %s (is_proto: %s)", self._LOG.fatal("Failed to deserialize message: %s (is_proto: %s)",
str(emsg), repr(emsg),
is_proto(emsg_id) is_proto(emsg_id)
) )
self._LOG.exception(e) self._LOG.exception(e)
@ -275,6 +275,7 @@ class CMClient(EventEmitter):
self._LOG.debug("Incoming: %s", repr(msg)) self._LOG.debug("Incoming: %s", repr(msg))
self.emit(emsg, msg) self.emit(emsg, msg)
return emsg, msg
def __handle_encrypt_request(self, req): def __handle_encrypt_request(self, req):
self._LOG.debug("Securing channel") self._LOG.debug("Securing channel")

8
steam/core/msg/__init__.py

@ -2,7 +2,9 @@ import fnmatch
from steam.core.msg.unified import get_um from steam.core.msg.unified import get_um
from steam.core.msg.structs import get_struct from steam.core.msg.structs import get_struct
from steam.core.msg.headers import MsgHdr, ExtendedMsgHdr, MsgHdrProtoBuf, GCMsgHdr, GCMsgHdrProto from steam.core.msg.headers import MsgHdr, ExtendedMsgHdr, MsgHdrProtoBuf, GCMsgHdr, GCMsgHdrProto
from steam.enums import EResult
from steam.enums.emsg import EMsg from steam.enums.emsg import EMsg
from steam.exceptions import SteamError
from steam.protobufs import steammessages_base_pb2 from steam.protobufs import steammessages_base_pb2
from steam.protobufs import steammessages_clientserver_pb2 from steam.protobufs import steammessages_clientserver_pb2
from steam.protobufs import steammessages_clientserver_2_pb2 from steam.protobufs import steammessages_clientserver_2_pb2
@ -144,12 +146,12 @@ class MsgProto(object):
self.header = self._header.proto self.header = self._header.proto
self.msg = msg self.msg = msg
if msg == EMsg.ServiceMethod: if msg in (EMsg.ServiceMethodResponse, EMsg.ServiceMethodSendToClient):
proto = get_um(self.header.target_job_name) proto = get_um(self.header.target_job_name, response=True)
if proto: if proto:
self.body = proto() self.body = proto()
else: else:
self.body = '!! Failed to resolve UM for: %s !!' % repr(self.header.target_job_name) self.body = '!! Failed to resolve UM: %s !!' % repr(self.header.target_job_name)
else: else:
proto = get_cmsg(msg) proto = get_cmsg(msg)

7
steam/exceptions.py

@ -0,0 +1,7 @@
from steam.enums import EResult
class SteamError(Exception):
def __init__(self, message, eresult=EResult.Fail):
Exception.__init__(self, message)
self.eresult = EResult(eresult) #: :class:`.EResult`

11
steam/guard.py

@ -142,15 +142,16 @@ class SteamAuthenticator(object):
if not backend.logged_on: if not backend.logged_on:
raise SteamAuthenticatorError("SteamClient instance not logged in") raise SteamAuthenticatorError("SteamClient instance not logged in")
resp, error = backend.unified_messages.send_and_wait("TwoFactor.%s#1" % action, resp = backend.send_um_and_wait("TwoFactor.%s#1" % action,
params, timeout=10) params, timeout=10)
if error:
raise SteamAuthenticatorError("Failed: %s" % str(error))
if resp is None: if resp is None:
raise SteamAuthenticatorError("Failed. Request timeout") raise SteamAuthenticatorError("Failed. Request timeout")
if resp.header.eresult != EResult.OK:
raise SteamAuthenticatorError("Failed: %s (%s)" % str(resp.header.error_message,
repr(resp.header.eresult)))
resp = proto_to_dict(resp) resp = proto_to_dict(resp.body)
if action == 'AddAuthenticator': if action == 'AddAuthenticator':
for key in ['shared_secret', 'identity_secret', 'secret_1']: for key in ['shared_secret', 'identity_secret', 'secret_1']:

Loading…
Cancel
Save