Add steamid utility module

This commit is contained in:
Mathieu Comandon 2023-10-05 21:53:54 -07:00
parent cf21587323
commit d180c7ee4c
4 changed files with 368 additions and 19 deletions

View file

@ -17,7 +17,7 @@ from lutris.services.service_game import ServiceGame
from lutris.services.service_media import ServiceMedia
from lutris.util.log import logger
from lutris.util.steam.appmanifest import AppManifest, get_appmanifests
from lutris.util.steam.config import get_steam_library, get_steamapps_dirs, get_user_steam_id
from lutris.util.steam.config import get_steam_library, get_steamapps_dirs, get_user_steam_id64
from lutris.util.strings import slugify
@ -91,7 +91,7 @@ class SteamService(BaseService):
def load(self):
"""Return importable Steam games"""
steamid = get_user_steam_id()
steamid = get_user_steam_id64()
if not steamid:
logger.error("Unable to find SteamID from Steam config")
return

View file

@ -77,8 +77,20 @@ def get_config_value(config, key):
return config[keymap[key.lower()]]
def get_user_steam_id():
"""Read user's SteamID from Steam config files"""
def get_steam_users():
"""Return the list of Steam users on this system and the base path where the settings are located"""
for steam_dir in STEAM_DATA_DIRS:
userdata_path = os.path.join(os.path.expanduser(steam_dir), "userdata")
if not os.path.exists(userdata_path):
continue
user_ids = [f for f in os.listdir(userdata_path) if f.isnumeric()]
if user_ids:
return userdata_path, user_ids
return "", []
def get_user_steam_id64():
"""Read user's SteamID64 from Steam config files"""
user_config = read_user_config()
if not user_config or "users" not in user_config:
return

View file

@ -11,20 +11,7 @@ from lutris.game import Game
from lutris.util import resources, system
from lutris.util.log import logger
from lutris.util.steam import vdf
from lutris.util.steam.config import STEAM_DATA_DIRS
def get_steam_users():
"""Return the list of Steam users on this system and the base path where the settings are located"""
logger.debug("Fetching Steam users")
for steam_dir in STEAM_DATA_DIRS:
userdata_path = os.path.join(os.path.expanduser(steam_dir), "userdata")
if not os.path.exists(userdata_path):
continue
user_ids = [f for f in os.listdir(userdata_path) if f.isnumeric()]
if user_ids:
return userdata_path, user_ids
return "", []
from lutris.util.steam.config import get_steam_users
def get_config_path() -> str:
@ -45,7 +32,7 @@ def get_config_path() -> str:
def get_shortcuts_vdf_path() -> str:
config_path = get_config_path()
if not config_path:
return None
return ""
return os.path.join(config_path, "shortcuts.vdf")

View file

@ -0,0 +1,350 @@
# 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<X>\d+):(?P<Y>\d+):(?P<Z>\d+)$")
COMMUNITY32_REGEX = re.compile(r".*/(?P<path>{paths})/\[(?P<type>[{type_chars}]):1:(?P<steamid>\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<path>{paths})/(?P<steamid>\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))
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):
"""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)
account_type = TYPE_INDIVIDUAL
universe = UNIVERSE_INDIVIDUAL
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