Skip to content

Commit 3e28af6

Browse files
committed
Add community session persistence helpers
1 parent 18ed52f commit 3e28af6

4 files changed

Lines changed: 580 additions & 2 deletions

File tree

examples/smoke_test.py

Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -824,6 +824,10 @@ def run_community_suite(client: SteamClient, args) -> None:
824824
"Get Web API Key Page State",
825825
lambda: _format_web_api_key_page_state(client.get_web_api_key_page_state()),
826826
)
827+
run_check(
828+
"Get Community Session State",
829+
lambda: _format_community_session_state(client.get_community_session_state()),
830+
)
827831
run_check(
828832
"Fetch Group ID64",
829833
lambda: str(client.get_group_id64(args.group_url)),
@@ -867,6 +871,26 @@ def run_community_suite(client: SteamClient, args) -> None:
867871
"Community Cookie Export/Import Roundtrip",
868872
lambda: _format_cookie_roundtrip(client, args),
869873
)
874+
run_check(
875+
"Community Cookie Mapping Roundtrip",
876+
lambda: _format_cookie_mapping_roundtrip(client, args),
877+
)
878+
run_check(
879+
"Community Session Bundle Roundtrip",
880+
lambda: _format_bundle_roundtrip(client, args),
881+
)
882+
run_check(
883+
"Community Session Bundle JSON Roundtrip",
884+
lambda: _format_bundle_json_roundtrip(client, args),
885+
)
886+
run_check(
887+
"Export Community Refresh Token",
888+
lambda: _format_refresh_token_export(client),
889+
)
890+
run_check(
891+
"Build Community Requests Session",
892+
lambda: _format_built_requests_session(client),
893+
)
870894
run_check(
871895
"Get Own Inventory Items",
872896
lambda: _format_inventory_items(
@@ -1552,6 +1576,16 @@ def _format_web_api_key_page_state(payload: dict) -> str:
15521576
)
15531577

15541578

1579+
def _format_community_session_state(payload: dict) -> str:
1580+
return "logged_in={0} steamid={1} refresh={2} access={3} cookie={4}".format(
1581+
payload.get("logged_in"),
1582+
payload.get("steam_id"),
1583+
payload.get("has_refresh_token"),
1584+
payload.get("has_access_token"),
1585+
payload.get("has_steam_login_secure"),
1586+
)
1587+
1588+
15551589
def _safe_console_text(value) -> str:
15561590
text = str(value)
15571591
return text.encode("cp1252", errors="replace").decode("cp1252")
@@ -1587,6 +1621,74 @@ def _format_cookie_roundtrip(client: SteamClient, args) -> str:
15871621
roundtrip_client.close()
15881622

15891623

1624+
def _format_cookie_mapping_roundtrip(client: SteamClient, args) -> str:
1625+
cookie_mapping = client.export_community_cookie_mapping()
1626+
roundtrip_client = build_client(args)
1627+
try:
1628+
roundtrip_client.set_community_credentials_from_cookie_mapping(cookie_mapping)
1629+
state = roundtrip_client.get_community_session_state()
1630+
return "steamid={0} logged_in={1} keys={2}".format(
1631+
state.get("steam_id", "<unknown>"),
1632+
state.get("logged_in", False),
1633+
",".join(sorted(cookie_mapping.keys())),
1634+
)
1635+
finally:
1636+
roundtrip_client.close()
1637+
1638+
1639+
def _format_bundle_roundtrip(client: SteamClient, args) -> str:
1640+
bundle = client.export_community_session_bundle()
1641+
roundtrip_client = build_client(args)
1642+
try:
1643+
roundtrip_client.set_community_credentials_from_bundle(bundle)
1644+
state = roundtrip_client.get_community_session_state()
1645+
return "steamid={0} logged_in={1} refresh={2}".format(
1646+
state.get("steam_id", "<unknown>"),
1647+
state.get("logged_in", False),
1648+
state.get("has_refresh_token", False),
1649+
)
1650+
finally:
1651+
roundtrip_client.close()
1652+
1653+
1654+
def _format_bundle_json_roundtrip(client: SteamClient, args) -> str:
1655+
bundle_json = client.export_community_session_bundle_json()
1656+
roundtrip_client = build_client(args)
1657+
try:
1658+
roundtrip_client.set_community_credentials_from_bundle_json(bundle_json)
1659+
state = roundtrip_client.get_community_session_state()
1660+
return "steamid={0} logged_in={1} json_length={2}".format(
1661+
state.get("steam_id", "<unknown>"),
1662+
state.get("logged_in", False),
1663+
len(bundle_json),
1664+
)
1665+
finally:
1666+
roundtrip_client.close()
1667+
1668+
1669+
def _format_refresh_token_export(client: SteamClient) -> str:
1670+
token = client.export_community_refresh_token()
1671+
return "present={0} length={1}".format(bool(token), len(token))
1672+
1673+
1674+
def _format_built_requests_session(client: SteamClient) -> str:
1675+
state = client.get_community_session_state()
1676+
session = client.build_community_requests_session()
1677+
try:
1678+
response = session.get(
1679+
"https://steamcommunity.com/profiles/{0}/edit/".format(state.get("steam_id")),
1680+
timeout=client.timeout,
1681+
)
1682+
has_profile_data = 'data-profile-edit="' in response.text and 'data-userinfo="' in response.text
1683+
return "cookies={0} status={1} profile_data={2}".format(
1684+
len(session.cookies),
1685+
response.status_code,
1686+
has_profile_data,
1687+
)
1688+
finally:
1689+
session.close()
1690+
1691+
15901692
def _capture_result(cache: dict, key: str, fetcher, formatter=str) -> str:
15911693
value = fetcher()
15921694
cache[key] = value

src/steamcommunitykit/client.py

Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
import requests
77

88
from steamcommunitykit.constants import DEFAULT_TIMEOUT
9+
from steamcommunitykit.exceptions import SteamValidationError
910
from steamcommunitykit.http import SteamHTTPTransport
1011
from steamcommunitykit.models import CommunityCredentials, CredentialLoginResult
1112
from steamcommunitykit.services import (
@@ -72,6 +73,14 @@ def __init__(
7273
def api_key(self) -> Optional[str]:
7374
return self._transport.api_key
7475

76+
@property
77+
def session(self) -> requests.Session:
78+
return self._transport.session
79+
80+
@property
81+
def timeout(self) -> float:
82+
return self._transport.timeout
83+
7584
def set_api_key(self, api_key: str) -> None:
7685
self._transport.api_key = api_key
7786

@@ -1768,6 +1777,21 @@ def set_community_credentials_from_cookie_string(self, cookie_string: str) -> Co
17681777
self.set_community_credentials(credentials)
17691778
return credentials
17701779

1780+
def set_community_credentials_from_cookie_mapping(self, cookies: dict) -> CommunityCredentials:
1781+
credentials = self.auth.community_credentials_from_cookie_mapping(cookies)
1782+
self.set_community_credentials(credentials)
1783+
return credentials
1784+
1785+
def set_community_credentials_from_bundle(self, bundle: dict) -> CommunityCredentials:
1786+
credentials = self.auth.community_credentials_from_bundle(bundle)
1787+
self.set_community_credentials(credentials)
1788+
return credentials
1789+
1790+
def set_community_credentials_from_bundle_json(self, bundle_json: str) -> CommunityCredentials:
1791+
credentials = self.auth.community_credentials_from_bundle_json(bundle_json)
1792+
self.set_community_credentials(credentials)
1793+
return credentials
1794+
17711795
def login_to_community_with_refresh_token(self, refresh_token: str) -> CommunityCredentials:
17721796
credentials = self.auth.community_credentials_from_refresh_token(refresh_token)
17731797
self.set_community_credentials(credentials)
@@ -1776,6 +1800,57 @@ def login_to_community_with_refresh_token(self, refresh_token: str) -> Community
17761800
def export_community_cookie_string(self) -> str:
17771801
return self.auth.export_cookie_string(self._transport.require_community_credentials())
17781802

1803+
def export_community_cookie_mapping(self) -> dict:
1804+
return self.auth.export_cookie_mapping(self._transport.require_community_credentials())
1805+
1806+
def export_community_session_bundle(self) -> dict:
1807+
return self.auth.export_credentials_bundle(self._transport.require_community_credentials())
1808+
1809+
def export_community_session_bundle_json(self, *, indent: Optional[int] = None) -> str:
1810+
return self.auth.export_credentials_bundle_json(
1811+
self._transport.require_community_credentials(),
1812+
indent=indent,
1813+
)
1814+
1815+
def export_community_refresh_token(self) -> str:
1816+
credentials = self._transport.require_community_credentials()
1817+
if not credentials.refresh_token:
1818+
raise SteamValidationError("Current community credentials do not include a refresh token.")
1819+
return credentials.refresh_token
1820+
1821+
def get_community_session_state(self) -> dict:
1822+
credentials = self._transport.community_credentials
1823+
if credentials is None:
1824+
return {
1825+
"logged_in": False,
1826+
"steam_id": None,
1827+
"session_id_present": False,
1828+
"has_access_token": False,
1829+
"has_refresh_token": False,
1830+
"has_steam_login_secure": False,
1831+
}
1832+
return {
1833+
"logged_in": True,
1834+
"steam_id": credentials.steam_id,
1835+
"session_id_present": bool(credentials.session_id),
1836+
"has_access_token": bool(credentials.access_token),
1837+
"has_refresh_token": bool(credentials.refresh_token),
1838+
"has_steam_login_secure": bool(credentials.steam_login_secure or credentials.access_token),
1839+
}
1840+
1841+
def build_community_requests_session(
1842+
self,
1843+
session: Optional[requests.Session] = None,
1844+
) -> requests.Session:
1845+
resolved_session = session or requests.Session()
1846+
user_agent = self._transport.session.headers.get("User-Agent")
1847+
if user_agent and "User-Agent" not in resolved_session.headers:
1848+
resolved_session.headers["User-Agent"] = user_agent
1849+
return self.auth.apply_community_credentials_to_session(
1850+
resolved_session,
1851+
self._transport.require_community_credentials(),
1852+
)
1853+
17791854
def get_community_profile_bundle(self, steam_id=None) -> dict:
17801855
return self.community.get_profile_bundle(steam_id)
17811856

src/steamcommunitykit/services/auth.py

Lines changed: 123 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,14 @@
11
from __future__ import annotations
22

33
import base64
4+
import json
45
import time
56
import urllib.parse
7+
from collections.abc import Mapping
68
from typing import Callable, Optional
79

810
import jwt
11+
import requests
912
import rsa
1013

1114
from steamcommunitykit.constants import COMMUNITY_BASE_URL, QR_IMAGE_BASE_URL, WEB_API_BASE_URL
@@ -370,23 +373,141 @@ def community_credentials_from_refresh_token(
370373

371374
def community_credentials_from_cookie_string(self, cookie_string: str) -> CommunityCredentials:
372375
cookies = parse_cookie_string(cookie_string)
376+
return self.community_credentials_from_cookie_mapping(cookies)
377+
378+
def community_credentials_from_cookie_mapping(
379+
self,
380+
cookies: Mapping[str, str],
381+
) -> CommunityCredentials:
382+
if not isinstance(cookies, Mapping):
383+
raise SteamAuthenticationError("community cookies must be a mapping.", status_code=400)
373384
session_id = cookies.get("sessionid")
374385
steam_login_secure = cookies.get("steamLoginSecure")
375386
if not session_id:
376-
raise RuntimeError("cookie_string must contain a sessionid cookie.")
387+
raise RuntimeError("community cookies must contain a sessionid cookie.")
377388
if not steam_login_secure:
378-
raise RuntimeError("cookie_string must contain a steamLoginSecure cookie.")
389+
raise RuntimeError("community cookies must contain a steamLoginSecure cookie.")
379390
return CommunityCredentials.from_cookie_pair(
380391
session_id=session_id,
381392
steam_login_secure=steam_login_secure,
382393
)
383394

395+
def community_credentials_from_bundle(self, bundle: dict) -> CommunityCredentials:
396+
if not isinstance(bundle, dict):
397+
raise SteamAuthenticationError("community bundle must be a dictionary.", status_code=400)
398+
399+
session_id = bundle.get("session_id") or bundle.get("sessionid")
400+
steam_login_secure = bundle.get("steam_login_secure") or bundle.get("steamLoginSecure")
401+
refresh_token = bundle.get("refresh_token")
402+
access_token = bundle.get("access_token")
403+
steam_id = bundle.get("steam_id") or bundle.get("steamid")
404+
405+
if steam_login_secure and session_id:
406+
credentials = CommunityCredentials.from_cookie_pair(
407+
session_id=str(session_id),
408+
steam_login_secure=str(steam_login_secure),
409+
)
410+
if steam_id is not None and str(steam_id) != credentials.steam_id:
411+
raise SteamAuthenticationError(
412+
"community bundle steam_id does not match steamLoginSecure.",
413+
status_code=400,
414+
)
415+
credentials.refresh_token = str(refresh_token) if refresh_token else None
416+
if access_token:
417+
credentials.access_token = str(access_token)
418+
return credentials
419+
420+
if refresh_token:
421+
return self.community_credentials_from_refresh_token(
422+
str(refresh_token),
423+
session_id=str(session_id) if session_id else None,
424+
)
425+
426+
if session_id and steam_id and access_token:
427+
return CommunityCredentials(
428+
steam_id=validate_steam_id(steam_id, "steam_id"),
429+
session_id=ensure_not_blank(str(session_id), "session_id"),
430+
access_token=ensure_not_blank(str(access_token), "access_token"),
431+
refresh_token=str(refresh_token) if refresh_token else None,
432+
)
433+
434+
raise SteamAuthenticationError(
435+
"community bundle must include either session_id + steamLoginSecure, refresh_token, or session_id + steam_id + access_token.",
436+
status_code=400,
437+
)
438+
439+
def community_credentials_from_bundle_json(self, bundle_json: str) -> CommunityCredentials:
440+
try:
441+
bundle = json.loads(ensure_not_blank(bundle_json, "bundle_json"))
442+
except json.JSONDecodeError as exc:
443+
raise SteamAuthenticationError(
444+
"community bundle JSON is not valid JSON.",
445+
status_code=400,
446+
) from exc
447+
return self.community_credentials_from_bundle(bundle)
448+
384449
@staticmethod
385450
def export_cookie_string(credentials: CommunityCredentials) -> str:
451+
cookies = AuthenticationService.export_cookie_mapping(credentials)
386452
return "sessionid={0}; steamLoginSecure={1}".format(
453+
cookies["sessionid"],
454+
cookies["steamLoginSecure"],
455+
)
456+
457+
@staticmethod
458+
def export_cookie_mapping(credentials: CommunityCredentials) -> dict:
459+
return {
460+
"sessionid": credentials.session_id,
461+
"steamLoginSecure": credentials.steam_login_secure_value,
462+
}
463+
464+
@staticmethod
465+
def export_credentials_bundle(credentials: CommunityCredentials) -> dict:
466+
bundle = {
467+
"steam_id": credentials.steam_id,
468+
"session_id": credentials.session_id,
469+
"has_access_token": bool(credentials.access_token),
470+
"has_refresh_token": bool(credentials.refresh_token),
471+
"has_steam_login_secure": bool(credentials.steam_login_secure or credentials.access_token),
472+
}
473+
if credentials.access_token:
474+
bundle["access_token"] = credentials.access_token
475+
if credentials.refresh_token:
476+
bundle["refresh_token"] = credentials.refresh_token
477+
if credentials.steam_login_secure:
478+
bundle["steam_login_secure"] = credentials.steam_login_secure
479+
return bundle
480+
481+
@staticmethod
482+
def export_credentials_bundle_json(
483+
credentials: CommunityCredentials,
484+
*,
485+
indent: Optional[int] = None,
486+
) -> str:
487+
return json.dumps(
488+
AuthenticationService.export_credentials_bundle(credentials),
489+
indent=indent,
490+
sort_keys=True,
491+
)
492+
493+
@staticmethod
494+
def apply_community_credentials_to_session(
495+
session: requests.Session,
496+
credentials: CommunityCredentials,
497+
) -> requests.Session:
498+
session.cookies.set(
499+
"sessionid",
387500
credentials.session_id,
501+
domain=".steamcommunity.com",
502+
path="/",
503+
)
504+
session.cookies.set(
505+
"steamLoginSecure",
388506
credentials.steam_login_secure_value,
507+
domain=".steamcommunity.com",
508+
path="/",
389509
)
510+
return session
390511

391512
@staticmethod
392513
def decode_jwt(token: str) -> dict:

0 commit comments

Comments
 (0)