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.
- python_xbox-0.1.0.dist-info/METADATA +217 -0
- python_xbox-0.1.0.dist-info/RECORD +64 -0
- python_xbox-0.1.0.dist-info/WHEEL +4 -0
- python_xbox-0.1.0.dist-info/entry_points.txt +6 -0
- python_xbox-0.1.0.dist-info/licenses/LICENSE +20 -0
- pythonxbox/__init__.py +4 -0
- pythonxbox/api/__init__.py +0 -0
- pythonxbox/api/client.py +166 -0
- pythonxbox/api/language.py +76 -0
- pythonxbox/api/provider/__init__.py +0 -0
- pythonxbox/api/provider/account/__init__.py +73 -0
- pythonxbox/api/provider/account/models.py +11 -0
- pythonxbox/api/provider/achievements/__init__.py +164 -0
- pythonxbox/api/provider/achievements/models.py +133 -0
- pythonxbox/api/provider/baseprovider.py +22 -0
- pythonxbox/api/provider/catalog/__init__.py +86 -0
- pythonxbox/api/provider/catalog/const.py +15 -0
- pythonxbox/api/provider/catalog/models.py +428 -0
- pythonxbox/api/provider/cqs/__init__.py +85 -0
- pythonxbox/api/provider/cqs/models.py +59 -0
- pythonxbox/api/provider/gameclips/__init__.py +167 -0
- pythonxbox/api/provider/gameclips/models.py +58 -0
- pythonxbox/api/provider/lists/__init__.py +71 -0
- pythonxbox/api/provider/lists/models.py +33 -0
- pythonxbox/api/provider/mediahub/__init__.py +64 -0
- pythonxbox/api/provider/mediahub/models.py +82 -0
- pythonxbox/api/provider/message/__init__.py +135 -0
- pythonxbox/api/provider/message/models.py +96 -0
- pythonxbox/api/provider/people/__init__.py +193 -0
- pythonxbox/api/provider/people/models.py +252 -0
- pythonxbox/api/provider/presence/__init__.py +110 -0
- pythonxbox/api/provider/presence/models.py +53 -0
- pythonxbox/api/provider/profile/__init__.py +140 -0
- pythonxbox/api/provider/profile/models.py +47 -0
- pythonxbox/api/provider/ratelimitedprovider.py +79 -0
- pythonxbox/api/provider/screenshots/__init__.py +167 -0
- pythonxbox/api/provider/screenshots/models.py +56 -0
- pythonxbox/api/provider/smartglass/__init__.py +402 -0
- pythonxbox/api/provider/smartglass/models.py +186 -0
- pythonxbox/api/provider/titlehub/__init__.py +143 -0
- pythonxbox/api/provider/titlehub/models.py +106 -0
- pythonxbox/api/provider/usersearch/__init__.py +29 -0
- pythonxbox/api/provider/usersearch/models.py +17 -0
- pythonxbox/api/provider/userstats/__init__.py +164 -0
- pythonxbox/api/provider/userstats/models.py +44 -0
- pythonxbox/authentication/__init__.py +0 -0
- pythonxbox/authentication/manager.py +161 -0
- pythonxbox/authentication/models.py +162 -0
- pythonxbox/authentication/xal.py +348 -0
- pythonxbox/common/__init__.py +0 -0
- pythonxbox/common/exceptions.py +59 -0
- pythonxbox/common/filetimes.py +81 -0
- pythonxbox/common/models.py +34 -0
- pythonxbox/common/ratelimits/__init__.py +268 -0
- pythonxbox/common/ratelimits/models.py +23 -0
- pythonxbox/common/request_signer.py +190 -0
- pythonxbox/common/signed_session.py +60 -0
- pythonxbox/py.typed +0 -0
- pythonxbox/scripts/__init__.py +15 -0
- pythonxbox/scripts/authenticate.py +159 -0
- pythonxbox/scripts/change_gamertag.py +111 -0
- pythonxbox/scripts/friends.py +80 -0
- pythonxbox/scripts/search.py +43 -0
- 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()
|