python-xbox 0.1.3__py3-none-any.whl → 0.2.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: python-xbox
3
- Version: 0.1.3
3
+ Version: 0.2.0
4
4
  Summary: A library to authenticate with Xbox Network and use their API
5
5
  Project-URL: Source, https://github.com/tr4nt0r/python-xbox
6
6
  Author-email: Manfred Dennerlein Rodelo <manfred@dennerlein.name>
@@ -18,7 +18,7 @@ Classifier: Programming Language :: Python :: 3.13
18
18
  Classifier: Programming Language :: Python :: 3.14
19
19
  Classifier: Topic :: Software Development :: Libraries :: Python Modules
20
20
  Requires-Python: >=3.11
21
- Requires-Dist: ecdsa>=0.19
21
+ Requires-Dist: cryptography>=42.0
22
22
  Requires-Dist: httpx>=0.25.1
23
23
  Requires-Dist: ms-cv>=0.1.1
24
24
  Requires-Dist: pydantic>=2.12
@@ -32,7 +32,6 @@ Description-Content-Type: text/markdown
32
32
  [![Build](https://github.com/tr4nt0r/python-xbox/actions/workflows/build.yml/badge.svg)](https://github.com/tr4nt0r/python-xbox/actions/workflows/build.yml)
33
33
  [![codecov](https://codecov.io/gh/tr4nt0r/python-xbox/graph/badge.svg?token=DWUTrV1tM4)](https://codecov.io/gh/tr4nt0r/python-xbox)
34
34
  ![PyPI - Downloads](https://img.shields.io/pypi/dm/python-xbox?style=flat&label=pypi%20downloads)
35
- [!["Buy Me A Coffee"](https://img.shields.io/badge/-buy_me_a%C2%A0coffee-gray?logo=buy-me-a-coffee)](https://www.buymeacoffee.com/tr4nt0r)
36
35
  [![GitHub Sponsor](https://img.shields.io/badge/GitHub-Sponsor-blue?logo=github)](https://github.com/sponsors/tr4nt0r)
37
36
 
38
37
  python-xbox is a python library to authenticate with Xbox Network via your Microsoft Account and provides Xbox related Web-API.
@@ -1,4 +1,4 @@
1
- pythonxbox/__init__.py,sha256=AQuq5q_abImOM875pyuPrWs1KpPVezDg51zxZs4FeNk,91
1
+ pythonxbox/__init__.py,sha256=P49-RpR6nYYDGMV7J8O1-71G2LyIO5w-wVFTAANXIL8,91
2
2
  pythonxbox/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
3
3
  pythonxbox/api/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
4
4
  pythonxbox/api/client.py,sha256=3OqKJDAk-5Jm3zISwRqnyzJeZPrvzzUeXvvkYyPe7h4,5776
@@ -23,7 +23,7 @@ pythonxbox/api/provider/mediahub/__init__.py,sha256=vit_AYdxxLJH7hrE2BOlIiF7dySz
23
23
  pythonxbox/api/provider/mediahub/models.py,sha256=0dqaUlDpRZVRLEQcgZIVmGT97Ya6T4ccqermXzwXavo,1863
24
24
  pythonxbox/api/provider/message/__init__.py,sha256=jkHyXXD1lpqFAaLlJPrfcV7HjUwA0k_2DXPDSUfnpe4,4388
25
25
  pythonxbox/api/provider/message/models.py,sha256=P1IJblzGUYYPXE04LHSVEUmXeUqm8g5FKrO0jnm9jNE,2026
26
- pythonxbox/api/provider/people/__init__.py,sha256=QZBChNtQ-pyhllc0LUFDvuZjXNT7n-g_XOmWnwCkTU8,6649
26
+ pythonxbox/api/provider/people/__init__.py,sha256=W8rxTaMIlVvjmr45pskGvQwXFdQsnLzBv2a8Aj9hp30,7933
27
27
  pythonxbox/api/provider/people/models.py,sha256=bjS4in0jE-6cVTnPvR8aVFE57KrxIEO_-dUDHWB72l4,6891
28
28
  pythonxbox/api/provider/presence/__init__.py,sha256=oHkOe5-V89CDXClmewjHfdnovB6BPeHMgLbsay5Qhd4,3451
29
29
  pythonxbox/api/provider/presence/models.py,sha256=PbWsOGdf__wJhRp-QqHvv6GLLJkQzp7ZdRz29ICHRWk,1127
@@ -47,7 +47,7 @@ pythonxbox/common/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU
47
47
  pythonxbox/common/exceptions.py,sha256=ZxR1TK3sPmnKYC_CAii7Le8AN0spZ3TUk20B69fQdD4,1589
48
48
  pythonxbox/common/filetimes.py,sha256=0XzKnKimxqWhVgzzYc5DxWA6coRjcB37fufHXZpx67I,3322
49
49
  pythonxbox/common/models.py,sha256=azQL9-g9I0kxvAegoS5kpEfI_PlAEsnues9n8-Kb74c,673
50
- pythonxbox/common/request_signer.py,sha256=scgrZvoihqoudSCAZvUhuyAm4jBaQ_yznBNNXmzSjRg,5526
50
+ pythonxbox/common/request_signer.py,sha256=af9xA3Nq91kWeuHm2nsT4gaKW-glh_RMf8N67wbwl68,6408
51
51
  pythonxbox/common/signed_session.py,sha256=TMnWAe7pX5z9Pa7A5oQNjnoI83PhNo0OpRk_hGwCD-s,1886
52
52
  pythonxbox/common/ratelimits/__init__.py,sha256=RWfJudbQinaFQg6NMlwfsK43_wzezpfni1tyRaC3xbs,10052
53
53
  pythonxbox/common/ratelimits/models.py,sha256=DI4B63Spsy5kYvjezsJ8pCjPYM5VP_VcHkaewhMzyg8,359
@@ -57,8 +57,8 @@ pythonxbox/scripts/change_gamertag.py,sha256=k4tAqc4rDT10a-ethejg9ufs_c-fVJeYK7P
57
57
  pythonxbox/scripts/friends.py,sha256=TmcpLJ3FJaNMCNc5gVyxabgnlFd2hS_tBYautx1xD6s,2139
58
58
  pythonxbox/scripts/search.py,sha256=DusjFE4q10sHVrbdE44kBw0xvv6i_muJqZLCcnO6zzc,1074
59
59
  pythonxbox/scripts/xal.py,sha256=rRM5Opl5GlHykaf4Ad755XzUbdG24tFMVnRCuY6Z1kk,2928
60
- python_xbox-0.1.3.dist-info/METADATA,sha256=5Bthd_k2nZaOJ-GX4QJyZqleMhXc1ZPdldw6lOnH514,7839
61
- python_xbox-0.1.3.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
62
- python_xbox-0.1.3.dist-info/entry_points.txt,sha256=9k_ot4_yHrDQTbXDWZPCHtn7S_rJLLv5Sw1Vf__TY78,267
63
- python_xbox-0.1.3.dist-info/licenses/LICENSE,sha256=NylgC1sD9vQJ92ofyOI0lvDYOuvbTyWa-tCSU8KgeeI,1104
64
- python_xbox-0.1.3.dist-info/RECORD,,
60
+ python_xbox-0.2.0.dist-info/METADATA,sha256=sDv3-WzZzqBKrFXIHsDkkMuo1-_GtBys9aled4iS040,7707
61
+ python_xbox-0.2.0.dist-info/WHEEL,sha256=QccIxa26bgl1E6uMy58deGWi-0aeIkkangHcxk2kWfw,87
62
+ python_xbox-0.2.0.dist-info/entry_points.txt,sha256=9k_ot4_yHrDQTbXDWZPCHtn7S_rJLLv5Sw1Vf__TY78,267
63
+ python_xbox-0.2.0.dist-info/licenses/LICENSE,sha256=NylgC1sD9vQJ92ofyOI0lvDYOuvbTyWa-tCSU8KgeeI,1104
64
+ python_xbox-0.2.0.dist-info/RECORD,,
@@ -1,4 +1,4 @@
1
1
  Wheel-Version: 1.0
2
- Generator: hatchling 1.28.0
2
+ Generator: hatchling 1.29.0
3
3
  Root-Is-Purelib: true
4
4
  Tag: py3-none-any
pythonxbox/__init__.py CHANGED
@@ -1,4 +1,4 @@
1
1
  """Top-level package for python-xbox."""
2
2
 
3
3
  __author__ = """tr4nt0r"""
4
- __version__ = "0.1.3"
4
+ __version__ = "0.2.0"
@@ -1,5 +1,5 @@
1
1
  """
2
- People - Access friendlist from own profiles and others
2
+ People - Access friendlist and profile info from own profile and others
3
3
  """
4
4
 
5
5
  from typing import TYPE_CHECKING, ClassVar
@@ -71,7 +71,10 @@ class PeopleProvider(RateLimitedProvider):
71
71
  **kwargs,
72
72
  ) -> PeopleResponse:
73
73
  """
74
- Get friendlist of own profile
74
+ Get friendlist of a user by their XUID
75
+
76
+ Args:
77
+ xuid: XUID of the user to get friends from
75
78
 
76
79
  Returns:
77
80
  :class:`PeopleResponse`: People Response
@@ -85,6 +88,33 @@ class PeopleProvider(RateLimitedProvider):
85
88
  ]
86
89
  decoration = self.SEPERATOR.join(decoration_fields)
87
90
 
91
+ url = f"{self.PEOPLE_URL}/users/xuid({xuid})/people/friends/decoration/{decoration}"
92
+ resp = await self.client.session.get(url, headers=self._headers, **kwargs)
93
+ resp.raise_for_status()
94
+ return PeopleResponse.model_validate_json(resp.text)
95
+
96
+ async def get_friend_by_xuid(self, xuid: str, decoration_fields: list[PeopleDecoration] | None = None, **kwargs) -> PeopleResponse:
97
+ """
98
+ Get a single friend's profile from the authenticated user's perspective
99
+
100
+ This returns relationship metadata (isFriend, canBeFriended, etc.) as seen
101
+ by the caller, making it useful for profile lookups and relationship checks.
102
+
103
+ Args:
104
+ xuid: XUID of the user to retrieve profile for
105
+
106
+ Returns:
107
+ :class:`PeopleResponse`: People Response with a single person entry
108
+ """
109
+ if not decoration_fields:
110
+ decoration_fields = [
111
+ PeopleDecoration.PREFERRED_COLOR,
112
+ PeopleDecoration.DETAIL,
113
+ PeopleDecoration.MULTIPLAYER_SUMMARY,
114
+ PeopleDecoration.PRESENCE_DETAIL,
115
+ ]
116
+ decoration = self.SEPERATOR.join(decoration_fields)
117
+
88
118
  url = f"{self.PEOPLE_URL}/users/me/people/xuids({xuid})/decoration/{decoration}"
89
119
  resp = await self.client.session.get(url, headers=self._headers, **kwargs)
90
120
  resp.raise_for_status()
@@ -1,5 +1,4 @@
1
- """
2
- Request Signer
1
+ """Request Signer
3
2
 
4
3
  Employed for generating the "Signature" header in authentication requests.
5
4
  """
@@ -9,7 +8,8 @@ from datetime import UTC, datetime
9
8
  import hashlib
10
9
  import struct
11
10
 
12
- from ecdsa import NIST256p, SigningKey, VerifyingKey
11
+ from cryptography.hazmat.primitives import hashes, serialization
12
+ from cryptography.hazmat.primitives.asymmetric import ec, utils
13
13
 
14
14
  from pythonxbox.authentication.models import SignaturePolicy
15
15
  from pythonxbox.common import filetimes
@@ -22,31 +22,39 @@ DEFAULT_SIGNING_POLICY = SignaturePolicy(
22
22
  class RequestSigner:
23
23
  def __init__(
24
24
  self,
25
- signing_key: SigningKey | None = None,
25
+ signing_key: ec.EllipticCurvePrivateKey | None = None,
26
26
  signing_policy: SignaturePolicy | None = None,
27
27
  ) -> None:
28
- self.signing_key = signing_key or SigningKey.generate(curve=NIST256p)
28
+ self.signing_key = signing_key or ec.generate_private_key(ec.SECP256R1())
29
29
  self.signing_policy = signing_policy or DEFAULT_SIGNING_POLICY
30
30
 
31
- pk_point = self.signing_key.verifying_key.pubkey.point
31
+ pub_nums = self.signing_key.public_key().public_numbers()
32
32
  self.proof_field = {
33
33
  "use": "sig",
34
34
  "alg": self.signing_policy.supported_algorithms[0],
35
35
  "kty": "EC",
36
36
  "crv": "P-256",
37
- "x": self.__encode_ec_coord(pk_point.x()),
38
- "y": self.__encode_ec_coord(pk_point.y()),
37
+ "x": self.__encode_ec_coord(pub_nums.x),
38
+ "y": self.__encode_ec_coord(pub_nums.y),
39
39
  }
40
40
 
41
41
  def export_signing_key(self) -> str:
42
- return self.signing_key.to_pem().decode()
42
+ return self.signing_key.private_bytes(
43
+ serialization.Encoding.PEM,
44
+ serialization.PrivateFormat.TraditionalOpenSSL,
45
+ serialization.NoEncryption(),
46
+ ).decode()
43
47
 
44
48
  @staticmethod
45
- def import_signing_key(signing_key: str) -> SigningKey:
46
- return SigningKey.from_pem(signing_key)
49
+ def import_signing_key(signing_key: str) -> ec.EllipticCurvePrivateKey:
50
+ key = serialization.load_pem_private_key(signing_key.encode(), password=None)
51
+ if not isinstance(key, ec.EllipticCurvePrivateKey):
52
+ msg = "Expected an EC private key"
53
+ raise TypeError(msg)
54
+ return key
47
55
 
48
56
  @classmethod
49
- def from_pem(cls, pem_string: str) -> SigningKey:
57
+ def from_pem(cls, pem_string: str) -> "RequestSigner":
50
58
  request_signer = RequestSigner.import_signing_key(pem_string)
51
59
  return cls(request_signer)
52
60
 
@@ -79,7 +87,7 @@ class RequestSigner:
79
87
  self,
80
88
  signature: bytes,
81
89
  digest: bytes,
82
- verifying_key: VerifyingKey | None = None,
90
+ verifying_key: ec.EllipticCurvePublicKey | None = None,
83
91
  ) -> bool:
84
92
  """
85
93
  Verify signature against digest
@@ -91,8 +99,12 @@ class RequestSigner:
91
99
 
92
100
  Returns: True on successful verification, False otherwise
93
101
  """
94
- verifier = verifying_key or self.signing_key.verifying_key
95
- return verifier.verify_digest(signature, digest)
102
+ verifier = verifying_key or self.signing_key.public_key()
103
+ r = int.from_bytes(signature[:32], "big")
104
+ s = int.from_bytes(signature[32:], "big")
105
+ der_sig = utils.encode_dss_signature(r, s)
106
+ verifier.verify(der_sig, digest, ec.ECDSA(utils.Prehashed(hashes.SHA256())))
107
+ return True
96
108
 
97
109
  def sign(
98
110
  self,
@@ -139,7 +151,12 @@ class RequestSigner:
139
151
  digest = self._hash(data)
140
152
 
141
153
  # Sign the hash
142
- signature = self.signing_key.sign_digest_deterministic(digest)
154
+ der_sig = self.signing_key.sign(
155
+ digest,
156
+ ec.ECDSA(utils.Prehashed(hashes.SHA256()), deterministic_signing=True),
157
+ )
158
+ r, s = utils.decode_dss_signature(der_sig)
159
+ signature = r.to_bytes(32, "big") + s.to_bytes(32, "big")
143
160
 
144
161
  # Return signature version + timestamp encoded + signature
145
162
  return signature_version_bytes + ts_bytes + signature