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

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)