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,167 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Gameclips - Get gameclip info
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from pythonxbox.api.provider.baseprovider import BaseProvider
|
|
6
|
+
from pythonxbox.api.provider.gameclips.models import GameclipsResponse
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class GameclipProvider(BaseProvider):
|
|
10
|
+
GAMECLIPS_METADATA_URL = "https://gameclipsmetadata.xboxlive.com"
|
|
11
|
+
HEADERS_GAMECLIPS_METADATA = {"x-xbl-contract-version": "1"}
|
|
12
|
+
|
|
13
|
+
async def get_recent_community_clips_by_title_id(
|
|
14
|
+
self, title_id: str, **kwargs
|
|
15
|
+
) -> GameclipsResponse:
|
|
16
|
+
"""
|
|
17
|
+
Get recent community clips by Title Id
|
|
18
|
+
|
|
19
|
+
Args:
|
|
20
|
+
title_id: Title Id to get clips for
|
|
21
|
+
|
|
22
|
+
Returns:
|
|
23
|
+
:class:`GameclipsResponse`: Game clip Response
|
|
24
|
+
"""
|
|
25
|
+
url = self.GAMECLIPS_METADATA_URL + f"/public/titles/{title_id}/clips"
|
|
26
|
+
params = {"qualifier": "created"}
|
|
27
|
+
resp = await self.client.session.get(
|
|
28
|
+
url, params=params, headers=self.HEADERS_GAMECLIPS_METADATA, **kwargs
|
|
29
|
+
)
|
|
30
|
+
resp.raise_for_status()
|
|
31
|
+
return GameclipsResponse(**resp.json())
|
|
32
|
+
|
|
33
|
+
async def get_recent_own_clips(
|
|
34
|
+
self, title_id: str = None, skip_items: int = 0, max_items: int = 25, **kwargs
|
|
35
|
+
) -> GameclipsResponse:
|
|
36
|
+
"""
|
|
37
|
+
Get own recent clips, optionally filter for title Id
|
|
38
|
+
|
|
39
|
+
Args:
|
|
40
|
+
title_id: Title ID to filter
|
|
41
|
+
skip_items: Item count to skip
|
|
42
|
+
max_items: Maximum item count to load
|
|
43
|
+
|
|
44
|
+
Returns:
|
|
45
|
+
:class:`GameclipsResponse`: Game clip Response
|
|
46
|
+
"""
|
|
47
|
+
url = self.GAMECLIPS_METADATA_URL + "/users/me"
|
|
48
|
+
if title_id:
|
|
49
|
+
url += f"/titles/{title_id}"
|
|
50
|
+
url += "/clips"
|
|
51
|
+
|
|
52
|
+
params = {"skipItems": skip_items, "maxItems": max_items}
|
|
53
|
+
resp = await self.client.session.get(
|
|
54
|
+
url, params=params, headers=self.HEADERS_GAMECLIPS_METADATA, **kwargs
|
|
55
|
+
)
|
|
56
|
+
resp.raise_for_status()
|
|
57
|
+
return GameclipsResponse(**resp.json())
|
|
58
|
+
|
|
59
|
+
async def get_recent_clips_by_xuid(
|
|
60
|
+
self,
|
|
61
|
+
xuid: str,
|
|
62
|
+
title_id: str = None,
|
|
63
|
+
skip_items: int = 0,
|
|
64
|
+
max_items: int = 25,
|
|
65
|
+
**kwargs,
|
|
66
|
+
) -> GameclipsResponse:
|
|
67
|
+
"""
|
|
68
|
+
Get clips by XUID, optionally filter for title Id
|
|
69
|
+
|
|
70
|
+
Args:
|
|
71
|
+
xuid: XUID of user to get clips from
|
|
72
|
+
title_id: Optional title id filter
|
|
73
|
+
skip_items: Item count to skip
|
|
74
|
+
max_items: Maximum item count to load
|
|
75
|
+
|
|
76
|
+
Returns:
|
|
77
|
+
:class:`GameclipsResponse`: Game clip Response
|
|
78
|
+
"""
|
|
79
|
+
url = self.GAMECLIPS_METADATA_URL + f"/users/xuid({xuid})"
|
|
80
|
+
if title_id:
|
|
81
|
+
url += f"/titles/{title_id}"
|
|
82
|
+
url += "/clips"
|
|
83
|
+
|
|
84
|
+
params = {"skipItems": skip_items, "maxItems": max_items}
|
|
85
|
+
resp = await self.client.session.get(
|
|
86
|
+
url, params=params, headers=self.HEADERS_GAMECLIPS_METADATA, **kwargs
|
|
87
|
+
)
|
|
88
|
+
resp.raise_for_status()
|
|
89
|
+
return GameclipsResponse(**resp.json())
|
|
90
|
+
|
|
91
|
+
async def get_saved_community_clips_by_title_id(
|
|
92
|
+
self, title_id: str, **kwargs
|
|
93
|
+
) -> GameclipsResponse:
|
|
94
|
+
"""
|
|
95
|
+
Get saved community clips by Title Id
|
|
96
|
+
|
|
97
|
+
Args:
|
|
98
|
+
title_id: Title Id to get screenshots for
|
|
99
|
+
|
|
100
|
+
Returns:
|
|
101
|
+
:class:`GameclipsResponse`: Game clip Response
|
|
102
|
+
"""
|
|
103
|
+
url = self.GAMECLIPS_METADATA_URL + f"/public/titles/{title_id}/clips/saved"
|
|
104
|
+
params = {"qualifier": "created"}
|
|
105
|
+
resp = await self.client.session.get(
|
|
106
|
+
url, params=params, headers=self.HEADERS_GAMECLIPS_METADATA, **kwargs
|
|
107
|
+
)
|
|
108
|
+
resp.raise_for_status()
|
|
109
|
+
return GameclipsResponse(**resp.json())
|
|
110
|
+
|
|
111
|
+
async def get_saved_own_clips(
|
|
112
|
+
self, title_id: str = None, skip_items: int = 0, max_items: int = 25, **kwargs
|
|
113
|
+
) -> GameclipsResponse:
|
|
114
|
+
"""
|
|
115
|
+
Get own saved clips, optionally filter for title Id an
|
|
116
|
+
|
|
117
|
+
Args:
|
|
118
|
+
title_id: Optional Title ID to filter
|
|
119
|
+
skip_items: Item count to skip
|
|
120
|
+
max_items: Maximum item count to load
|
|
121
|
+
|
|
122
|
+
Returns:
|
|
123
|
+
:class:`GameclipsResponse`: Game clip Response
|
|
124
|
+
"""
|
|
125
|
+
url = self.GAMECLIPS_METADATA_URL + "/users/me"
|
|
126
|
+
if title_id:
|
|
127
|
+
url += f"/titles/{title_id}"
|
|
128
|
+
url += "/clips/saved"
|
|
129
|
+
|
|
130
|
+
params = {"skipItems": skip_items, "maxItems": max_items}
|
|
131
|
+
resp = await self.client.session.get(
|
|
132
|
+
url, params=params, headers=self.HEADERS_GAMECLIPS_METADATA, **kwargs
|
|
133
|
+
)
|
|
134
|
+
resp.raise_for_status()
|
|
135
|
+
return GameclipsResponse(**resp.json())
|
|
136
|
+
|
|
137
|
+
async def get_saved_clips_by_xuid(
|
|
138
|
+
self,
|
|
139
|
+
xuid: str,
|
|
140
|
+
title_id: str = None,
|
|
141
|
+
skip_items: int = 0,
|
|
142
|
+
max_items: int = 25,
|
|
143
|
+
**kwargs,
|
|
144
|
+
) -> GameclipsResponse:
|
|
145
|
+
"""
|
|
146
|
+
Get saved clips by XUID, optionally filter for title Id
|
|
147
|
+
|
|
148
|
+
Args:
|
|
149
|
+
xuid: XUID of user to get screenshots from
|
|
150
|
+
title_id: Optional title id filter
|
|
151
|
+
skip_items: Item count to skip
|
|
152
|
+
max_items: Maximum item count to load
|
|
153
|
+
|
|
154
|
+
Returns:
|
|
155
|
+
:class:`GameclipsResponse`: Game clip Response
|
|
156
|
+
"""
|
|
157
|
+
url = self.GAMECLIPS_METADATA_URL + f"/users/xuid({xuid})"
|
|
158
|
+
if title_id:
|
|
159
|
+
url += f"/titles/{title_id}"
|
|
160
|
+
url += "/clips/saved"
|
|
161
|
+
|
|
162
|
+
params = {"skipItems": skip_items, "maxItems": max_items}
|
|
163
|
+
resp = await self.client.session.get(
|
|
164
|
+
url, params=params, headers=self.HEADERS_GAMECLIPS_METADATA, **kwargs
|
|
165
|
+
)
|
|
166
|
+
resp.raise_for_status()
|
|
167
|
+
return GameclipsResponse(**resp.json())
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
from datetime import datetime
|
|
2
|
+
|
|
3
|
+
from pythonxbox.common.models import CamelCaseModel
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class Thumbnail(CamelCaseModel):
|
|
7
|
+
uri: str
|
|
8
|
+
file_size: int
|
|
9
|
+
thumbnail_type: int
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class GameClipUri(CamelCaseModel):
|
|
13
|
+
uri: str
|
|
14
|
+
file_size: int
|
|
15
|
+
uri_type: int
|
|
16
|
+
expiration: str
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class GameClip(CamelCaseModel):
|
|
20
|
+
game_clip_id: str
|
|
21
|
+
state: int
|
|
22
|
+
date_published: datetime
|
|
23
|
+
date_recorded: datetime
|
|
24
|
+
last_modified: datetime
|
|
25
|
+
user_caption: str
|
|
26
|
+
type: int
|
|
27
|
+
duration_in_seconds: int
|
|
28
|
+
scid: str
|
|
29
|
+
title_id: int
|
|
30
|
+
rating: float
|
|
31
|
+
rating_count: int
|
|
32
|
+
views: int
|
|
33
|
+
title_data: str
|
|
34
|
+
system_properties: str
|
|
35
|
+
saved_by_user: bool
|
|
36
|
+
achievement_id: str
|
|
37
|
+
greatest_moment_id: str
|
|
38
|
+
thumbnails: list[Thumbnail]
|
|
39
|
+
game_clip_uris: list[GameClipUri]
|
|
40
|
+
xuid: str
|
|
41
|
+
clip_name: str
|
|
42
|
+
title_name: str
|
|
43
|
+
game_clip_locale: str
|
|
44
|
+
clip_content_attributes: int
|
|
45
|
+
device_type: str
|
|
46
|
+
comment_count: int
|
|
47
|
+
like_count: int
|
|
48
|
+
share_count: int
|
|
49
|
+
partial_views: int
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
class PagingInfo(CamelCaseModel):
|
|
53
|
+
continuation_token: str | None = None
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
class GameclipsResponse(CamelCaseModel):
|
|
57
|
+
game_clips: list[GameClip]
|
|
58
|
+
paging_info: PagingInfo
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
"""
|
|
2
|
+
EPLists - Mainly used for XBL Pins
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from pythonxbox.api.provider.baseprovider import BaseProvider
|
|
6
|
+
from pythonxbox.api.provider.lists.models import ListMetadata, ListsResponse
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class ListsProvider(BaseProvider):
|
|
10
|
+
LISTS_URL = "https://eplists.xboxlive.com"
|
|
11
|
+
HEADERS_LISTS = {"Content-Type": "application/json", "x-xbl-contract-version": "2"}
|
|
12
|
+
|
|
13
|
+
SEPERATOR = "."
|
|
14
|
+
|
|
15
|
+
async def remove_items(
|
|
16
|
+
self, xuid: str, post_body: dict, listname: str = "XBLPins", **kwargs
|
|
17
|
+
) -> ListMetadata:
|
|
18
|
+
"""
|
|
19
|
+
Remove items from specific list, defaults to "XBLPins"
|
|
20
|
+
|
|
21
|
+
Args:
|
|
22
|
+
xuid (str/int): Xbox User Id
|
|
23
|
+
listname (str): Name of list to edit
|
|
24
|
+
|
|
25
|
+
Returns:
|
|
26
|
+
:class:`ListMetadata`: List Metadata Response
|
|
27
|
+
"""
|
|
28
|
+
url = self.LISTS_URL + f"/users/xuid({xuid})/lists/PINS/{listname}"
|
|
29
|
+
resp = await self.client.session.delete(
|
|
30
|
+
url, json=post_body, headers=self.HEADERS_LISTS, **kwargs
|
|
31
|
+
)
|
|
32
|
+
resp.raise_for_status()
|
|
33
|
+
return ListMetadata(**resp.json())
|
|
34
|
+
|
|
35
|
+
async def get_items(
|
|
36
|
+
self, xuid: str, listname: str = "XBLPins", **kwargs
|
|
37
|
+
) -> ListsResponse:
|
|
38
|
+
"""
|
|
39
|
+
Get items from specific list, defaults to "XBLPins"
|
|
40
|
+
|
|
41
|
+
Args:
|
|
42
|
+
xuid (str/int): Xbox User Id
|
|
43
|
+
listname (str): Name of list to edit
|
|
44
|
+
|
|
45
|
+
Returns:
|
|
46
|
+
:class:`ListsResponse`: List Response
|
|
47
|
+
"""
|
|
48
|
+
url = self.LISTS_URL + f"/users/xuid({xuid})/lists/PINS/{listname}"
|
|
49
|
+
resp = await self.client.session.get(url, headers=self.HEADERS_LISTS, **kwargs)
|
|
50
|
+
resp.raise_for_status()
|
|
51
|
+
return ListsResponse(**resp.json())
|
|
52
|
+
|
|
53
|
+
async def insert_items(
|
|
54
|
+
self, xuid: str, post_body: dict, listname: str = "XBLPins", **kwargs
|
|
55
|
+
) -> ListMetadata:
|
|
56
|
+
"""
|
|
57
|
+
Insert items to specific list, defaults to "XBLPins"
|
|
58
|
+
|
|
59
|
+
Args:
|
|
60
|
+
xuid (str/int): Xbox User Id
|
|
61
|
+
listname (str): Name of list to edit
|
|
62
|
+
|
|
63
|
+
Returns:
|
|
64
|
+
:class:`ListMetadata`: List Metadata Response
|
|
65
|
+
"""
|
|
66
|
+
url = self.LISTS_URL + f"/users/xuid({xuid})/lists/PINS/{listname}"
|
|
67
|
+
resp = await self.client.session.post(
|
|
68
|
+
url, json=post_body, headers=self.HEADERS_LISTS, **kwargs
|
|
69
|
+
)
|
|
70
|
+
resp.raise_for_status()
|
|
71
|
+
return ListMetadata(**resp.json())
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
from pythonxbox.common.models import PascalCaseModel
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
class Item(PascalCaseModel):
|
|
5
|
+
item_id: str
|
|
6
|
+
content_type: str
|
|
7
|
+
title: str | None = None
|
|
8
|
+
device_type: str
|
|
9
|
+
provider: str | None = None
|
|
10
|
+
provider_id: str | None = None
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class ListItem(PascalCaseModel):
|
|
14
|
+
date_added: str
|
|
15
|
+
date_modified: str
|
|
16
|
+
index: int
|
|
17
|
+
k_value: int
|
|
18
|
+
item: Item
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class ListMetadata(PascalCaseModel):
|
|
22
|
+
list_title: str
|
|
23
|
+
list_version: int
|
|
24
|
+
list_count: int
|
|
25
|
+
allow_duplicates: bool
|
|
26
|
+
max_list_size: int
|
|
27
|
+
access_setting: str
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
class ListsResponse(PascalCaseModel):
|
|
31
|
+
impression_id: str
|
|
32
|
+
list_items: list[ListItem]
|
|
33
|
+
list_metadata: ListMetadata
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Mediahub - Fetch screenshots and gameclips
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from pythonxbox.api.provider.baseprovider import BaseProvider
|
|
6
|
+
from pythonxbox.api.provider.mediahub.models import (
|
|
7
|
+
MediahubGameclips,
|
|
8
|
+
MediahubScreenshots,
|
|
9
|
+
)
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class MediahubProvider(BaseProvider):
|
|
13
|
+
MEDIAHUB_URL = "https://mediahub.xboxlive.com"
|
|
14
|
+
HEADERS = {"x-xbl-contract-version": "3"}
|
|
15
|
+
|
|
16
|
+
async def fetch_own_clips(
|
|
17
|
+
self, skip: int = 0, count: int = 500, **kwargs
|
|
18
|
+
) -> MediahubGameclips:
|
|
19
|
+
"""
|
|
20
|
+
Fetch own clips
|
|
21
|
+
|
|
22
|
+
Args:
|
|
23
|
+
skip: Number of items to skip
|
|
24
|
+
count: Max entries to fetch
|
|
25
|
+
|
|
26
|
+
Returns:
|
|
27
|
+
:class:`MediahubGameclips`: Gameclips
|
|
28
|
+
"""
|
|
29
|
+
url = f"{self.MEDIAHUB_URL}/gameclips/search"
|
|
30
|
+
post_data = {
|
|
31
|
+
"max": count,
|
|
32
|
+
"query": f"OwnerXuid eq {self.client.xuid}",
|
|
33
|
+
"skip": skip,
|
|
34
|
+
}
|
|
35
|
+
resp = await self.client.session.post(
|
|
36
|
+
url, json=post_data, headers=self.HEADERS, **kwargs
|
|
37
|
+
)
|
|
38
|
+
resp.raise_for_status()
|
|
39
|
+
return MediahubGameclips(**resp.json())
|
|
40
|
+
|
|
41
|
+
async def fetch_own_screenshots(
|
|
42
|
+
self, skip: int = 0, count: int = 500, **kwargs
|
|
43
|
+
) -> MediahubScreenshots:
|
|
44
|
+
"""
|
|
45
|
+
Fetch own screenshots
|
|
46
|
+
|
|
47
|
+
Args:
|
|
48
|
+
skip: Number of items to skip
|
|
49
|
+
count: Max entries to fetch
|
|
50
|
+
|
|
51
|
+
Returns:
|
|
52
|
+
:class:`MediahubScreenshots`: Screenshots
|
|
53
|
+
"""
|
|
54
|
+
url = f"{self.MEDIAHUB_URL}/screenshots/search"
|
|
55
|
+
post_data = {
|
|
56
|
+
"max": count,
|
|
57
|
+
"query": f"OwnerXuid eq {self.client.xuid}",
|
|
58
|
+
"skip": skip,
|
|
59
|
+
}
|
|
60
|
+
resp = await self.client.session.post(
|
|
61
|
+
url, json=post_data, headers=self.HEADERS, **kwargs
|
|
62
|
+
)
|
|
63
|
+
resp.raise_for_status()
|
|
64
|
+
return MediahubScreenshots(**resp.json())
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
from pythonxbox.common.models import CamelCaseModel
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
class ContentSegment(CamelCaseModel):
|
|
5
|
+
segment_id: int
|
|
6
|
+
creation_type: str
|
|
7
|
+
creator_channel_id: str | None = None
|
|
8
|
+
creator_xuid: int
|
|
9
|
+
record_date: str
|
|
10
|
+
duration_in_seconds: int
|
|
11
|
+
offset: int
|
|
12
|
+
secondary_title_id: int | None = None
|
|
13
|
+
title_id: int
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class ContentLocator(CamelCaseModel):
|
|
17
|
+
expiration: str | None = None
|
|
18
|
+
file_size: int | None = None
|
|
19
|
+
locator_type: str
|
|
20
|
+
uri: str
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class GameclipContent(CamelCaseModel):
|
|
24
|
+
content_id: str
|
|
25
|
+
content_locators: list[ContentLocator]
|
|
26
|
+
content_segments: list[ContentSegment]
|
|
27
|
+
creation_type: str
|
|
28
|
+
duration_in_seconds: int
|
|
29
|
+
local_id: str
|
|
30
|
+
owner_xuid: int
|
|
31
|
+
sandbox_id: str
|
|
32
|
+
shared_to: list[int]
|
|
33
|
+
title_id: int
|
|
34
|
+
title_name: str
|
|
35
|
+
upload_date: str
|
|
36
|
+
upload_language: str
|
|
37
|
+
upload_region: str
|
|
38
|
+
upload_title_id: int
|
|
39
|
+
upload_device_type: str
|
|
40
|
+
comment_count: int
|
|
41
|
+
like_count: int
|
|
42
|
+
share_count: int
|
|
43
|
+
view_count: int
|
|
44
|
+
content_state: str
|
|
45
|
+
enforcement_state: str
|
|
46
|
+
sessions: list[str]
|
|
47
|
+
tournaments: list[str]
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
class MediahubGameclips(CamelCaseModel):
|
|
51
|
+
values: list[GameclipContent]
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
class ScreenshotContent(CamelCaseModel):
|
|
55
|
+
content_id: str
|
|
56
|
+
capture_date: str
|
|
57
|
+
content_locators: list[ContentLocator]
|
|
58
|
+
local_id: str
|
|
59
|
+
owner_xuid: int
|
|
60
|
+
resolution_height: int
|
|
61
|
+
resolution_width: int
|
|
62
|
+
date_uploaded: str
|
|
63
|
+
sandbox_id: str
|
|
64
|
+
shared_to: list[int]
|
|
65
|
+
title_id: int
|
|
66
|
+
title_name: str
|
|
67
|
+
upload_language: str
|
|
68
|
+
upload_region: str
|
|
69
|
+
upload_title_id: int
|
|
70
|
+
upload_device_type: str
|
|
71
|
+
comment_count: int
|
|
72
|
+
like_count: int
|
|
73
|
+
share_count: int
|
|
74
|
+
view_count: int
|
|
75
|
+
content_state: str
|
|
76
|
+
enforcement_state: str
|
|
77
|
+
sessions: list[str]
|
|
78
|
+
tournaments: list[str]
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
class MediahubScreenshots(CamelCaseModel):
|
|
82
|
+
values: list[ScreenshotContent]
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Message - Read and send messages
|
|
3
|
+
|
|
4
|
+
TODO: Support group messaging
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from pythonxbox.api.provider.baseprovider import BaseProvider
|
|
8
|
+
from pythonxbox.api.provider.message.models import (
|
|
9
|
+
ConversationResponse,
|
|
10
|
+
InboxResponse,
|
|
11
|
+
SendMessageResponse,
|
|
12
|
+
)
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class MessageProvider(BaseProvider):
|
|
16
|
+
MSG_URL = "https://xblmessaging.xboxlive.com"
|
|
17
|
+
HEADERS_MESSAGE = {"x-xbl-contract-version": "1"}
|
|
18
|
+
HEADERS_HORIZON = {"x-xbl-contract-version": "2"}
|
|
19
|
+
|
|
20
|
+
async def get_inbox(self, max_items: int = 100, **kwargs) -> InboxResponse:
|
|
21
|
+
"""
|
|
22
|
+
Get messages
|
|
23
|
+
|
|
24
|
+
Returns:
|
|
25
|
+
:class:`InboxResponse`: Inbox Response
|
|
26
|
+
"""
|
|
27
|
+
url = f"{self.MSG_URL}/network/Xbox/users/me/inbox"
|
|
28
|
+
params = {"maxItems": max_items}
|
|
29
|
+
resp = await self.client.session.get(
|
|
30
|
+
url, params=params, headers=self.HEADERS_MESSAGE, **kwargs
|
|
31
|
+
)
|
|
32
|
+
resp.raise_for_status()
|
|
33
|
+
return InboxResponse(**resp.json())
|
|
34
|
+
|
|
35
|
+
async def get_conversation(
|
|
36
|
+
self, xuid: str, max_items: int = 100, **kwargs
|
|
37
|
+
) -> ConversationResponse:
|
|
38
|
+
"""
|
|
39
|
+
Get detailed conversation info
|
|
40
|
+
|
|
41
|
+
Args:
|
|
42
|
+
xuid: Xuid of user having a conversation with
|
|
43
|
+
|
|
44
|
+
Returns:
|
|
45
|
+
:class:`ConversationResponse`: Conversation Response
|
|
46
|
+
"""
|
|
47
|
+
url = f"{self.MSG_URL}/network/Xbox/users/me/conversations/users/xuid({xuid})"
|
|
48
|
+
params = {"maxItems": max_items}
|
|
49
|
+
resp = await self.client.session.get(
|
|
50
|
+
url, params=params, headers=self.HEADERS_MESSAGE, **kwargs
|
|
51
|
+
)
|
|
52
|
+
resp.raise_for_status()
|
|
53
|
+
return ConversationResponse(**resp.json())
|
|
54
|
+
|
|
55
|
+
async def delete_conversation(
|
|
56
|
+
self, conversation_id: str, horizon: str, **kwargs
|
|
57
|
+
) -> bool:
|
|
58
|
+
"""
|
|
59
|
+
Delete message
|
|
60
|
+
|
|
61
|
+
**NOTE**: Returns HTTP Status Code **200** on success
|
|
62
|
+
|
|
63
|
+
Args:
|
|
64
|
+
conversation_id: Message Id
|
|
65
|
+
horizon: Delete horizon from get conversation response
|
|
66
|
+
|
|
67
|
+
Returns: True on success, False otherwise
|
|
68
|
+
"""
|
|
69
|
+
url = f"{self.MSG_URL}/network/Xbox/users/me/conversations/horizon"
|
|
70
|
+
post_data = {
|
|
71
|
+
"conversations": [
|
|
72
|
+
{
|
|
73
|
+
"conversationId": conversation_id,
|
|
74
|
+
"conversationType": "OneToOne",
|
|
75
|
+
"horizonType": "Delete",
|
|
76
|
+
"horizon": horizon,
|
|
77
|
+
}
|
|
78
|
+
]
|
|
79
|
+
}
|
|
80
|
+
resp = await self.client.session.put(
|
|
81
|
+
url, json=post_data, headers=self.HEADERS_HORIZON, **kwargs
|
|
82
|
+
)
|
|
83
|
+
return resp.status_code == 200
|
|
84
|
+
|
|
85
|
+
async def delete_message(
|
|
86
|
+
self, conversation_id: str, message_id: str, **kwargs
|
|
87
|
+
) -> bool:
|
|
88
|
+
"""
|
|
89
|
+
Delete message
|
|
90
|
+
|
|
91
|
+
**NOTE**: Returns HTTP Status Code **200** on success
|
|
92
|
+
|
|
93
|
+
Args:
|
|
94
|
+
conversation_id: Conversation Id
|
|
95
|
+
message_id: Message Id
|
|
96
|
+
|
|
97
|
+
Returns: True on success, False otherwise
|
|
98
|
+
"""
|
|
99
|
+
url = f"{self.MSG_URL}/network/Xbox/users/me/conversations/{conversation_id}/messages/{message_id}"
|
|
100
|
+
resp = await self.client.session.delete(
|
|
101
|
+
url, headers=self.HEADERS_MESSAGE, **kwargs
|
|
102
|
+
)
|
|
103
|
+
return resp.status_code == 200
|
|
104
|
+
|
|
105
|
+
async def send_message(
|
|
106
|
+
self, xuid: str, message_text: str, **kwargs
|
|
107
|
+
) -> SendMessageResponse:
|
|
108
|
+
"""
|
|
109
|
+
Send message to an xuid
|
|
110
|
+
|
|
111
|
+
Args:
|
|
112
|
+
xuid: Xuid
|
|
113
|
+
message_text: Message text
|
|
114
|
+
|
|
115
|
+
Returns:
|
|
116
|
+
:class:`SendMessageResponse`: Send Message Response
|
|
117
|
+
"""
|
|
118
|
+
if len(message_text) > 256:
|
|
119
|
+
raise ValueError("Message text exceeds max length of 256 chars")
|
|
120
|
+
|
|
121
|
+
url = f"{self.MSG_URL}/network/Xbox/users/me/conversations/users/xuid({xuid})"
|
|
122
|
+
post_data = {
|
|
123
|
+
"parts": [
|
|
124
|
+
{
|
|
125
|
+
"contentType": "text",
|
|
126
|
+
"version": 0,
|
|
127
|
+
"text": message_text,
|
|
128
|
+
}
|
|
129
|
+
]
|
|
130
|
+
}
|
|
131
|
+
resp = await self.client.session.post(
|
|
132
|
+
url, json=post_data, headers=self.HEADERS_MESSAGE, **kwargs
|
|
133
|
+
)
|
|
134
|
+
resp.raise_for_status()
|
|
135
|
+
return SendMessageResponse(**resp.json())
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
from datetime import datetime
|
|
2
|
+
from typing import Any
|
|
3
|
+
|
|
4
|
+
from pythonxbox.common.models import CamelCaseModel
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class Part(CamelCaseModel):
|
|
8
|
+
content_type: str
|
|
9
|
+
version: int
|
|
10
|
+
text: str | None = None
|
|
11
|
+
unsuitable_for: list | None = None
|
|
12
|
+
locator: str | None = None
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class Content(CamelCaseModel):
|
|
16
|
+
parts: list[Part]
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class ContentPayload(CamelCaseModel):
|
|
20
|
+
content: Content
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class Message(CamelCaseModel):
|
|
24
|
+
content_payload: ContentPayload | None = None
|
|
25
|
+
timestamp: datetime
|
|
26
|
+
last_update_timestamp: datetime
|
|
27
|
+
type: str
|
|
28
|
+
network_id: str
|
|
29
|
+
conversation_type: str
|
|
30
|
+
conversation_id: str
|
|
31
|
+
owner: int | None = None
|
|
32
|
+
sender: str
|
|
33
|
+
message_id: str
|
|
34
|
+
is_deleted: bool
|
|
35
|
+
is_server_updated: bool
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
class Conversation(CamelCaseModel):
|
|
39
|
+
timestamp: datetime
|
|
40
|
+
network_id: str
|
|
41
|
+
type: str
|
|
42
|
+
conversation_id: str
|
|
43
|
+
voice_id: str
|
|
44
|
+
participants: list[str]
|
|
45
|
+
read_horizon: str
|
|
46
|
+
delete_horizon: str
|
|
47
|
+
is_read: bool
|
|
48
|
+
muted: bool
|
|
49
|
+
folder: str
|
|
50
|
+
last_message: Message
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
class Primary(CamelCaseModel):
|
|
54
|
+
folder: str
|
|
55
|
+
total_count: int
|
|
56
|
+
unread_count: int
|
|
57
|
+
conversations: list[Conversation]
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
class SafetySettings(CamelCaseModel):
|
|
61
|
+
version: int
|
|
62
|
+
primary_inbox_media: str
|
|
63
|
+
primary_inbox_text: str
|
|
64
|
+
primary_inbox_url: str
|
|
65
|
+
secondary_inbox_media: str
|
|
66
|
+
secondary_inbox_text: str
|
|
67
|
+
secondary_inbox_url: str
|
|
68
|
+
can_unobscure: bool
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
class InboxResponse(CamelCaseModel):
|
|
72
|
+
primary: Primary
|
|
73
|
+
folders: list[Any]
|
|
74
|
+
safety_settings: SafetySettings
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
class ConversationResponse(CamelCaseModel):
|
|
78
|
+
timestamp: datetime
|
|
79
|
+
network_id: str
|
|
80
|
+
type: str
|
|
81
|
+
conversation_id: str
|
|
82
|
+
participants: list[str] | None = None
|
|
83
|
+
read_horizon: str
|
|
84
|
+
delete_horizon: str
|
|
85
|
+
is_read: bool
|
|
86
|
+
muted: bool
|
|
87
|
+
folder: str
|
|
88
|
+
messages: list[Message] | None = None
|
|
89
|
+
continuation_token: str | None = None
|
|
90
|
+
voice_id: str
|
|
91
|
+
voice_roster: list[Any] | None = None
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
class SendMessageResponse(CamelCaseModel):
|
|
95
|
+
message_id: str
|
|
96
|
+
conversation_id: str
|