pythonhacktoberfeststeamauthenticationauthenticatorsteam-authenticatorsteam-clientsteam-guard-codessteam-websteamworksvalvewebapi
You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
607 lines
17 KiB
607 lines
17 KiB
import struct
|
|
import json
|
|
import sys
|
|
import re
|
|
import requests
|
|
from steam.enums.base import SteamIntEnum
|
|
from steam.enums import EType, EUniverse, EInstanceFlag
|
|
from steam.core.crypto import md5_hash
|
|
from steam.utils.web import make_requests_session
|
|
|
|
if sys.version_info < (3,):
|
|
intBase = long
|
|
else:
|
|
intBase = int
|
|
|
|
class ETypeChar(SteamIntEnum):
|
|
I = EType.Invalid
|
|
U = EType.Individual
|
|
M = EType.Multiseat
|
|
G = EType.GameServer
|
|
A = EType.AnonGameServer
|
|
P = EType.Pending
|
|
C = EType.ContentServer
|
|
g = EType.Clan
|
|
T = EType.Chat
|
|
L = EType.Chat # lobby chat, 'c' for clan chat
|
|
c = EType.Chat # clan chat
|
|
a = EType.AnonUser
|
|
|
|
def __str__(self):
|
|
return self.name
|
|
|
|
ETypeChars = ''.join(ETypeChar.__members__.keys())
|
|
|
|
_icode_hex = "0123456789abcdef"
|
|
_icode_custom = "bcdfghjkmnpqrtvw"
|
|
_icode_all_valid = _icode_hex + _icode_custom
|
|
_icode_map = dict(zip(_icode_hex, _icode_custom))
|
|
_icode_map_inv = dict(zip(_icode_custom, _icode_hex ))
|
|
_csgofrcode_chars = 'ABCDEFGHJKLMNPQRSTUVWXYZ23456789'
|
|
|
|
|
|
class SteamID(intBase):
|
|
"""
|
|
Object for converting steamID to its' various representations
|
|
|
|
.. code:: python
|
|
|
|
SteamID() # invalid steamid
|
|
SteamID(12345) # accountid
|
|
SteamID('12345')
|
|
SteamID(id=12345, type='Invalid', universe='Invalid', instance=0)
|
|
SteamID(103582791429521412) # steam64
|
|
SteamID('103582791429521412')
|
|
SteamID('STEAM_1:0:2') # steam2
|
|
SteamID('[g:1:4]') # steam3
|
|
"""
|
|
EType = EType #: reference to EType
|
|
EUniverse = EUniverse #: reference to EUniverse
|
|
EInstanceFlag = EInstanceFlag #: reference to EInstanceFlag
|
|
|
|
def __new__(cls, *args, **kwargs):
|
|
steam64 = make_steam64(*args, **kwargs)
|
|
return super(SteamID, cls).__new__(cls, steam64)
|
|
|
|
def __init__(self, *args, **kwargs):
|
|
pass
|
|
|
|
def __str__(self):
|
|
return str(int(self))
|
|
|
|
def __repr__(self):
|
|
return "%s(id=%s, type=%s, universe=%s, instance=%s)" % (
|
|
self.__class__.__name__,
|
|
self.id,
|
|
repr(self.type.name),
|
|
repr(self.universe.name),
|
|
self.instance,
|
|
)
|
|
|
|
@property
|
|
def id(self):
|
|
"""
|
|
:return: account id
|
|
:rtype: :class:`int`
|
|
"""
|
|
return int(self) & 0xFFffFFff
|
|
|
|
@property
|
|
def account_id(self):
|
|
"""
|
|
:return: account id
|
|
:rtype: :class:`int`
|
|
"""
|
|
return int(self) & 0xFFffFFff
|
|
|
|
@property
|
|
def instance(self):
|
|
"""
|
|
:rtype: :class:`int`
|
|
"""
|
|
return (int(self) >> 32) & 0xFFffF
|
|
|
|
@property
|
|
def type(self):
|
|
"""
|
|
:rtype: :py:class:`steam.enum.EType`
|
|
"""
|
|
return EType((int(self) >> 52) & 0xF)
|
|
|
|
@property
|
|
def universe(self):
|
|
"""
|
|
:rtype: :py:class:`steam.enum.EUniverse`
|
|
"""
|
|
return EUniverse((int(self) >> 56) & 0xFF)
|
|
|
|
@property
|
|
def as_32(self):
|
|
"""
|
|
:return: account id
|
|
:rtype: :class:`int`
|
|
"""
|
|
return self.id
|
|
|
|
@property
|
|
def as_64(self):
|
|
"""
|
|
:return: steam64 format
|
|
:rtype: :class:`int`
|
|
"""
|
|
return int(self)
|
|
|
|
@property
|
|
def as_steam2(self):
|
|
"""
|
|
:return: steam2 format (e.g ``STEAM_1:0:1234``)
|
|
:rtype: :class:`str`
|
|
|
|
.. note::
|
|
``STEAM_X:Y:Z``. The value of ``X`` should represent the universe, or ``1``
|
|
for ``Public``. However, there was a bug in GoldSrc and Orange Box games
|
|
and ``X`` was ``0``. If you need that format use :attr:`SteamID.as_steam2_zero`
|
|
|
|
|
|
"""
|
|
return "STEAM_%d:%d:%d" % (
|
|
int(self.universe),
|
|
self.id % 2,
|
|
self.id >> 1,
|
|
)
|
|
|
|
@property
|
|
def as_steam2_zero(self):
|
|
"""
|
|
For GoldSrc and Orange Box games.
|
|
See :attr:`SteamID.as_steam2`
|
|
|
|
:return: steam2 format (e.g ``STEAM_0:0:1234``)
|
|
:rtype: :class:`str`
|
|
"""
|
|
return self.as_steam2.replace("_1", "_0")
|
|
|
|
@property
|
|
def as_steam3(self):
|
|
"""
|
|
:return: steam3 format (e.g ``[U:1:1234]``)
|
|
:rtype: :class:`str`
|
|
"""
|
|
typechar = str(ETypeChar(self.type))
|
|
instance = None
|
|
|
|
if self.type in (EType.AnonGameServer, EType.Multiseat):
|
|
instance = self.instance
|
|
elif self.type == EType.Individual:
|
|
if self.instance != 1:
|
|
instance = self.instance
|
|
elif self.type == EType.Chat:
|
|
if self.instance & EInstanceFlag.Clan:
|
|
typechar = 'c'
|
|
elif self.instance & EInstanceFlag.Lobby:
|
|
typechar = 'L'
|
|
else:
|
|
typechar = 'T'
|
|
|
|
parts = [typechar, int(self.universe), self.id]
|
|
|
|
if instance is not None:
|
|
parts.append(instance)
|
|
|
|
return '[%s]' % (':'.join(map(str, parts)))
|
|
|
|
@property
|
|
def as_invite_code(self):
|
|
"""
|
|
:return: s.team invite code format (e.g. ``cv-dgb``)
|
|
:rtype: :class:`str`
|
|
"""
|
|
if self.type == EType.Individual and self.is_valid():
|
|
def repl_mapper(x):
|
|
return _icode_map[x.group()]
|
|
|
|
invite_code = re.sub("["+_icode_hex+"]", repl_mapper, "%x" % self.id)
|
|
split_idx = len(invite_code) // 2
|
|
|
|
if split_idx:
|
|
invite_code = invite_code[:split_idx] + '-' + invite_code[split_idx:]
|
|
|
|
return invite_code
|
|
|
|
@property
|
|
def as_csgo_friend_code(self):
|
|
"""
|
|
:return: CS:GO Friend code (e.g. ``AEBJA-ABDC``)
|
|
:rtype: :class:`str`
|
|
"""
|
|
if self.type != EType.Individual or not self.is_valid():
|
|
return
|
|
|
|
h = b'CSGO' + struct.pack('>L', self.account_id)
|
|
h, = struct.unpack('<L', md5_hash(h[::-1])[:4])
|
|
steamid = self.as_64
|
|
result = 0
|
|
|
|
for i in range(8):
|
|
id_nib = (steamid >> (i * 4)) & 0xF
|
|
hash_nib = (h >> i) & 0x1
|
|
a = (result << 4) | id_nib
|
|
|
|
result = ((result >> 28) << 32) | a
|
|
result = ((result >> 31) << 32) | ((a << 1) | hash_nib)
|
|
|
|
result, = struct.unpack('<Q', struct.pack('>Q', result))
|
|
code = ''
|
|
|
|
for i in range(13):
|
|
if i in (4, 9):
|
|
code += '-'
|
|
|
|
code += _csgofrcode_chars[result & 31]
|
|
result = result >> 5
|
|
|
|
return code[5:]
|
|
|
|
@property
|
|
def invite_url(self):
|
|
"""
|
|
:return: e.g ``https://s.team/p/cv-dgb``
|
|
:rtype: :class:`str`
|
|
"""
|
|
code = self.as_invite_code
|
|
if code:
|
|
return "https://s.team/p/" + code
|
|
|
|
@property
|
|
def community_url(self):
|
|
"""
|
|
:return: e.g https://steamcommunity.com/profiles/123456789
|
|
:rtype: :class:`str`
|
|
"""
|
|
suffix = {
|
|
EType.Individual: "profiles/%s",
|
|
EType.Clan: "gid/%s",
|
|
}
|
|
if self.type in suffix:
|
|
url = "https://steamcommunity.com/%s" % suffix[self.type]
|
|
return url % self.as_64
|
|
|
|
return None
|
|
|
|
def is_valid(self):
|
|
"""
|
|
Check whether this SteamID is valid
|
|
|
|
:rtype: :py:class:`bool`
|
|
"""
|
|
if self.type == EType.Invalid or self.type >= EType.Max:
|
|
return False
|
|
|
|
if self.universe == EUniverse.Invalid or self.universe >= EUniverse.Max:
|
|
return False
|
|
|
|
if self.type == EType.Individual:
|
|
if self.id == 0 or self.instance > 4:
|
|
return False
|
|
|
|
if self.type == EType.Clan:
|
|
if self.id == 0 or self.instance != 0:
|
|
return False
|
|
|
|
if self.type == EType.GameServer:
|
|
if self.id == 0:
|
|
return False
|
|
|
|
if self.type == EType.AnonGameServer:
|
|
if self.id == 0 and self.instance == 0:
|
|
return False
|
|
|
|
return True
|
|
|
|
|
|
def make_steam64(id=0, *args, **kwargs):
|
|
"""
|
|
Returns steam64 from various other representations.
|
|
|
|
.. code:: python
|
|
|
|
make_steam64() # invalid steamid
|
|
make_steam64(12345) # accountid
|
|
make_steam64('12345')
|
|
make_steam64(id=12345, type='Invalid', universe='Invalid', instance=0)
|
|
make_steam64(103582791429521412) # steam64
|
|
make_steam64('103582791429521412')
|
|
make_steam64('STEAM_1:0:2') # steam2
|
|
make_steam64('[g:1:4]') # steam3
|
|
"""
|
|
|
|
accountid = id
|
|
etype = EType.Invalid
|
|
universe = EUniverse.Invalid
|
|
instance = None
|
|
|
|
if len(args) == 0 and len(kwargs) == 0:
|
|
value = str(accountid)
|
|
|
|
# numeric input
|
|
if value.isdigit():
|
|
value = int(value)
|
|
|
|
# 32 bit account id
|
|
if 0 < value < 2**32:
|
|
accountid = value
|
|
etype = EType.Individual
|
|
universe = EUniverse.Public
|
|
# 64 bit
|
|
elif value < 2**64:
|
|
accountid = int(value & 0xFFFFFFFF)
|
|
instance = int((value >> 32) & 0xFFFFF)
|
|
etype = int((value >> 52) & 0xF)
|
|
universe = int((value >> 56) & 0xFF)
|
|
# invalid account id
|
|
else:
|
|
accountid = 0
|
|
|
|
# textual input e.g. [g:1:4]
|
|
else:
|
|
result = steam2_to_tuple(value) or steam3_to_tuple(value)
|
|
|
|
if result:
|
|
(accountid,
|
|
etype,
|
|
universe,
|
|
instance,
|
|
) = result
|
|
else:
|
|
accountid = 0
|
|
|
|
elif len(args) > 0:
|
|
length = len(args)
|
|
if length == 1:
|
|
etype, = args
|
|
elif length == 2:
|
|
etype, universe = args
|
|
elif length == 3:
|
|
etype, universe, instance = args
|
|
else:
|
|
raise TypeError("Takes at most 4 arguments (%d given)" % length)
|
|
|
|
if len(kwargs) > 0:
|
|
etype = kwargs.get('type', etype)
|
|
universe = kwargs.get('universe', universe)
|
|
instance = kwargs.get('instance', instance)
|
|
|
|
etype = (EType(etype)
|
|
if isinstance(etype, (int, EType))
|
|
else EType[etype]
|
|
)
|
|
|
|
universe = (EUniverse(universe)
|
|
if isinstance(universe, (int, EUniverse))
|
|
else EUniverse[universe]
|
|
)
|
|
|
|
if instance is None:
|
|
instance = 1 if etype in (EType.Individual, EType.GameServer) else 0
|
|
|
|
assert instance <= 0xffffF, "instance larger than 20bits"
|
|
|
|
return (universe << 56) | (etype << 52) | (instance << 32) | accountid
|
|
|
|
|
|
def steam2_to_tuple(value):
|
|
"""
|
|
:param value: steam2 (e.g. ``STEAM_1:0:1234``)
|
|
:type value: :class:`str`
|
|
:return: (accountid, type, universe, instance)
|
|
:rtype: :class:`tuple` or :class:`None`
|
|
|
|
.. note::
|
|
The universe will be always set to ``1``. See :attr:`SteamID.as_steam2`
|
|
"""
|
|
match = re.match(r"^STEAM_(?P<universe>\d+)"
|
|
r":(?P<reminder>[0-1])"
|
|
r":(?P<id>\d+)$", value
|
|
)
|
|
|
|
if not match:
|
|
return None
|
|
|
|
steam32 = (int(match.group('id')) << 1) | int(match.group('reminder'))
|
|
universe = int(match.group('universe'))
|
|
|
|
# Games before orange box used to incorrectly display universe as 0, we support that
|
|
if universe == 0:
|
|
universe = 1
|
|
|
|
return (steam32, EType(1), EUniverse(universe), 1)
|
|
|
|
|
|
def steam3_to_tuple(value):
|
|
"""
|
|
:param value: steam3 (e.g. ``[U:1:1234]``)
|
|
:type value: :class:`str`
|
|
:return: (accountid, type, universe, instance)
|
|
:rtype: :class:`tuple` or :class:`None`
|
|
"""
|
|
match = re.match(r"^\["
|
|
r"(?P<type>[i%s]):" # type char
|
|
r"(?P<universe>[0-4]):" # universe
|
|
r"(?P<id>\d{1,10})" # accountid
|
|
r"(:(?P<instance>\d+))?" # instance
|
|
r"\]$" % ETypeChars,
|
|
value
|
|
)
|
|
if not match:
|
|
return None
|
|
|
|
steam32 = int(match.group('id'))
|
|
universe = EUniverse(int(match.group('universe')))
|
|
typechar = match.group('type').replace('i', 'I')
|
|
etype = EType(ETypeChar[typechar])
|
|
instance = match.group('instance')
|
|
|
|
if typechar in 'gT':
|
|
instance = 0
|
|
elif instance is not None:
|
|
instance = int(instance)
|
|
elif typechar == 'L':
|
|
instance = EInstanceFlag.Lobby
|
|
elif typechar == 'c':
|
|
instance = EInstanceFlag.Clan
|
|
elif etype in (EType.Individual, EType.GameServer):
|
|
instance = 1
|
|
else:
|
|
instance = 0
|
|
|
|
instance = int(instance)
|
|
|
|
return (steam32, etype, universe, instance)
|
|
|
|
def from_invite_code(code, universe=EUniverse.Public):
|
|
"""
|
|
Invites urls can be generated at https://steamcommunity.com/my/friends/add
|
|
|
|
:param code: invite code (e.g. ``https://s.team/p/cv-dgb``, ``cv-dgb``)
|
|
:type code: :class:`str`
|
|
:param universe: Steam universe (default: ``Public``)
|
|
:type universe: :class:`EType`
|
|
:return: (accountid, type, universe, instance)
|
|
:rtype: :class:`tuple` or :class:`None`
|
|
"""
|
|
if not code:
|
|
return None
|
|
|
|
m = re.match(r'(https?://s\.team/p/(?P<code1>[\-'+_icode_all_valid+']+))'
|
|
r'|(?P<code2>[\-'+_icode_all_valid+']+$)'
|
|
, code)
|
|
if not m:
|
|
return None
|
|
|
|
code = (m.group('code1') or m.group('code2')).replace('-', '')
|
|
|
|
def repl_mapper(x):
|
|
return _icode_map_inv[x.group()]
|
|
|
|
accountid = int(re.sub("["+_icode_custom+"]", repl_mapper, code), 16)
|
|
|
|
if 0 < accountid < 2**32:
|
|
return SteamID(accountid, EType.Individual, EUniverse(universe), 1)
|
|
|
|
SteamID.from_invite_code = staticmethod(from_invite_code)
|
|
|
|
def from_csgo_friend_code(code, universe=EUniverse.Public):
|
|
"""
|
|
Takes CS:GO friend code and returns SteamID
|
|
|
|
:param code: CS:GO friend code (e.g. ``AEBJA-ABDC``)
|
|
:type code: :class:`str`
|
|
:param universe: Steam universe (default: ``Public``)
|
|
:type universe: :class:`EType`
|
|
:return: SteamID instance
|
|
:rtype: :class:`.SteamID` or :class:`None`
|
|
"""
|
|
if not re.match(r'^['+_csgofrcode_chars+'\-]{10}$', code):
|
|
return None
|
|
|
|
code = ('AAAA-' + code).replace('-', '')
|
|
result = 0
|
|
|
|
for i in range(13):
|
|
index = _csgofrcode_chars.find(code[i])
|
|
if index == -1:
|
|
return None
|
|
result = result | (index << 5 * i)
|
|
|
|
result, = struct.unpack('<Q', struct.pack('>Q', result))
|
|
accountid = 0
|
|
|
|
for i in range(8):
|
|
result = result >> 1
|
|
id_nib = result & 0xF
|
|
result = result >> 4
|
|
accountid = (accountid << 4) | id_nib
|
|
|
|
return SteamID(accountid, EType.Individual, EUniverse(universe), 1)
|
|
|
|
SteamID.from_csgo_friend_code = staticmethod(from_csgo_friend_code)
|
|
|
|
def steam64_from_url(url, http_timeout=30):
|
|
"""
|
|
Takes a Steam Community url and returns steam64 or None
|
|
|
|
.. warning::
|
|
Each call makes a http request to ``steamcommunity.com``
|
|
|
|
.. note::
|
|
For a reliable resolving of vanity urls use ``ISteamUser.ResolveVanityURL`` web api
|
|
|
|
:param url: steam community url
|
|
:type url: :class:`str`
|
|
:param http_timeout: how long to wait on http request before turning ``None``
|
|
:type http_timeout: :class:`int`
|
|
:return: steam64, or ``None`` if ``steamcommunity.com`` is down
|
|
:rtype: :class:`int` or :class:`None`
|
|
|
|
Example URLs::
|
|
|
|
https://steamcommunity.com/gid/[g:1:4]
|
|
https://steamcommunity.com/gid/103582791429521412
|
|
https://steamcommunity.com/groups/Valve
|
|
https://steamcommunity.com/profiles/[U:1:12]
|
|
https://steamcommunity.com/profiles/76561197960265740
|
|
https://steamcommunity.com/id/johnc
|
|
https://steamcommunity.com/user/cv-dgb/
|
|
"""
|
|
|
|
match = re.match(r'^(?P<clean_url>https?://steamcommunity.com/'
|
|
r'(?P<type>profiles|id|gid|groups|user)/(?P<value>.*?))(?:/(?:.*)?)?$', url)
|
|
|
|
if not match:
|
|
return None
|
|
|
|
web = make_requests_session()
|
|
|
|
try:
|
|
# user profiles
|
|
if match.group('type') in ('id', 'profiles', 'user'):
|
|
text = web.get(match.group('clean_url'), timeout=http_timeout).text
|
|
data_match = re.search("g_rgProfileData = (?P<json>{.*?});[ \t\r]*\n", text)
|
|
|
|
if data_match:
|
|
data = json.loads(data_match.group('json'))
|
|
return int(data['steamid'])
|
|
# group profiles
|
|
else:
|
|
text = web.get(match.group('clean_url'), timeout=http_timeout).text
|
|
data_match = re.search("OpenGroupChat\( *'(?P<steamid>\d+)'", text)
|
|
|
|
if data_match:
|
|
return int(data_match.group('steamid'))
|
|
except requests.exceptions.RequestException:
|
|
return None
|
|
|
|
|
|
def from_url(url, http_timeout=30):
|
|
"""
|
|
Takes Steam community url and returns a SteamID instance or ``None``
|
|
|
|
See :py:func:`steam64_from_url` for details
|
|
|
|
:param url: steam community url
|
|
:type url: :class:`str`
|
|
:param http_timeout: how long to wait on http request before turning ``None``
|
|
:type http_timeout: :class:`int`
|
|
:return: `SteamID` instance
|
|
:rtype: :py:class:`steam.SteamID` or :class:`None`
|
|
|
|
"""
|
|
|
|
steam64 = steam64_from_url(url, http_timeout)
|
|
|
|
if steam64:
|
|
return SteamID(steam64)
|
|
|
|
return None
|
|
|
|
SteamID.from_url = staticmethod(from_url)
|
|
|