Browse Source

Merge branch 'master' of https://github.com/ValvePython/steam

pull/34/head
root 9 years ago
parent
commit
bbcbc97ff8
  1. 4
      README.rst
  2. 5
      docs/api/steam.guard.rst
  3. 1
      docs/api/steam.rst
  4. 10
      docs/api/steam.util.rst
  5. 4
      docs/api/steam.webapi.rst
  6. 9
      docs/conf.py
  7. 27
      docs/user_guide.rst
  8. 5
      setup.py
  9. 4
      steam/__init__.py
  10. 168
      steam/client/builtins/misc.py
  11. 7
      steam/client/builtins/web.py
  12. 7
      steam/core/cm.py
  13. 20
      steam/core/crypto.py
  14. 8
      steam/core/msg.py
  15. 22
      steam/enums/common.py
  16. 88
      steam/guard.py
  17. 19
      steam/util/__init__.py
  18. 63
      steam/util/throttle.py
  19. 285
      steam/webapi.py
  20. 31
      steam/webauth.py
  21. 8
      tests/generete_webauth_vcr.py
  22. 19
      tests/test_guard.py
  23. 24
      tests/test_webapi.py
  24. 5
      tests/test_webauth.py
  25. 5
      vcr/webauth_user_pass_only_success.yaml

4
README.rst

@ -4,7 +4,7 @@ A python module for interacting with various parts of Steam_.
Supports Python ``2.7+`` and ``3.4+``. Supports Python ``2.7+`` and ``3.4+``.
Documentation: http://steam.readthedocs.io Documentation: http://steam.readthedocs.io/en/latest/
Main features Main features
------------- -------------
@ -58,7 +58,7 @@ To run for ``python 2.7`` and ``3.4`` assuming you have them installed::
.. _Steam: https://store.steampowered.com/ .. _Steam: https://store.steampowered.com/
.. |pypi| image:: https://img.shields.io/pypi/v/steam.svg?style=flat&label=latest%20version .. |pypi| image:: https://img.shields.io/pypi/v/steam.svg?style=flat&label=stable
:target: https://pypi.python.org/pypi/steam :target: https://pypi.python.org/pypi/steam
:alt: Latest version released on PyPi :alt: Latest version released on PyPi

5
docs/api/steam.guard.rst

@ -0,0 +1,5 @@
guard
=====
.. automodule:: steam.guard
:members:

1
docs/api/steam.rst

@ -6,6 +6,7 @@ steam
steam.core steam.core
steam.enums steam.enums
steam.globalid steam.globalid
steam.guard
steam.steamid steam.steamid
steam.webapi steam.webapi
steam.webauth steam.webauth

10
docs/api/steam.util.rst

@ -6,8 +6,16 @@ util
:undoc-members: :undoc-members:
:show-inheritance: :show-inheritance:
util.throttle
-------------
.. automodule:: steam.util.throttle
:members:
:undoc-members:
:show-inheritance:
util.web util.web
----------- --------
.. automodule:: steam.util.web .. automodule:: steam.util.web
:members: :members:

4
docs/api/steam.webapi.rst

@ -3,7 +3,3 @@ webapi
.. automodule:: steam.webapi .. automodule:: steam.webapi
:members: :members:
:undoc-members:
:show-inheritance:

9
docs/conf.py

@ -30,6 +30,7 @@ sys.path.insert(0, os.path.abspath('../'))
# ones. # ones.
extensions = [ extensions = [
'sphinx.ext.autodoc', 'sphinx.ext.autodoc',
'sphinx.ext.intersphinx',
# 'sphinx.ext.githubpages', # 'sphinx.ext.githubpages',
] ]
@ -289,6 +290,14 @@ texinfo_documents = [
# If true, do not generate a @detailmenu in the "Top" node's menu. # If true, do not generate a @detailmenu in the "Top" node's menu.
#texinfo_no_detailmenu = False #texinfo_no_detailmenu = False
# LINK PYTHON DOCS
intersphinx_mapping = {
'python': ('https://docs.python.org/3.4', None),
'gevent': ('http://www.gevent.org', None),
'requests': ('http://docs.python-requests.org/en/master', None),
}
# AUTODOC # AUTODOC
autodoc_member_order = 'bysource' autodoc_member_order = 'bysource'
autoclass_content = 'both' autoclass_content = 'both'

27
docs/user_guide.rst

@ -10,14 +10,14 @@ overview of the functionality available in the ``steam`` module.
SteamID SteamID
======= =======
:mod:`SteamID <steam.steamid>` can be used to convert the universal steam id :mod:`SteamID <steam.steamid.SteamID>` can be used to convert the universal steam id
to its' various representations. to its' various representations.
.. note:: .. note::
``SteamID`` is immutable as it inherits from ``int``. :class:`SteamID <steam.steamid.SteamID>` is immutable as it inherits from :class:`int`.
Example usage Converting between representations
------------- ----------------------------------
.. code:: python .. code:: python
@ -54,10 +54,10 @@ Example usage
'https://steamcommunity.com/gid/103582791429521412' 'https://steamcommunity.com/gid/103582791429521412'
Resolving community urls to ``SteamID`` Resolving community urls to :class:`SteamID <steam.steamid.SteamID>`
----------------------------------------- --------------------------------------------------------------------
The ``steamid`` submodule provides function to resolve community urls. The :mod:`steam.steamid` submodule provides function to resolve community urls.
Here are some examples: Here are some examples:
.. code:: python .. code:: python
@ -76,12 +76,19 @@ WebAPI
:mod:`WebAPI <steam.webapi>` is a thin Wrapper around `Steam Web API`_. Requires `API Key`_. Upon initialization the :mod:`WebAPI <steam.webapi>` is a thin Wrapper around `Steam Web API`_. Requires `API Key`_. Upon initialization the
instance will fetch all available interfaces and populate the namespace. instance will fetch all available interfaces and populate the namespace.
Obtaining a key
---------------
Any steam user can get a key by visiting http://steamcommunity.com/dev/apikey.
The only requirement is that the user has verified their email.
Then the key can be used on the ``public`` WebAPI. See :class:`steam.webapi.APIHost`
.. note:: .. note::
Interface availability depends on the ``key``. Interface availability depends on the ``key``.
Unless the schema is loaded manually. Unless the schema is loaded manually.
Example usage Calling an endpoint
------------- -------------------
.. code:: python .. code:: python
@ -234,7 +241,7 @@ Alternatively, a callback can be registered to handle the response event every t
Web Authentication Web Authentication
================== ==================
There are currently two paths for gaining accessing to steam websites. There are currently two paths for gaining access to steam websites.
Either using :class:`WebAuth <steam.webauth.WebAuth>`, or via a :meth:`SteamClient.get_web_session() <steam.client.builtins.web.Web.get_web_session>` instance. Either using :class:`WebAuth <steam.webauth.WebAuth>`, or via a :meth:`SteamClient.get_web_session() <steam.client.builtins.web.Web.get_web_session>` instance.
.. code:: python .. code:: python

5
setup.py

@ -7,7 +7,10 @@ import sys
here = path.abspath(path.dirname(__file__)) here = path.abspath(path.dirname(__file__))
with open(path.join(here, 'README.rst'), encoding='utf-8') as f: with open(path.join(here, 'README.rst'), encoding='utf-8') as f:
long_description = f.read() long_description = f.read()\
.replace('.io/en/latest/', '.io/en/stable/')\
.replace('?badge=latest', '?badge=stable')\
.replace('projects/steam/badge/?version=latest', 'projects/steam/badge/?version=stable')
with open(path.join(here, 'steam/__init__.py'), encoding='utf-8') as f: with open(path.join(here, 'steam/__init__.py'), encoding='utf-8') as f:
__version__ = f.readline().split('"')[1] __version__ = f.readline().split('"')[1]

4
steam/__init__.py

@ -1,7 +1,7 @@
__version__ = "0.8.1" __version__ = "0.8.3"
__author__ = "Rossen Georgiev" __author__ = "Rossen Georgiev"
version_info = (0, 8, 1) version_info = (0, 8, 3)
from steam.steamid import SteamID from steam.steamid import SteamID
from steam.globalid import GlobalID from steam.globalid import GlobalID

168
steam/client/builtins/misc.py

@ -4,8 +4,11 @@ Various features that don't have a category
import logging import logging
from eventemitter import EventEmitter 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, ELeaderboardDataRequest, ELeaderboardSortMethod, ELeaderboardDisplayType
from steam.enums.emsg import EMsg from steam.enums.emsg import EMsg
from steam.util import WeakRefKeyDict from steam.util import WeakRefKeyDict, _range, chunks
from steam.util.throttle import ConstantRateLimit
class Misc(object): class Misc(object):
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
@ -33,6 +36,33 @@ class Misc(object):
self.send(message) self.send(message)
def get_leaderboard(self, app_id, name):
""".. versionadded:: 0.8.2
Find a leaderboard
:param app_id: application id
:type app_id: :class:`int`
:param name: leaderboard name
:type name: :class:`str`
:return: leaderboard instance
:rtype: :class:`SteamLeaderboard`
:raises: :class:`LookupError` on message timeout or error
"""
message = MsgProto(EMsg.ClientLBSFindOrCreateLB)
message.header.routing_appid = app_id
message.body.app_id = app_id
message.body.leaderboard_name = name
message.body.create_if_not_found = False
resp = self.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))
return SteamLeaderboard(self, app_id, name, resp)
class SteamUnifiedMessages(EventEmitter): class SteamUnifiedMessages(EventEmitter):
"""Simple API for send/recv of unified messages """Simple API for send/recv of unified messages
@ -78,7 +108,7 @@ class SteamUnifiedMessages(EventEmitter):
:param method_name: name for the method (e.g. ``Player.GetGameBadgeLevels#1``) :param method_name: name for the method (e.g. ``Player.GetGameBadgeLevels#1``)
:type method_name: :class:`str` :type method_name: :class:`str`
:return: proto message instance, or ``None`` if not found :return: proto message instance, or :class:`None` if not found
""" """
proto = get_um(method_name) proto = get_um(method_name)
if proto is None: if proto is None:
@ -90,7 +120,7 @@ class SteamUnifiedMessages(EventEmitter):
def send(self, message): def send(self, message):
"""Send service method request """Send service method request
:param message: proto message instance (use :meth:`get`) :param message: proto message instance (use :meth:`SteamUnifiedMessages.get`)
:return: ``jobid`` event identifier :return: ``jobid`` event identifier
:rtype: :class:`str` :rtype: :class:`str`
@ -114,7 +144,7 @@ class SteamUnifiedMessages(EventEmitter):
:param message: proto message instance (use :meth:`get`) :param message: proto message instance (use :meth:`get`)
: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 ``False`` return ``None``, else raise ``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 proto message instance
:rtype: proto message, :class:`None` :rtype: proto message, :class:`None`
@ -126,3 +156,133 @@ class SteamUnifiedMessages(EventEmitter):
return None return None
else: else:
return resp[0] return resp[0]
class SteamLeaderboard(object):
""".. versionadded:: 0.8.2
Steam leaderboard object.
Generated via :meth:`Misc.get_leaderboard()`
Works more or less like a :class:`list` to access entries.
.. note::
Each slice will produce a message to steam.
Steam and protobufs might not like large slices.
Avoid accessing individual entries by index and instead use iteration or well sized slices.
Example usage:
.. code:: python
lb = client.get_leaderboard(...)
print len(lb)
for entry in lb[:100]: # top 100
pass
"""
app_id = 0
name = '' #: leaderboard name
id = 0 #: leaderboard id
entry_count = 0
sort_method = ELeaderboardSortMethod.NONE #: :class:`steam.enums.common.ELeaderboardSortMethod`
display_type = ELeaderboardDisplayType.NONE #: :class:`steam.enums.common.ELeaderboardDisplayType`
data_request = ELeaderboardDataRequest.Global #: :class:`steam.enums.common.ELeaderboardDataRequest`
def __init__(self, steam, app_id, name, data):
self._steam = steam
self.app_id = app_id
for field in data.DESCRIPTOR.fields:
if field.name.startswith('leaderboard_'):
self.__dict__[field.name.replace('leaderboard_', '')] = getattr(data, field.name)
self.sort_method = ELeaderboardSortMethod(self.sort_method)
self.display_type = ELeaderboardDisplayType(self.display_type)
def __repr__(self):
return "<%s(%d, %s, %d, %s, %s)>" % (
self.__class__.__name__,
self.app_id,
repr(self.name),
len(self),
self.sort_method,
self.display_type,
)
def __len__(self):
return self.entry_count
def get_entries(self, start=0, end=0, data_request=ELeaderboardDataRequest.Global):
"""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`
: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 = data_request
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))
return resp.entries
def __getitem__(self, x):
if isinstance(x, slice):
stop_max = len(self)
start = 0 if x.start is None else x.start if x.start >= 0 else max(0, x.start + stop_max)
stop = stop_max if x.stop is None else x.stop if x.stop >= 0 else max(0, x.stop + stop_max)
step = x.step or 1
if step < 0:
start, stop = stop, start
step = abs(step)
else:
start, stop, step = x, x + 1, 1
if start >= stop: return []
entries = self.get_entries(start+1, stop, self.data_request)
return [entries[i] for i in _range(0, len(entries), step)]
def get_iter(self, times, seconds, chunk_size=2000):
"""Make a iterator over the entries
See :class:`steam.util.throttle.ConstantRateLimit` for ``times`` and ``seconds`` parameters.
:param chunk_size: number of entries per request
:type chunk_size: :class:`int`
:returns: generator object
:rtype: :class:`generator`
The iterator essentially buffers ``chuck_size`` number of entries, and ensures
we are not sending messages too fast.
For example, the ``__iter__`` method on this class uses ``get_iter(1, 1, 2000)``
"""
def entry_generator():
with ConstantRateLimit(times, seconds, use_gevent=True) as r:
for entries in chunks(self, chunk_size):
if not entries:
raise StopIteration
for entry in entries:
yield entry
r.wait()
return entry_generator()
def __iter__(self):
return self.get_iter(1, 1, 2000)

7
steam/client/builtins/web.py

@ -2,7 +2,7 @@
Web related features Web related features
""" """
from binascii import hexlify from binascii import hexlify
from steam import WebAPI from steam import webapi
from steam.core.crypto import generate_session_key, symmetric_encrypt, sha1_hash, random_bytes from steam.core.crypto import generate_session_key, symmetric_encrypt, sha1_hash, random_bytes
from steam.util.web import make_requests_session from steam.util.web import make_requests_session
@ -15,7 +15,7 @@ class Web(object):
"""Get web authentication cookies via WebAPI's ``AuthenticateUser`` """Get web authentication cookies via WebAPI's ``AuthenticateUser``
.. note:: .. note::
only valid during the current steam session A session is only valid during the current steam session.
:return: dict with authentication cookies :return: dict with authentication cookies
:rtype: :class:`dict`, :class:`None` :rtype: :class:`dict`, :class:`None`
@ -32,8 +32,7 @@ class Web(object):
} }
try: try:
api = WebAPI(None) resp = webapi.post('ISteamUserAuth', 'AuthenticateUser', 1, params=data)
resp = api.ISteamUserAuth.AuthenticateUser(**data)
except Exception as exp: except Exception as exp:
self._logger.debug("get_web_session_cookies error: %s" % str(exp)) self._logger.debug("get_web_session_cookies error: %s" % str(exp))
return None return None

7
steam/core/cm.py

@ -347,7 +347,7 @@ class CMClient(EventEmitter):
self.steam_id = SteamID(msg.header.steamid) self.steam_id = SteamID(msg.header.steamid)
self.session_id = msg.header.client_sessionid self.session_id = msg.header.client_sessionid
self.webapi_authenticate_user_nonce = msg.body.webapi_authenticate_user_nonce self.webapi_authenticate_user_nonce = msg.body.webapi_authenticate_user_nonce.encode('ascii')
if self._heartbeat_loop: if self._heartbeat_loop:
self._heartbeat_loop.kill() self._heartbeat_loop.kill()
@ -431,11 +431,10 @@ class CMServerList(object):
:return: booststrap success :return: booststrap success
:rtype: :class:`bool` :rtype: :class:`bool`
""" """
from steam import WebAPI from steam import _webapi
try: try:
api = WebAPI(None) resp = _webapi.get('ISteamDirectory', 'GetCMList', 1, params={'cellid': cellid})
resp = api.ISteamDirectory.GetCMList_v1(cellid=cellid)
except Exception as exp: except Exception as exp:
self._log.error("WebAPI boostrap failed: %s" % str(exp)) self._log.error("WebAPI boostrap failed: %s" % str(exp))
return False return False

20
steam/core/crypto.py

@ -53,13 +53,8 @@ def symmetric_encrypt(message, key):
def symmetric_encrypt_HMAC(message, key, hmac_secret): def symmetric_encrypt_HMAC(message, key, hmac_secret):
prefix = random_bytes(3) prefix = random_bytes(3)
hmac = hmac_sha1(hmac_secret, prefix + message)
hmac = HMAC(hmac_secret, SHA1(), backend) iv = hmac[:13] + prefix
hmac.update(prefix)
hmac.update(message)
iv = hmac.finalize()[:13] + prefix
return symmetric_encrypt_with_iv(message, key, iv) return symmetric_encrypt_with_iv(message, key, iv)
def symmetric_encrypt_iv(iv, key): def symmetric_encrypt_iv(iv, key):
@ -81,11 +76,9 @@ def symmetric_decrypt_HMAC(cyphertext, key, hmac_secret):
iv = symmetric_decrypt_iv(cyphertext, key) iv = symmetric_decrypt_iv(cyphertext, key)
message = symmetric_decrypt_with_iv(cyphertext, key, iv) message = symmetric_decrypt_with_iv(cyphertext, key, iv)
hmac = HMAC(hmac_secret, SHA1(), backend) hmac = hmac_sha1(hmac_secret, iv[-3:] + message)
hmac.update(iv[-3:])
hmac.update(message)
if iv[:13] != hmac.finalize()[:13]: if iv[:13] != hmac[:13]:
raise RuntimeError("Unable to decrypt message. HMAC does not match.") raise RuntimeError("Unable to decrypt message. HMAC does not match.")
return message return message
@ -98,6 +91,11 @@ def symmetric_decrypt_with_iv(cyphertext, key, iv):
decryptor = Cipher(AES(key), CBC(iv), backend).decryptor() decryptor = Cipher(AES(key), CBC(iv), backend).decryptor()
return unpad(decryptor.update(cyphertext[BS:]) + decryptor.finalize()) return unpad(decryptor.update(cyphertext[BS:]) + decryptor.finalize())
def hmac_sha1(secret, data):
hmac = HMAC(secret, SHA1(), backend)
hmac.update(data)
return hmac.finalize()
def sha1_hash(data): def sha1_hash(data):
sha = Hash(SHA1(), backend) sha = Hash(SHA1(), backend)
sha.update(data) sha.update(data)

8
steam/core/msg.py

@ -213,10 +213,16 @@ def get_cmsg(emsg):
""" """
global cmsg_lookup, cmsg_lookup2 global cmsg_lookup, cmsg_lookup2
if not isinstance(emsg, EMsg):
emsg = EMsg(emsg)
if emsg in cmsg_lookup_predefined: if emsg in cmsg_lookup_predefined:
return cmsg_lookup_predefined[emsg] return cmsg_lookup_predefined[emsg]
else: else:
cmsg_name = "cmsg" + str(emsg).split('.', 1)[1].lower() enum_name = emsg.name.lower()
if enum_name.startswith("econ"): # special case for 'EconTrading_'
enum_name = enum_name[4:]
cmsg_name = "cmsg" + enum_name
if not cmsg_lookup: if not cmsg_lookup:
cmsg_list = steammessages_clientserver_pb2.__dict__ cmsg_list = steammessages_clientserver_pb2.__dict__

22
steam/enums/common.py

@ -330,6 +330,7 @@ class EPersonaStateFlag(SteamIntEnum):
OnlineUsingWeb = 256 OnlineUsingWeb = 256
OnlineUsingMobile = 512 OnlineUsingMobile = 512
OnlineUsingBigPicture = 1024 OnlineUsingBigPicture = 1024
OnlineUsingVR = 2048
class EClientPersonaStateFlag(SteamIntEnum): class EClientPersonaStateFlag(SteamIntEnum):
@ -346,6 +347,27 @@ class EClientPersonaStateFlag(SteamIntEnum):
ClanTag = 1024 ClanTag = 1024
Facebook = 2048 Facebook = 2048
class ELeaderboardDataRequest(SteamIntEnum):
Global = 0
GlobalAroundUser = 1
Friends = 2
Users = 3
class ELeaderboardSortMethod(SteamIntEnum):
NONE = 0
Ascending = 1
Descending = 2
class ELeaderboardDisplayType(SteamIntEnum):
NONE = 0
Numeric = 1
TimeSeconds = 2
TimeMilliSeconds = 3
class ELeaderboardUploadScoreMethod(SteamIntEnum):
NONE = 0
KeepBest = 1
ForceUpdate = 2
# Do not remove # Do not remove
from sys import modules from sys import modules

88
steam/guard.py

@ -0,0 +1,88 @@
import struct
from binascii import hexlify
from time import time
from steam import webapi
from steam.core.crypto import hmac_sha1, sha1_hash
def generate_twofactor_code(shared_secret):
"""Generate Steam 2FA code for login with current time
:param shared_secret: authenticator shared shared_secret
:type shared_secret: bytes
:return: steam two factor code
:rtype: str
"""
return generate_twofactor_code_for_time(shared_secret, time() + get_time_offset())
def generate_twofactor_code_for_time(shared_secret, timestamp):
"""Generate Steam 2FA code for timestamp
:param shared_secret: authenticator shared secret
:type shared_secret: bytes
:param timestamp: timestamp to use, if left out uses current time
:type timestamp: int
:return: steam two factor code
:rtype: str
"""
hmac = hmac_sha1(bytes(shared_secret),
struct.pack('>Q', int(timestamp)//30)) # this will NOT stop working in 2038
start = ord(hmac[19:20]) & 0xF
codeint = struct.unpack('>I', hmac[start:start+4])[0] & 0x7fffffff
charset = '23456789BCDFGHJKMNPQRTVWXY'
code = ''
for _ in range(5):
codeint, i = divmod(codeint, len(charset))
code += charset[i]
return code
def generate_confirmation_key(identity_secret, timestamp, tag=''):
"""Generate confirmation key for trades. Can only be used once.
:param identity_secret: authenticator identity secret
:type identity_secret: bytes
:param timestamp: timestamp to use for generating key
:type timestamp: int
:param tag: tag identifies what the request, see list below
:type tag: str
:return: confirmation key
:rtype: bytes
Tag choices:
* ``conf`` to load the confirmations page
* ``details`` to load details about a trade
* ``allow`` to confirm a trade
* ``cancel`` to cancel a trade
"""
data = struct.pack('>Q', int(timestamp)) + tag.encode('ascii') # this will NOT stop working in 2038
return hmac_sha1(bytes(identity_secret), data)
def get_time_offset():
"""Get time offset from steam server time via WebAPI
:return: time offset
:rtype: int
"""
try:
resp = webapi.post('ITwoFactorService', 'QueryTime', 1, params={'http_timeout': 5})
except:
return 0
ts = int(time())
return int(resp.get('response', {}).get('server_time', ts)) - ts
def generate_device_id(steamid):
"""Generate Android device id
:param steamid: Steam ID
:type steamid: :class:`.SteamID`, :class:`int`
:return: android device id
:rtype: str
"""
h = hexlify(sha1(str(steamid).encode('ascii'))).decode('ascii')
return "android:%s-%s-%s-%s-%s" % (h[:8], h[8:12], h[12:16], h[16:20], h[20:32])

19
steam/util/__init__.py

@ -3,6 +3,12 @@
import weakref import weakref
import struct import struct
import socket import socket
import sys
if sys.version_info < (3,):
_range = xrange
else:
_range = range
def ip_from_int(ip): def ip_from_int(ip):
"""Convert IP to :py:class:`int` """Convert IP to :py:class:`int`
@ -53,6 +59,19 @@ def clear_proto_bit(emsg):
""" """
return int(emsg) & ~protobuf_mask return int(emsg) & ~protobuf_mask
def chunks(arr, size):
"""Splits a list into chunks
:param arr: list to split
:type arr: :class:`list`
:param size: number of elements in each chunk
:type size: :class:`int`
:return: generator object
:rtype: :class:`generator`
"""
for i in _range(0, len(arr), size):
yield arr[i:i+size]
class WeakRefKeyDict(object): class WeakRefKeyDict(object):
"""Pretends to be a dictionary. """Pretends to be a dictionary.

63
steam/util/throttle.py

@ -0,0 +1,63 @@
import sys
import time
import gevent
if sys.version_info >= (3,3):
_monotonic = time.monotonic
else:
_monotonic = time.time # not really monotonic vOv
class ConstantRateLimit(object):
def __init__(self, times, seconds, exit_wait=False, use_gevent=False):
"""Context manager for enforcing constant rate on code inside the block .
`rate = seconds / times`
:param times: times to execute per...
:type times: :class:`int`
:param seconds: ...seconds
:type seconds: :class:`int`
:param exit_wait: whether to automatically call :meth:`wait` before exiting the block
:type exit_wait: :class:`bool`
:param use_gevent: whether to use `gevent.sleep()` instead of `time.sleep()`
:type use_gevent: :class:`bool`
Example:
.. code:: python
with RateLimiter(1, 5) as r:
# code taking 1s
r.wait() # blocks for 4s
# code taking 7s
r.wait() # doesn't block
# code taking 1s
r.wait() # blocks for 4s
"""
self.__dict__.update(locals())
self.rate = float(seconds) / times
def __enter__(self):
self._update_ref()
return self
def __exit__(self, etype, evalue, traceback):
if self.exit_wait:
self.wait()
def _update_ref(self):
self._ref = _monotonic() + self.rate
def wait(self):
"""Blocks until the rate is met"""
now = _monotonic()
if now < self._ref:
delay = max(0, self._ref - now)
if self.use_gevent:
gevent.sleep(delay)
else:
time.sleep(delay)
self._update_ref()

285
steam/webapi.py

@ -1,14 +1,23 @@
""" """
WebAPI provides a thin wrapper over `Steam's Web API <https://developer.valvesoftware.com/wiki/Steam_Web_API>`_ WebAPI provides a thin wrapper over `Steam's Web API <https://developer.valvesoftware.com/wiki/Steam_Web_API>`_
Calling an endpoint It is very fiendly to exploration and prototyping when using ``ipython``, ``notebooks`` or similar.
The ``key`` will determine what WebAPI interfaces and methods are available.
.. note::
Some endpoints don't require a key
Currently the WebAPI can be accessed via one of two API hosts. See :class:`APIHost`.
Example code:
.. code:: python .. code:: python
>>> api = WebAPI(key) >>> api = WebAPI(key)
>>> api.ISteamUser.ResolveVanityURL(vanityurl="valve", url_type=2)
>>> api.call('ISteamUser.ResolveVanityURL', vanityurl="valve", url_type=2) >>> api.call('ISteamUser.ResolveVanityURL', vanityurl="valve", url_type=2)
{u'response': {u'steamid': u'103582791429521412', u'success': 1}} >>> api.ISteamUser.ResolveVanityURL(vanityurl="valve", url_type=2)
>>> api.ISteamUser.ResolveVanityURL_v1(vanityurl="valve", url_type=2)
{'response': {'steamid': '103582791429521412', 'success': 1}}
All globals params (``key``, ``https``, ``format``, ``raw``) can be specified on per call basis. All globals params (``key``, ``https``, ``format``, ``raw``) can be specified on per call basis.
@ -21,11 +30,23 @@ All globals params (``key``, ``https``, ``format``, ``raw``) can be specified on
"success" "1" "success" "1"
} }
""" """
import json import json as _json
from steam.util.web import make_requests_session from steam.util.web import make_requests_session as _make_session
class APIHost(object):
"""Enum of currently available API hosts."""
Public = 'api.steampowered.com'
""" available over HTTP (port 80) and HTTPS (port 443)"""
Partner = 'partner.steam-api.com'
"""available over HTTPS (port 443) only
.. note::
Key is required for every request. If not supplied you will get HTTP 403.
"""
DEFAULT_PARAMS = { DEFAULT_PARAMS = {
# api parameters # api parameters
'apihost': APIHost.Public,
'key': None, 'key': None,
'format': 'json', 'format': 'json',
# internal # internal
@ -35,80 +56,11 @@ DEFAULT_PARAMS = {
} }
def webapi_request(path, method='GET', caller=None, params={}, session=None):
"""
Low level function for calling Steam's WebAPI
:param path: request url
:type path: :class:`str`
:param method: HTTP method (GET or POST)
:type method: :class:`str`
:param caller: caller reference, caller.last_response is set to the last response
:param params: dict of WebAPI and endpoint specific params
:type params: :class:`dict`
:param session: an instance requests session, or one is created per call
:type session: :class:`requests.Session`
:return: response based on paramers
:rtype: :class:`dict`, :class:`lxml.etree.Element`, :class:`str`
"""
if method not in ('GET', 'POST'):
raise NotImplemented("HTTP method: %s" % repr(self.method))
onetime = {}
for param in DEFAULT_PARAMS:
params[param] = onetime[param] = params.get(param,
DEFAULT_PARAMS[param],
)
path = "%s://api.steampowered.com/%s" % ('https' if params.get('https', True) else 'http',
path)
del params['raw']
del params['https']
del params['http_timeout']
if onetime['format'] not in ('json', 'vdf', 'xml'):
raise ValueError("Expected format to be json,vdf or xml; got %s" % onetime['format'])
# serialize some parameter types properly
for k, v in params.items():
if isinstance(v, bool):
params[k] = 1 if v else 0
elif isinstance(v, (list, dict)):
params[k] = json.dumps(v)
# move params to data, if data is not specified for POST
# simplifies code calling this method
kwargs = {'params': params} if method == "GET" else {'data': params}
f = getattr(session, method.lower())
resp = f(path, stream=False, timeout=onetime['http_timeout'], **kwargs)
# we keep a reference of the last response instance on the caller
if caller is not None:
caller.last_response = resp
# 4XX and 5XX will cause this to raise
resp.raise_for_status()
# response
if onetime['raw']:
return resp.text
if onetime['format'] == 'json':
return resp.json()
elif onetime['format'] == 'xml':
import lxml.etree
return lxml.etree.fromstring(resp.content)
elif onetime['format'] == 'vdf':
import vdf
return vdf.loads(resp.text)
class WebAPI(object): class WebAPI(object):
""" """Steam WebAPI wrapper
Steam WebAPI wrapper. See https://developer.valvesoftware.com/wiki/Steam_Web_API
.. note:: .. note::
Interfaces and methods are populated automatically from WebAPI. Interfaces and methods are populated automatically from Steam WebAPI.
:param key: api key from https://steamcommunity.com/dev/apikey :param key: api key from https://steamcommunity.com/dev/apikey
:type key: :class:`str` :type key: :class:`str`
@ -120,7 +72,9 @@ class WebAPI(object):
:type https: :class:`bool` :type https: :class:`bool`
:param http_timeout: HTTP timeout in seconds :param http_timeout: HTTP timeout in seconds
:type http_timeout: :class:`int` :type http_timeout: :class:`int`
:param auto_load_interfaces: load interfaces from the WebAPI :param apihost: api hostname, see :class:`APIHost`
:type apihost: :class:`str`
:param auto_load_interfaces: load interfaces from the Steam WebAPI
:type auto_load_interfaces: :class:`bool` :type auto_load_interfaces: :class:`bool`
These can be specified per method call for one off calls These can be specified per method call for one off calls
@ -130,20 +84,23 @@ class WebAPI(object):
raw = DEFAULT_PARAMS['raw'] raw = DEFAULT_PARAMS['raw']
https = DEFAULT_PARAMS['https'] https = DEFAULT_PARAMS['https']
http_timeout = DEFAULT_PARAMS['http_timeout'] http_timeout = DEFAULT_PARAMS['http_timeout']
apihost = DEFAULT_PARAMS['apihost']
interfaces = [] interfaces = []
def __init__(self, key, format = DEFAULT_PARAMS['format'], def __init__(self, key, format = DEFAULT_PARAMS['format'],
raw = DEFAULT_PARAMS['raw'], raw = DEFAULT_PARAMS['raw'],
https = DEFAULT_PARAMS['https'], https = DEFAULT_PARAMS['https'],
http_timeout = DEFAULT_PARAMS['http_timeout'], http_timeout = DEFAULT_PARAMS['http_timeout'],
apihost = DEFAULT_PARAMS['apihost'],
auto_load_interfaces = True): auto_load_interfaces = True):
self.key = key #: api key self.key = key #: api key
self.format = format #: format (``json``, ``vdf``, or ``xml``) self.format = format #: format (``json``, ``vdf``, or ``xml``)
self.raw = raw #: return raw reponse or parse self.raw = raw #: return raw reponse or parse
self.https = https #: use https or not self.https = https #: use https or not
self.http_timeout = http_timeout #: HTTP timeout in seconds self.http_timeout = http_timeout #: HTTP timeout in seconds
self.apihost = apihost #: ..versionadded:: 0.8.3 apihost hostname
self.interfaces = [] #: list of all interfaces self.interfaces = [] #: list of all interfaces
self.session = make_requests_session() #: :class:`requests.Session` from :func:`steam.util.web.make_requests_session` self.session = _make_session() #: :class:`requests.Session` from :func:`steam.util.web.make_requests_session`
if auto_load_interfaces: if auto_load_interfaces:
self.load_interfaces(self.fetch_interfaces()) self.load_interfaces(self.fetch_interfaces())
@ -163,14 +120,14 @@ class WebAPI(object):
The returned value can passed to :py:func:`WebAPI.load_interfaces` The returned value can passed to :py:func:`WebAPI.load_interfaces`
""" """
return webapi_request( return get('ISteamWebAPIUtil', 'GetSupportedAPIList', 1,
"ISteamWebAPIUtil/GetSupportedAPIList/v1/", https=self.https,
method="GET", apihost=self.apihost,
caller=None, caller=None,
session=self.session,
params={'format': 'json', params={'format': 'json',
'key': self.key, 'key': self.key,
}, },
session=self.session,
) )
def load_interfaces(self, interfaces_dict): def load_interfaces(self, interfaces_dict):
@ -213,7 +170,7 @@ class WebAPI(object):
def doc(self): def doc(self):
""" """
:return: Documentation for all interfaces and their methods :return: Documentation for all interfaces and their methods
:rtype: :class:`str` :rtype: str
""" """
doc = "Steam Web API - List of all interfaces\n\n" doc = "Steam Web API - List of all interfaces\n\n"
for interface in self.interfaces: for interface in self.interfaces:
@ -257,6 +214,10 @@ class WebAPIInterface(object):
def key(self): def key(self):
return self._parent.key return self._parent.key
@property
def apihost(self):
return self._parent.apihost
@property @property
def https(self): def https(self):
return self._parent.https return self._parent.https
@ -278,6 +239,10 @@ class WebAPIInterface(object):
return self._parent.session return self._parent.session
def doc(self): def doc(self):
"""
:return: Documentation for all methods on this interface
:rtype: str
"""
return self.__doc__ return self.__doc__
@property @property
@ -340,33 +305,31 @@ class WebAPIMethod(object):
islist = param['_array'] islist = param['_array']
optional = param['optional'] optional = param['optional']
# raise if we are missing a required parameter
if not optional and name not in kwargs and name != 'key': if not optional and name not in kwargs and name != 'key':
raise ValueError("Method requires %s to be set" % repr(name)) raise ValueError("Method requires %s to be set" % repr(name))
# populate params that will be passed to _api_request
if name in kwargs: if name in kwargs:
# some parameters can be an array, they need to be send as seperate field if islist and not isinstance(kwargs[name], list):
# the array index is append to the name (e.g. name[0], name[1] etc) raise ValueError("Expected %s to be a list, got %s" % (
if islist: repr(name),
if not isinstance(kwargs[name], list): repr(type(kwargs[name])))
raise ValueError("Expected %s to be a list, got %s" % ( )
repr(name), params[name] = kwargs[name]
repr(type(kwargs[name])))
) url = "%s://%s/%s/%s/v%s/" % (
'https' if self._parent.https else 'http',
for idx, value in enumerate(kwargs[name]): self._parent.apihost,
params['%s[%d]' % (name, idx)] = value self._parent.name,
else: self.name,
params[name] = kwargs[name] self.version,
)
# make the request
return webapi_request( return webapi_request(
"%s/%s/v%s/" % (self._parent.name, self.name, self.version), url=url,
method=self.method, method=self.method,
caller=self, caller=self,
params=params,
session=self._parent.session, session=self._parent.session,
params=params,
) )
@property @property
@ -386,6 +349,10 @@ class WebAPIMethod(object):
return self._dict['name'] return self._dict['name']
def doc(self): def doc(self):
"""
:return: Documentation for this method
:rtype: str
"""
return self.__doc__ return self.__doc__
@property @property
@ -411,3 +378,117 @@ class WebAPIMethod(object):
) )
return doc return doc
def webapi_request(url, method='GET', caller=None, session=None, params=None):
"""Low level function for calling Steam's WebAPI
.. versionchanged:: 0.8.3
:param url: request url (e.g. ``https://api.steampowered.com/A/B/v001/``)
:type url: :class:`str`
:param method: HTTP method (GET or POST)
:type method: :class:`str`
:param caller: caller reference, caller.last_response is set to the last response
:param params: dict of WebAPI and endpoint specific params
:type params: :class:`dict`
:param session: an instance requests session, or one is created per call
:type session: :class:`requests.Session`
:return: response based on paramers
:rtype: :class:`dict`, :class:`lxml.etree.Element`, :class:`str`
"""
if method not in ('GET', 'POST'):
raise NotImplemented("HTTP method: %s" % repr(self.method))
if params is None:
params = {}
onetime = {}
for param in DEFAULT_PARAMS:
params[param] = onetime[param] = params.get(param, DEFAULT_PARAMS[param])
for param in ('raw', 'apihost', 'https', 'http_timeout'):
del params[param]
if onetime['format'] not in ('json', 'vdf', 'xml'):
raise ValueError("Expected format to be json,vdf or xml; got %s" % onetime['format'])
for k, v in list(params.items()): # serialize some types
if isinstance(v, bool): params[k] = 1 if v else 0
elif isinstance(v, dict): params[k] = _json.dumps(v)
elif isinstance(v, list):
del params[k]
for i, lvalue in enumerate(v):
params["%s[%d]" % (k, i)] = lvalue
kwargs = {'params': params} if method == "GET" else {'data': params} # params to data for POST
if session is None: session = _make_session()
f = getattr(session, method.lower())
resp = f(url, stream=False, timeout=onetime['http_timeout'], **kwargs)
# we keep a reference of the last response instance on the caller
if caller is not None: caller.last_response = resp
# 4XX and 5XX will cause this to raise
resp.raise_for_status()
if onetime['raw']:
return resp.text
elif onetime['format'] == 'json':
return resp.json()
elif onetime['format'] == 'xml':
from lxml import etree as _etree
return _etree.fromstring(resp.content)
elif onetime['format'] == 'vdf':
import vdf as _vdf
return _vdf.loads(resp.text)
def get(interface, method, version=1,
apihost=DEFAULT_PARAMS['apihost'], https=DEFAULT_PARAMS['https'],
caller=None, session=None, params=None):
"""Send GET request to an API endpoint
.. versionadded:: 0.8.3
:param interface: interface name
:type interface: str
:param method: method name
:type method: str
:param version: method version
:type version: int
:param apihost: API hostname
:type apihost: str
:param https: whether to use HTTPS
:type https: bool
:param params: parameters for endpoint
:type params: dict
:return: endpoint response
:rtype: :class:`dict`, :class:`lxml.etree.Element`, :class:`str`
"""
url = u"%s://%s/%s/%s/v%s/" % (
'https' if https else 'http', apihost, interface, method, version)
return webapi_request(url, 'GET', caller=caller, session=session, params=params)
def post(interface, method, version=1,
apihost=DEFAULT_PARAMS['apihost'], https=DEFAULT_PARAMS['https'],
caller=None, session=None, params=None):
"""Send POST request to an API endpoint
.. versionadded:: 0.8.3
:param interface: interface name
:type interface: str
:param method: method name
:type method: str
:param version: method version
:type version: int
:param apihost: API hostname
:type apihost: str
:param https: whether to use HTTPS
:type https: bool
:param params: parameters for endpoint
:type params: dict
:return: endpoint response
:rtype: :class:`dict`, :class:`lxml.etree.Element`, :class:`str`
"""
url = "%s://%s/%s/%s/v%s/" % (
'https' if https else 'http', apihost, interface, method, version)
return webapi_request(url, 'POST', caller=caller, session=session, params=params)

31
steam/webauth.py

@ -4,6 +4,15 @@ This module simplifies the process of obtaining an authenticated session for ste
After authentication is complete, a :class:`requests.Session` is created containing the auth cookies. After authentication is complete, a :class:`requests.Session` is created containing the auth cookies.
The session can be used to access ``steamcommunity.com``, ``store.steampowered.com``, and ``help.steampowered.com``. The session can be used to access ``steamcommunity.com``, ``store.steampowered.com``, and ``help.steampowered.com``.
.. warning::
A web session may expire randomly, or when you login from different IP address.
Some pages will return status code `401` when that happens.
Keep in mind if you are trying to write robust code.
.. note::
If you are using :class:`steam.client.SteamClient`, use :meth:`steam.client.builtins.web.Web.get_web_session()`
Example usage: Example usage:
.. code:: python .. code:: python
@ -23,7 +32,10 @@ Example usage:
except wa.TwoFactorCodeRequired: except wa.TwoFactorCodeRequired:
user.login(twofactor_code='ZXC123') user.login(twofactor_code='ZXC123')
wa.session.get('https://store.steampowered.com/account/history/') user.session.get('https://store.steampowered.com/account/history/')
# OR
session = user.login()
session.get('https://store.steampowered.com/account/history')
Alternatively, if Steam Guard is not enabled on the account: Alternatively, if Steam Guard is not enabled on the account:
@ -34,9 +46,8 @@ Alternatively, if Steam Guard is not enabled on the account:
except wa.HTTPError: except wa.HTTPError:
pass pass
.. note:: The :class:`WebAuth` instance should be discarded once a session is obtained
If you are using :class:`steam.client.SteamClient`, see :meth:`steam.client.builtins.web.Web.get_web_session()` as it is not reusable.
""" """
from time import time from time import time
import sys import sys
@ -142,7 +153,7 @@ class WebAuth(object):
"captcha_text": captcha, "captcha_text": captcha,
"loginfriendlyname": "python-steam webauth", "loginfriendlyname": "python-steam webauth",
"rsatimestamp": self.timestamp, "rsatimestamp": self.timestamp,
"remember_login": False, "remember_login": 'true',
"donotcache": int(time() * 100000), "donotcache": int(time() * 100000),
} }
@ -157,16 +168,14 @@ class WebAuth(object):
self.complete = True self.complete = True
self.password = None self.password = None
self.session.cookies.clear()
data = resp['transfer_parameters'] data = resp['transfer_parameters']
self.steamid = SteamID(data['steamid']) self.steamid = SteamID(data['steamid'])
for cookie in list(self.session.cookies):
for domain in ['store.steampowered.com', 'help.steampowered.com', 'steamcommunity.com']:
self.session.cookies.set(cookie.name, cookie.value, domain=domain, secure=cookie.secure)
for domain in ['store.steampowered.com', 'help.steampowered.com', 'steamcommunity.com']: for domain in ['store.steampowered.com', 'help.steampowered.com', 'steamcommunity.com']:
self.session.cookies.set('steamLogin', '%s||%s' % (data['steamid'], data['token']),
domain=domain, secure=False)
self.session.cookies.set('steamLoginSecure', '%s||%s' % (data['steamid'], data['token_secure']),
domain=domain, secure=True)
self.session.cookies.set('Steam_Language', language, domain=domain) self.session.cookies.set('Steam_Language', language, domain=domain)
self.session.cookies.set('birthtime', '-3333', domain=domain) self.session.cookies.set('birthtime', '-3333', domain=domain)

8
tests/generete_webauth_vcr.py

@ -26,7 +26,11 @@ def request_scrubber(r):
def response_scrubber(r): def response_scrubber(r):
if 'set-cookie' in r['headers']: if 'set-cookie' in r['headers']:
del r['headers']['set-cookie'] r['headers']['set-cookie'] = [
'steamLogin=0%7C%7C{}; path=/; httponly'.format('A'*16),
'steamLoginSecure=0%7C%7C{}; path=/; httponly; secure'.format('B'*16),
'steamMachineAuth=0%7C%7C{}; path=/; httponly'.format('C'*16),
]
if r.get('body', ''): if r.get('body', ''):
data = json.loads(r['body']['string']) data = json.loads(r['body']['string'])
@ -37,7 +41,7 @@ def response_scrubber(r):
data['transfer_parameters']['steamid'] = '0' data['transfer_parameters']['steamid'] = '0'
data['transfer_parameters']['token'] = 'A'*16 data['transfer_parameters']['token'] = 'A'*16
data['transfer_parameters']['token_secure'] = 'B'*16 data['transfer_parameters']['token_secure'] = 'B'*16
data['transfer_parameters']['auth'] = 'C'*16 data['transfer_parameters']['auth'] = 'Z'*16
body = json.dumps(data) body = json.dumps(data)
r['body']['string'] = body r['body']['string'] = body

19
tests/test_guard.py

@ -0,0 +1,19 @@
import unittest
import mock
from steam import guard as g
class TCguard(unittest.TestCase):
def test_generate_twofactor_code_for_time(self):
code = g.generate_twofactor_code_for_time(b'superdupersecret', timestamp=3000030)
self.assertEqual(code, 'YRGQJ')
code = g.generate_twofactor_code_for_time(b'superdupersecret', timestamp=3000029)
self.assertEqual(code, '94R9D')
def test_generate_confirmation_key(self):
key = g.generate_confirmation_key(b'itsmemario', 100000)
self.assertEqual(key, b'\xed\xb5\xe5\xad\x8f\xf1\x99\x01\xc8-w\xd6\xb5 p\xccz\xd7\xd1\x05')
key = g.generate_confirmation_key(b'itsmemario', 100000, 'allow')
self.assertEqual(key, b"Q'\x06\x80\xe1g\xa8m$\xb2hV\xe6g\x8b'\x8f\xf1L\xb0")

24
tests/test_webapi.py

@ -2,13 +2,14 @@ import unittest
import mock import mock
import vcr import vcr
from steam import webapi
from steam.webapi import WebAPI from steam.webapi import WebAPI
from steam.enums import EType, EUniverse from steam.enums import EType, EUniverse
test_api_key = 'test_api_key' test_api_key = 'test_api_key'
test_vcr = vcr.VCR( test_vcr = vcr.VCR(
record_mode='new_episodes', record_mode='none', # change to 'new_episodes' when recording
serializer='yaml', serializer='yaml',
filter_query_parameters=['key'], filter_query_parameters=['key'],
filter_post_data_parameters=['key'], filter_post_data_parameters=['key'],
@ -26,7 +27,7 @@ class TCwebapi(unittest.TestCase):
@test_vcr.use_cassette('webapi.yaml') @test_vcr.use_cassette('webapi.yaml')
def test_simple_api_call(self): def test_simple_api_call(self):
resp = self.api.ISteamWebAPIUtil.GetServerInfo() resp = self.api.ISteamWebAPIUtil.GetServerInfo_v1()
self.assertTrue('servertime' in resp) self.assertTrue('servertime' in resp)
@test_vcr.use_cassette('webapi.yaml') @test_vcr.use_cassette('webapi.yaml')
@ -44,5 +45,22 @@ class TCwebapi(unittest.TestCase):
resp = self.api.ISteamRemoteStorage.GetPublishedFileDetails(itemcount=5, publishedfileids=[1,1,1,1,1]) resp = self.api.ISteamRemoteStorage.GetPublishedFileDetails(itemcount=5, publishedfileids=[1,1,1,1,1])
self.assertEqual(resp['response']['resultcount'], 5) self.assertEqual(resp['response']['resultcount'], 5)
resp = self.api.ISteamUser.ResolveVanityURL(vanityurl='valve', url_type=2) @test_vcr.use_cassette('webapi.yaml')
def test_get(self):
resp = webapi.get('ISteamUser', 'ResolveVanityURL', 1,
session=self.api.session, params={
'key': test_api_key,
'vanityurl': 'valve',
'url_type': 2,
})
self.assertEqual(resp['response']['steamid'], '103582791429521412') self.assertEqual(resp['response']['steamid'], '103582791429521412')
@test_vcr.use_cassette('webapi.yaml')
def test_post(self):
resp = webapi.post('ISteamRemoteStorage', 'GetPublishedFileDetails', 1,
session=self.api.session, params={
'key': test_api_key,
'itemcount': 5,
'publishedfileids': [1,1,1,1,1],
})
self.assertEqual(resp['response']['resultcount'], 5)

5
tests/test_webauth.py

@ -36,8 +36,9 @@ class WACase(unittest.TestCase):
self.assertIsInstance(s, requests.Session) self.assertIsInstance(s, requests.Session)
for domain in s.cookies.list_domains(): for domain in s.cookies.list_domains():
self.assertEqual(s.cookies.get('steamLogin', domain=domain), '0||%s' % ('A'*16)) self.assertEqual(s.cookies.get('steamLogin', domain=domain), '0%7C%7C{}'.format('A'*16))
self.assertEqual(s.cookies.get('steamLoginSecure', domain=domain), '0||%s' % ('B'*16)) self.assertEqual(s.cookies.get('steamLoginSecure', domain=domain), '0%7C%7C{}'.format('B'*16))
self.assertEqual(s.cookies.get('steamMachineAuth', domain=domain), '0%7C%7C{}'.format('C'*16))
self.assertEqual(s, user.login()) self.assertEqual(s, user.login())

5
vcr/webauth_user_pass_only_success.yaml

@ -39,7 +39,7 @@ interactions:
body: {string: !!python/unicode '{"requires_twofactor": false, "login_complete": body: {string: !!python/unicode '{"requires_twofactor": false, "login_complete":
true, "transfer_urls": ["https://steamcommunity.com/login/transfer", "https://help.steampowered.com/login/transfer"], true, "transfer_urls": ["https://steamcommunity.com/login/transfer", "https://help.steampowered.com/login/transfer"],
"transfer_parameters": {"steamid": "0", "remember_login": false, "token": "transfer_parameters": {"steamid": "0", "remember_login": false, "token":
"AAAAAAAAAAAAAAAA", "token_secure": "BBBBBBBBBBBBBBBB", "auth": "CCCCCCCCCCCCCCCC"}, "AAAAAAAAAAAAAAAA", "token_secure": "BBBBBBBBBBBBBBBB", "auth": "ZZZZZZZZZZZZZZZZ"},
"success": true}'} "success": true}'}
headers: headers:
cache-control: [no-cache] cache-control: [no-cache]
@ -49,6 +49,9 @@ interactions:
date: ['Fri, 13 May 2016 03:01:25 GMT'] date: ['Fri, 13 May 2016 03:01:25 GMT']
expires: ['Mon, 26 Jul 1997 05:00:00 GMT'] expires: ['Mon, 26 Jul 1997 05:00:00 GMT']
server: [Apache] server: [Apache]
set-cookie: [steamLogin=0%7C%7CAAAAAAAAAAAAAAAA; path=/; httponly, steamLoginSecure=0%7C%7CBBBBBBBBBBBBBBBB;
path=/; httponly; secure, steamMachineAuth=0%7C%7CCCCCCCCCCCCCCCCC; path=/;
httponly]
x-frame-options: [DENY] x-frame-options: [DENY]
status: {code: 200, message: OK} status: {code: 200, message: OK}
version: 1 version: 1

Loading…
Cancel
Save