Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
33 changes: 24 additions & 9 deletions src/pythonxbox/api/provider/people/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,10 +19,17 @@ class PeopleProvider(RateLimitedProvider):
SOCIAL_URL = "https://social.xboxlive.com"
HEADERS_SOCIAL: ClassVar = {"x-xbl-contract-version": "2"}
PEOPLE_URL = "https://peoplehub.xboxlive.com"
HEADERS_PEOPLE: ClassVar = {
# Contract v7 provides full relationship fields (isFriend, canBeFriended, etc), but only works for
# get_friends_own, not get_friends_by_xuid
HEADERS_PEOPLE_V7: ClassVar = {
"x-xbl-contract-version": "7",
"Accept-Language": "overwrite in __init__",
}
# Contract v5 works for all endpoints including get_friends_by_xuid
HEADERS_PEOPLE_V5: ClassVar = {
"x-xbl-contract-version": "5",
"Accept-Language": "overwrite in __init__",
}
SEPERATOR = ","

# NOTE: Rate Limits are noted for social.xboxlive.com ONLY
Expand All @@ -38,8 +45,12 @@ def __init__(self, client: "XboxLiveClient") -> None:
client (:class:`XboxLiveClient`): Instance of client
"""
super().__init__(client)
self._headers = {**self.HEADERS_PEOPLE}
self._headers.update({"Accept-Language": self.client.language.locale})
# Headers for endpoints that work with v7 (provides more fields)
self._headers_v7 = {**self.HEADERS_PEOPLE_V7}
self._headers_v7.update({"Accept-Language": self.client.language.locale})
# Headers for endpoints that require v5 (get_friends_by_xuid)
self._headers_v5 = {**self.HEADERS_PEOPLE_V5}
self._headers_v5.update({"Accept-Language": self.client.language.locale})

async def get_friends_own(
self, decoration_fields: list[PeopleDecoration] | None = None, **kwargs
Expand All @@ -60,7 +71,7 @@ async def get_friends_own(
decoration = self.SEPERATOR.join(decoration_fields)

url = f"{self.PEOPLE_URL}/users/me/people/friends/decoration/{decoration}"
resp = await self.client.session.get(url, headers=self._headers, **kwargs)
resp = await self.client.session.get(url, headers=self._headers_v7, **kwargs)
resp.raise_for_status()
return PeopleResponse.model_validate_json(resp.text)

Expand All @@ -71,7 +82,10 @@ async def get_friends_by_xuid(
**kwargs,
) -> PeopleResponse:
"""
Get friendlist of own profile
Get friendlist of a user by their XUID

Args:
xuid: XUID of the user to get friends from

Returns:
:class:`PeopleResponse`: People Response
Expand All @@ -85,8 +99,9 @@ async def get_friends_by_xuid(
]
decoration = self.SEPERATOR.join(decoration_fields)

url = f"{self.PEOPLE_URL}/users/me/people/xuids({xuid})/decoration/{decoration}"
resp = await self.client.session.get(url, headers=self._headers, **kwargs)
url = f"{self.PEOPLE_URL}/users/xuid({xuid})/people/social/decoration/{decoration}"
# Use v5 headers - contract v7 returns empty people list for other users
resp = await self.client.session.get(url, headers=self._headers_v5, **kwargs)
resp.raise_for_status()
return PeopleResponse.model_validate_json(resp.text)

Expand Down Expand Up @@ -116,7 +131,7 @@ async def get_friends_own_batch(

url = f"{self.PEOPLE_URL}/users/me/people/batch/decoration/{decoration}"
resp = await self.client.session.post(
url, json={"xuids": xuids}, headers=self._headers, **kwargs
url, json={"xuids": xuids}, headers=self._headers_v7, **kwargs
)
resp.raise_for_status()
return PeopleResponse.model_validate_json(resp.text)
Expand All @@ -137,7 +152,7 @@ async def get_friend_recommendations(
url = (
f"{self.PEOPLE_URL}/users/me/people/recommendations/decoration/{decoration}"
)
resp = await self.client.session.get(url, headers=self._headers, **kwargs)
resp = await self.client.session.get(url, headers=self._headers_v7, **kwargs)
resp.raise_for_status()
return PeopleResponse.model_validate_json(resp.text)

Expand Down
28 changes: 14 additions & 14 deletions src/pythonxbox/api/provider/people/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -117,22 +117,22 @@ class Detail(CamelCaseModel):
is_verified: bool
location: str | None = None
tenure: str | None = None
watermarks: list[str]
watermarks: list[str] = Field(default_factory=list)
blocked: bool
mute: bool
follower_count: int
following_count: int
has_game_pass: bool
can_be_friended: bool
can_be_followed: bool
is_friend: bool
friend_count: int
is_friend_request_received: bool
is_friend_request_sent: bool
is_friend_list_shared: bool
is_following_caller: bool
is_followed_by_caller: bool
is_favorite: bool
can_be_friended: bool | None = None
can_be_followed: bool | None = None
is_friend: bool | None = None
friend_count: int | None = None
is_friend_request_received: bool | None = None
is_friend_request_sent: bool | None = None
is_friend_list_shared: bool | None = None
is_following_caller: bool | None = None
is_followed_by_caller: bool | None = None
is_favorite: bool | None = None


class SocialManager(CamelCaseModel):
Expand Down Expand Up @@ -201,9 +201,9 @@ class Person(CamelCaseModel):
preferred_flag: str
preferred_platforms: list[Any]
friended_date_time_utc: datetime | None = None
is_friend: bool
is_friend_request_received: bool
is_friend_request_sent: bool
is_friend: bool | None = None
is_friend_request_received: bool | None = None
is_friend_request_sent: bool | None = None


class RecommendationSummary(CamelCaseModel):
Expand Down
74 changes: 74 additions & 0 deletions tests/data/responses/people_friends_by_xuid_v5.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
{
"people": [
{
"xuid": "2533274812261808",
"isFavorite": false,
"isFollowingCaller": false,
"isFollowedByCaller": true,
"isIdentityShared": false,
"addedDateTimeUtc": null,
"displayName": "VolekTheFNDwarf",
"realName": "",
"displayPicRaw": "https://images-eds-ssl.xboxlive.com/image?url=wHwbXKif8cus8csoZ03RW3apWESZjav65Yncai8aRmVbSlZ3zqRpg1sdxEje_JmFTQaIPE.QhWezFkHjetzbapvRXDLBSJ6wiY_cMsEgnqAxqpD1OrJZcC6lG1e2UMcpLmHsW8LHADcb1wfWVA4BUP6kQwwmBrbaYrX3XQiXI.8-&format=png",
"showUserAsAvatar": "1",
"gamertag": "VolekTheFNDwarf",
"gamerScore": "70700",
"modernGamertag": "VolekTheFNDwarf",
"modernGamertagSuffix": "",
"uniqueModernGamertag": "VolekTheFNDwarf",
"xboxOneRep": "GoodPlayer",
"presenceState": "Offline",
"presenceText": "Offline",
"presenceDevices": null,
"isBroadcasting": false,
"isCloaked": null,
"isQuarantined": false,
"isXbox360Gamerpic": false,
"lastSeenDateTimeUtc": null,
"suggestion": null,
"recommendation": null,
"search": null,
"titleHistory": null,
"multiplayerSummary": {
"inMultiplayerSession": 0,
"inParty": 0
},
"recentPlayer": null,
"follower": null,
"preferredColor": {
"primaryColor": "1081ca",
"secondaryColor": "10314f",
"tertiaryColor": "105080"
},
"presenceDetails": [],
"titlePresence": null,
"titleSummaries": null,
"presenceTitleIds": null,
"detail": {
"accountTier": "Gold",
"bio": "Lover of nerd culture & those who embrace it. 🇺🇸 | 🎮 gamer & collector | 🎲 d&d | 💪 fitness | 💻 twitch | ⚔️cosplay twitch.tv/volek_the_fn_dwarf",
"isVerified": false,
"location": "Charlotte",
"tenure": "12",
"watermarks": [],
"blocked": false,
"mute": false,
"followerCount": 50,
"followingCount": 34,
"hasGamePass": false
},
"communityManagerTitles": null,
"socialManager": null,
"broadcast": null,
"tournamentSummary": null,
"avatar": null,
"linkedAccounts": [],
"colorTheme": "gamerpicblur",
"preferredFlag": "",
"preferredPlatforms": []
}
],
"recommendationSummary": null,
"friendFinderState": null,
"accountLinkDetails": null
}
15 changes: 15 additions & 0 deletions tests/test_people.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,21 @@ async def test_people_friends_by_xuid(
assert route.called


@pytest.mark.asyncio
async def test_people_by_xuid_v5(
respx_mock: MockRouter, xbl_client: XboxLiveClient
) -> None:
route = respx_mock.get("https://peoplehub.xboxlive.com").mock(
return_value=Response(200, json=get_response_json("people_friends_by_xuid_v5"))
)
ret = await xbl_client.people.get_friends_by_xuid("2669321029139235")

assert len(ret.people) == 1
assert ret.people[0].gamertag == "VolekTheFNDwarf"
assert ret.people[0].is_friend is None # This field is missing in v5
assert route.called


@pytest.mark.asyncio
async def test_profiles_batch(
respx_mock: MockRouter, xbl_client: XboxLiveClient
Expand Down
Loading