python-xbox 0.1.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.
Files changed (64) hide show
  1. python_xbox-0.1.0.dist-info/METADATA +217 -0
  2. python_xbox-0.1.0.dist-info/RECORD +64 -0
  3. python_xbox-0.1.0.dist-info/WHEEL +4 -0
  4. python_xbox-0.1.0.dist-info/entry_points.txt +6 -0
  5. python_xbox-0.1.0.dist-info/licenses/LICENSE +20 -0
  6. pythonxbox/__init__.py +4 -0
  7. pythonxbox/api/__init__.py +0 -0
  8. pythonxbox/api/client.py +166 -0
  9. pythonxbox/api/language.py +76 -0
  10. pythonxbox/api/provider/__init__.py +0 -0
  11. pythonxbox/api/provider/account/__init__.py +73 -0
  12. pythonxbox/api/provider/account/models.py +11 -0
  13. pythonxbox/api/provider/achievements/__init__.py +164 -0
  14. pythonxbox/api/provider/achievements/models.py +133 -0
  15. pythonxbox/api/provider/baseprovider.py +22 -0
  16. pythonxbox/api/provider/catalog/__init__.py +86 -0
  17. pythonxbox/api/provider/catalog/const.py +15 -0
  18. pythonxbox/api/provider/catalog/models.py +428 -0
  19. pythonxbox/api/provider/cqs/__init__.py +85 -0
  20. pythonxbox/api/provider/cqs/models.py +59 -0
  21. pythonxbox/api/provider/gameclips/__init__.py +167 -0
  22. pythonxbox/api/provider/gameclips/models.py +58 -0
  23. pythonxbox/api/provider/lists/__init__.py +71 -0
  24. pythonxbox/api/provider/lists/models.py +33 -0
  25. pythonxbox/api/provider/mediahub/__init__.py +64 -0
  26. pythonxbox/api/provider/mediahub/models.py +82 -0
  27. pythonxbox/api/provider/message/__init__.py +135 -0
  28. pythonxbox/api/provider/message/models.py +96 -0
  29. pythonxbox/api/provider/people/__init__.py +193 -0
  30. pythonxbox/api/provider/people/models.py +252 -0
  31. pythonxbox/api/provider/presence/__init__.py +110 -0
  32. pythonxbox/api/provider/presence/models.py +53 -0
  33. pythonxbox/api/provider/profile/__init__.py +140 -0
  34. pythonxbox/api/provider/profile/models.py +47 -0
  35. pythonxbox/api/provider/ratelimitedprovider.py +79 -0
  36. pythonxbox/api/provider/screenshots/__init__.py +167 -0
  37. pythonxbox/api/provider/screenshots/models.py +56 -0
  38. pythonxbox/api/provider/smartglass/__init__.py +402 -0
  39. pythonxbox/api/provider/smartglass/models.py +186 -0
  40. pythonxbox/api/provider/titlehub/__init__.py +143 -0
  41. pythonxbox/api/provider/titlehub/models.py +106 -0
  42. pythonxbox/api/provider/usersearch/__init__.py +29 -0
  43. pythonxbox/api/provider/usersearch/models.py +17 -0
  44. pythonxbox/api/provider/userstats/__init__.py +164 -0
  45. pythonxbox/api/provider/userstats/models.py +44 -0
  46. pythonxbox/authentication/__init__.py +0 -0
  47. pythonxbox/authentication/manager.py +161 -0
  48. pythonxbox/authentication/models.py +162 -0
  49. pythonxbox/authentication/xal.py +348 -0
  50. pythonxbox/common/__init__.py +0 -0
  51. pythonxbox/common/exceptions.py +59 -0
  52. pythonxbox/common/filetimes.py +81 -0
  53. pythonxbox/common/models.py +34 -0
  54. pythonxbox/common/ratelimits/__init__.py +268 -0
  55. pythonxbox/common/ratelimits/models.py +23 -0
  56. pythonxbox/common/request_signer.py +190 -0
  57. pythonxbox/common/signed_session.py +60 -0
  58. pythonxbox/py.typed +0 -0
  59. pythonxbox/scripts/__init__.py +15 -0
  60. pythonxbox/scripts/authenticate.py +159 -0
  61. pythonxbox/scripts/change_gamertag.py +111 -0
  62. pythonxbox/scripts/friends.py +80 -0
  63. pythonxbox/scripts/search.py +43 -0
  64. pythonxbox/scripts/xal.py +113 -0
@@ -0,0 +1,159 @@
1
+ """
2
+ Example scripts that performs XBL authentication
3
+ """
4
+
5
+ import argparse
6
+ import asyncio
7
+ import http.server
8
+ import os
9
+ import queue
10
+ import socketserver
11
+ import threading
12
+ from urllib.parse import parse_qs, urlparse
13
+ import webbrowser
14
+
15
+ from pythonxbox.authentication.manager import AuthenticationManager
16
+ from pythonxbox.authentication.models import OAuth2TokenResponse
17
+ from pythonxbox.common.signed_session import SignedSession
18
+ from pythonxbox.scripts import CLIENT_ID, CLIENT_SECRET, REDIRECT_URI, TOKENS_FILE
19
+
20
+ QUEUE = queue.Queue(1)
21
+
22
+
23
+ class AuthCallbackRequestHandler(http.server.BaseHTTPRequestHandler):
24
+ """
25
+ Handles the auth callback that's received when Windows Live auth flow completed
26
+ """
27
+
28
+ def do_GET(self) -> None:
29
+ try:
30
+ url_path = self.requestline.split(" ")[1]
31
+ query_params = parse_qs(urlparse(url_path).query)
32
+ except Exception as e:
33
+ self.send_error(
34
+ 400,
35
+ explain=f"Invalid request='{self.requestline}' - Failed to parse URL Path, error={e}",
36
+ )
37
+ self.end_headers()
38
+ return
39
+
40
+ if query_params.get("error"):
41
+ error_description = query_params.get("error_description")
42
+ self.send_error(
43
+ 400, explain=f"Auth callback failed - Error: {error_description}"
44
+ )
45
+ self.end_headers()
46
+ return
47
+
48
+ auth_code = query_params.get("code")
49
+ if not auth_code:
50
+ self.send_error(
51
+ 400,
52
+ explain=f"Auth callback failed - No code received - Original request: {self.requestline}",
53
+ )
54
+ self.end_headers()
55
+ return
56
+
57
+ if isinstance(auth_code, list):
58
+ auth_code = auth_code[0]
59
+ elif isinstance(auth_code, str):
60
+ pass
61
+ else:
62
+ raise Exception(f"Invalid code query param: {auth_code}")
63
+
64
+ # Put auth_code into queue for do_auth to receive
65
+ QUEUE.put(auth_code)
66
+ response_body = b"<script>window.close()</script>"
67
+ self.send_response(200)
68
+ self.send_header("Content-Type", "text/html")
69
+ self.send_header("Content-Length", str(len(response_body)))
70
+ self.end_headers()
71
+ self.wfile.write(response_body)
72
+
73
+
74
+ async def do_auth(
75
+ client_id: str, client_secret: str, redirect_uri: str, token_filepath: str
76
+ ) -> None:
77
+ async with SignedSession() as session:
78
+ auth_mgr = AuthenticationManager(
79
+ session, client_id, client_secret, redirect_uri
80
+ )
81
+
82
+ # Refresh tokens if we have them
83
+ if os.path.exists(token_filepath):
84
+ with open(token_filepath) as f:
85
+ tokens = f.read()
86
+ auth_mgr.oauth = OAuth2TokenResponse.model_validate_json(tokens)
87
+ await auth_mgr.refresh_tokens()
88
+
89
+ # Request new ones if they are not valid
90
+ if not (auth_mgr.xsts_token and auth_mgr.xsts_token.is_valid()):
91
+ auth_url = auth_mgr.generate_authorization_url()
92
+ webbrowser.open(auth_url)
93
+ # Wait for auth code from http server thread
94
+ code = QUEUE.get()
95
+ await auth_mgr.request_tokens(code)
96
+
97
+ with open(token_filepath, mode="w") as f:
98
+ print(f"Finished authentication, writing tokens to {token_filepath}")
99
+ f.write(auth_mgr.oauth.model_dump_json())
100
+
101
+
102
+ async def async_main() -> None:
103
+ parser = argparse.ArgumentParser(description="Authenticate with XBL")
104
+ parser.add_argument(
105
+ "--tokens",
106
+ "-t",
107
+ default=TOKENS_FILE,
108
+ help=f"Token filepath. Default: '{TOKENS_FILE}'",
109
+ )
110
+ parser.add_argument(
111
+ "--client-id",
112
+ "-cid",
113
+ default=os.environ.get("CLIENT_ID", CLIENT_ID),
114
+ help="OAuth2 Client ID",
115
+ )
116
+ parser.add_argument(
117
+ "--client-secret",
118
+ "-cs",
119
+ default=os.environ.get("CLIENT_SECRET", CLIENT_SECRET),
120
+ help="OAuth2 Client Secret",
121
+ )
122
+ parser.add_argument(
123
+ "--redirect-uri",
124
+ "-ru",
125
+ default=os.environ.get("REDIRECT_URI", REDIRECT_URI),
126
+ help="OAuth2 Redirect URI",
127
+ )
128
+ parser.add_argument(
129
+ "--port",
130
+ "-p",
131
+ default=8080,
132
+ type=int,
133
+ help="""
134
+ HTTP Server port for awaiting auth callback
135
+ * NOTE: Changing this will break default auth flow and requires providing own OAUTH parameters
136
+ """,
137
+ )
138
+ args = parser.parse_args()
139
+
140
+ with socketserver.TCPServer(
141
+ ("0.0.0.0", args.port), AuthCallbackRequestHandler
142
+ ) as httpd:
143
+ print(f"Serving HTTP Server for auth callback at port {args.port}")
144
+ server_thread = threading.Thread(target=httpd.serve_forever)
145
+ # Exit the server thread when the main thread terminates
146
+ server_thread.daemon = True
147
+ server_thread.start()
148
+
149
+ await do_auth(
150
+ args.client_id, args.client_secret, args.redirect_uri, args.tokens
151
+ )
152
+
153
+
154
+ def main() -> None:
155
+ asyncio.run(async_main())
156
+
157
+
158
+ if __name__ == "__main__":
159
+ main()
@@ -0,0 +1,111 @@
1
+ """
2
+ Example script that enables using your one-time-free gamertag change
3
+ """
4
+
5
+ import argparse
6
+ import asyncio
7
+ import os
8
+ import sys
9
+
10
+ from httpx import HTTPStatusError
11
+
12
+ from pythonxbox.api.client import XboxLiveClient
13
+ from pythonxbox.api.provider.account.models import (
14
+ ChangeGamertagResult,
15
+ ClaimGamertagResult,
16
+ )
17
+ from pythonxbox.authentication.manager import AuthenticationManager
18
+ from pythonxbox.authentication.models import OAuth2TokenResponse
19
+ from pythonxbox.common.signed_session import SignedSession
20
+ from pythonxbox.scripts import CLIENT_ID, CLIENT_SECRET, TOKENS_FILE
21
+
22
+
23
+ async def async_main() -> None:
24
+ parser = argparse.ArgumentParser(description="Change your gamertag")
25
+ parser.add_argument(
26
+ "--tokens",
27
+ "-t",
28
+ default=TOKENS_FILE,
29
+ help=f"Token filepath. Default: '{TOKENS_FILE}'",
30
+ )
31
+ parser.add_argument(
32
+ "--client-id",
33
+ "-cid",
34
+ default=os.environ.get("CLIENT_ID", CLIENT_ID),
35
+ help="OAuth2 Client ID",
36
+ )
37
+ parser.add_argument(
38
+ "--client-secret",
39
+ "-cs",
40
+ default=os.environ.get("CLIENT_SECRET", CLIENT_SECRET),
41
+ help="OAuth2 Client Secret",
42
+ )
43
+ parser.add_argument("gamertag", help="Desired Gamertag")
44
+
45
+ args = parser.parse_args()
46
+
47
+ if len(args.gamertag) > 15:
48
+ print("Desired gamertag exceedes limit of 15 chars")
49
+ sys.exit(-1)
50
+
51
+ if not os.path.exists(args.tokens):
52
+ print("No token file found, run xbox-authenticate")
53
+ sys.exit(-1)
54
+
55
+ async with SignedSession() as session:
56
+ auth_mgr = AuthenticationManager(
57
+ session, args.client_id, args.client_secret, ""
58
+ )
59
+
60
+ with open(args.tokens) as f:
61
+ tokens = f.read()
62
+ auth_mgr.oauth = OAuth2TokenResponse.model_validate_json(tokens)
63
+ try:
64
+ await auth_mgr.refresh_tokens()
65
+ except HTTPStatusError:
66
+ print("Could not refresh tokens")
67
+ sys.exit(-1)
68
+
69
+ with open(args.tokens, mode="w") as f:
70
+ f.write(auth_mgr.oauth.json())
71
+
72
+ xbl_client = XboxLiveClient(auth_mgr)
73
+
74
+ print(
75
+ ":: Trying to change gamertag to '%s' for xuid '%i'..."
76
+ % (args.gamertag, xbl_client.xuid)
77
+ )
78
+
79
+ print("Claiming gamertag...")
80
+ try:
81
+ resp = await xbl_client.account.claim_gamertag(
82
+ xbl_client.xuid, args.gamertag
83
+ )
84
+ if resp == ClaimGamertagResult.NotAvailable:
85
+ print("Claiming gamertag failed - Desired gamertag is unavailable")
86
+ sys.exit(-1)
87
+ except HTTPStatusError:
88
+ print("Invalid HTTP response from claim")
89
+ sys.exit(-1)
90
+
91
+ print("Changing gamertag...")
92
+ try:
93
+ resp = await xbl_client.account.change_gamertag(
94
+ xbl_client.xuid, args.gamertag
95
+ )
96
+ if resp == ChangeGamertagResult.NoFreeChangesAvailable:
97
+ print("Changing gamertag failed - You are out of free changes")
98
+ sys.exit(-1)
99
+ except HTTPStatusError:
100
+ print("Invalid HTTP response from change")
101
+ sys.exit(-1)
102
+
103
+ print("Gamertag successfully changed to %s" % args.gamertag)
104
+
105
+
106
+ def main() -> None:
107
+ asyncio.run(async_main())
108
+
109
+
110
+ if __name__ == "__main__":
111
+ main()
@@ -0,0 +1,80 @@
1
+ """
2
+ Example script that enables using your one-time-free gamertag change
3
+ """
4
+
5
+ import argparse
6
+ import asyncio
7
+ import os
8
+ from pprint import pprint
9
+ import sys
10
+
11
+ from httpx import HTTPStatusError
12
+
13
+ from pythonxbox.api.client import XboxLiveClient
14
+ from pythonxbox.authentication.manager import AuthenticationManager
15
+ from pythonxbox.authentication.models import OAuth2TokenResponse
16
+ from pythonxbox.common.signed_session import SignedSession
17
+ from pythonxbox.scripts import CLIENT_ID, CLIENT_SECRET, TOKENS_FILE
18
+
19
+
20
+ async def async_main() -> None:
21
+ parser = argparse.ArgumentParser(description="Change your gamertag")
22
+ parser.add_argument(
23
+ "--tokens",
24
+ "-t",
25
+ default=TOKENS_FILE,
26
+ help=f"Token filepath. Default: '{TOKENS_FILE}'",
27
+ )
28
+ parser.add_argument(
29
+ "--client-id",
30
+ "-cid",
31
+ default=os.environ.get("CLIENT_ID", CLIENT_ID),
32
+ help="OAuth2 Client ID",
33
+ )
34
+ parser.add_argument(
35
+ "--client-secret",
36
+ "-cs",
37
+ default=os.environ.get("CLIENT_SECRET", CLIENT_SECRET),
38
+ help="OAuth2 Client Secret",
39
+ )
40
+
41
+ args = parser.parse_args()
42
+
43
+ if not os.path.exists(args.tokens):
44
+ print("No token file found, run xbox-authenticate")
45
+ sys.exit(-1)
46
+
47
+ async with SignedSession() as session:
48
+ auth_mgr = AuthenticationManager(
49
+ session, args.client_id, args.client_secret, ""
50
+ )
51
+
52
+ with open(args.tokens) as f:
53
+ tokens = f.read()
54
+ auth_mgr.oauth = OAuth2TokenResponse.model_validate_json(tokens)
55
+ try:
56
+ await auth_mgr.refresh_tokens()
57
+ except HTTPStatusError:
58
+ print("Could not refresh tokens")
59
+ sys.exit(-1)
60
+
61
+ with open(args.tokens, mode="w") as f:
62
+ f.write(auth_mgr.oauth.json())
63
+
64
+ xbl_client = XboxLiveClient(auth_mgr)
65
+
66
+ try:
67
+ resp = await xbl_client.people.get_friends_own()
68
+ except HTTPStatusError:
69
+ print("Invalid HTTP response")
70
+ sys.exit(-1)
71
+
72
+ pprint(resp.dict())
73
+
74
+
75
+ def main() -> None:
76
+ asyncio.run(async_main())
77
+
78
+
79
+ if __name__ == "__main__":
80
+ main()
@@ -0,0 +1,43 @@
1
+ """
2
+ Example script that utilizes EDSProvider to search XBL marketplace
3
+ """
4
+
5
+ import argparse
6
+ import asyncio
7
+ from pprint import pprint
8
+ import sys
9
+
10
+ from httpx import HTTPStatusError
11
+
12
+ from pythonxbox.api.client import XboxLiveClient
13
+ from pythonxbox.authentication.manager import AuthenticationManager
14
+ from pythonxbox.common.signed_session import SignedSession
15
+
16
+
17
+ async def async_main() -> None:
18
+ parser = argparse.ArgumentParser(description="Search for Content on XBL")
19
+ parser.add_argument("search_query", help="Name to search for")
20
+
21
+ args = parser.parse_args()
22
+
23
+ async with SignedSession() as session:
24
+ auth_mgr = AuthenticationManager(session, "", "", "")
25
+
26
+ # No Auth necessary for catalog searches
27
+ xbl_client = XboxLiveClient(auth_mgr)
28
+
29
+ try:
30
+ resp = await xbl_client.catalog.product_search(args.search_query)
31
+ except HTTPStatusError:
32
+ print("Search failed")
33
+ sys.exit(-1)
34
+
35
+ pprint(resp.dict())
36
+
37
+
38
+ def main() -> None:
39
+ asyncio.run(async_main())
40
+
41
+
42
+ if __name__ == "__main__":
43
+ main()
@@ -0,0 +1,113 @@
1
+ """
2
+ Example scripts that performs XBL authentication via XAL
3
+ """
4
+
5
+ import argparse
6
+ import asyncio
7
+ import json
8
+ import os
9
+ import uuid
10
+
11
+ from pydantic import BaseModel
12
+ from pydantic.json import pydantic_encoder
13
+
14
+ from pythonxbox.authentication.models import (
15
+ SisuAuthorizationResponse,
16
+ XalAppParameters,
17
+ XalClientParameters,
18
+ )
19
+ from pythonxbox.authentication.xal import (
20
+ APP_PARAMS_GAMEPASS_BETA,
21
+ CLIENT_PARAMS_ANDROID,
22
+ XALManager,
23
+ )
24
+ from pythonxbox.common.signed_session import SignedSession
25
+ from pythonxbox.scripts import XAL_TOKENS_FILE
26
+
27
+
28
+ class XALStore(BaseModel):
29
+ """Used to store/load authorization data"""
30
+
31
+ sisu: SisuAuthorizationResponse
32
+ device_id: uuid.UUID
33
+ app_params: XalAppParameters
34
+ client_params: XalClientParameters
35
+
36
+
37
+ def user_prompt_authentication(auth_url: str) -> str:
38
+ """
39
+ Handles the auth callback when user is prompted to authenticate via URL
40
+ in webbrowser
41
+
42
+ Takes the redirect URL from stdin
43
+ """
44
+
45
+ redirect_url = input(
46
+ f"Continue auth with the following URL:\n\n"
47
+ f"URL: {auth_url}\n\n"
48
+ f"Provide redirect URI: "
49
+ )
50
+ return redirect_url
51
+
52
+
53
+ async def do_auth(device_id: uuid.UUID, token_filepath: str) -> None:
54
+ async with SignedSession() as session:
55
+ app_params = APP_PARAMS_GAMEPASS_BETA
56
+ client_params = CLIENT_PARAMS_ANDROID
57
+
58
+ store = None
59
+ # Load existing sisu authorization data, if it exists
60
+ if os.path.exists(token_filepath):
61
+ with open(token_filepath) as f:
62
+ store = json.load(f)
63
+
64
+ # Convert SISU authorization data
65
+ store = XALStore(**store)
66
+
67
+ if store:
68
+ raise NotImplementedError("Token refreshing")
69
+
70
+ # Do authentication
71
+ xal = XALManager(session, device_id, app_params, client_params)
72
+ response = await xal.auth_flow(user_prompt_authentication)
73
+ print(f"Sisu auth finished:\n\n{response}")
74
+
75
+ # Save authorization data
76
+ store = XALStore(
77
+ sisu=response,
78
+ device_id=device_id,
79
+ app_params=app_params,
80
+ client_params=client_params,
81
+ )
82
+
83
+ with open(token_filepath, mode="w") as f:
84
+ print(f"Finished authentication, writing tokens to {token_filepath}")
85
+ json.dump(store, f, default=pydantic_encoder)
86
+
87
+
88
+ async def async_main() -> None:
89
+ parser = argparse.ArgumentParser(description="Authenticate with XBL via XAL")
90
+ parser.add_argument(
91
+ "--tokens",
92
+ "-t",
93
+ default=XAL_TOKENS_FILE,
94
+ help=f"Token filepath. Default: '{XAL_TOKENS_FILE}'",
95
+ )
96
+ parser.add_argument(
97
+ "--device-id",
98
+ "-did",
99
+ default=uuid.uuid4(),
100
+ type=uuid.UUID,
101
+ help="Device ID (for device auth)",
102
+ )
103
+ args = parser.parse_args()
104
+
105
+ await do_auth(args.device_id, args.tokens)
106
+
107
+
108
+ def main() -> None:
109
+ asyncio.run(async_main())
110
+
111
+
112
+ if __name__ == "__main__":
113
+ main()