# Copyright (C) 2013-2017 Oliver Ainsworth # Original source: https://github.com/serverstf/python-valve """Provides utilities for representing SteamIDs See: https://developer.valvesoftware.com/wiki/SteamID """ import re import urllib.parse import warnings UNIVERSE_INDIVIDUAL = 0 #: UNIVERSE_PUBLIC = 1 #: UNIVERSE_BETA = 2 #: UNIVERSE_INTERNAL = 3 #: UNIVERSE_DEV = 4 #: UNIVERSE_RC = 5 #: UNIVERSES = [ UNIVERSE_INDIVIDUAL, UNIVERSE_PUBLIC, UNIVERSE_BETA, UNIVERSE_INTERNAL, UNIVERSE_DEV, UNIVERSE_RC, ] TYPE_INVALID = 0 #: TYPE_INDIVIDUAL = 1 #: TYPE_MULTISEAT = 2 #: TYPE_GAME_SERVER = 3 #: TYPE_ANON_GAME_SERVER = 4 #: TYPE_PENDING = 5 #: TYPE_CONTENT_SERVER = 6 #: TYPE_CLAN = 7 #: TYPE_CHAT = 8 #: TYPE_P2P_SUPER_SEEDER = 9 #: TYPE_ANON_USER = 10 #: ACCOUNT_TYPES = [ TYPE_INVALID, TYPE_INDIVIDUAL, TYPE_MULTISEAT, TYPE_GAME_SERVER, TYPE_ANON_GAME_SERVER, TYPE_PENDING, TYPE_CONTENT_SERVER, TYPE_CLAN, TYPE_CHAT, TYPE_P2P_SUPER_SEEDER, TYPE_ANON_USER, ] TYPE_LETTER_MAP = { TYPE_INDIVIDUAL: "U", TYPE_CLAN: "g", TYPE_CHAT: "T", } LETTER_TYPE_MAP = {v: k for k, v in TYPE_LETTER_MAP.items()} TYPE_URL_PATH_MAP = { TYPE_INDIVIDUAL: ["profiles", "id"], TYPE_CLAN: ["groups", "gid"], } TEXTUAL_ID_REGEX = re.compile(r"^STEAM_(?P\d+):(?P\d+):(?P\d+)$") COMMUNITY32_REGEX = re.compile( r".*/(?P{paths})/\[(?P[{type_chars}]):1:(?P\d+)\]$".format( paths="|".join("|".join(paths) for paths in TYPE_URL_PATH_MAP.values()), type_chars="".join(c for c in TYPE_LETTER_MAP.values()), ) ) COMMUNITY64_REGEX = re.compile( r".*/(?P{paths})/(?P\d+)$".format( paths="|".join("|".join(paths) for paths in TYPE_URL_PATH_MAP.values()) ) ) class SteamIDError(ValueError): """Raised when parsing or building invalid SteamIDs""" class SteamID: """Represents a SteamID A SteamID is broken up into four components: a 32 bit account number, a 20 bit "instance" identifier, a 4 bit account type and an 8 bit "universe" identifier. There are 10 known accounts types as listed below. Generally you won't encounter types other than "individual" and "group". +----------------+---------+---------------+---------------------------+ | Type | Numeric | Can be mapped | Constant | | | value | to URL | | +================+=========+===============+===========================+ | Invalid | 0 | No | ``TYPE_INVALID`` | +----------------+---------+---------------+---------------------------+ | Individual | 1 | Yes | ``TYPE_INDIVIDUAL`` | +----------------+---------+---------------+---------------------------+ | Multiseat | 2 | No | ``TYPE_MULTISEAT`` | +----------------+---------+---------------+---------------------------+ | Game server | 3 | No | ``TYPE_GAME_SERVER`` | +----------------+---------+---------------+---------------------------+ | Anonymous game | 4 | No | ``TYPE_ANON_GAME_SERVER`` | | server | | | | +----------------+---------+---------------+---------------------------+ | Pending | 5 | No | ``TYPE_PENDING`` | +----------------+---------+---------------+---------------------------+ | Content server | 6 | No | ``TYPE_CONTENT_SERVER`` | +----------------+---------+---------------+---------------------------+ | Group | 7 | Yes | ``TYPE_CLAN`` | +----------------+---------+---------------+---------------------------+ | Chat | 8 | No | ``TYPE_CHAT`` | +----------------+---------+---------------+---------------------------+ | "P2P Super | 9 | No | ``TYPE_P2P_SUPER_SEEDER`` | | Seeder" | | | | +----------------+---------+---------------+---------------------------+ | Anonymous user | 10 | No | ``TYPE_ANON_USER`` | +----------------+---------+---------------+---------------------------+ ``TYPE_``-prefixed constants are provided by the :mod:`valve.steam.id` module for the numerical values of each type. All SteamIDs can be represented textually as well as by their numerical components. This is typically in the STEAM_X:Y:Z form where X, Y, Z are the "universe", "instance" and the account number respectively. There are two special cases however. If the account type if invalid then "UNKNOWN" is the textual representation. Similarly "STEAM_ID_PENDING" is used when the type is pending. As well as the the textual representation of SteamIDs there are also the 64 and 32 bit versions which contain the SteamID components encoded into integers of corresponding width. However the 32-bit representation also includes a letter to indicate account type. """ #: Used for building community URLs base_community_url = "http://steamcommunity.com/" @classmethod def from_community_url(cls, steam_id, universe=UNIVERSE_INDIVIDUAL): """Parse a Steam community URL into a :class:`.SteamID` instance This takes a Steam community URL for a profile or group and converts it to a SteamID. The type of the ID is infered from the type character in 32-bit community urls (``[U:1:1]`` for example) or from the URL path (``/profile`` or ``/groups``) for 64-bit URLs. As there is no way to determine the universe directly from URL it must be expliticly set, defaulting to :data:`UNIVERSE_INDIVIDUAL`. Raises :class:`.SteamIDError` if the URL cannot be parsed. """ url = urllib.parse.urlparse(steam_id) match = COMMUNITY32_REGEX.match(url.path) if match: if match.group("path") not in TYPE_URL_PATH_MAP[LETTER_TYPE_MAP[match.group("type")]]: warnings.warn( "Community URL ({}) path doesn't match type character".format(url.path), stacklevel=2, ) steamid = int(match.group("steamid")) instance = steamid & 1 account_number = (steamid - instance) / 2 return cls(account_number, instance, LETTER_TYPE_MAP[match.group("type")], universe) match = COMMUNITY64_REGEX.match(url.path) if match: steamid = int(match.group("steamid")) instance = steamid & 1 if match.group("path") in TYPE_URL_PATH_MAP[TYPE_INDIVIDUAL]: account_number = cls.get_account_number_from_steamid(steamid) account_type = TYPE_INDIVIDUAL elif match.group("path") in TYPE_URL_PATH_MAP[TYPE_CLAN]: account_number = (steamid - instance - 0x0170000000000000) / 2 account_type = TYPE_CLAN return cls(account_number, instance, account_type, universe) raise SteamIDError("Invalid Steam community URL ({})".format(url)) @classmethod def from_steamid64(cls, steamid, account_type=TYPE_INDIVIDUAL, universe=UNIVERSE_INDIVIDUAL): """Create an instance of SteamID from a SteamID64. Only for normal user accounts.""" instance = steamid & 1 account_number = cls.get_account_number_from_steamid(steamid) return cls(account_number, instance, account_type, universe) @staticmethod def get_account_number_from_steamid(steamid): """Return account number from 64bit SteamID. For individuals only.""" instance = steamid & 1 return (steamid - instance - 0x0110000100000000) / 2 @classmethod def from_text(cls, steam_id, account_type=TYPE_INDIVIDUAL): """Parse a SteamID in the STEAM_X:Y:Z form Takes a teaxtual SteamID in the form STEAM_X:Y:Z and returns a corresponding :class:`.SteamID` instance. The X represents the account's 'universe,' Z is the account number and Y is either 1 or 0. As the account type cannot be directly inferred from the SteamID it must be explicitly specified, defaulting to :data:`TYPE_INDIVIDUAL`. The two special IDs ``STEAM_ID_PENDING`` and ``UNKNOWN`` are also handled returning SteamID instances with the appropriate types set (:data:`TYPE_PENDING` and :data:`TYPE_INVALID` respectively) and with all other components of the ID set to zero. """ if steam_id == "STEAM_ID_PENDING": return cls(0, 0, TYPE_PENDING, 0) if steam_id == "UNKNOWN": return cls(0, 0, TYPE_INVALID, 0) match = TEXTUAL_ID_REGEX.match(steam_id) if not match: raise SteamIDError("ID '{}' doesn't match format {}".format(steam_id, TEXTUAL_ID_REGEX.pattern)) return cls(int(match.group("Z")), int(match.group("Y")), account_type, int(match.group("X"))) def __init__(self, account_number, instance, account_type, universe): if universe not in UNIVERSES: raise SteamIDError("Invalid universe {}".format(universe)) if account_type not in ACCOUNT_TYPES: raise SteamIDError("Invalid type {}".format(account_type)) if account_number < 0 or account_number > (2**32) - 1: raise SteamIDError("Account number ({}) out of range".format(account_number)) if instance not in [1, 0]: raise SteamIDError("Expected instance to be 1 or 0, got {}".format(instance)) self.account_number = int(account_number) # Z self.instance = instance # Y self.account_type = account_type self.universe = universe # X @property def type_name(self): """The account type as a string""" return {v: k for k, v in globals().iteritems() if k.startswith("TYPE_")}.get( self.account_type, self.account_type ) def __str__(self): """The textual representation of the SteamID This is in the STEAM_X:Y:Z form and can be parsed by :meth:`.from_text` to produce an equivalent :class:`.` instance. Alternately ``STEAM_ID_PENDING`` or ``UNKNOWN`` may be returned if the account type is :data:`TYPE_PENDING` or :data:`TYPE_INVALID` respectively. .. note:: :meth:`.from_text` will still handle the ``STEAM_ID_PENDING`` and ``UNKNOWN`` cases. """ if self.account_type == TYPE_PENDING: return "STEAM_ID_PENDING" if self.account_type == TYPE_INVALID: return "UNKNOWN" return "STEAM_{}:{}:{}".format(self.universe, self.instance, self.account_number) def __int__(self): """The 64 bit representation of the SteamID 64 bit SteamIDs are only valid for those with the type :data:`TYPE_INDIVIDUAL` or :data:`TYPE_CLAN`. For all other types :class:`.SteamIDError` will be raised. The 64 bit representation is calculated by multiplying the account number by two then adding the "instance" and then adding another constant which varies based on the account type. For :data:`TYPE_INDIVIDUAL` the constant is ``0x0110000100000000``, whereas for :data:`TYPE_CLAN` it's ``0x0170000000000000``. """ if self.account_type == TYPE_INDIVIDUAL: return (self.account_number * 2) + 0x0110000100000000 + self.instance if self.account_type == TYPE_CLAN: return (self.account_number * 2) + 0x0170000000000000 + self.instance raise SteamIDError("Cannot create 64-bit identifier for SteamID with type {}".format(self.type_name)) def __eq__(self, other): try: return ( self.account_number == other.account_number and self.instance == other.instance and self.account_type == other.type and self.universe == other.universe ) except AttributeError: return False # Should probably raise TypeError def __ne__(self, other): return not self == other def as_32(self): """Returns the 32 bit community ID as a string This is only applicable for :data:`TYPE_INDIVIDUAL`, :data:`TYPE_CLAN` and :data:`TYPE_CHAT` types. For any other types, attempting to generate the 32-bit representation will result in a :class:`.SteamIDError` being raised. """ try: return "[{}:1:{}]".format(TYPE_LETTER_MAP[self.account_type], self.get_32_bit_community_id()) except KeyError as ex: raise SteamIDError( "Cannot create 32-bit indentifier for SteamID with type {}".format(self.type_name) ) from ex def get_32_bit_community_id(self): return (self.account_number * 2) + self.instance def as_64(self): """Returns the 64 bit representation as a string This is only possible if the ID type is :data:`TYPE_INDIVIDUAL` or :data:`TYPE_CLAN`, otherwise :class:`.SteamIDError` is raised. """ return str(int(self)) def community_url(self, id64=True): """Returns the full URL to the Steam Community page for the SteamID This can either be generate a URL from the 64 bit representation (the default) or the 32 bit one. Generating community URLs is only supported for IDs of type :data:`TYPE_INDIVIDUAL` and :data:`TYPE_CLAN`. Attempting to generate a URL for any other type will result in a :class:`.SteamIDError` being raised. """ path_func = self.as_64 if id64 else self.as_32 try: return urllib.parse.urljoin( self.base_community_url, "/".join((TYPE_URL_PATH_MAP[self.account_type][0], path_func())) ) except KeyError as ex: raise SteamIDError("Cannot generate community URL for type {}".format(self.type_name)) from ex