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
- 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
- Renamed `medium` param to `backend` on `SteamAuthenticator`
- 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`
- Fixed `create_emergency_codes()` not returning codes
- Fixed `validate_phone_number()` returning no data
- Added protos for new chat via unified messages
- Updated protobufs
- Removed `SteamClient.change_email()`
- Removed `SteamClient.create_account()`
- `SteanClient.unified_messages` now expose errors by returning a tuple `(result, error)`
- `get_product_info()` now replaces invalid unicode chars
- Updated `SteamID.is_valid`
- Updated various Enums

45
steam/client/__init__.py

@ -21,13 +21,14 @@ from getpass import getpass
import logging
import six
from steam.core.crypto import sha1_hash
from eventemitter import EventEmitter
from steam.enums.emsg import EMsg
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.msg import MsgProto
from steam.core.crypto import sha1_hash
from steam.steamid import SteamID
from steam.exceptions import SteamError
from steam.client.builtins import BuiltinBase
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")
# register listners
self.on(None, self._handle_jobs)
self.on(self.EVENT_DISCONNECTED, self._handle_disconnect)
self.on(self.EVENT_RECONNECT, self._handle_disconnect)
self.on(EMsg.ClientNewLoginKey, self._handle_login_key)
@ -94,6 +94,27 @@ class SteamClient(CMClient, BuiltinBase):
self.logged_on = False
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):
if not self.credential_location or self.cm_servers.last_updated > 0:
return
@ -154,18 +175,6 @@ class SteamClient(CMClient, BuiltinBase):
except IOError as 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):
self.logged_on = False
self.current_jobid = 0
@ -514,7 +523,7 @@ class SteamClient(CMClient, BuiltinBase):
message = MsgProto(EMsg.ClientLogon)
message.header.steamid = SteamID(type='Individual', universe='Public')
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_language = "english"
message.body.should_remember_password = True
@ -571,6 +580,7 @@ class SteamClient(CMClient, BuiltinBase):
message = MsgProto(EMsg.ClientLogon)
message.header.steamid = SteamID(type='AnonUser', universe='Public')
message.body.client_package_version = 1561159470
message.body.protocol_version = 65580
self.send(message)
@ -669,4 +679,3 @@ class SteamClient(CMClient, BuiltinBase):
result = self.login(username, password, None, auth_code, two_factor_code)
return result

59
steam/client/builtins/gameservers.py

@ -36,6 +36,7 @@ Filter code What it does
"""
from steam.steamid import SteamID
from steam.core.msg import MsgProto
from steam.enums import EResult
from steam.enums.emsg import EMsg
from steam.util import ip_to_int, ip_from_int, proto_to_dict
@ -50,7 +51,6 @@ class GameServers(object):
class SteamGameServers(object):
def __init__(self, steam):
self._s = steam
self._um = steam.unified_messages
def query(self, filter_text, max_servers=10, timeout=30, **kw):
r"""
@ -117,7 +117,7 @@ class SteamGameServers(object):
:type timeout: int
:returns: list of servers, see below. (``None`` is returned steam doesn't respond)
:rtype: :class:`list`, :class:`None`
:raises: :class:`.UnifiedMessageError`
:raises: :class:`.SteamError`
Sample response:
@ -143,20 +143,20 @@ class SteamGameServers(object):
'version': '1.35.4.0'}
]
"""
resp, error = self._um.send_and_wait("GameServers.GetServerList#1",
{
"filter": filter_text,
"limit": max_servers,
},
timeout=20,
)
if error:
raise error
resp = self._s.send_um_and_wait("GameServers.GetServerList#1",
{
"filter": filter_text,
"limit": max_servers,
},
timeout=20,
)
if resp is 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:
return []
@ -175,7 +175,7 @@ class SteamGameServers(object):
:type timeout: int
:return: map of ips to steamids
:rtype: dict
:raises: :class:`.UnifiedMessageError`
:raises: :class:`.SteamError`
Sample response:
@ -183,16 +183,16 @@ class SteamGameServers(object):
{SteamID(id=123456, type='AnonGameServer', universe='Public', instance=1234): '1.2.3.4:27060'}
"""
resp, error = self._um.send_and_wait("GameServers.GetServerIPsBySteamID#1",
{"server_steamids": server_steam_ids},
timeout=timeout,
)
if error:
raise error
resp = self._s.send_um_and_wait("GameServers.GetServerIPsBySteamID#1",
{"server_steamids": server_steam_ids},
timeout=timeout,
)
if resp is 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):
"""Resolve SteamIDs from IPs
@ -203,7 +203,7 @@ class SteamGameServers(object):
:type timeout: int
:return: map of steamids to ips
:rtype: dict
:raises: :class:`.UnifiedMessageError`
:raises: :class:`.SteamError`
Sample response:
@ -211,14 +211,13 @@ class SteamGameServers(object):
{'1.2.3.4:27060': SteamID(id=123456, type='AnonGameServer', universe='Public', instance=1234)}
"""
resp, error = self._um.send_and_wait("GameServers.GetServerSteamIDsByIP#1",
{"server_ips": server_ips},
timeout=timeout,
)
if error:
raise error
resp = self._s.send_um_and_wait("GameServers.GetServerSteamIDsByIP#1",
{"server_ips": server_ips},
timeout=timeout,
)
if resp is 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:
.. code:: python
# the easy way
response = client.unified_messages.send_and_wait('Player.GetGameBadgeLevels#1', {
response = client.send_um_and_wait('Player.GetGameBadgeLevels#1', {
'property': 1,
'something': 'value',
})
print(response.body)
# the other way
jobid = client.unified_message.send('Player.GetGameBadgeLevels#1', {'something': 1})
response, = client.unified_message.wait_event(jobid)
jobid = client.send_um('Player.GetGameBadgeLevels#1', {'something': 1})
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.enums import EResult
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):
def __init__(self, *args, **kwargs):
super(UnifiedMessages, self).__init__(*args, **kwargs)
name = "%s.unified_messages" % self.__class__.__name__
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):
def send_um(self, method_name, params=None):
"""Send service method request
:param message:
proto message instance (use :meth:`SteamUnifiedMessages.get`)
or method name (e.g. ``Player.GetGameBadgeLevels#1``)
:type message: :class:`str`, proto message instance
:param method_name: method name (e.g. ``Player.GetGameBadgeLevels#1``)
:type method_name: :class:`str`
:param params: message parameters
:type params: :class:`dict`
:return: ``jobid`` event identifier
:type params: :class:`dict`
:return: ``job_id`` 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 isinstance(message, str):
message = self.get(message)
if message not in self._data:
raise ValueError("Supplied message is invalid. Use 'get' method.")
proto = get_um(method_name)
if params:
proto_fill_from_dict(message, params)
if proto is None:
raise ValueError("Failed to find method named: %s" % method_name)
capsule = MsgProto(EMsg.ClientServiceMethod)
capsule.body.method_name = self._data[message]
capsule.body.serialized_method = message.SerializeToString()
message = MsgProto(EMsg.ServiceMethodCallFromClient)
message.header.target_job_name = method_name
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
:param message:
proto message instance (use :meth:`SteamUnifiedMessages.get`)
or method name (e.g. ``Player.GetGameBadgeLevels#1``)
:type message: :class:`str`, proto message instance
:param method_name: method name (e.g. ``Player.GetGameBadgeLevels#1``)
:type method_name: :class:`str`
:param params: message parameters
:type params: :class:`dict`
:type params: :class:`dict`
: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`
:type raises: :class:`bool`
:return: response proto message instance
:rtype: (proto message, :class:`.UnifiedMessageError`)
:type raises: :class:`bool`
:return: response message
:rtype: proto message instance
:raises: :class:`gevent.Timeout`
"""
job_id = self.send(message, params)
resp = self.wait_event(job_id, timeout, raises=raises)
return (None, None) if resp is None else resp
job_id = self.send_um(method_name, params)
return self.wait_msg(job_id, timeout, raises=raises)

10
steam/client/cdn.py

@ -562,8 +562,10 @@ class CDNClient(object):
:type item_id: int
:returns: manifest instance
: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],
'includetags': False,
'includeadditionalpreviews': False,
@ -576,10 +578,10 @@ class CDNClient(object):
'language': 0
}, timeout=7)
if error:
raise error
if resp.header.eresult != EResult.OK:
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:
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)
except Exception as e:
self._LOG.fatal("Failed to deserialize message: %s (is_proto: %s)",
str(emsg),
repr(emsg),
is_proto(emsg_id)
)
self._LOG.exception(e)
@ -275,6 +275,7 @@ class CMClient(EventEmitter):
self._LOG.debug("Incoming: %s", repr(msg))
self.emit(emsg, msg)
return emsg, msg
def __handle_encrypt_request(self, req):
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.structs import get_struct
from steam.core.msg.headers import MsgHdr, ExtendedMsgHdr, MsgHdrProtoBuf, GCMsgHdr, GCMsgHdrProto
from steam.enums import EResult
from steam.enums.emsg import EMsg
from steam.exceptions import SteamError
from steam.protobufs import steammessages_base_pb2
from steam.protobufs import steammessages_clientserver_pb2
from steam.protobufs import steammessages_clientserver_2_pb2
@ -144,12 +146,12 @@ class MsgProto(object):
self.header = self._header.proto
self.msg = msg
if msg == EMsg.ServiceMethod:
proto = get_um(self.header.target_job_name)
if msg in (EMsg.ServiceMethodResponse, EMsg.ServiceMethodSendToClient):
proto = get_um(self.header.target_job_name, response=True)
if proto:
self.body = proto()
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:
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:
raise SteamAuthenticatorError("SteamClient instance not logged in")
resp, error = backend.unified_messages.send_and_wait("TwoFactor.%s#1" % action,
params, timeout=10)
resp = backend.send_um_and_wait("TwoFactor.%s#1" % action,
params, timeout=10)
if error:
raise SteamAuthenticatorError("Failed: %s" % str(error))
if resp is None:
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':
for key in ['shared_secret', 'identity_secret', 'secret_1']:

Loading…
Cancel
Save